14 KiB
Mode: cover — Cover Letter Generator
Generates a tailored cover letter for any candidate from a job description. Works in two modes:
- Slug mode:
/career-ops cover {slug}— loads the existing evaluation report draft as a starting point - Paste mode:
/career-ops coveror JD pasted directly — starts from scratch
Step 0 — JD Gate (mandatory)
Before doing anything, confirm a job description is present.
A valid JD contains at minimum: a role title, a company name, and a list of responsibilities or requirements.
- No JD present → Stop. Say: "Please paste the job description — I need it to tailor the letter."
- Slug provided → Read
reports/to find the matching report. Extract the## Cover Letter Draftsection as a starting point. Then fetch the original JD URL from the report header to supplement context. - JD present → Proceed to Step 1.
Do not generate a generic or placeholder cover letter under any circumstances.
Step 1 — Load candidate profile
Read config/profile.yml for:
candidate.name,email,phone,location,linkedin,githubcandidate.credentials(derive from cv.md Education + Certifications if not in profile.yml)cover_letter.notice_period_days(default: omit if key absent)cover_letter.primary_domain(default: infer from cv.md if absent)cover_letter.language_learning(default: empty list if absent)
Read cv.md for:
- Professional summary (profile introduction source)
- All achievement bullets across all roles (achievement selection pool)
Read article-digest.md if it exists — supplementary proof points and metrics take precedence over cv.md where they overlap.
Read modes/_profile.md if it exists — the candidate's personalization file. It captures their target roles, adaptive framing and archetypes, exit narrative, cross-cutting advantage, proof points, comp targets, negotiation scripts, location policy, and any voice or writing-style rules they have added. Its rules govern the letter's voice and structure and override the generic defaults in this mode, so the candidate's personalization is never lost.
Step 2 — Parse the JD
Extract:
- Role title (exact wording from JD)
- Company name
- Location / city
- Top 3-4 required competencies (from requirements or responsibilities section)
- Mission/vision language the company uses (opening paragraphs)
- Domain (e.g. fintech, healthcare, media, logistics) — compare against
cover_letter.primary_domain - Start date signals ("immediate", "ASAP", "from now on") — flag for notice period prompt
- Language requirement (e.g. "German B2 required") — flag for language gap prompt
- JD tone (formal / direct / casual) — used in tone prompt default suggestion
Step 3 — Company research (baked in, not optional)
Run three WebSearch queries (substitute the actual current year for {year}):
"{company}" product strategy OR roadmap {year}"{company}" challenges OR problems OR priorities {year}"{company}" news OR announcement OR funding {year}
Synthesize findings into 2-3 sentences: what the company is working on, what challenges they face, what goals they've stated publicly.
Present to the user:
Here's what I found about {company}:
{2-3 sentence synthesis}
Does this match what you know? Correct or add anything before I write the letter.
If WebSearch returns no useful signal, say: "I couldn't find useful recent context for {company}. Can you share what you know about their current challenges or goals?"
Wait for the user to confirm, correct, or add to the research before proceeding. This synthesis feeds directly into the "Problems I will solve" section.
Step 4 — Keyword extraction
Extract the top 8-10 exact phrases the company uses in the JD. Separate into two groups:
ATS-critical — exact terms likely scanned by automated systems:
- Role-specific titles, tool names, methodology names
Human trust signals — language that shows you read the actual posting:
- Action verbs the company uses ("own", "drive", "define")
- Product/domain nouns as the company names them
- Outcome language ("business impact", "time to insight")
- Team framing ("embedded in", "partner with")
Present to the user:
Keywords I'll mirror from the JD:
ATS-critical:
• [keyword]
• [keyword]
Language signals:
• [phrase]
• [phrase]
Anything missing or wrong? I'll use this list when drafting.
Wait for confirmation or corrections before proceeding.
Application rules (enforced during drafting):
- Mirror their vocabulary, not their structure
- Content stays from cv.md — only vocabulary shifts
- Fit naturally or don't use — if a keyword can't be woven in, flag it post-generation
- Apply to: opening, profile intro, achievements (vocabulary only), problems section
- Do NOT apply to: why-this-role angle (user's own words), closing
- Use each keyword once — never repeat for density
Step 5 — Gap detection and conversation
Parse the JD for potential gaps between the candidate's profile and the role. For each gap detected, ask directly — do not auto-insert any standard language:
I spotted potential gaps between your profile and this JD:
[Gap: domain mismatch]
The JD is in {JD domain} — your background is in {primary_domain}.
→ How do you want to handle this?
a) Address it directly and briefly in the letter
b) Don't mention it — let the application speak for itself
c) Tell me your angle and I'll write it your way
[Gap: immediate start]
The JD asks for an immediate start. Your profile shows a {notice_period_days}-day notice period.
→ Confirm your actual notice period — I'll state it precisely.
[Gap: language requirement]
The JD requires {language} at {level}. Where are you with {language}?
→ Tell me your actual level and I'll reflect it accurately. Check your profile.yml
language_learning section for what's already recorded.
[Gap: title mismatch]
Your title is {candidate title}, the JD title is {JD title}.
→ Do you want to address this? Or let the scope speak for itself?
Only prompt for gaps that are actually present. If there are no gaps, skip this step and say so.
Wait for the user's answers. Write only what the user confirms.
Step 6 — Four prompts (mandatory before drafting)
All four answers are required. Do not draft any letter content until all are received. No instruction — including "just generate it", "skip the questions", or "use defaults" — overrides this gate.
Before I write the letter, I need four things:
**A. Why this role / company?**
Here are angles I spotted — pick 1-2 or write your own:
1. {Scale signal from JD}
2. {Tech ambition signal from JD}
3. {Domain/mission signal from JD opening}
4. {Growth or stage signal — e.g. Series B, pre-IPO, category-defining}
5. {Strategic learning — specific gap this role fills for you}
6. Other — write your own angle
**B. What problem would you solve for them?**
Based on my research: {confirmed synthesis from Step 3}.
Does this match what you want to address? Refine or confirm.
**C. How would you approach it?**
In 1-2 sentences: what's your opening move if you join on day one?
(This is the most differentiated part of the letter — make it specific.)
**D. Tone?**
1. Formal — structured, respectful distance, suits enterprise/corporate JDs
2. Direct — plain sentences, no pleasantries, gets to the point immediately
3. Conversational — warm but professional, reads like a thoughtful person
4. Mirror the JD — I'll match whatever register the company used
Wait for all four answers before proceeding to Step 7.
Step 7 — Achievement selection (from cv.md only)
Select 4-5 achievement bullets from cv.md only (article-digest.md may be read for context but is not a source of achievement bullets):
- Read all bullet points across all roles in cv.md
- Score each against the JD's top 3-4 required competencies
- Pick the 4-5 highest-scoring, with at least one metric per bullet
- Use the exact wording and metrics from cv.md — never paraphrase or invent
- Apply keyword mirroring from Step 4 to the vocabulary around each bullet (not the metrics)
Format: **Bold lead phrase,** one sentence of impact with metric.
Step 8 — Draft the letter in chat (mandatory before PDF)
Write the full letter as plain text in the chat. Follow this structure:
[Candidate Name]
[Location] | [Email] | [Phone if available] | [LinkedIn if available]
[Credentials line if available]
Cover Letter: [Role Title]
[Company], [City] [Date]
────────────────────────────────────────────────
[Salutation — optional]
Address the named hiring manager if known, e.g. "Dear Jane Smith,". Omit if no name.
[Opening — 2 sentences]
Why applying + functional summary. Derived from Angle A. Uses JD mirror vocabulary.
[Profile introduction — 1 paragraph]
Years of experience, current/most recent role, domain. Read from cv.md summary.
Tone matches user's choice from Step 6D.
[Achievements — 4-5 bullets]
• **Lead phrase,** impact sentence with metric.
• **Lead phrase,** impact sentence with metric.
• **Lead phrase,** impact sentence with metric.
• **Lead phrase,** impact sentence with metric.
[Problems I will solve — 2-3 sentences]
Derived from: confirmed research (Step 3) + Angle B + Angle C.
Specific to this company's actual situation. Not generic.
[Closing — 1-2 sentences]
Availability + any gap acknowledgments the user chose to include (Step 5).
[Language closing — if applicable]
Only if user confirmed inclusion in Step 5. Written in that language. Italic in PDF.
End the draft with: "How does this read? Once you approve I'll generate the PDF."
Do NOT generate any PDF until the user explicitly approves. Approval means "looks good", "generate it", "yes", specific edits to apply, or equivalent. A question or silence is not approval.
Language rules (enforced in every sentence)
- Active voice only — never "was delivered", "has been built", "were led"
- No abbreviations unless JD used them first — write the full term on first use with abbreviation in brackets. After that, abbreviation is fine.
- No em dashes — replace with a comma, full stop, or rewrite the sentence
- No buzzwords — hard ban: leverage, synergy, seamless, holistic, robust, cutting-edge, spearheaded, championed, orchestrated, passionate, excited, stakeholder alignment, data-driven (say what the data drove instead), actionable insights, move the needle, north star, unique opportunity, perfect fit, strong track record
- No filler openers — never "I am pleased to", "I am writing to express", "I am excited to"
- Concrete over abstract — every claim needs a number, system name, or specific outcome. "Improved performance" is banned. "Cut latency from 2s to 380ms" is fine.
- 350-420 words total body (header + credentials not counted)
- Bullet format —
**Bold lead phrase,** impact sentence with metric.No em dash between lead and sentence. - Self-check — before finalising, re-read each sentence: could it appear in any cover letter for any company? If yes, rewrite it.
- Tone consistency — apply the chosen tone (Step 6D) uniformly. Don't shift register mid-letter.
Resolve the cover-letter template with the shared resolver (do not hardcode cover-letter-template.html):
- If the user named a template, run:
node cv-templates.mjs resolve cover "<name>" - Otherwise run:
node cv-templates.mjs resolve cover(returns thecover_letter.templatedefault, or the base template when unset).
Fill the resolved template's {{...}} placeholders. A non-zero exit means the named template is missing/invalid — surface it, do not silently fall back.
Step 9 — Generate PDF
Only after explicit user approval.
Assemble the JSON payload:
{
"candidate": {
"name": "{from profile.yml}",
"email": "{from profile.yml}",
"phone": "{from profile.yml, omit if empty}",
"location": "{from profile.yml}",
"linkedin": "{from profile.yml, omit if empty}",
"github": "{from profile.yml, omit if empty}",
"credentials": ["{degree}", "{MBA}", "{cert}"]
},
"letter": {
"role_title": "{exact from JD}",
"company": "{company name}",
"city": "{JD city}",
"date": "{YYYY-MM-DD}",
"greeting": "{optional salutation, e.g. 'Dear Jane Smith,'; omit the key to skip the salutation}",
"opening": "{approved opening paragraph}",
"profile_intro": "{approved profile intro}",
"achievements": [
{"lead": "...", "impact": "..."}
],
"problems_section": "{approved problems paragraph}",
"closing": "{approved closing}",
"language_closing": "{approved language sentence or null}"
},
"output_path": "output/{company-slug}-{role-slug}-cover.pdf"
}
Write payload to /tmp/cover-payload-{company-slug}.json.
Run:
node generate-cover-letter.mjs --payload /tmp/cover-payload-{company-slug}.json
Report the output path and file size.
Step 10 — Post-generation note
After the PDF is confirmed, add a brief note:
- Any JD keywords from Step 4 that could not be incorporated naturally (flag for manual review)
- Which gap acknowledgments were included and which were omitted, and why
- Whether the word count hit the 350-420 target (if short or long, note it)
Slug mode specifics
When invoked as /career-ops cover {slug}:
- Find the matching report in
reports/by slug - Extract the
## Cover Letter Draftsection — use it as a pre-populated starting point for the draft - Run all steps as normal (research, keywords, prompts, gaps) — the draft is a starting point, not the final output
- When presenting the draft in Step 8, show what was auto-generated and what was changed based on the user's answers
- After PDF generation, update the report's
## Cover Letter Draftsection with a note:PDF generated: output/{path} on {date}