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.
Files changed (116) hide show
  1. package/.env.example +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +400 -0
  4. package/config/profile.example.yml +67 -0
  5. package/cv.example.md +53 -0
  6. package/dist/cli.js +385 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config.js +63 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/browser.js +27 -0
  11. package/dist/core/browser.js.map +1 -0
  12. package/dist/core/content_hash.js +11 -0
  13. package/dist/core/content_hash.js.map +1 -0
  14. package/dist/core/csv.js +107 -0
  15. package/dist/core/csv.js.map +1 -0
  16. package/dist/core/cv_parse.js +201 -0
  17. package/dist/core/cv_parse.js.map +1 -0
  18. package/dist/core/html.js +10 -0
  19. package/dist/core/html.js.map +1 -0
  20. package/dist/core/jd_normalize.js +99 -0
  21. package/dist/core/jd_normalize.js.map +1 -0
  22. package/dist/core/jobs.js +106 -0
  23. package/dist/core/jobs.js.map +1 -0
  24. package/dist/core/llm.js +227 -0
  25. package/dist/core/llm.js.map +1 -0
  26. package/dist/core/modes.js +55 -0
  27. package/dist/core/modes.js.map +1 -0
  28. package/dist/core/outreach_safety.js +77 -0
  29. package/dist/core/outreach_safety.js.map +1 -0
  30. package/dist/core/profile.js +88 -0
  31. package/dist/core/profile.js.map +1 -0
  32. package/dist/core/providers/amazon.js +36 -0
  33. package/dist/core/providers/amazon.js.map +1 -0
  34. package/dist/core/providers/ashby.js +31 -0
  35. package/dist/core/providers/ashby.js.map +1 -0
  36. package/dist/core/providers/google.js +46 -0
  37. package/dist/core/providers/google.js.map +1 -0
  38. package/dist/core/providers/greenhouse.js +55 -0
  39. package/dist/core/providers/greenhouse.js.map +1 -0
  40. package/dist/core/providers/http.js +36 -0
  41. package/dist/core/providers/http.js.map +1 -0
  42. package/dist/core/providers/index.js +53 -0
  43. package/dist/core/providers/index.js.map +1 -0
  44. package/dist/core/providers/lever.js +32 -0
  45. package/dist/core/providers/lever.js.map +1 -0
  46. package/dist/core/providers/playwright_generic.js +53 -0
  47. package/dist/core/providers/playwright_generic.js.map +1 -0
  48. package/dist/core/providers/types.js +2 -0
  49. package/dist/core/providers/types.js.map +1 -0
  50. package/dist/core/providers/workday.js +44 -0
  51. package/dist/core/providers/workday.js.map +1 -0
  52. package/dist/core/render.js +253 -0
  53. package/dist/core/render.js.map +1 -0
  54. package/dist/core/reports.js +257 -0
  55. package/dist/core/reports.js.map +1 -0
  56. package/dist/core/resources.js +40 -0
  57. package/dist/core/resources.js.map +1 -0
  58. package/dist/core/scan_engine.js +164 -0
  59. package/dist/core/scan_engine.js.map +1 -0
  60. package/dist/core/scheduler.js +117 -0
  61. package/dist/core/scheduler.js.map +1 -0
  62. package/dist/db.js +60 -0
  63. package/dist/db.js.map +1 -0
  64. package/dist/http/app.js +35 -0
  65. package/dist/http/app.js.map +1 -0
  66. package/dist/http/dashboard.js +131 -0
  67. package/dist/http/dashboard.js.map +1 -0
  68. package/dist/mcp/define.js +35 -0
  69. package/dist/mcp/define.js.map +1 -0
  70. package/dist/mcp/server.js +103 -0
  71. package/dist/mcp/server.js.map +1 -0
  72. package/dist/mcp/tools/apply_prefill.js +167 -0
  73. package/dist/mcp/tools/apply_prefill.js.map +1 -0
  74. package/dist/mcp/tools/batch_evaluate.js +143 -0
  75. package/dist/mcp/tools/batch_evaluate.js.map +1 -0
  76. package/dist/mcp/tools/evaluate_job.js +181 -0
  77. package/dist/mcp/tools/evaluate_job.js.map +1 -0
  78. package/dist/mcp/tools/generate_materials.js +126 -0
  79. package/dist/mcp/tools/generate_materials.js.map +1 -0
  80. package/dist/mcp/tools/get_report.js +24 -0
  81. package/dist/mcp/tools/get_report.js.map +1 -0
  82. package/dist/mcp/tools/ops.js +321 -0
  83. package/dist/mcp/tools/ops.js.map +1 -0
  84. package/dist/mcp/tools/outreach.js +481 -0
  85. package/dist/mcp/tools/outreach.js.map +1 -0
  86. package/dist/mcp/tools/render_pdf.js +27 -0
  87. package/dist/mcp/tools/render_pdf.js.map +1 -0
  88. package/dist/mcp/tools/scan_portals.js +35 -0
  89. package/dist/mcp/tools/scan_portals.js.map +1 -0
  90. package/dist/mcp/tools/scheduler.js +32 -0
  91. package/dist/mcp/tools/scheduler.js.map +1 -0
  92. package/dist/mcp/tools/stories.js +172 -0
  93. package/dist/mcp/tools/stories.js.map +1 -0
  94. package/dist/mcp/tools/tracker.js +183 -0
  95. package/dist/mcp/tools/tracker.js.map +1 -0
  96. package/dist/mcp/tools/visa.js +219 -0
  97. package/dist/mcp/tools/visa.js.map +1 -0
  98. package/dist/migrations/001_initial.sql +505 -0
  99. package/dist/migrations/002_llm_and_digest.sql +42 -0
  100. package/dist/server.js +55 -0
  101. package/dist/server.js.map +1 -0
  102. package/fonts/dm-sans-latin-ext.woff2 +0 -0
  103. package/fonts/dm-sans-latin.woff2 +0 -0
  104. package/fonts/space-grotesk-latin-ext.woff2 +0 -0
  105. package/fonts/space-grotesk-latin.woff2 +0 -0
  106. package/modes/career_packet.md +91 -0
  107. package/modes/negotiation_playbook.md +64 -0
  108. package/modes/outreach_tone.md +80 -0
  109. package/modes/report_format.md +83 -0
  110. package/modes/rubric.md +119 -0
  111. package/modes/tailoring_rules.md +102 -0
  112. package/package.json +67 -0
  113. package/portals.example.yml +95 -0
  114. package/templates/cover-template.html +64 -0
  115. package/templates/cv-template.html +421 -0
  116. 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.
@@ -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
+ }