job_ops-mcp 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +33 -0
- package/LICENSE +21 -0
- package/README.md +400 -0
- package/config/profile.example.yml +67 -0
- package/cv.example.md +53 -0
- package/dist/cli.js +385 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/core/browser.js +27 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/content_hash.js +11 -0
- package/dist/core/content_hash.js.map +1 -0
- package/dist/core/csv.js +107 -0
- package/dist/core/csv.js.map +1 -0
- package/dist/core/cv_parse.js +201 -0
- package/dist/core/cv_parse.js.map +1 -0
- package/dist/core/html.js +10 -0
- package/dist/core/html.js.map +1 -0
- package/dist/core/jd_normalize.js +99 -0
- package/dist/core/jd_normalize.js.map +1 -0
- package/dist/core/jobs.js +106 -0
- package/dist/core/jobs.js.map +1 -0
- package/dist/core/llm.js +227 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/modes.js +55 -0
- package/dist/core/modes.js.map +1 -0
- package/dist/core/outreach_safety.js +77 -0
- package/dist/core/outreach_safety.js.map +1 -0
- package/dist/core/profile.js +88 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/providers/amazon.js +36 -0
- package/dist/core/providers/amazon.js.map +1 -0
- package/dist/core/providers/ashby.js +31 -0
- package/dist/core/providers/ashby.js.map +1 -0
- package/dist/core/providers/google.js +46 -0
- package/dist/core/providers/google.js.map +1 -0
- package/dist/core/providers/greenhouse.js +55 -0
- package/dist/core/providers/greenhouse.js.map +1 -0
- package/dist/core/providers/http.js +36 -0
- package/dist/core/providers/http.js.map +1 -0
- package/dist/core/providers/index.js +53 -0
- package/dist/core/providers/index.js.map +1 -0
- package/dist/core/providers/lever.js +32 -0
- package/dist/core/providers/lever.js.map +1 -0
- package/dist/core/providers/playwright_generic.js +53 -0
- package/dist/core/providers/playwright_generic.js.map +1 -0
- package/dist/core/providers/types.js +2 -0
- package/dist/core/providers/types.js.map +1 -0
- package/dist/core/providers/workday.js +44 -0
- package/dist/core/providers/workday.js.map +1 -0
- package/dist/core/render.js +253 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/reports.js +257 -0
- package/dist/core/reports.js.map +1 -0
- package/dist/core/resources.js +40 -0
- package/dist/core/resources.js.map +1 -0
- package/dist/core/scan_engine.js +164 -0
- package/dist/core/scan_engine.js.map +1 -0
- package/dist/core/scheduler.js +117 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/db.js +60 -0
- package/dist/db.js.map +1 -0
- package/dist/http/app.js +35 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/dashboard.js +131 -0
- package/dist/http/dashboard.js.map +1 -0
- package/dist/mcp/define.js +35 -0
- package/dist/mcp/define.js.map +1 -0
- package/dist/mcp/server.js +103 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/apply_prefill.js +167 -0
- package/dist/mcp/tools/apply_prefill.js.map +1 -0
- package/dist/mcp/tools/batch_evaluate.js +143 -0
- package/dist/mcp/tools/batch_evaluate.js.map +1 -0
- package/dist/mcp/tools/evaluate_job.js +181 -0
- package/dist/mcp/tools/evaluate_job.js.map +1 -0
- package/dist/mcp/tools/generate_materials.js +126 -0
- package/dist/mcp/tools/generate_materials.js.map +1 -0
- package/dist/mcp/tools/get_report.js +24 -0
- package/dist/mcp/tools/get_report.js.map +1 -0
- package/dist/mcp/tools/ops.js +321 -0
- package/dist/mcp/tools/ops.js.map +1 -0
- package/dist/mcp/tools/outreach.js +481 -0
- package/dist/mcp/tools/outreach.js.map +1 -0
- package/dist/mcp/tools/render_pdf.js +27 -0
- package/dist/mcp/tools/render_pdf.js.map +1 -0
- package/dist/mcp/tools/scan_portals.js +35 -0
- package/dist/mcp/tools/scan_portals.js.map +1 -0
- package/dist/mcp/tools/scheduler.js +32 -0
- package/dist/mcp/tools/scheduler.js.map +1 -0
- package/dist/mcp/tools/stories.js +172 -0
- package/dist/mcp/tools/stories.js.map +1 -0
- package/dist/mcp/tools/tracker.js +183 -0
- package/dist/mcp/tools/tracker.js.map +1 -0
- package/dist/mcp/tools/visa.js +219 -0
- package/dist/mcp/tools/visa.js.map +1 -0
- package/dist/migrations/001_initial.sql +505 -0
- package/dist/migrations/002_llm_and_digest.sql +42 -0
- package/dist/server.js +55 -0
- package/dist/server.js.map +1 -0
- package/fonts/dm-sans-latin-ext.woff2 +0 -0
- package/fonts/dm-sans-latin.woff2 +0 -0
- package/fonts/space-grotesk-latin-ext.woff2 +0 -0
- package/fonts/space-grotesk-latin.woff2 +0 -0
- package/modes/career_packet.md +91 -0
- package/modes/negotiation_playbook.md +64 -0
- package/modes/outreach_tone.md +80 -0
- package/modes/report_format.md +83 -0
- package/modes/rubric.md +119 -0
- package/modes/tailoring_rules.md +102 -0
- package/package.json +67 -0
- package/portals.example.yml +95 -0
- package/templates/cover-template.html +64 -0
- package/templates/cv-template.html +421 -0
- package/templates/cv-template.tex +123 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Career Packet
|
|
2
|
+
|
|
3
|
+
This file is the **active career packet** — the superset of every claim the candidate is
|
|
4
|
+
allowed to make. `generate_materials(job_id)` picks subsets from this per JD; it never
|
|
5
|
+
invents claims outside this set.
|
|
6
|
+
|
|
7
|
+
When you run `npx job_ops-mcp init` the server seeds this from your `cv.md` +
|
|
8
|
+
`config/profile.yml` and stores versioned copies in the `career_packet` table. Re-edit
|
|
9
|
+
`cv.md`, then call the `update_career_packet` MCP tool to bump a new version.
|
|
10
|
+
|
|
11
|
+
This file is also exposed as the `mcp-jsa://career_packet/active` MCP resource so the
|
|
12
|
+
chat can reason against it directly without round-tripping through the DB.
|
|
13
|
+
|
|
14
|
+
> **Note:** the headings below are a **template**. After `init` the server replaces
|
|
15
|
+
> Section 1 (identity) from `config/profile.yml` and leaves the rest of the file as
|
|
16
|
+
> editable scaffold. Replace `<TODO>` markers with bullets pulled from your own CV — keep
|
|
17
|
+
> them concrete (verbs + metrics) and never list anything you can't defend in an interview.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. Identity
|
|
22
|
+
|
|
23
|
+
(seeded from `config/profile.yml` → `candidate` block on server start)
|
|
24
|
+
|
|
25
|
+
## 2. Tagline alternatives
|
|
26
|
+
|
|
27
|
+
The job rater picks ONE based on detected `role_category`. Write 4–6 variants — one per
|
|
28
|
+
role archetype you target. Example shape:
|
|
29
|
+
|
|
30
|
+
- **A. Default generalist** — "<one-line positioning that works across all your target roles>"
|
|
31
|
+
- **B. ML / Applied AI Engineer** — "<role-specific tagline emphasizing your AI/ML credibility marker>"
|
|
32
|
+
- **C. Forward Deployed / Solutions** — "<tagline emphasizing customer-facing delivery>"
|
|
33
|
+
- **D. Builder PM / Technical PM** — "<tagline emphasizing PM scope + technical depth>"
|
|
34
|
+
- **E. Data / Analytics Engineer** — "<tagline emphasizing data/infra ownership>"
|
|
35
|
+
- **F. Generalist SWE / lean teams** — "<tagline emphasizing range and shipping speed>"
|
|
36
|
+
|
|
37
|
+
## 3. Most recent role — bullet bank (5–8 to pick from)
|
|
38
|
+
|
|
39
|
+
Source: `cv.md` work-experience section. One sentence per bullet, action-verb start,
|
|
40
|
+
~15–25 words, real metrics from your CV.
|
|
41
|
+
|
|
42
|
+
- <TODO bullet 1 — what you owned, the metric you moved>
|
|
43
|
+
- <TODO bullet 2>
|
|
44
|
+
- <TODO bullet 3>
|
|
45
|
+
- <TODO bullet 4>
|
|
46
|
+
- <TODO bullet 5>
|
|
47
|
+
|
|
48
|
+
## 4. Previous role — bullet bank (3–5 to pick from)
|
|
49
|
+
|
|
50
|
+
- <TODO bullet 1>
|
|
51
|
+
- <TODO bullet 2>
|
|
52
|
+
- <TODO bullet 3>
|
|
53
|
+
|
|
54
|
+
## 5. Earlier role(s) — bullet bank (2–4 to pick from)
|
|
55
|
+
|
|
56
|
+
- <TODO bullet 1>
|
|
57
|
+
- <TODO bullet 2>
|
|
58
|
+
|
|
59
|
+
> If you have more than three jobs worth highlighting, add new sections (`## 6. ...`) in
|
|
60
|
+
> the same shape. `generate_materials` will pick subsets per JD; only the bullets in this
|
|
61
|
+
> packet are eligible for the tailored resume.
|
|
62
|
+
|
|
63
|
+
## 6. Projects bank (pick 2–3 per resume)
|
|
64
|
+
|
|
65
|
+
- **<Project name>** — <one-sentence description with the credibility marker and stack>.
|
|
66
|
+
Link or GitHub if public.
|
|
67
|
+
- **<Project name>** — <description>.
|
|
68
|
+
- **<Project name>** — <description>.
|
|
69
|
+
|
|
70
|
+
## 7. Skills bank (categorized — reorder per JD)
|
|
71
|
+
|
|
72
|
+
- **AI / LLM Systems:** <list the tools / frameworks / paradigms you can defend>
|
|
73
|
+
- **Data & Analytics Engineering:** <SQL flavours, warehouses, ETL, libs>
|
|
74
|
+
- **Infrastructure & DevOps:** <containers, clouds, networks>
|
|
75
|
+
- **Product:** <PRDs, frameworks, processes>
|
|
76
|
+
- **Web & Tools:** <stacks, no-code, design>
|
|
77
|
+
- **Languages:** <primary first>
|
|
78
|
+
|
|
79
|
+
## 8. Education
|
|
80
|
+
|
|
81
|
+
- **<Degree>** — <Institution> (<year range>). <optional 1-line context>
|
|
82
|
+
|
|
83
|
+
## 9. Hard rules
|
|
84
|
+
|
|
85
|
+
- **Never invent metrics.** Only numbers that already appear here or in `cv.md` are usable.
|
|
86
|
+
- **Never surface visa / work-auth** in any resume bullet, cover letter, or outreach DM.
|
|
87
|
+
Visa data is internal scoring only — and the whole visa surface can be disabled via
|
|
88
|
+
`MCP_JSA_VISA_SCORING=false` if it doesn't apply to you.
|
|
89
|
+
- **Never use cliché phrases** ("passionate about", "leveraged", "spearheaded",
|
|
90
|
+
"facilitated", "synergies", "robust", "seamless", "cutting-edge", "innovative",
|
|
91
|
+
"results-oriented", "proven track record").
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Negotiation Playbook
|
|
2
|
+
|
|
3
|
+
Loaded as an MCP resource. Used by `negotiation_brief(job_id)` together with the comp
|
|
4
|
+
`enrichment` row for that company.
|
|
5
|
+
|
|
6
|
+
## Frame
|
|
7
|
+
|
|
8
|
+
You are negotiating from a position of evidence, not need. Three pillars:
|
|
9
|
+
|
|
10
|
+
1. **Salary framework** — anchor on total comp (base + sign-on + equity + bonus + benefits),
|
|
11
|
+
not base alone. Levels.fyi / Glassdoor numbers + the offer in hand define the band you
|
|
12
|
+
work within.
|
|
13
|
+
2. **Geographic-discount pushback** — refuse silent geo-adjusted offers when the work is
|
|
14
|
+
identical to a higher-band geo. Ask explicitly: "is the band the same for <your city>
|
|
15
|
+
remote as it is for the SF office?" If different, ask what specifically changes about
|
|
16
|
+
the role.
|
|
17
|
+
3. **Competing-offer leverage** — never lie about an offer that doesn't exist. Real offers
|
|
18
|
+
in hand, or genuine recruiter conversations with named companies + stages, are the
|
|
19
|
+
only valid forms.
|
|
20
|
+
|
|
21
|
+
## Scripts (adapt — do not paste verbatim)
|
|
22
|
+
|
|
23
|
+
### Initial response to first offer
|
|
24
|
+
> "Thanks — really excited about the role. Before I commit, can we walk through total comp
|
|
25
|
+
> together so I understand the full picture (base, sign-on, equity vest, refresher policy,
|
|
26
|
+
> bonus target, benefits)? Once I have that I can give a clean answer."
|
|
27
|
+
|
|
28
|
+
### Counter — base
|
|
29
|
+
> "Based on Levels.fyi for [role][level] at companies in [comparable group], the band I'm
|
|
30
|
+
> seeing is [X–Y]. Given my <strongest differentiator from career_packet>, I'd land at
|
|
31
|
+
> [Y]. Can you check if there's room there?"
|
|
32
|
+
|
|
33
|
+
### Counter — equity
|
|
34
|
+
> "Can you help me understand the equity grant — preferred-price strike, last 409a, refresh
|
|
35
|
+
> schedule? At early stage I weight equity heavily, so the actual delta in [grant value]
|
|
36
|
+
> vs [target] matters more to me than base."
|
|
37
|
+
|
|
38
|
+
### Pushback — geographic discount
|
|
39
|
+
> "I noticed the band looks different by location. Help me understand: is the *role* the
|
|
40
|
+
> same in <your city> as it is in SF? Same scope, same expectations? If yes, I'd want to be
|
|
41
|
+
> in the same band — comp tied to the role rather than the zip code."
|
|
42
|
+
|
|
43
|
+
### When they ask for a number first
|
|
44
|
+
> "I'd rather hear yours — you have more data on the band than I do. I can share what I'm
|
|
45
|
+
> seeing in market data once I see the structure of the offer."
|
|
46
|
+
|
|
47
|
+
## Knobs to push beyond base
|
|
48
|
+
|
|
49
|
+
- Sign-on bonus (one-time, easiest yes)
|
|
50
|
+
- Equity refresh schedule (annual refresh > grant size for long retention)
|
|
51
|
+
- Title (one level up costs the company nothing if the actual scope matches)
|
|
52
|
+
- Remote allowance / coworking stipend
|
|
53
|
+
- Start date (gives time to wrap commitments)
|
|
54
|
+
- Review cadence (6-month review with clear promotion criteria)
|
|
55
|
+
- Relocation if you'll move within first year
|
|
56
|
+
|
|
57
|
+
## Hard rules
|
|
58
|
+
|
|
59
|
+
- **Never accept verbally same-day.** Always ask for the offer in writing and a few days.
|
|
60
|
+
- **Never lie about competing offers.** If you don't have one, don't claim one.
|
|
61
|
+
- **Visa / OPT stays out of negotiation conversation** until the company has *named the
|
|
62
|
+
comp* in writing. Then it's standard FTE onboarding — treat as logistics, not leverage.
|
|
63
|
+
- **Don't negotiate against yourself.** If they ask "what would it take," give one number
|
|
64
|
+
and let them respond — do not lower preemptively.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Outreach Tone
|
|
2
|
+
|
|
3
|
+
Distilled from JSA study guide §4.4 (Warm Outreach) + §4.5 (Founder Outreach).
|
|
4
|
+
|
|
5
|
+
## Hard rules — apply to every outreach draft
|
|
6
|
+
|
|
7
|
+
- **No emojis. No exclamation marks.** No "I hope this finds you well." No "I'd love to
|
|
8
|
+
pick your brain."
|
|
9
|
+
- **No mention** of work authorization, EAD, OPT, visa, sponsorship, or anything legal.
|
|
10
|
+
The play is relationship now, action later.
|
|
11
|
+
- **Never ask "refer me"** or "can you put my resume in front of someone."
|
|
12
|
+
- **Soft close.** End with "no worries if not a fit" (warm) or "no worries if too busy /
|
|
13
|
+
genuinely curious / happy to be ignored" (founder).
|
|
14
|
+
- Lowercase casual fine. Native tech English. Short sentences, action verbs.
|
|
15
|
+
|
|
16
|
+
## Warm intro DM (existing LinkedIn connection at the company)
|
|
17
|
+
|
|
18
|
+
| Constraint | Value |
|
|
19
|
+
|----------------|--------------------------------------------------------------|
|
|
20
|
+
| Character cap | **< 600 chars total (hard limit)** |
|
|
21
|
+
| Lead | ONE specific thing about *their* work, company, or role they hold — NOT a generic compliment |
|
|
22
|
+
| Ask | Make it SMALL: a 15-minute chat, a curious question about the team, or how they got into [role] |
|
|
23
|
+
| Persona | Peer / builder, NOT job seeker — drop one tiny credibility marker from your career_packet (a side project, a shipped product, a public artifact — keep it under 10 words) |
|
|
24
|
+
| Close | "no worries if not a fit" |
|
|
25
|
+
|
|
26
|
+
**Variants by connection type:**
|
|
27
|
+
|
|
28
|
+
- **Engineering peer** → curious technical question about the team / stack
|
|
29
|
+
- **Recruiter** → slightly more direct ("curious if there is still room in [role]") but
|
|
30
|
+
still no "refer me"
|
|
31
|
+
- **Leadership** → one thoughtful question about how they're thinking about
|
|
32
|
+
[team/space] — make them want to reply
|
|
33
|
+
|
|
34
|
+
## Founder DM (peer-to-peer, non-stealth founder/CEO/CTO/c-suite)
|
|
35
|
+
|
|
36
|
+
| Constraint | Value |
|
|
37
|
+
|----------------|--------------------------------------------------------------|
|
|
38
|
+
| Character cap | **< 300 chars total (hard limit — shorter than warm)** |
|
|
39
|
+
| Lead | "saw you're building {resolved_company or company_raw}" + ONE specific, curious, technical question about what they're working on (architecture choice, the wedge, the GTM) |
|
|
40
|
+
| Bridge | Drop ONE adjacent candidate project (1 short phrase): "I've been building X, curious how y'all approach Z" |
|
|
41
|
+
| Persona | Peer with adjacent project — NOT looking for a job |
|
|
42
|
+
| Close | "no worries if too busy" or "genuinely curious / happy to be ignored" |
|
|
43
|
+
|
|
44
|
+
**Adjacent project bank** — populate from `career_packet.md` section 6. Each entry should
|
|
45
|
+
be one short phrase you could drop into a 300-char DM. Example shape:
|
|
46
|
+
|
|
47
|
+
1. **<Project name>** — <one-line credibility marker>. Useful when their company is
|
|
48
|
+
<domain X / Y / Z>.
|
|
49
|
+
2. **<Project name>** — <one-line>. Useful when <domain>.
|
|
50
|
+
3. **<Project name>** — <one-line>. Useful when <domain>.
|
|
51
|
+
|
|
52
|
+
**Forbidden moves:**
|
|
53
|
+
|
|
54
|
+
- NOT a job ask. NO "I'm looking for a job", NO "are you hiring", NO "refer me".
|
|
55
|
+
|
|
56
|
+
## Followup DM (sent-but-unanswered after N days)
|
|
57
|
+
|
|
58
|
+
1–2 line nudge. NO new ask. Tone: "still curious about X — drop me a line if it ever makes
|
|
59
|
+
sense." Soft close again. No guilt.
|
|
60
|
+
|
|
61
|
+
## Reply draft (someone replied — chat now drafts the human's response)
|
|
62
|
+
|
|
63
|
+
Match their energy. If they replied warmly with a question, answer it tightly and add one
|
|
64
|
+
forward-moving piece (link to a project, a specific time window for a chat). If they
|
|
65
|
+
deflected politely, send 1-line thanks + an open door ("happy to come back if anything
|
|
66
|
+
opens up — no pressure either way"). Never beg.
|
|
67
|
+
|
|
68
|
+
## Output contract (chat / api)
|
|
69
|
+
|
|
70
|
+
When drafting, return STRICT JSON:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"message": "the full DM, within the char cap, no emojis",
|
|
75
|
+
"opening_hook": "the specific thing you led with, 1 sentence",
|
|
76
|
+
"primary_ask": "what you actually asked them to do, 1 short phrase",
|
|
77
|
+
"strategy_note": "1-2 sentence note on why this framing should work for this contact",
|
|
78
|
+
"subject_line": "email subject if channel=email, else 1-line placeholder"
|
|
79
|
+
}
|
|
80
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Evaluation Report Format
|
|
2
|
+
|
|
3
|
+
When `evaluate_job(input, mode="chat")` returns the rubric + normalized JD, you (the chat
|
|
4
|
+
client) write the 6-block report below and POST it back. The server persists it to
|
|
5
|
+
`eval_reports`, renders an HTML view, and returns a localhost link.
|
|
6
|
+
|
|
7
|
+
Keep blocks tight — the report is for *you* to act on, not a deliverable.
|
|
8
|
+
|
|
9
|
+
## Block A — Role Summary
|
|
10
|
+
|
|
11
|
+
Single table:
|
|
12
|
+
|
|
13
|
+
| Field | Value |
|
|
14
|
+
|------------------|--------------------------------------------------------|
|
|
15
|
+
| Archetype | one of the 6, or hybrid (e.g. "Agentic / FDE") |
|
|
16
|
+
| Domain | platform / agentic / LLMOps / ML / enterprise |
|
|
17
|
+
| Function | build / consult / manage / deploy |
|
|
18
|
+
| Seniority | intern → principal |
|
|
19
|
+
| Remote | full / hybrid / onsite |
|
|
20
|
+
| Team size | if mentioned |
|
|
21
|
+
| TL;DR | 1 sentence |
|
|
22
|
+
|
|
23
|
+
## Block B — CV Match
|
|
24
|
+
|
|
25
|
+
Table: each JD requirement → exact line(s) in `cv.md`. Then a **Gaps** subsection with one
|
|
26
|
+
row per gap: hard blocker vs nice-to-have, adjacent experience the candidate has, mitigation
|
|
27
|
+
phrase for the cover letter.
|
|
28
|
+
|
|
29
|
+
## Block C — Level Strategy
|
|
30
|
+
|
|
31
|
+
1. **Level detected in the JD** vs **candidate's natural level for that archetype**
|
|
32
|
+
2. **"Sell senior without lying" plan** — concrete phrases, achievements to highlight,
|
|
33
|
+
how to frame your most senior-shaped experience (PRD ownership, team lead, end-to-end
|
|
34
|
+
delivery) as senior-equivalent scope
|
|
35
|
+
3. **"If they downlevel me" plan** — accept if comp is fair, negotiate 6-month review, clear
|
|
36
|
+
promotion criteria
|
|
37
|
+
|
|
38
|
+
## Block D — Comp & Demand
|
|
39
|
+
|
|
40
|
+
Table with market data (Glassdoor / Levels.fyi / Blind / news) + cited URLs. Pull from
|
|
41
|
+
`enrichment` table for that company when `kind = 'comp'` exists. If no data, state that
|
|
42
|
+
rather than inventing numbers.
|
|
43
|
+
|
|
44
|
+
## Block E — Personalization Plan
|
|
45
|
+
|
|
46
|
+
Top 5 changes to CV + top 5 changes to LinkedIn to maximize match. Table:
|
|
47
|
+
|
|
48
|
+
| # | Section | Current | Proposed | Why |
|
|
49
|
+
|---|---------|---------|----------|-----|
|
|
50
|
+
|
|
51
|
+
## Block F — Interview Plan
|
|
52
|
+
|
|
53
|
+
6–10 STAR + Reflection stories mapped to JD requirements. Table:
|
|
54
|
+
|
|
55
|
+
| # | JD requirement | Story title | S | T | A | R | Reflection |
|
|
56
|
+
|---|----------------|-------------|---|---|---|---|------------|
|
|
57
|
+
|
|
58
|
+
Append new stories to `story_bank` via `extract_stories(job_id)`. Over time the bank holds
|
|
59
|
+
5–10 master stories you adapt per interview.
|
|
60
|
+
|
|
61
|
+
Also include:
|
|
62
|
+
- 1 case study (which project to present, framed for this role)
|
|
63
|
+
- Red-flag questions and how to answer them ("why did you sell your company?",
|
|
64
|
+
"do you have direct reports?")
|
|
65
|
+
|
|
66
|
+
## Block G — Posting Legitimacy (optional, career-ops parity)
|
|
67
|
+
|
|
68
|
+
Three tiers: **High Confidence** / **Proceed with Caution** / **Suspicious**. Signals:
|
|
69
|
+
|
|
70
|
+
- Posting age (under 30d good, 30–60 mixed, 60+ concerning — adjusted for role type)
|
|
71
|
+
- Apply button active
|
|
72
|
+
- Tech specificity vs boilerplate ratio
|
|
73
|
+
- Internal contradictions (entry-level title + staff requirements)
|
|
74
|
+
- Recent layoff news
|
|
75
|
+
- Reposting pattern (same role 2+ times in 90 days)
|
|
76
|
+
- Salary transparency (low-reliability signal)
|
|
77
|
+
|
|
78
|
+
Present observations, not accusations. Every signal has legitimate explanations.
|
|
79
|
+
|
|
80
|
+
## Keywords
|
|
81
|
+
|
|
82
|
+
15–20 keywords from the JD for ATS optimization — verbatim phrases the future tailored
|
|
83
|
+
resume should preserve.
|
package/modes/rubric.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Rating Rubric
|
|
2
|
+
|
|
3
|
+
Distilled from career-ops' scoring system + the JSA-style three-dimension formula. Edit
|
|
4
|
+
this file to retune scoring; the MCP server loads it at runtime and serves it as a
|
|
5
|
+
resource to the chat.
|
|
6
|
+
|
|
7
|
+
> **Visa scoring is optional.** Set `MCP_JSA_VISA_SCORING=false` to drop `visa_fit` from
|
|
8
|
+
> the formula and switch to renormalized weights (`resume 0.6 + taste 0.4`). When disabled,
|
|
9
|
+
> the server prepends an override block to the top of this rubric, hides the visa-related
|
|
10
|
+
> tools (`visa_signal`, `import_h1b`, `import_linkedin`), and strips visa columns from
|
|
11
|
+
> tool responses. If sponsorship is irrelevant to you (US citizen, non-US user, etc.),
|
|
12
|
+
> turn it off and use the 2-dimension form.
|
|
13
|
+
|
|
14
|
+
## About the candidate
|
|
15
|
+
|
|
16
|
+
The candidate's identity, experience summary, target roles, and constraints live in
|
|
17
|
+
`config/profile.yml` (seeded by `init`) and the active `career_packet`. **Read those
|
|
18
|
+
before scoring** — every dimension is judged against the candidate's actual background,
|
|
19
|
+
not a hypothetical average applicant.
|
|
20
|
+
|
|
21
|
+
## Role priority order
|
|
22
|
+
|
|
23
|
+
Defined in `config/profile.yml` → `target_roles.archetypes`. Each archetype has a `fit`
|
|
24
|
+
band:
|
|
25
|
+
|
|
26
|
+
- **primary** — dream role; you'd take it tomorrow at the right comp
|
|
27
|
+
- **secondary** — good fit; you'd take it if the company is right
|
|
28
|
+
- **adjacent** — stretch; you'd consider it if the rest is excellent
|
|
29
|
+
|
|
30
|
+
If a job clearly maps to one of the listed archetypes, set `role_category` accordingly:
|
|
31
|
+
`pm | ml_eng | data_eng | analytics_eng | swe | forward_deployed | other`.
|
|
32
|
+
|
|
33
|
+
**Archetype override:** if the user has set `declared_archetype` on a job, use that
|
|
34
|
+
instead of inferring `role_category` — it represents an explicit preference and wins.
|
|
35
|
+
|
|
36
|
+
## Rating dimensions (each 0–100)
|
|
37
|
+
|
|
38
|
+
### `resume_fit` — how well does the candidate's background match the role's stated requirements?
|
|
39
|
+
|
|
40
|
+
- 90+ excellent match
|
|
41
|
+
- 70–89 strong match with some gaps
|
|
42
|
+
- 50–69 stretch but not absurd
|
|
43
|
+
- <50 role wants very different background
|
|
44
|
+
|
|
45
|
+
### `taste_fit` — does this role match what excites the candidate?
|
|
46
|
+
|
|
47
|
+
Compare the JD against `config/profile.yml` → `narrative.likes` and
|
|
48
|
+
`narrative.dislikes`, plus the active career_packet's positioning.
|
|
49
|
+
|
|
50
|
+
- 90+ role + company strongly match preferences
|
|
51
|
+
- 70–89 good match
|
|
52
|
+
- 50–69 neutral
|
|
53
|
+
- <50 conflicts with stated dislikes
|
|
54
|
+
|
|
55
|
+
### `visa_fit` — will this company / role work for someone who needs visa sponsorship?
|
|
56
|
+
|
|
57
|
+
Only applied when `MCP_JSA_VISA_SCORING=true`. Pull from:
|
|
58
|
+
|
|
59
|
+
- The job description itself (mentions of sponsorship, work authorization, citizenship)
|
|
60
|
+
- The company's H1B record via `visa_signal(company)` (requires `import_h1b` to have been
|
|
61
|
+
run with a DOL OFLC CSV)
|
|
62
|
+
- The candidate's situation in `config/profile.yml` → `location.visa_status`
|
|
63
|
+
|
|
64
|
+
Scoring:
|
|
65
|
+
|
|
66
|
+
- 90+ company actively sponsors, role is standard FTE
|
|
67
|
+
- 70–89 company sponsors but role unclear
|
|
68
|
+
- 50–69 unknown sponsorship, small company, no clear signal
|
|
69
|
+
- <50 contract role / US-citizens only / no-sponsor / region-locked away from candidate
|
|
70
|
+
|
|
71
|
+
### `score_total` — weighted
|
|
72
|
+
|
|
73
|
+
When visa scoring is **on**:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
round( 0.5 * resume_fit + 0.3 * taste_fit + 0.2 * visa_fit )
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When visa scoring is **off** (the server enforces this server-side too, regardless of
|
|
80
|
+
what the chat returns):
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
round( 0.6 * resume_fit + 0.4 * taste_fit )
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Tier shorthand: A ≥ 85, B 75–84, C 60–74, D 40–59, F < 40.
|
|
87
|
+
|
|
88
|
+
## Output contract (chat mode)
|
|
89
|
+
|
|
90
|
+
When the chat client returns scores via `evaluate_job` it MUST emit STRICT JSON:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"resume_fit": 0,
|
|
95
|
+
"taste_fit": 0,
|
|
96
|
+
"visa_fit": 0,
|
|
97
|
+
"score_total": 0,
|
|
98
|
+
"reasoning": "2–3 sentences on main fit signal",
|
|
99
|
+
"concerns": "1–2 sentences on biggest concerns, or null",
|
|
100
|
+
"role_category": "pm | ml_eng | data_eng | analytics_eng | swe | forward_deployed | other",
|
|
101
|
+
"seniority": "intern | junior | mid | senior | staff | principal | lead | unclear"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
(When `MCP_JSA_VISA_SCORING=false`, omit `visa_fit` — see the override block the server
|
|
106
|
+
prepends.)
|
|
107
|
+
|
|
108
|
+
If you cannot parse or anything is uncertain, leave `concerns` populated and set
|
|
109
|
+
`role_category: "other"` / `seniority: "unclear"` — never silent zeros.
|
|
110
|
+
|
|
111
|
+
## Hard rules (NEVER violate)
|
|
112
|
+
|
|
113
|
+
1. **Never surface visa / work-auth in any resume, cover letter, or outreach.** Visa data
|
|
114
|
+
is internal scoring (`visa_fit`) and `visa_signal` only.
|
|
115
|
+
2. **Never invent claims** not present in the career_packet / cv.md.
|
|
116
|
+
3. **Human-in-the-loop everywhere** — no tool auto-submits an application or auto-sends a
|
|
117
|
+
DM. `apply_prefill` is preview-only.
|
|
118
|
+
4. **Strict-JSON parsing on the api path.** On parse failure, record a `PARSE_ERROR` in
|
|
119
|
+
`score_detail` — never silently default to zeros.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Tailoring Rules
|
|
2
|
+
|
|
3
|
+
Distilled from career-ops + JSA materials generation. Used by
|
|
4
|
+
`generate_materials(job_id)` to pick bullets/tagline/projects from the active career
|
|
5
|
+
packet per JD.
|
|
6
|
+
|
|
7
|
+
## Input
|
|
8
|
+
|
|
9
|
+
You receive:
|
|
10
|
+
1. The full **career packet** (superset of every claim the candidate is allowed to make)
|
|
11
|
+
2. The **JD** (title, description, requirements)
|
|
12
|
+
3. The detected **role_category** (or `declared_archetype` if the user set one)
|
|
13
|
+
4. Optional **enrichment summaries** (comp, culture, recent_news)
|
|
14
|
+
|
|
15
|
+
## Output contract (STRICT JSON)
|
|
16
|
+
|
|
17
|
+
`experience_bullets` is keyed by a slug per employer / role from your career packet
|
|
18
|
+
(e.g. `current_role`, `previous_role`, or whatever short employer slugs you used in
|
|
19
|
+
your own packet). Use the same keys your packet uses; the renderer reads them by name.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"tagline": "from career packet section 2, pick the most appropriate alternative",
|
|
24
|
+
"experience_bullets": {
|
|
25
|
+
"<employer_slug_a>": ["\\resumeItem-wrapped bullet", "...5-8 bullets..."],
|
|
26
|
+
"<employer_slug_b>": ["...3-5 bullets..."],
|
|
27
|
+
"<employer_slug_c>": ["...2-4 bullets..."]
|
|
28
|
+
},
|
|
29
|
+
"projects_section": "full LaTeX string with 2-3 project headings",
|
|
30
|
+
"skills_section": "LaTeX string with reordered \\item categories",
|
|
31
|
+
"cover_letter_body": "250-350 words plain prose, NO latex",
|
|
32
|
+
"tailoring_notes": "why you picked these specific bullets/projects"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tailoring decisions by role type
|
|
37
|
+
|
|
38
|
+
Pick the tagline + the experience emphasis + the project order + the skills order based
|
|
39
|
+
on `role_category`. The exact lead bullets depend on what's in *your* packet — the
|
|
40
|
+
guidance below is about *which dimensions to surface*, not specific projects.
|
|
41
|
+
|
|
42
|
+
| `role_category` | Tagline option | Lead with | Skills order |
|
|
43
|
+
|------------------------------|----------------|----------------------------------------------------|----------------------------------------------------|
|
|
44
|
+
| `ml_eng` / Applied AI | B | AI / agent / model work; tool-calling pipelines | AI/LLM → Data Science → Stacks → Product → Infra |
|
|
45
|
+
| `forward_deployed` | C | Customer-facing delivery; shipped enterprise wins | AI/LLM → Product → Stacks → Infra |
|
|
46
|
+
| `pm` | D | Product ownership; PRDs; cross-functional leadership| Product → AI/LLM → Data Science → Infra |
|
|
47
|
+
| `data_eng` / `analytics_eng` | E | Pipeline + warehouse + observability work | Data Science → Infra → AI/LLM → Stacks |
|
|
48
|
+
| `swe` (generalist, lean) | default A or F | Full-stack shipping evidence | Stacks → AI/LLM → Infra → Data Science |
|
|
49
|
+
| `other` | default | Mirror highest-fit role from the table | Product → AI/LLM → Data Science |
|
|
50
|
+
|
|
51
|
+
## Bullet formatting (CRITICAL)
|
|
52
|
+
|
|
53
|
+
- Wrap every bullet exactly: `\resumeItem{...}` (in JSON: `\\resumeItem{...}`)
|
|
54
|
+
- Bold 1–3 things per bullet with `\textbf{...}` — tools, metrics, key claims
|
|
55
|
+
- Escape LaTeX specials inside bullet text: `&` → `\&`, `%` → `\%`, `$` → `\$`,
|
|
56
|
+
`#` → `\#`, `_` → `\_`
|
|
57
|
+
- One sentence, ~15–25 words, action-verb start
|
|
58
|
+
- **Use only metrics that appear in the career packet / cv.md.** DO NOT invent numbers.
|
|
59
|
+
|
|
60
|
+
## Projects section
|
|
61
|
+
|
|
62
|
+
Build a complete LaTeX `\resumeProjectHeading` block. 2–3 projects total, 2–4 bullets each.
|
|
63
|
+
Format:
|
|
64
|
+
|
|
65
|
+
```latex
|
|
66
|
+
\resumeProjectHeading
|
|
67
|
+
{\textbf{<project name>} --- <short description> $|$ \href{<url>}{<display>}}{<years>}
|
|
68
|
+
\resumeItemListStart
|
|
69
|
+
\resumeItem{<bullet emphasising the credibility marker for this project>}
|
|
70
|
+
\resumeItem{...}
|
|
71
|
+
\resumeItemListEnd
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Skills section
|
|
75
|
+
|
|
76
|
+
```latex
|
|
77
|
+
\item \textbf{AI / LLM Systems:} <comma-separated list ordered by JD relevance>
|
|
78
|
+
\item \textbf{Data Science & Engg.:} <list>
|
|
79
|
+
\item \textbf{Product:} <list>
|
|
80
|
+
\item \textbf{Infra & DevOps:} <list>
|
|
81
|
+
\item \textbf{Stacks & Tech:} <list>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Pick which categories to include + their order based on JD relevance.
|
|
85
|
+
|
|
86
|
+
## Cover letter rules
|
|
87
|
+
|
|
88
|
+
- 250–350 words, plain prose, **NO LaTeX commands at all**
|
|
89
|
+
- Open with ONE specific observation about the company / product pulled from the JD
|
|
90
|
+
- Mid: connect 1–2 candidate projects/experiences to the role's needs
|
|
91
|
+
- Close: state interest as a conversation — substantive, not "looking forward"
|
|
92
|
+
- No "I am writing to apply for ..."
|
|
93
|
+
- No exclamation points, no emojis
|
|
94
|
+
- **DO NOT mention work authorization / EAD / visa / sponsorship**
|
|
95
|
+
- Optional: one unexpected detail per cover letter (a side project, a non-obvious angle on
|
|
96
|
+
the role)
|
|
97
|
+
|
|
98
|
+
## Tailoring notes
|
|
99
|
+
|
|
100
|
+
In `tailoring_notes`, explain in 2–3 sentences:
|
|
101
|
+
- Why you picked these specific bullets/projects (which JD signals they match)
|
|
102
|
+
- What you deliberately left out and why
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "job_ops-mcp",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Self-hosted MCP server for the full job-search loop: portal scanning, JD evaluation, tailored resume + cover PDFs, outreach drafting, story bank, negotiation brief — chat-driven, human-in-the-loop.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/mohith-das/job_ops-mcp#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/mohith-das/job_ops-mcp.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/mohith-das/job_ops-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp", "model-context-protocol", "claude", "job-search", "resume",
|
|
18
|
+
"cover-letter", "ats", "career", "agent", "playwright"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"job_ops-mcp": "dist/cli.js",
|
|
22
|
+
"job-ops-mcp": "dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/",
|
|
26
|
+
"modes/",
|
|
27
|
+
"templates/",
|
|
28
|
+
"fonts/",
|
|
29
|
+
"cv.example.md",
|
|
30
|
+
"config/profile.example.yml",
|
|
31
|
+
"portals.example.yml",
|
|
32
|
+
".env.example",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc && npm run copy-assets",
|
|
38
|
+
"copy-assets": "node -e \"const{cpSync,existsSync,mkdirSync}=require('fs');mkdirSync('dist/migrations',{recursive:true});cpSync('src/migrations','dist/migrations',{recursive:true});\"",
|
|
39
|
+
"start": "node dist/cli.js start",
|
|
40
|
+
"init": "node dist/cli.js init",
|
|
41
|
+
"doctor": "node dist/cli.js doctor",
|
|
42
|
+
"connect": "node dist/cli.js connect",
|
|
43
|
+
"dev": "tsx watch src/cli.ts start",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"prepublishOnly": "npm run build",
|
|
46
|
+
"playwright:install": "playwright install chromium"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
50
|
+
"better-sqlite3": "^11.3.0",
|
|
51
|
+
"express": "^4.21.0",
|
|
52
|
+
"js-yaml": "^4.1.0",
|
|
53
|
+
"playwright": "^1.48.0",
|
|
54
|
+
"zod": "^3.23.8"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
58
|
+
"@types/express": "^4.17.21",
|
|
59
|
+
"@types/js-yaml": "^4.0.9",
|
|
60
|
+
"@types/node": "^22.7.4",
|
|
61
|
+
"tsx": "^4.19.1",
|
|
62
|
+
"typescript": "^5.6.2"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=20"
|
|
66
|
+
}
|
|
67
|
+
}
|