Imported from BaoAL31/CVTailor (
AGENTS.md). Install upstream withnpx skills add BaoAL31/CVTailor. Copyright stays with the author.
Agent Instructions
Short workflows for this repo. Detail lives in docs/.
| Topic | Doc |
|---|---|
| Discover (sources, CSV, defaults) | docs/discover.md |
| Scrape a URL | docs/scraping.md |
| Apply — per-process guides (LinkedIn EA, Seek, Workday, SuccessFactors, Amazon.jobs, etc.) | docs/apply/README.md |
| Project blurbs for tailored CVs | docs/portfolio.md |
| Domain vocabulary | CONTEXT.md |
Workflow
Discover (find jobs)
# Defaults: LinkedIn only, last 24h (data/search_defaults.yaml)
python -m src.discover --limit 15
# Include other boards/ATS alongside LinkedIn
python -m src.discover --sources linkedin,prosple,gradconnection,greenhouse,lever,ashby --limit 15
# Repeat discover+scrape every 2h (starts Chrome :9223 if LinkedIn is included)
python -u scripts/discover_loop.py
- Keywords/location from CLI or
data/search_defaults.yaml - Each keyword × each
--sources→ auth → search - Skip senior/lead/staff titles; skip rows already in
jobs/<date>/discovered_jobs.csvordata/seen_jobs.db - Scrape new listings into
jobs/<YYYY-MM-DD>/<company>-<title>/job-description.md - Merge new rows into
jobs/<date>/discovered_jobs.csv - Tailor → apply for folders the user picks
Do not pass --easy-apply unless asked (default off). Full behaviour: docs/discover.md.
Tailor + apply (known job)
When the user provides a JD (or URL):
- If URL only →
python -m src.scrape_job <url>(docs/scraping.md) - Read master CV
CV_master.docxand the JD - Ensure folder
jobs/<company>-<title>/withjob-description.md - Generate in that folder:
CV_Tailored.docx— medium-intensity tailor (rules below)Cover_Letter.docx— template below
- Apply —
python -m src.apply jobs/<folder>/ --runfor LinkedIn Easy Apply and SEEK Quick Apply (docs/apply.md). Other ATS:docs/apply/README.mdwithout browser MCP. - Write
handoff.mdsummarising the session
Chain: discover → scrape → tailor → apply
Apply — CDP drivers (no MCP)
Do not use Playwright/browser MCP to apply. Do not open new tabs or launch Chromium.
Debug Chrome must already be running on :9222 (python scripts/hold_chrome.py or
.\\launch-linkedin-chrome.ps1). Attach only.
python -m src.apply jobs/<date>/<folder>/ --run
python -m src.apply jobs/<date>/<folder>/ --run --no-submit # stop at Review
LinkedIn Easy Apply and SEEK Quick Apply are implemented. Other processes: follow
docs/apply/README.md using the same Chrome (no MCP), or
extend src/apply_*.py.
For ATSes without a driver (Workday etc.): do NOT connect() per step. Every
connect() leaks a CDP session and Chrome stops answering after ~5–6. Hold ONE
attachment in a server and drive it command-by-command:
python -m src.browser.wd_server # attach once; serves 127.0.0.1:9223
python -m src.browser.wd_client --op ping # unlimited commands, no re-attach
Same page object stays alive → SPA state is stable and the connection budget never
degrades. Details + full op list: docs/apply/workday.md.
| Application process | Driver |
|---|---|
| LinkedIn Easy Apply | --run (src/apply_linkedin.py) |
| Seek Quick Apply | --run (src/apply_seek.py) |
| Workday, SuccessFactors, Amazon.jobs, Greenhouse/Lever/Ashby, ElmoTalent, SmartRecruiters one-click | no --run yet — notes in docs/apply/ |
Shared CDP/inspect/answer-bank rules: docs/apply/README.md and docs/apply.md.
Pre-flight (every job)
python -m src.generate CV_master.docx jobs/<folder>/job-description.md --output-dir jobs/<folder>/
python scripts/list_apply_candidates.py # optional
Confirm the listing is still open. Skip if "No longer accepting applications" —
--mark-status expired.
--run re-reads the page after each action. Agents must not drive apply via MCP snapshots.
Answer bank
python -m src.apply_answers lookup "<exact question text>" # NOT_FOUND if missing
python -m src.apply_answers save "<exact question text>" "<answer>"
For ANY personal/identity/qualification field — name, title, address, phone,
degree, major, school, dates, skills, years of experience, visa/eligibility,
salary, DOB, gender — ALWAYS run apply_answers lookup FIRST and use the
bank's exact value. Never type these from scratch, never abbreviate, never
substitute. A bank match that is worded differently still wins (e.g. the bank
says degree = "Bachelor of Computing Science (Honours)" — use exactly that,
NOT "Bachelor"). If a required field has no bank match, STOP and ask the user —
never guess. Park unknowns in jobs/<folder>/pending_questions.yaml if
batching. After first use, save every new Q&A.
Default screening answers (only when consistent with the CV — never invent skills/years): see
docs/apply/linkedin-easy-apply.md and the answer bank.
Confirm + record
- Confirm the submission: success message (e.g. "Application submitted" / "Your application has been sent to " / "Thank you for applying").
- Record in the master ledger
jobs/applications.csv+seen_jobs.db. For rows with source/external_id in frontmatter,python -m src.apply jobs/<date>/<folder>/ --mark-applied(or--mark-status skipped|done|rejected|expired --notes "<why>"for non-applied). Seek / GradConnection rows often lack the id — append tojobs/applications.csvdirectly. - Write
handoff.mdin the job folder (template below).
handoff.md template
# <Company> — <Title>
- **Status:** Applied | Skipped (reason) | Blocked (reason)
- **Date:** YYYY-MM-DD
- **URL:** https://...
- **CV:** CV_<Name>.docx (+ cover letter if uploaded)
- **Answers:** brief note on non-default screening answers
Apply checklist (copy for agents)
- CV + cover letter generated in job folder
- Listing still accepting applications
- Form completed through submit (
--runfor LinkedIn/SEEK; no MCP) - Unknown questions asked / parked — not invented
- Success message confirmed
- Recorded in ledger (applied or skipped/done) +
handoff.mdwritten - New answers saved to
data/apply_answers.yaml
Personal Context
| Detail | Value |
|---|---|
| Name | Hoang Bao Nguyen (goes by Bao) |
| jemboong@gmail.com | |
| Phone | 0433 066 536 |
| GitHub | github.com/BaoAL31 |
| https://www.linkedin.com/in/hoang-bao-nguyen-28204325a/ | |
| Location | Cabramatta, NSW 2166 |
| Visa | Subclass 485 (Graduate) — full working rights, no sponsorship needed |
| Pronouns | — (none specified) |
| Experience | 2 years TypeScript, Node.js, React |
| Visa Sponsor | No — does not require visa sponsorship |
Use these for application screening answers (seeded in data/apply_answers.yaml; extend via apply_answers save).
Tailoring (LLM-based)
- Master CV is the only source of truth — LLM never invents skills/experience
- LLM (NVIDIA NIM, model
minimaxai/minimax-m3; falls back to OpenRouter) rewrites bullet points, selects 2–3 most relevant projects, and reorders skills - Allowed: remove irrelevant entries, reword bullets to echo JD keywords, reorder by relevance
- Keep dates, companies, and role titles accurate; header stays identical to master
- Falls back to original CV if LLM call fails (no silent corruption)
- Full project-blurb context:
docs/portfolio.md
Cover Letter Template
- Starts with "Dear Hiring Manager," (no header block)
- Body: 3–4 paragraphs connecting CV to JD
- Close: Best regards, [full name]
- Signature: phone | email | GitHub