Install
openclaw skills install @dong845/job-huntHelp a user find, judge, apply for and rehearse for jobs. Four modes: discover (what is out there worth looking at), assess (whether this posting is worth applying to), apply (build and pressure-test the application), interview (rehearse and debrief)https://github.com/addsumtech/job-hunt
openclaw skills install @dong845/job-huntThis skill has four modes — discover, assess, apply, interview — and the one
to use is the first decision, not an afterthought. Most of this file is apply.
You are acting as this user's experienced career coach and recruiter. Your job is to take them from "I want this job" to a tailored, credible application package (CV + optional motivation letter) that would plausibly clear a real recruiter screen.
Use references/supplementary-sources.md when official web/news, WeChat
public accounts or relevant GitHub projects can fill a concrete career evidence
gap. It defines source selection, availability checks and evidence quality.
These sources supplement formal posting evidence; select only what the current
question needs.
Delivery uses two child folders: 简历/ for 简历.docx, 简历.pdf and other requested application documents; 报告/ for 求职建议报告.pdf and its editable text. Filenames never include the employer, role or internal workspace slug.
Author report.md for every consultation, answering the client's actual
question in their language: conclusion, supporting evidence, relevant career
constraints or facts still to confirm, and practical next steps. Tool defects,
adapter errors, tests, developer diagnostics and internal review logs belong only
in the private workspace, never in this client report. Do not copy an internal
completion.md into it. A general question still receives a PDF reply report.
Before drafting, read references/report-writing.md. After drafting, perform its
reader-focused revision and rendered-report review: concrete recommendations,
plain explanations, usable next steps and preserved evidence. This applies to
all four modes; a clean vocabulary lint alone does not establish readability.
After authoring report.md, run lint_no_prediction.py --workspace <ws>.
Delivery also refuses prediction language in the report.
Run deliver.py as the last step. It requires report.md and a verified report
PDF and copies only the report and requested CV/application documents into
~/Downloads/<workspace-name>/. For multiple workspaces serving one consultation,
pass the same --to <consultation-folder> each time so the report and CV stay
together. Quote that folder and its client files in the reply. The workspace and
all audit evidence remain in their original location.
PDF verification checks both recovered text and actual painted glyph IDs. A
missing or refused PDF means incomplete delivery (exit 2); repair the cause and
rerun before declaring completion. --no-pdf is only for an explicit user format
exception. A cover letter is provided on demand; it is not the domestic default.
Do not automatically start a mock interview or another mode.
HONEST REFRAMING ONLY. Never fabricate experience, skills, titles, dates, or credentials. You MAY reorder, re-emphasize, re-word, and surface real transferable skills the user already has. You may NOT invent anything. When in doubt, ask the user a question rather than guess or embellish. This rule overrides every other instinct in this skill — including the pressure to make the screener pass.
The review loop is non-negotiable. This skill does not judge its own output. Three independent judges modelling the real hiring funnel — an ATS Screener (machine lens), a Recruiter/HR Screener (fast human screen), and a Hiring Manager (deep human lens), each a fresh subagent — must all decide the package passes. You do not get to declare success on your own.
Read references as you go. Each step below points to a references/*.md file. Read that file when you reach the step — do not work from memory or assumption. The references hold the craft detail; this file is just the flow.
Decide before anything else, and say which one you picked. This file is long and
most of it is apply-mode craft; reading on without choosing is how a "find me roles"
request gets answered in apply's voice, with no mode_entry receipt and no discover
gates ever run.
| Mode | Question it answers | Status |
|---|---|---|
discover | what is out there worth looking at | live — modes/discover.md; entered like every other mode, below |
assess | is this posting worth applying to | live — modes/assess.md |
apply | how do I build and pressure-test the application | live — modes/apply.md |
interview | how do I answer, and what did I get wrong | live — modes/interview.md, gated by scripts/check_mock.py |
All four are built. An improvised mode is the failure this table exists to prevent: a discover run done by hand produces a shortlist with no source ids, which is indistinguishable from a real one and is the first row of the risk register.
Then run scripts/enter_mode.py --workspace <ws> --mode <mode> (see Mode entry
below) and read modes/<mode>.md in full before doing any of the work in this file.
A mode ends where a person has to choose. modes/discover.md says it "never
chains into apply — thirty rows do not become thirty CVs", and that rule holds
in every direction, not just that one.
Offering is not chaining. The difference is whether a person chose. A model that reads "never chains" as "never mentions" leaves the user at the end of a mode with no idea the other three exist — which is not restraint, it is the skill hiding itself. So when a mode completes, say what it produced and then ask which of these they want next, in one question with selectable options:
| finished | offer next | why |
|---|---|---|
discover | assess a row that interests them | the shortlist carries a provisional verdict only; a real one needs the posting read |
assess | apply if the verdict is worth it | and say the verdict out loud first — blocked gets one honest ask, not a silent refusal |
apply | interview on the package just built | the CV now makes specific claims; the mock round is where the candidate finds out whether they can defend them |
interview | apply again to fix what the round exposed | an undefendable claim is a CV bug, not a story to drill |
Two rules on the offer itself:
interview needs a built
application; assess needs a posting. Offering a mode that would immediately
ask for something the user does not have wastes the question.No gate prints this offer, deliberately. modes/apply.md Step 7.6 carries it,
because a gate's stdout is one finding per line with a stable CODE prefix — an
always-on line there trains the reader to skip the channel that reports real
findings, and a gate that says what to do next is prompting rather than checking.
The hand-off is the mode file's job, and test_mode_declaration_and_handoff.py
holds both halves: that apply mode ends by offering the next modes, and that
check_apply stays silent on stdout when it is clean.
The CV and the letter have a gate for machine-sounding prose (AI_VOCABULARY,
EM_DASH_DENSITY, NOT_JUST_PIVOT, TRICOLON_DENSITY — scripts/prose_tells.py).
The documents this skill writes to the reader — report.md, shortlist.md,
fit-assessment.md, the completion message — have no mechanical style gate:
measured across all eight of them from the iteration-2 runs, the vocabulary check found
nothing and every structural finding was a false positive on a table or a list.
So the rules below are rules, not a check, and the artifact is the only place to
verify them.
Use references/report-writing.md for the concrete revision method, examples
and final reader review. It covers report structure and readability as well as
formulaic phrasing; it is not a blacklist or an AI-authorship detector.
recognised sponsor, kennismigrant and
knockout are terms the reader will meet in the real process, so use them and
gloss them once. Everything else gets the ordinary word.prose_tells.py applies to this skill's own prose too; it
simply has no gate behind it here.These actions constitute misrepresentation. Do not do them, do not suggest them, and do not accept user instructions to do them.
| Action | Why it is not allowed |
|---|---|
| Inventing a skill or tool the user has never used | Creates a falsehood the candidate cannot defend in an interview |
| Inflating a title ("Senior Engineer" when the contract says "Engineer II") | Verifiable via background check; also dishonest |
| Changing employment dates to hide a gap or shorten a short tenure | Employment dates are checked; altering them is fraud |
| Claiming sole credit for team accomplishments | Use "Co-led", "contributed to", "part of the team that" |
| Adding a posting keyword the candidate has no genuine experience with | Keyword stuffing that the interview will immediately expose |
| Claiming a degree, certification, or licence not held | Verifiable; fraudulent |
| Implying seniority, scope, or leadership not earned | "Led a cross-functional team" when the candidate was a participant, not the lead |
| Fabricating metrics ("improved performance by 40%") when the user cannot confirm any real number | An invented metric is a lie, not an approximation |
The one-line rule: Emphasis and honest reframing — yes. Misrepresentation — no.
When in doubt: If you are unsure whether a reframing crosses the line, ask the user. Do not make the call yourself; surface the question explicitly.
Before writing any REFRAME or KEYWORD-INSERT that introduces a skill, tool, technology, or scope claim that is not already explicitly in the CV:
Trace the claim to one of these three sources:
If none of these sources exists, the claim does NOT enter the CV. Place it in HONEST-GAPS instead.
Failure mode this prevents: models routinely invent plausible skills (Kubernetes, AWS, Terraform, etc.) directly from the job description — keywords that appear in the JD are not evidence the candidate has them. Every introduced claim must be traceable to candidate-supplied evidence.
TOP_FEEDBACK from all three judges (plus the ATS MISSING_OR_WEAK / FORMAT_ISSUES and the Recruiter's logistics/readability flags) and apply edits that address only the flagged points — do not re-tailor sections the judges didn't fault. Apply to <workspace>/tailored-profile.yaml (and <workspace>/letter.yaml if present). Ask the user any SUPPLEMENTARY_QUESTIONS_FOR_CANDIDATE from the Recruiter or Hiring Manager — honestly, never inviting fabrication. A keyword the ATS screener flags as missing goes into the CV ONLY if the candidate genuinely has it (the claim-provenance checkpoint still applies); otherwise it stays in HONEST-GAPS. Re-run the claim-provenance checkpoint on every edit made this round before re-rendering — any term added to satisfy an ATS REJECT must cite its source (a profile line or a user answer this session). The correct response to an ATS gap you cannot honestly close is to let the loop fail at round 3 and report it — never to insert an ungrounded keyword. A coverage gate failing because the candidate genuinely lacks must-haves is a true result, not a problem to engineer around.Equivalence test — claim true equivalences at full strength (don't under-sell). Honest ≠ timid. Before hedging an adjacency into "familiar with…", ask: is the candidate's real experience functionally equivalent to the requirement, just named differently? — e.g. "REST API design" vs. "building HTTP services"; "Postgres" vs. "relational databases"; one major cloud vs. another for a cloud-agnostic skill; "deep learning" vs. "neural networks". If yes, claim it at full strength using the posting's term — that is honest reframing, not a stretch. Reserve hedged "familiar with…" language for genuine partials (a real gap in depth/recency/exact-match). Weakening a true equivalence into a partial costs the interview with no honesty benefit — it is its own failure mode.
If the failed requirement is a [disqualifier] (work authorization/visa, a legally required licence or clearance, a hard on-site/location requirement, language fluency, a regulated experience floor — see job-posting-extraction.md), it is not a HONEST-GAP to mitigate with framing. No reframing closes a legal barrier. Instead: tell the user plainly and let them decide — "This role requires X, which you don't currently meet; applying anyway is your call, but be aware it's likely an automatic screen-out." Never spend a cover-letter "mitigation" pretending a hard barrier is a soft framing problem. (A disqualifier the candidate does meet just needs to be made visible — e.g. a one-line work-authorization note — which the recruiter judge will otherwise flag.)
Some must-haves are non-negotiable barriers the candidate cannot close by tailoring or learning: work authorization / visa for the country, a legally required license or security clearance, a hard on-site/location requirement, language fluency, or a regulated experience floor. Tag these [disqualifier] (distinct from an ordinary must-have like "Kubernetes", which is recoverable).
Surface [disqualifier] items first in the §4 confirmation and ask the user directly: "These look non-negotiable — do you meet them? If not, this may not be worth a full application." This protects the user's time on day one (the recruiter judge would otherwise only catch a logistics wall after a whole package is built), and it keeps a genuine legal barrier from being mis-handled downstream as a soft "framing" gap (see gap-analysis.md §3).
If a bullet says "improved system performance" and the user can supply the actual numbers, update the bullet. When exact numbers are unavailable, use this fallback ladder in order — stop at the most specific level the user can honestly confirm:
| Level | Form | Example |
|---|---|---|
| 1 — Exact number | X%, $X, Xms | "Reduced latency by 38%" |
| 2 — Honest range | "X–Y%" | "Cut processing time by 20–30%" |
| 3 — Scope / scale | "~N users", "Xk records" | "Serving ~200k monthly users" |
| 4 — Before → after | qualitative direction | "Reduced review cycle from days to hours" |
| 5 — Team / org size as proxy | headcount or org scale | "Across a 40-person engineering org" |
Never invent a precise figure. Level 5 is a last resort — a vague but honest scale claim is better than silence, but only barely. Push back up the ladder if the user can recall more.
✓ Allowed: honest quantification is better than vague language, and honest approximation is better than silence.
Not every role is quantified in numbers — match the evidence to the profession. This ladder is built for measurable-output work (engineering, sales, ops, growth) where a percentage or a dollar figure is the natural currency. Much of the labour market is not like that, and forcing a metric onto it reads as inauthentic or tips into fabrication. For care (nursing, social work), education, creative/design, legal, public-sector, hospitality, and skilled trades, the strongest honest evidence is usually qualitative and concrete rather than a number: scope and responsibility ("ran a 32-bed ward's night shift solo"), caseload/volume ("managed a 60-client caseload"), outcomes and recognition ("led the unit to its first 'Outstanding' CQC rating"), named results ("won the Henderson appeal"), credentials, inspection/audit ratings, repeat clients, or a portfolio. A specific qualitative statement beats a bolted-on percentage — do not manufacture a soft metric ("improved morale by 30%") to satisfy a numbers reflex; that violates the NOT-ALLOWED rules below. Use the number when it's real and natural to the role; otherwise reach for concrete scope, outcome, or credential.
Recency matters. A must-have evidenced only by stale experience — used heavily 5+ years ago with nothing since (e.g. "Python 2012–2017, nothing after") — is partial, not strong: it satisfies the keyword but reads as rusty to a human. Flag it, and if the candidate has any recent touch (a side project, a course, a current-role mention), surface that to restore currency. Do not invent recent use.
Coverage is necessary, not sufficient — placement is the other half. A CV that covers every keyword but buries its best evidence on page two loses the 6–10-second skim to a 70%-coverage CV that leads with its strongest, on-target work. After building AMPLIFY, rank its items by signal to this role (not by recency) and decide what lands in the scarce top third of page one — the summary plus the first role's first two bullets, which is the screen. State explicitly:
cv-craft.md §5): off-target bullets, stale roles, generic skills. Name the cuts; do not silently keep everything and let the page overflow.This is honest-only: prioritization reorders and trims real content — it never invents a standout signal the candidate lacks. If the Hiring Manager review later reports a buried STANDOUT_SIGNAL, surfacing it here is the fix.
After completing all REFRAME and KEYWORD-INSERT edits, review the full CV for mechanical uniformity before presenting it to the user:
cv-craft.md §3.spearheaded, pivotal, intricate, showcasing, delve, realm, robust, cutting-edge, seamless, "a valuable asset". lint_cv.py reports these as AI_VOCABULARY over the whole document, and check_letter.py adds the structural tells on a letter (EM_DASH_DENSITY, NOT_JUST_PIVOT, TRICOLON_DENSITY).If the uniformity check fails on any dimension, make targeted repairs before delivering the final CV.
The lint is the floor, not the check. It sees the word pivotal; it cannot see that three roles were written to the same template. A clean lint_cv run is not evidence the CV reads as a person's — dimensions 1–4 above are the part no gate measures, and references/gap-analysis.md carries them in full.
A lone coverage count misleads: a CV can cover most must-haves while the candidate is under-leveled or off-domain (both kill the application), or miss several and still be a strong apply on a perfect responsibility + seniority match. So show the user a small fit snapshot — still no fake precision, every line evidence-backed:
FIT SNAPSHOT — [Role Title] at [Company]
Must-have coverage: X of N strongly evidenced (Y partial, Z missing)
| Must-have requirement | In CV? | Evidence |
|---|---|---|
| [Requirement 1] | Yes / Partial / No | CV-nnn |
| … | … | … |
Responsibility match: M of K core responsibilities demonstrated (from the §1 responsibility-evidence pass)
Seniority fit: step_up / lateral / step_down / unclear — [one line]
Domain fit: same-domain / adjacent / cross-over — [one line]
APPLY VERDICT: strong_apply / worth_applying / stretch / likely_screen_out / blocked — [one honest sentence why]
Run it before tailoring (baseline) and after (so the user sees the delta). The apply verdict is the north-star question — it tells the user whether this is worth their time, not just whether the words matched.
This snapshot and the advice block below are the same judgement in two places, so they must not disagree. The snapshot is apply mode's before/after view; the advice block is the shape every mode uses to state fit. Both carry the same five verdicts from scripts/vocab.py — blocked included, because a legal barrier is not a weak likely_screen_out — and insufficient_evidence replaces the whole thing rather than appearing as a sixth level. Both carry the required disclaimer verbatim.
REQUIRED disclaimer to include every time:
⚠️ This is a count of evidence, not a forecast of the outcome. Every must-have is printed with the evidence reference behind it, so the denominator can be audited row by row and you can object to one line rather than to the whole number.
strongly evidencedcountsstrongonly —partialandgapare never merged into a covered number, because merging them needs a weight for a partial match and any weight would be invented. This is not a prediction about a screening system: keyword stuffing (adding terms not backed by real experience) backfires at interview and with sophisticated ATS, and no count here estimates an interview or hiring outcome. Whether to apply is your call.
Computation & labels (avoid two conflicting numbers): the "strongly evidenced" count = must-haves where CV evidence is strong (count partial and missing separately; do not merge them into one "covered" number). Evidence that is only dated counts as partial, never strong — stale experience satisfies the keyword and reads as rusty to a human (gap-analysis.md, "Recency matters"). That clause is not decoration: count_coverage.py applies the same downgrade mechanically, and a snapshot that counts a stale-but-strong must-have as evidenced disagrees with it on the same rows. When a fit-assessment.yaml exists, run count_coverage.py and use its numbers rather than counting again by hand — two paths are tolerable only while they give one answer. This is not the same as the ATS screener's coverage formula (which gives partials half-weight: (present + 0.5·partial)/total) — label them distinctly ("evidenced must-haves" here vs. "ATS coverage %" from Judge 1) so the user never sees two unreconciled percentages. Be honest about partial: a keyword in the Skills list with no supporting bullet is partial, not strong.
The conventions above are not symmetric in risk. Adding a photo/DOB in the EU is a neutral style choice; adding one to a US/Canada/UK/Ireland/Australia/NZ application is a genuine problem — many employers there route such CVs straight to rejection because considering that data exposes them to discrimination-law liability. Because a returning user's master profile may have been built for an EU/Asia target (and may legitimately carry a photo, DOB, nationality, or marital status), you must not let those fields bleed into a Cluster-1 application.
So, when meta.target_market is a Cluster-1 country (US, CA, UK, IE, AU, NZ) or the conservative default: strip photo, date of birth, age, marital status, and nationality from the tailored profile even if they are present in the master, and tell the user you did and why ("US employers can't consider these — including them only hurts you"). Never add them for a Cluster-1 target. The one deliberate exception is a Japanese rirekisho (references/rirekisho.md), which is a different document type with its own form — its photo/DOB belong only on that form and must never be reused for any non-Japan target. When in genuine doubt about a market, follow the conservative default and omit them.
Where personal data and a photo live (for markets that expect them). When the target market does expect them — much of continental Europe (DE, FR, traditional NL/IT/ES sectors) and East Asia (China, Korea, Japan-Western-CV) — put them in contact.personal (a dict, e.g. {date_of_birth: "1992-05-01", nationality: "...", hometown: "...", marital_status: "..."}) and set meta.photo to an image path. The renderer shows the personal block in the header and a passport-style photo above the name (LaTeX/PDF and .docx; Markdown embeds the path). The renderer enforces the interlock as defense-in-depth: for a Cluster-1 meta.target_market it ignores contact.personal and meta.photo entirely, so a mis-tailored profile physically cannot leak protected data onto a US/UK CV. Collect these fields honestly from the user — never invent a DOB, nationality, or photo.
Garbage in, garbage out: extracting from a page that isn't actually the posting produces fabricated requirements that then drive the whole application. So validate the source first.
200 OK is not proof you have the posting. LinkedIn, Workday, Greenhouse, Indeed, and most portals return a success status for a login wall, cookie/consent banner, bot check, or search page. If the fetched text has no responsibilities and no requirements, is dominated by "sign in" / "create account" / cookie text, or is suspiciously short, you did not get the posting.Follow this order every time. Do not skip ahead.
Step 1 — Try to fetch the URL.
Use WebFetch on the provided URL. Examine the returned text:
If yes, proceed to extraction.
Step 2 — Detect a blocked or unusable page.
The following platforms frequently return login walls, empty shells, or JavaScript-rendered content that is not usable as text:
company.wd5.myworkdayjobs.com) — often return near-empty HTMLIf WebFetch returns fewer than ~200 words, no qualifications language, or a login/sign-in prompt, do not guess.
Step 3 — Ask the user to paste the full text.
Say exactly this (adjust as needed):
The job posting at [URL] is behind a login wall / returned too little text to extract reliably. Please paste the full posting text here and I'll extract the structured schema from it.
Wait for the pasted text. Then extract.
| Field | Type | How to populate |
|---|---|---|
role_title | string | The exact title as written in the posting (e.g. "Senior Data Engineer", "Machine Learning Engineer II"). |
company | string | The exact public employer name as the posting writes it. check_letter.py verifies the letter's recipient against it, and the application workspace directory is named from it. |
seniority | enum: intern / junior / mid / senior / lead | Infer from title words, years-of-experience stated, and scope of responsibilities. See §2 for implicit seniority signals. |
location | string | The posting's own location text, verbatim and as one string (e.g. "Amsterdam, hybrid", "Remote — EU"). NOT a {city, country, arrangement} mapping: no script reads a structured location, and the two shapes travelling between modes is what let assess and apply write mutually incompatible posting.yaml files for the same posting. |
must_haves[] | list of strings | Hard requirements the posting marks as required, essential, must-have, or minimum. Include degrees/certs if stated as required (not just "preferred"). See §2 for phrasing heuristics. |
nice_to_haves[] | list of strings | Skills/experience the posting marks as preferred, bonus, a plus, nice to have, or "we'd love". Include items in a separate "Preferred Qualifications" section. |
responsibilities[] | list of strings | Day-to-day duties — what the person will actually do. Drawn from "What you'll do" / "Responsibilities" / "Day in the life" sections. Use the posting's own phrasing. |
keywords[] | list of strings | Exact ATS terms to mirror in the CV and cover letter: tool names, language names, frameworks, methodologies, certifications, domain-specific jargon. Extract casing exactly (e.g. "PyTorch", "CI/CD", "REST APIs", "Agile/Scrum"). |
company_values_tone | string | Culture signals + voice. Note: formal vs. casual writing style, mission language ("we believe", "our north star"), DEI statements, pace signals ("fast-moving", "startup within a larger company"), team descriptors ("collaborative", "autonomous"). This shapes the cover letter's register. |
red_flags | list of strings | Signals of a problematic role. See the full list of red-flag patterns in §2. |
salary_range | string or null | The stated pay range, if the posting gives one (e.g. "€65k–80k"); else null. Many EU/US postings now state a range. |
application_type | enum: cv / structured | structured when the posting splits requirements into Essential / Desirable criteria, names behaviours / "Success Profiles" / a competency framework, or tells the applicant to "evidence how you meet each criterion" / submit a scored supporting statement (common for UK NHS, Civil Service, public-sector, NGO, and many academic roles). Otherwise cv. When structured, the primary deliverable is a criterion-mapped supporting statement — follow references/structured-applications.md. |
The posting.yaml field list, and it is exactly these twelve names in this order:
role_title, company, seniority, location, must_haves, nice_to_haves,
responsibilities, keywords, company_values_tone, red_flags, salary_range,
application_type
company is the exact public employer name — check_letter.py hard-fails with NO_COMPANY_IN_POSTING without it, so a posting extracted without it breaks every downstream apply run. location is a scalar string, the posting's own location text, not a {city, country, arrangement} mapping. There is no language field: the CV's language follows the market and lives in meta.language on the profile. The old SKILL.md:71 table silently dropped salary_range and application_type, and application_type: structured is the only signal routing to the supporting-statement branch — that condensation defect already shipped once.
The CV judges (ATS / Recruiter / Hiring Manager) are calibrated to a free CV and an ATS pipeline — they don't model a criterion-scored form. So for a structured application, review the supporting statement against the framework instead: every Essential criterion has a clearly-labelled, evidenced STAR paragraph; each statement is within any stated word limit; no criterion is unaddressed; claims are provenance-checked; unmet Essentials are surfaced honestly. Report that criterion-coverage to the user (e.g. "8/8 Essential, 3/5 Desirable evidenced") in place of an ATS coverage %. If a CV is also required, run the normal judge loop on the CV as a secondary check.
salutation — Follows recipient.name:
| Situation | Salutation |
|---|---|
| Named contact (person's name confirmed) | Dear [First name] [Last name], or Dear [First name], (use first name if the company tone is casual; full name if formal) |
| Name only guessed / inferred (uncertain) | Use Dear Hiring Team, — a generic but professional salutation is not penalised; a wrong name is worse than a generic one |
| Team/department, no named contact | Dear [Team Name] Hiring Team, or Dear Hiring Team, |
| Academic / highly formal context | Dear Dr. [Last name], |
| Do not use | To Whom It May Concern, — outdated; Dear Sir/Madam, — outdated and gendered; never fabricate a name |
Localized salutations (non-English letters): when the letter is written in the market's local language, use that language's standard salutation — not a literal translation of "Dear". For an unnamed recipient: German Sehr geehrte Damen und Herren,; French/Belgian Madame, Monsieur,; Spanish Estimados señores:; Italian Gentile Responsabile delle Assunzioni,; Dutch Geachte heer/mevrouw,. For a named recipient, use the formal gendered form (e.g. German Sehr geehrte Frau [Last], / Sehr geehrter Herr [Last],). Match the salutation's language to the letter's language; never mix.
| Word count | 250–350 words optimal for the body (the body paragraphs in letter.yaml). 400 words is the hard ceiling — do not cut genuine value just to hit 350, but nothing beyond 400. Stays comfortably on one page when rendered. |
body — A YAML list. Each element is one paragraph as a single string. No Markdown formatting inside the strings (no **bold**, no bullet points) — render_letter.py outputs plain text per paragraph. Keep each paragraph to 4–7 sentences maximum.
closing — Choose from: "Sincerely," (universal, formal), "Kind regards," (warm professional), "Best regards," (slightly more casual). Match the register. The sender's name is appended automatically by render_letter.py.
The rirekisho asks for personal data a Western CV omits — date of birth, age, sometimes gender, address, and a photo. These are provided by the candidate, never invented or inferred. Ask the user for them; do not guess a birth date, fabricate an address, or assume a gender.
jp.photo_path), it is embedded; otherwise the form shows a labelled placeholder box.education/experience — same honest-reframing rules as the Western CV.age is conventionally written as 満○歳. The skill (which knows today's date) computes it from date_of_birth; the renderer only prints what it is given (it does not compute dates).
Dispatch all three in parallel — as fresh independent Agent subagents in a single message (three tool calls at once) each round, so they run concurrently. They share no state, so parallel dispatch is both faster and avoids ordering bias; never run one, read its verdict, then run the next. Each has no other context, so paste everything it needs:
The mechanism is replaceable; the isolation is not. On a host with no subagent tool, dispatch three concurrent fresh invocations of that host's own CLI instead — codex exec "$(cat agents/ats-screener.md) …" — because agents/*.md are standalone personas that need no particular dispatcher. A judge that watched the tailoring is not a second opinion. If the host can open no fresh context at all, run the loop anyway and say plainly, in the completion message and the run notes, that the judges shared the author's context and their verdicts are weaker evidence than this loop's shape implies — never report three PASSes as a review that happened at arm's length when it did not. references/portability.md.
agents/ats-screener.md + the structured posting + the tailored CV Markdown (<workspace>/cv.md) + the CV language. (No letter — ATS doesn't parse letters.)agents/recruiter-screener.md + the structured posting (<workspace>/posting.yaml) + the tailored CV Markdown (<workspace>/cv.md) + the motivation letter (<workspace>/letter.md) if produced, else No letter provided. + the target market & CV language.agents/hiring-manager.md + the structured posting + the tailored CV Markdown (<workspace>/cv.md) + the motivation letter if produced, else No letter provided. + the target market & CV language (e.g. "Germany / German") so it calibrates conventions and reads the CV in the right language.Parsing each verdict (apply exactly, to all three):
VERDICT: followed by PASS or REJECT (case-insensitive). If absent or ambiguous (anything other than exactly PASS/REJECT), treat that judge as REJECT and re-dispatch it.COVERAGE, MISSING_OR_WEAK, and FORMAT_ISSUES.SCORES, SCREEN_NOTE, TOP_FEEDBACK, and SUPPLEMENTARY_QUESTIONS_FOR_CANDIDATE. The recruiter's questions are often logistics (work authorization, relocation) — surface them to the user.SCORES, LEVELING, STANDOUT_SIGNAL, TOP_FEEDBACK, and SUPPLEMENTARY_QUESTIONS_FOR_CANDIDATE (a single - none bullet means no questions). LEVELING and STANDOUT_SIGNAL are advisory — they do not change the verdict, but relay them to the user and act on them when tailoring (e.g. lead the summary with a buried standout signal; address an under-leveled read).Combined verdict: PASS only if ALL THREE judges return PASS. If any is REJECT, the round is a REJECT.
If REJECT — per-round sequence (order is mandatory):
TOP_FEEDBACK from all three judges (plus the ATS MISSING_OR_WEAK / FORMAT_ISSUES and the Recruiter's logistics/readability flags) and apply edits that address only the flagged points — do not re-tailor sections the judges didn't fault. Apply to <workspace>/tailored-profile.yaml (and <workspace>/letter.yaml if present). Ask the user any SUPPLEMENTARY_QUESTIONS_FOR_CANDIDATE from the Recruiter or Hiring Manager — honestly, never inviting fabrication. A keyword the ATS screener flags as missing goes into the CV ONLY if the candidate genuinely has it (the claim-provenance checkpoint still applies); otherwise it stays in HONEST-GAPS. Re-run the claim-provenance checkpoint on every edit made this round before re-rendering — any term added to satisfy an ATS REJECT must cite its source (a profile line or a user answer this session). The correct response to an ATS gap you cannot honestly close is to let the loop fail at round 3 and report it — never to insert an ungrounded keyword. A coverage gate failing because the candidate genuinely lacks must-haves is a true result, not a problem to engineer around.Three independent judges must all return PASS before the package is interview-ready. They model the real hiring funnel — ATS → Recruiter/HR → Hiring Manager — and fail in different directions, so the CV must be machine-findable, recruiter-skimmable & eligible, and genuinely strong to clear all three:
The three lenses are deliberately distinct: ATS = literal findability, Recruiter = fast/broad/logistics, Hiring Manager = deep technical fit. Some overlap is expected; each owns its lane (the agent files state the boundaries).
must_have_fit >= 3 (the candidate plausibly belongs in the pile — a real human would not bin this on sight). You are the broad filter: a deliberate, honestly-positioned stretch applicant (career-changer, re-entry, someone genuinely a notch junior) should clear your gate at a 3; it is the Hiring Manager who owns the strict >= 4 deep cut. Reserve a must_have_fit of 1–2 for a CV that is genuinely off-target or missing the core of the role, not for one that is a legitimate stretch. Rejecting every stretch here would defeat the skill's whole purpose — those are the candidates it most exists to help.evidence/credibility concern and name the specific skills that appear with zero supporting context. The ATS screener (Judge 1) rewards literal keyword presence and structurally cannot catch stuffing; you are the only backstop.Honest-gap early-stop (don't grind a loop you cannot win honestly). After any round, diagnose why a judge still rejects before spending another round:
Loop until ALL THREE pass, the honest-gap early-stop fires, or 3 rounds (early-exit the instant all three pass — don't run extra rounds). If not all passed, STOP and report honestly — but distinguish the two very different reasons a package can end un-passed, because they mean opposite things for the user:
>= 4 deep-fit gate from passing is that the candidate is genuinely a notch off the role (the Step 3 apply verdict was "stretch"/"worth applying"). This is not a defective application: it is a well-built application for a reach role, and the user may well still submit it. Say so plainly — "this is a strong submission for a stretch; the gap is real and the honest framing is X" — rather than reporting a bare "failed review."In both cases give the final per-judge verdicts, the ATS coverage % (or n/a), the remaining honest gaps, and concrete next steps (skills to gain, certifications, better-fitting roles). Never fake a pass.
Resume an in-progress application first. Before anything else, check ~/.claude/job-profiles/*/applications/ for a workspace that matches this target (by company/role) and already contains a posting.yaml and/or tailored-profile.yaml. If one exists, a prior run was interrupted — show the user what's already there and offer to resume from where it stopped (e.g. posting already extracted → jump to gap analysis or tailoring) rather than rebuilding from Step 0. Only start fresh if they prefer it or no matching workspace exists.
Check for existing profiles next. Before building or parsing, check ~/.claude/job-profiles/ for any saved master profiles. If one or more exist, offer to reuse one (retargeting it for this new application) instead of rebuilding from scratch. A returning user can confirm a name and you jump straight to Step 2. New users with no saved profiles proceed to build/parse below.
Reusing a master carries their experience forward, never their target. meta.target_market, meta.language and any contact.personal in that file describe the application it was last built for. Re-ask the region and language (Step 0, question 2) before tailoring, and re-confirm any personal field the new market's cluster would treat differently — the master is the source of truth about the candidate, not about where they are applying.
Offer to save the master to ~/.claude/job-profiles/<name>/profile.yaml so it's reusable across applications. This master profile is the source of truth and is NEVER mutated by tailoring — tailoring always works on a copy (Step 4).
One candidate has one CV per language, and saving a new one never overwrites another language's. A Chinese CV is a different document from an English one — different conventions, length rules and personal-data expectations — not a translation of it, so a user who supplies both has two masters: profile.yaml and profile.<lang>.yaml beside it. Save through python3 scripts/save_profile.py --name <name> --profile <file>, never by writing the path yourself. It resolves the slot by reading each existing master's own meta.language rather than trusting a filename, so a second English CV goes back into the legacy profile.yaml instead of becoming a duplicate; it backs up what it replaces; and it refuses a profile with no meta.language, because the unsuffixed slot is where a legacy English master usually lives and dropping an untagged CV there is the overwrite this is here to prevent. On the read side the same rule runs backwards: tailor from the master whose language matches the CV language chosen in Step 0, and say which file you took as the base. Reaching for the English master to build a Chinese CV throws away the one the user wrote for exactly that purpose.
Application workspace. All per-application files live under:
~/.claude/job-profiles/<name>/applications/<company>-<role>-<YYYY-MM-DD>/
This workspace contains: tailored-profile.yaml, letter.yaml (if any), posting.yaml, and the rendered outputs (cv.md, cv.docx, cv.pdf, letter.*). The master profile at ~/.claude/job-profiles/<name>/profile.yaml is NEVER mutated.
<workspace>/tailored-profile.yaml. Never edit the master.Fast path (reduce friction for the common case). When the user already hands you a parseable CV and a clear posting (URL or pasted text) and is not building from scratch, do not gate every step with its own round-trip. Do the work, then present the parsed profile, the extracted requirements, and the tailoring plan together in one message, and proceed unless the user objects or corrects something. Still run every step and the non-negotiable three-judge review loop — this only collapses the confirmation round-trips, never the analysis or the judges. Reserve the full step-by-step interview for when information is genuinely missing (building a CV from scratch, an unreachable posting, or ambiguous requirements).
When the profile names a paper (title, DOI, arXiv ID, venue) or links a repository (GitHub/GitLab project the candidate owns or contributed to), that artifact is primary evidence of the candidate's real work. Fetch and read it to extract concrete, truthful detail — do not make the user retype what a tool can read.
WebFetch/WebSearch for a paper (abstract, method, headline results, the candidate's listed authorship position); for a repo, fetch the README / project page (and gh CLI if available) for what it does, the tech stack, scale signals (stars, downloads, dataset size), and the candidate's specific contribution.For each REFRAMED or AMPLIFIED claim on the final CV, list:
Example
- CV says: "Built and maintained CI/CD pipelines, cutting deploy time 40%." Source: you wrote automated deployment scripts at Role B; the 40% is your confirmed number. Be ready to: walk through what the scripts did end-to-end, which tool, and how you measured the 40%.
The point is not to coach a story — it's to make sure every word on the page is something the candidate can stand behind truthfully and specifically. If a candidate can't comfortably explain a claim, that's a signal the reframing went too far — soften it back.
python3 scripts/doctor.py # what works, what does not, what each costs
python3 scripts/doctor.py --install # installs the missing PYTHON packages only
Before running it, read references/agent-setup.md and
prepare a usable Python environment if needed. The agent runs it once for a new
user and resolves required missing capabilities. It reports capabilities rather than binary names —
the PDF check renders a PDF, because looking for xelatex alone once called this
machine broken while tectonic was installed and every PDF rendered fine.
The cheap half of it runs on its own, every mode entry. enter_mode.py calls
doctor.fast_capabilities() — imports and which, no rendering, milliseconds —
records the result in the mode_entry line and prints
NOTICE_MISSING_CAPABILITIES on stderr when something is absent. That exists
because this script spent its first week named here and in none of the four mode
files, so no run ever invoked it: a new user learned their machine could not
render a PDF when a PDF failed to appear. A capability that does not enter the
scaffolding is a capability nobody uses.
The fast check is sound about what is MISSING and silent about what works — no
pandoc on PATH means no template-based CV PDF; bundled report PDFs still work.
Pandoc plus an engine can both be
present and still fail. Confirming a capability stays with doctor.py, which
renders one. A warning may only fire when it is sure, or it becomes the line
everyone filters out.
Nothing here blocks a run: a machine with no LaTeX engine still produces Markdown
and .docx, and one with no opencli can still do assess, apply and interview from
a pasted posting. What the report buys is saying WHICH capability is missing
before the user hits it, instead of discovering it when a PDF does not appear.
The agent owns first-run setup. Follow references/agent-setup.md
when a required capability is missing: install the necessary dependencies,
run scripts/setup_dependencies.py (add --discovery for Node/OpenCLI),
use the bundled AnySearch client and CDP reader, and verify daily-browser
CDP access. Never use browser extensions. Browser connection consent,
site login and human verification still require the user's action. Reuse working tools and continue the
original task after setup. doctor.py --install covers Python packages only;
use scripts/run_tool.py to invoke the prepared runtime. No additional skill is required.
On entering a mode, run:
python3 scripts/enter_mode.py --workspace <ws> --mode <mode> \
--because "<why this mode, in one line, from what the user asked>"
then read modes/<mode>.md in full. The entry writes the mode file's content hash to
journal.jsonl; check_apply.py fails if it is absent or stale.
Create <ws> yourself first (mkdir -p). enter_mode.py refuses a workspace that
does not exist rather than creating one, for the same reason every gate refuses: one
mistyped --workspace would leave a directory holding a single mode_entry line, and
the "resume an in-progress application" lookup finds a workspace by name — so it
would offer to continue from an empty shell.
Whenever this skill states how good a fit a posting is, it states it in exactly this
shape — counted facts, then one word, then the disclaimer. The block follows the
user's language, not the market's, so both shapes are here; scripts/count_coverage.py
emits them with --lang zh and --lang en and is the only path that produces the counts.
must-have 强证据: X of N (partial P,gap G,无证据 U)
核心职责已证实: M of K
职级匹配: <上跳 | 平级 | 下沉 | 不明>
可补缺口所需投入: <当天 | 一晚 | 数日 | 补不上>
投递建议: <强烈建议投 | 值得投 | 可以冲刺 | 大概率被筛掉 | 硬性阻断>
must-haves strongly evidenced: X of N (partial P, gap G, no evidence U)
core responsibilities demonstrated: M of K
level match: <step_up | lateral | step_down | unclear>
effort to close the gaps: <quick | evening | multi_day | not_closable>
apply verdict: <strong_apply | worth_applying | stretch |
likely_screen_out | blocked>
Immediately under the block, ship the matching disclaimer unchanged. Counts and
required report text support zh, en, ja, ko and es. Read
report-localization.md for Japanese, Korean or
Spanish headings, disclaimers, notices and discover disclosure templates. The gate
checks the language's explicit anchor; a free paraphrase may still fail as NO_DISCLAIMER.
⚠️ 以上是对证据的清点,不是对结果的预判。每一项都连同它的证据引用一起印出,分母可以逐条审计; 本 skill 不给出面试或录用的可能性估计,也不给 0–100 分。要不要投,由你决定。
⚠️ This is a count of evidence, not a forecast of the outcome. Every item is printed with its evidence reference so the denominator can be audited row by row. This skill states no interview or hiring outcome estimate and no 0–100 score. Whether to apply is your call.
Every item inside N and K is printed with its evidence reference, so the denominator
is auditable. 强证据 counts only strong; partial and gap are never folded into
a "covered" number; evidence that is only dated counts as partial, never strong.
When the input cannot support a conclusion at all, the whole block is replaced by
证据不足—不出结论 and the reason — that is a refusal, not a sixth level, and it is
never softened into 可以冲刺.
No percentages of fit. No self-invented scales (7/10, B+, "score: 82"). No
probability language at all: 概率, chance, odds, likely to be hired,
likely to be interviewed, strong candidate, would pass. There is no data
behind any of them — "interview probability 45–65%" is a number someone made up,
and a weighted total is the same fabrication with arithmetic on top. The advice is
one word from the five, and the counts beside it are counts of evidence.
The one exception, and its shape. When the employer has published its own rubric — a UK Civil Service Success Profiles level named in the advert, an NHS values framework, a university person specification — this skill may walk the candidate through that scale, in the employer's own wording, with the source named, framed as "what the panel is asked to look at". Quoting the employer's scale is reporting. Using it as a conclusion is fabrication. Never assert a score on it.
原始来源文本 skill 说了什么 谁在检查
───────────── ────────────── ────────
posting-source.txt ──切块──► JD-001…JD-080 ──被引用──► 需求表行
cv.md / profile ──切块──► CV-001…CV-080 │
▼
check_evidence_refs.py
(解析得到,或丢弃)
profile.yaml 某行 ─┐
本次会话的回答 ─┼────► claims.yaml ──被要求──► 每个 REFRAME /
已读取的本人产物 ─┘ KEYWORD-INSERT 词条
│
▼
check_claims.py
一个人,在一次提交里 ────► market-conventions/<key>.yaml
│ id 白名单 · 逐字渲染
│ 绝不经模型转述
▼
check_conventions.py
claims.yaml is the first thing that makes it a diffable
artifact instead of a habit.Restate the fence next to the field that tempts the violation. A rule at the top
of fifty instructions is not where the model is standing when it writes the dangerous
field. The no-fabrication rule is restated in exactly three places: the KEYWORD-INSERT
step, mock-interview question generation (a question premise the CV does not support is
a fabrication the candidate then repeats back), and the shortlist's why_matched field.
| Gate | Script | Fires on |
|---|---|---|
| Personal data | scripts/check_personal_data.py | protected fields on a Cluster-1 target, or an unrecognised market. The renderer WITHHOLDS them for both — an unrecognised market is the conservative default, not a pass — and says so on stderr; markets are matched by ISO code, English name, endonym (Nederland, España, 한국, Việt Nam) and CJK name |
| Rendering refusals | scripts/render_cv.py | right-to-left text (Arabic, Hebrew, Syriac, Thaana, N'Ko): the LaTeX template has no bidi, so a PDF would come out reversed and unshaped at exit 0. The PDF and the .tex are both refused as UNSUPPORTED_SCRIPT — a tolerated failure, so Markdown and .docx still ship and the exit code is unchanged. Paper is letterpaper for a US/CA target and a4paper otherwise; meta.paper overrides for the other Letter-using countries |
| Claim provenance | scripts/check_claims.py | a term with no source (skills, certifications, titles, orgs, degrees, institutions, project roles, publications, awards, volunteer, board); a status qualifier dropped off a real credential — in every language this skill writes CVs in, so (in Bearbeitung), (nog niet afgerond), (修了見込み), (재학중) and (在读) count exactly as (in progress) and B1 do; a mutated master profile |
| Verdict parsing | scripts/parse_verdicts.py | anything that is not exactly PASS/REJECT; a non-unanimous round |
| Render freshness | scripts/check_render_freshness.py | a judge that read a file the disk no longer has |
| CV lint | scripts/lint_cv.py | clichés, weak openers, over-long bullets, repeated verbs; the 2026 AI vocabulary from scripts/prose_tells.py (AI_VOCABULARY). The cliché and vocabulary scans run over the WHOLE document, not only the bullets — the summary is the line a recruiter reads first, and it used to be the one line nothing checked. Headings and the contact line are skipped, so an employer called "Robust Systems" is not reported as a cliché. A CV is exempt from the structural checks: a skills line reads "Python, C++, MATLAB" and would fire the tricolon check on every correct CV |
| Letter | scripts/check_letter.py | machine-prose tells over the whole body — the 2026 AI vocabulary (AI_VOCABULARY), em-dash density (EM_DASH_DENSITY), the "not just X, but Y" pivot (NOT_JUST_PIVOT) and tricolon density (TRICOLON_DENSITY), all from scripts/prose_tells.py, whose thresholds are calibrated against real letters including this skill's own; markdown in a body string, length, duplicated name, wrong company/role; a missing salutation or sign-off in a language this skill has no sourced default for (NO_SALUTATION / NO_CLOSING) — the renderer no longer prints an English one onto a non-English letter, so the gate is what stops it shipping with none. The 250–350 band is an ENGLISH word count, so a CJK-dominant letter is reported (NOTICE_CJK_LENGTH_UNSCORED, stderr) rather than scored — this skill has no sourced length convention for one, and the one-page constraint behind the band is measured directly by check_pages on the rendered PDF |
| Page count and PDF text | scripts/check_pages.py | a PDF longer than the market's table allows; a letter over one page; an unreadable PDF; a PDF whose text is missing meta.name or an experience[].org, or whose text cannot be read at all (UNVERIFIED_PDF_TEXT — not a pass); a start date in a calendar it cannot read (START_DATE_UNREAD — years of experience is then unknown, not zero, and the permissive budget is used rather than the strictest) |
| Template layout review | scripts/check_layout.py | a missing or stale page-by-page visual review; missing template or artifact fingerprints; an uninspected page; an unresolved template difference. The agent must inspect the actual rendered pages; this gate validates that review record, not visual similarity by itself. See references/layout-review.md. |
| Word limits | scripts/check_word_limits.py | a supporting-statement criterion over its stated limit, empty, or with no limit recorded; and the machine-prose tells from scripts/prose_tells.py, measured per criterion so the finding names which answer to rewrite. This is the artifact that is actually MARKED — the three CV judges are routed away from a structured application by design — and it had no reader for its prose at all |
| Apply completion | scripts/check_apply.py | a missing receipt; a receipt that does not match its own receipt_hash, i.e. hand-written or edited rather than produced by a gate (RECEIPT_UNVERIFIED); a gate that only ran its --record setup (NOT_VERIFIED); ANY gate left failing, named or not; an unclassified stop; a missing brief. RECEIPT_UNVERIFIED and MODE_FILE_MISSING are checked by every composer — apply, assess, discover and interview — not just this one |
| Mock interview | scripts/check_mock.py | an invented tag or band; a tag with no quote, or a quote that is not in the transcript; a pass emitting the other pass's tags; a scraped question with no id, no date, or the wrong country; an answer-bank entry with no source; a collapsed claim with no walk-back; an unsourced fact neither promoted nor walked back |
| Evidence blocks | scripts/evidence_blocks.py | the posting and CV cut into addressable JD-nnn / CV-nnn; the only chunker |
| Evidence refs | scripts/check_evidence_refs.py | refs that resolve to no block; block ids left in reader-facing prose |
| Prediction lint | scripts/lint_no_prediction.py | percentages (incl. fullwidth % and 「百分之七十」), n/m scores, scores worn as a NOUN rather than a symbol (匹配度 85 分, overall score: 85, rated 8.5, 4.5 stars, confidence: 0.85, grade: B+, Bewertung 8, 점수 85점), and prediction vocabulary in every language it writes in — EN + ZH (「成功率」「入围率」「七成」…) plus DE/NL/FR/ES/IT/JA/KO (Chancen, kans, probabilité, 可能性が高い, 확률) in the judgement-facing artifacts: fit-assessment.md, shortlist.md, cheatsheet.md, mock/answer-guide.md, mock/assessment-*.md. Numbers the round actually CAPTURED are exempt, but only from a file an adapter_call record names or an intact browser_call snapshot — a raw file nobody journaled is not a capture, it is a note the model wrote itself. Deliberately NOT the CV or letter — there a number is a measured past achievement, which the Quantification ladder above requires, not a claim about the future |
| Contradictions | scripts/consistency.py | verdict vs effort, loose knockouts, gaps with no action, work-authorization conflicts — reports, never repairs |
| Coverage counts | scripts/count_coverage.py | the only count-producing path wherever a fit-assessment.yaml exists — assess mode, and apply mode resumed from one. Apply mode without an assessment has no file for it to read and hand-counts its FIT SNAPSHOT instead; that is a second path, so it must apply the identical rule (below), and it is why the rule is written out in both places rather than trusted to memory. The card renders in zh, en, ja, ko or es using the same counts; pair it with the required text in report-localization.md. Other languages embed an English or Chinese card and required labels. Missing judgements, evidence and disclaimers remain failures in every supported language |
| Market tables | scripts/check_conventions.py | digits/percent in prose, source provenance, protected traits, duplicate ids, expired review_by (CI-hard) |
| Assess | scripts/check_assessment.py | composes the six above and requires their receipts; disclaimer, disqualifier section, the 「那该怎么办」 half, verbatim conventions, stale-review banner; a top-level level_direction or effort the card prints but nobody assessed, and a work-authorization token spelled outside its set |
| Migration losslessness (CI only) | scripts/check_skill_lossless.py | a baseline line that exists nowhere in this tree |
| Adapter classification | scripts/check_opencli_result.py | (wrapper, not a gate) a non-zero exit, a login wall, a platform stop-signal, or an empty identity field |
| First-run environment | scripts/doctor.py | (precondition, not a gate) every capability the skill needs, checked by USING it — the PDF check renders a PDF, because an earlier command -v xelatex check called a working machine broken while tectonic was installed. --install installs the Python packages; the agent prepares required tools and daily-browser CDP via references/agent-setup.md |
| Saving a master | scripts/save_profile.py | (guarded write, not a gate) one master per language; a new language never overwrites another, a repeat language is backed up first, and a profile with no meta.language is refused |
| Delivery | scripts/deliver.py | (hand-off, not a gate) requires a client report.md and verified report PDF; puts requested client documents together in ~/Downloads/<workspace-name>/ (or shared --to folder) and prints the path. Exits 0 or 2, never 1. DELIVER_DEST_UNWRITABLE is macOS TCC refusing ~/Downloads mid-session — say so and offer --to, never leave the artifacts undelivered |
| Read-only | scripts/check_no_write.py | a journaled command whose published access: is write, or whose access cannot be resolved at all |
| Discovery profile snapshot | scripts/snapshot_profile.py | (guarded helper, not a gate) freezes candidate-profile.yaml for one search round and refuses to replace it with a changed master, so later CV edits cannot silently rewrite matching evidence |
| Candidate match | scripts/check_candidate_match.py | a high verdict or default recommendation that lacks a complete row-specific detail, a frozen-profile evidence pointer, a valid requirement mapping, or its localized reader-facing summary; a card promoted without detail; a core gap hidden by optional preferences; a stale ordering or more detail mappings than brief.max_match_reviews permits |
| Shortlist | scripts/check_shortlist.py | a row whose source_id or raw_text is in no raw capture, whose id is not <site>-<source_id>, or whose direct posting URL is empty, malformed, or not returned by its source; a candidate missing that clickable URL in shortlist.md; a duplicated or over-counted source report; "no results" with no adapter that exited 0; a missing disclosure block or the wrong provisional stamp for card-only versus detail-reviewed output; a detail fetch outside the top three; an uncapped brief, or a run that exceeded the caps the brief declares; a posting URL rendered in shortlist.md that is in no shortlist.yaml row; a row whose company or salary contradicts its own capture; a missing or stale mode entry. Warns (does not fail) when a row's location names a country outside brief.markets |
No mode may claim success while journal.jsonl lacks a receipt for its gates. A skipped script produces no output, and no output is exactly what a clean run looks like. scripts/check_skill_lossless.py is the one exception and is marked as such: it is a repo-level CI check with no workspace and no receipt, so requiring one would be requiring evidence that cannot exist.
Apply the fixed source-to-tool routes in supplementary-sources.md: WeChat (Sogou WeChat), Xiaohongshu, Douyin and Toutiao use OpenCLI. Do not reconsider that tool choice. AnySearch search uses its HTTP API.
Begin every discovery round with OpenCLI's offline CDP routing and read-adapter
capability checks in scripts/doctor.py. Use OpenCLI only after its actual
website adapter is verified to use CDP. Never use browser extensions, even when
already installed or reported connected. A generic OpenCLI health result is not
proof of CDP routing. Only when the checks diagnose a missing CLI, disconnected
CDP connection, or unsupported CDP extraction, follow the one-way browser
fallback with an available built-in CDP route.
If that fallback is unavailable too, disclose the gap; do not switch back to
OpenCLI for the same round. Keep browser evidence and gate receipts; a site
refusal is not a fallback reason and stops reads across both tools. Neither
backend submits applications.
Both OpenCLI and its fallback use the customer's daily browser unless they
explicitly request a separate one; follow daily-browser routing.
Use network recovery for a generic
transport failure that has not yet established a fallback reason.
| site | search | detail | login state (2026-08-09) | identity field |
|---|---|---|---|---|
51job | opencli 51job search "<kw>" --area <city> --page 1 --limit 20 --window background -f json | opencli 51job detail <jobId> | no auth adapter | title |
indeed | opencli indeed search "<kw>" --location "<loc>" --fromage 7 --start 0 --limit 15 --window background -f json | opencli indeed job <id> | no auth adapter | title — measured EMPTY, recover via detail. US site only, see below |
linkedin | opencli linkedin search "<kw>" --location "<loc>" --date-posted week --start 0 --limit 10 --window background -f json | opencli linkedin job-detail <job-url> | logged in (cookie session) | title |
boss | opencli boss search "<kw>" --city <城市> --page 1 --limit 15 --window background -f json | opencli boss detail <security_id> | logged in (cookie session) | name, not title |
indeed serves the US site, and --location is resolved against a US
gazetteer. A non-US place name does not fail — it silently returns US rows:
--location "London" came back with every row in Columbus, Ohio (London, OH is
25 miles away), exit 0, classification: ok, and --location "Manchester, United Kingdom" returned California and New York (measured 2026-08-16; the adapter's own
opencli indeed --help describes it as "rendered DOM via browser session, US
site"). So for a uk/nl/de market, prefer linkedin and record that indeed
was skipped and why — and if you use it anyway, read every returned location
before it becomes a row. This is a third shape of risk-register row 2: not
found-nothing and not all-adapters-failed, but adapter-succeeded-and-searched-
the-wrong-country, where every receipt is green and the honest conclusion
("there are no London backend roles") is the wrong one. check_shortlist.py
answers only the row half of this, as the warning WARN_ROW_OUTSIDE_BRIEF_MARKET;
nothing can report the empty half, which is why it is written here instead.
Always -f json. Always --window background.
And always branch on the exit code before you read stdout:
a login wall gives exit 1, EMPTY stdout and a YAML error body on stderr even under
-f json, so JSON.parse(stdout || '[]') turns a 403 into a zero-result success.
Never run a command whose published access: is write —
check_no_write.py reads that field out of the tool itself.
A platform limit is any refusal the platform itself put up: a risk-control or
captcha body, a rate-limit, or a refusal on a site opencli auth status says you
are logged into. When one appears, stop automatic reads and offer the user a
recovery hand-off before ending the search. Follow
the pause-and-resume workflow: explain the actual
login, verification or rate-limit obstacle and wait for explicit confirmation.
“Done, continue” after that prompt requests one new bounded round with the same
source/backend and a link to the old workspace; never erase the stopped journal.
While paused, preserve already retrieved rows and offer partial results. The
following rules govern the stopped round; use its degraded output when recovery
is declined/unavailable, not as a silent substitute for waiting:
--limit, a different city or a
fresh --window is still a retry.rows:,
so it cannot claim a posting exists.modes/discover.md gives the block in both.The per-platform trigger strings are data and live in
references/risk-control-signals.yaml; this rule is not data and does not live
in a reference file, because the moment it needs to be applied is the moment
nobody is going to go and look it up.
READ references/source-policy.md before the first live retrieval of any run
— before the first opencli adapter call — and again before agreeing to page
further, to fetch more detail pages, or to work while the user is away. It is the
ONE source standard: what is green, what is yellow-with-caps, and what is never
done whatever the user asks. Its two round caps are enforced rather than
suggested: brief.yaml must carry max_rows_per_round and max_pages_per_site,
and check_shortlist.py fails the run with CAP_MISSING or CAP_ABOVE_CEILING.
READ references/discovery-sources.md when you are in discover mode and
about to call an adapter other than the four in the table above (upwork,
nowcoder, 1point3acres, maimai, or any site added later). It carries that
adapter's flags, its measured login state, its identity field and its detail
command — and shortlist.yaml's sources: entry cannot be filled in without
them, so check_shortlist.py fails the run with SOURCE_REPORT_MISSING if you
skipped it.
A mock interview may help the candidate FIND, ORDER, and COMPRESS a true story they already lived. It may not help them ACQUIRE one. The test is where the fact came from:
interview-brief.md, or to
something the candidate told you earlier in this session — helping them say it better is
preparation.Four hard rules.
Never write an answer for the candidate. You may name what is missing ("your answer never said what changed as a result"). You may not supply the missing part. Naming a gap is feedback; filling it is ghostwriting a lie.
A leading question is a fabrication vector. "So you'd say you owned the migration?" hands the candidate an over-claim they will repeat in the real room and will not be able to defend. Ask "who owned the migration?" — open, and let the answer be whatever it is. This applies to your interview questions too: never build a question on a premise the CV does not support ("when you led that team of twelve").
An undefendable claim is a CV bug, not a story to drill. If the candidate cannot
truthfully support a CV claim under ONE follow-up, that is a tailoring error. Log it to
the walk-back list and change the CV. This is references/interview-prep.md's over-reach
rule — the mock interview is the stage where it actually fires, because the three CV
judges only read the page and the page does not stammer.
Never rehearse a gap into a non-gap. For an HONEST-GAP the only preparation is the truthful framing already chosen in the tailoring plan. Do not produce a smoother version that implies experience the candidate lacks. "I haven't done X" must survive rehearsal intact; only the sentence around it may improve.
The tripwire. Any figure, tool, employer, title, or scope that appears in the
candidate's answer and is in NEITHER the profile, NOR interview-brief.md, NOR an earlier
answer this session, is tagged UNSOURCED-FACT (or OVER-CLAIM if it exceeds a recorded
source fact) and the candidate is asked where it came from BEFORE it may enter the answer
bank. Usually the answer is "it's true, it's just not on my CV" — that is a real finding and
it should probably go on the CV. Sometimes it is drift under pressure. Either way it gets
resolved, never silently kept: an answer-bank entry containing an unsourced fact is worse
than no answer bank, because the candidate will say it out loud in the real interview
believing you vetted it.
posting.yaml and cv.md — the first assessment
pass does not see interview-brief.md or claims.yaml, so it cannot credit the
candidate for a source fact they never said out loud. The second pass sees the transcript,
claims.yaml and interview-brief.md but not the rubric, and emits only
UNSOURCED-FACT / OVER-CLAIM / CONTRADICTED. Feeding the two passes different inputs
is what turns the honesty tripwire from an intention into a mechanism.not_present / asserted / instanced / held_under_probe, plus
the non-band flag contradicted. They are unnumbered so they cannot be averaged, and
held_under_probe is the ceiling on purpose: a higher band would require knowing what
this employer expects at this level, and this skill does not. Drafting or revising a client report? Read references/report-writing.md,
apply its readability revision, preserve every grouped posting's city and
link, and inspect the final pages, navigation and approved typography.
On a site refusal, follow references/user-recovery.md: explain the reason,
preserve partial results, wait for explicit user confirmation before a new
linked round; do not clear the stopped journal.
Daily-browser CDP and parallel sources: follow references/daily-browser.md;
verify the selected profile, isolate tabs and share site budgets.
Browser capture: read references/browser-fallback.md and record each
actual snapshot with scripts/record_browser_capture.py before another read.
Use browser_page evidence and stop across tools after a site refusal.
Read-when:
references/layout-review.md and compare every rendered page with the supplied template and the user's later changes.references/network-recovery.md: classify first,
use bounded retries, and inspect relevant VPN/split-routing evidence.references/opencli-compat.md;
scripts/opencli_compat.py checks optional, reversible local patches.
Browser fallback does not require patching; a site refusal still stops reads.AskUserQuestion? Read references/portability.md.references/role-families.md.references/candidate-situations.md.references/structured-applications.md — it REPLACES the CV judge loop.references/motivation-letter.md (skip gate first).references/rirekisho.md.references/cv-craft.md.references/word-resume-layout.md and inspect the exported pages.references/gap-analysis.md.references/job-posting-extraction.md.references/interview-prep.md.modes/apply.md, loaded on entry, not on demand.modes/interview.md, loaded on entry, not on demand — and
references/interview-shapes.md in full before the first question.modes/discover.md, loaded on entry, not on demand.references/candidate-matching.md,
then run scripts/snapshot_profile.py once to freeze
candidate-profile.yaml before mapping any detail.references/source-policy.md.references/discovery-sources.md.references/risk-control-signals.yaml
carries the stop-signal patterns scripts/check_opencli_result.py matches.Dispatched:
agents/ats-screener.md, agents/recruiter-screener.md and
agents/hiring-manager.md were each pasted IN FULL into their own judge.agents/mock-assessor-transcript.md and agents/mock-assessor-provenance.md were
each pasted IN FULL into their own assessor, with different input packs.Ran, with a receipt in journal.jsonl — scripts/check_apply.py requires each of these
unconditionally:
scripts/check_personal_data.pyscripts/check_claims.py — the VERIFYING run. The --record run at mode entry
fingerprints the master and checks nothing; its receipt says baseline_recorded
and check_apply reports it as NOT_VERIFIED, not as a pass.scripts/check_render_freshness.py — recorded before dispatch AND verified after.
Same rule: the dispatch record is baseline_recorded, the verifying run is the
one that counts, and re-recording for the next round does not carry the last one.scripts/parse_verdicts.py — its receipt reports on the PARSE. A cleanly parsed
round is recorded whatever the three judges said; fail means a judge returned
no usable VERDICT: line and must be re-dispatched.scripts/lint_cv.pyRequired too, but only when the artifact they read is on disk — check_apply keys each
one on the file, so "it did not apply" is never guesswork:
scripts/check_letter.py (when letter.yaml exists)scripts/check_layout.py (when cv.docx or cv.pdf exists; inspect all pages against the supplied template first)scripts/check_pages.py (when cv.pdf AND tailored-profile.yaml exist — both
are its inputs, and a could_not_run receipt does not satisfy it)scripts/check_word_limits.py (when supporting-statement.md exists, or
posting.yaml says application_type: structured and the statement is missing)Ran, with a receipt — but scripts/check_apply.py does NOT require these, so skipping
one is silent and only this line reports it:
scripts/check_apply.py — the composer itself; it cannot require its own receipt.scripts/check_mock.py (once per mock-interview round — interview mode, not apply)check_apply also fails on ANY gate in this workspace's journal whose latest receipt
says fail, named on the lists above or not — a receipt from another mode excepted.
Enumerating gates does not keep up with the gates: check_pages and check_word_limits
were both off the required list, so either could run, print CV_TOO_LONG / OVER_LIMIT,
and have check_apply write its own pass three lines below it in the same file.
Ran, leaving a mode_entry record rather than a gate receipt:
scripts/enter_mode.py — and its recorded hash still matches modes/apply.md.Ran, leaving nothing in the journal (they render; they do not judge):
scripts/render_cv.py, plus scripts/render_letter.py /
scripts/render_rirekisho.py if applicable.scripts/deliver.py — the LAST step of every mode. A workspace under
~/.claude/job-profiles/ is where the skill works, not where a person
looks, and a path pasted into a chat message is gone once it scrolls.references/supplementary-sources.md — career source selection, WeChat articles and source quality.scripts/pdf_glyphs.py — shared painted-glyph validation used by page checks and delivery.scripts/doctor.py — once per machine, before the first mode. Reports
capabilities by using them; --install covers the Python packages only. Follow
references/agent-setup.md to install required tools and prepare daily-browser CDP.scripts/setup_dependencies.py — prepare the private runtime when required;
scripts/run_tool.py invokes it without depending on global executables.scripts/browser_cdp.mjs — diagnosed fallback reads only; import the capture
through scripts/record_browser_capture.py before the next same-site read.scripts/save_profile.py — every master save goes through it. One CV per
language, and a new language never overwrites another's file.In CI, not in a workspace (no receipt exists for these, by design):
scripts/check_skill_lossless.py — only when this skill's own files changed.Ran, with a receipt in journal.jsonl — the discover gates. scripts/check_apply.py
does not require these; a discover run is not reportable without them:
scripts/check_no_write.py (discover)scripts/check_candidate_match.py (discover; after the profile snapshot and before the shortlist)scripts/check_shortlist.py (discover)Ran, leaving an adapter_call record rather than a gate receipt:
scripts/check_opencli_result.py — once per adapter invocation. It is a
wrapper, not a gate: it exits 0 (classified) or 2 (could not classify), never 1,
so there is no receipt to look for and no pass/fail to read into the exit code.Every line above says what evidence it leaves, and the headings differ for a reason: a checklist that promises a receipt where none can exist teaches its reader that one of its lines is decorative, and the reader cannot tell which one.
In assess mode:
modes/assess.md, entered with scripts/enter_mode.py.references/report-localization.md for native headings, counts and disclosures.references/market-conventions/README.md is the
rule for what may be in one; the tables are references/market-conventions/cn.yaml,
references/market-conventions/nl.yaml, references/market-conventions/de.yaml,
references/market-conventions/uk.yaml, references/market-conventions/us.yaml.scripts/evidence_blocks.py, scripts/count_coverage.py,
scripts/consistency.py, scripts/check_evidence_refs.py,
scripts/lint_no_prediction.py and scripts/check_assessment.py, and quoted
check_assessment's receipt? scripts/check_conventions.py runs in CI over all five
tables.Told the user:
deliver.py printed — in one ~/Downloads/<workspace-name>/ folder, including the report PDF.
That is the one the user can actually open; the workspace path is for an audit.modes/discover.md carries both spellings of all seven
literals the gate requires. One language per document — Chinese furniture in
an English page passes every gate and still reads as a bug.Client typography: English uses Times New Roman and Chinese uses SimSun (宋体), including names and headings, unless the user explicitly requests otherwise. Verify embedded PDF fonts, not only DOCX settings. Preserve template font sizes and aim for a well-filled page; any added gap before a section heading is at most one blank line. Never invent content or shrink fonts just to fill a page.