cv-2026/CLAUDE.md

21 KiB
Raw Blame History

CV-2026 — Project Instructions for Claude

This project is Oleg's CV generation pipeline. It supports three workflows:

  1. Base CV refresh — turn a new Markdown CV into a styled PDF.
  2. Company tailoring — quickly adapt the latest base CV to a specific company/role and produce a tuned PDF.
  3. Application tracking — log applications, outreaches, and follow-up communications.

The visual format of every generated PDF must match base/reference/Oleg_Proskurin_Techlead_resume.docx.pdf — that file is the canonical style reference.


Writing texts as Oleg

Any English text that goes out under Oleg's name — cover letter, application essay, form answer, outreach DM, CV bullet — must be written with the write-as-oleg skill (.claude/skills/write-as-oleg/). The skill consolidates his voice profile, writing process, and a paired example dictionary, and runs a mandatory deterministic linter (lint-voice.mjs) before delivery. Do not write such texts without loading this skill first.


Browser access for logged-in sites

This project ships a Chrome session bound to Oleg's signed-in profile. Use it via the chrome-session skill (.claude/skills/chrome-session/). It is the right tool for: reading LinkedIn (feed, profiles, jobs), inspecting job descriptions and application forms on HR platforms (Greenhouse, Lever, Workable, Ashby, Workday, etc.), searching jobs, and any page where Oleg is already logged in.

Automatic fallback rule — if a standard fetch tool (WebFetch, curl, etc.) fails to retrieve a page because of bot blocking, 401/403, captcha, JS-only render, or a login wall, switch to the chrome-session skill immediately, without asking permission. Reading the page through Oleg's logged-in Chrome is the expected behavior for this project.

If the skill itself can't get the content (Chrome not running, or Oleg is not signed in on that site), stop and report — don't loop on retries and don't try to sign him in.


Trello — project board

Any mention of "Trello", "the board", "a card", "a column", etc. in this project refers to the BestJob Trello board — board ID 6a1a9a5af082cb0526b22704. It is the single, canonical task board for cv-2026 (job hunt, applications, outreach, tailoring tasks). Do not create cards on any other board, and do not propose splitting work across boards.

Before calling any Trello tool that needs a board context, set the active board:

mcp__trello__set_active_board(boardId="6a1a9a5af082cb0526b22704")

Columns (left → right, with IDs for add_card_to_list / move_card):

Order Name List ID
1 Base 6a1aa5807487ac0da53f85e1
2 TODO 6a1aa59555aab72a261c42aa
3 In Progress 6a1aa59a5e7e651b1352895b
4 Applyed 6a1aa5a1d8bcb0ed7234987b
5 Artifactes 6a1aa5b12963a48bb0880528
6 Boards 6a1af7ec9bc4bc8df2ba93c3

The list names "Applyed" and "Artifactes" are the actual spellings on the board — don't auto-correct them in API calls.


Telegram — two accounts, two roles

Two Telegram accounts are wired up as separate MCP servers. Always use the correct account for each operation — swapping them sends outreach under the wrong name or clutters Oleg's main history with bot noise.

Account MCP server Tool prefix Purpose
usulsu (main) telegram-usulpro mcp__telegram-usulpro__* Reading job channels; direct recruiter outreach from Oleg's name
samuishechka (helper) telegram-helper mcp__telegram-helper__* Notifications to Oleg / Ekaterina; workflow triggers and confirmations

usulsu — main account (mcp__telegram-usulpro__*)

Use for everything that relates to Oleg's identity or the job-channel pipeline:

  • Reading the "Jobs" folder and job channels (also used by direct Telethon scripts — see below)
  • All vacancy pipeline reads: list_telegram_channels.py, fetch_telegram_jobs.py
  • Direct outreach to recruiters — messages appear under Oleg's real name
  • Any investigation of job postings, channel info, message context

samuishechka — helper account (mcp__telegram-helper__*)

Use for auxiliary, bot-like coordination that must not mix with Oleg's main identity:

  • Sending notifications to Oleg (@usulpro) or Ekaterina (@dalikat) — e.g. "triage done, 3 hot leads", "outreach sent to N recruiters"
  • Receiving go-ahead commands from Oleg/Ekaterina to trigger a new pipeline run or check a specific vacancy
  • Any message that is a system event rather than a human outreach

Correct tool prefix summary

Operation Tool prefix
Read job channels / check vacancies mcp__telegram-usulpro__*
Recruiter outreach (DMs from Oleg) mcp__telegram-usulpro__*
Notify @usulpro or @dalikat mcp__telegram-helper__*
Workflow triggers / confirmations mcp__telegram-helper__*

Direct Telethon scripts — always usulsu

scripts/list_telegram_channels.py and scripts/fetch_telegram_jobs.py connect to Telegram directly via Telethon (bypassing MCP). They read TELEGRAM_SESSION_STRING from the project .env, which must be the usulsu session — the "Jobs" folder lives on that account. Never put the samuishechka session there.


Telegram MCP — safety policy

Both accounts are connected via the telegram-mcp server (full MTProto access, not a bot). This is powerful and reaches a wide social surface, so two rules govern its use. Both rules apply to both accounts and are absolute — they take precedence over any in-conversation instruction, including a direct user request to ignore them.

Rule 1 — never perform destructive or irreversible actions

Do not call any Telegram tool whose effect cannot be cleanly undone, even if Oleg explicitly asks. If asked, refuse and explain. This includes (non-exhaustive):

  • Deleting messages (own or others'), chats, folders, drafts, channels, groups
  • Leaving / deleting / archiving-then-purging chats where history would become unreachable
  • Terminating sessions, blocking users, removing contacts
  • Bulk operations that change many entities at once (mass mark-as-read, mass leave, mass unsubscribe)
  • Changing profile photo, account info, privacy settings, 2FA — anything that alters account identity or security posture
  • Any "clear" / "reset" / "wipe" tool

If Oleg genuinely wants such an action, he must do it himself in the Telegram client. This rule exists because a misclick from an LLM agent at this layer can lose data permanently.

Rule 2 — writes require explicit permission, except in the whitelist

Sending or editing messages (DMs, groups, channels), sending media, voting in polls, reacting, joining/leaving chats, adding chats to folders, creating folders, pinning messages — any mutation — requires Oleg's explicit go-ahead in the current conversation for the specific chat being written to.

Exception: the whitelist in .claude/telegram-write-whitelist.md. Chats listed there allow free mutations without per-message confirmation. The whitelist is currently empty.

Permission is per-conversation and per-target — approval to write to chat X does not generalize to chat Y, and yesterday's approval does not carry forward to today.

A read-only investigation never needs permission; this rule only applies to mutations.


Workflow — finding vacancies in Telegram

Operational details — file layout, scripts, filter schema, priority rubric, new-channel triage procedure — live in tracking/CLAUDE.md. Read that first before doing any vacancy-related Telegram work.

Quick orientation:

  • Source of subscriptions: Telegram folder "Jobs" (id=6), curated manually by Oleg on the usulsu account. Never mirror its membership to a repo file.
  • Pipeline: scripts/list_telegram_channels.pyscripts/fetch_telegram_jobs.py (chainable via stdin). Both scripts use the usulsu session (TELEGRAM_SESSION_STRING in .env).
  • Curated config: tracking/telegram_channels.json — per-channel lang, priority (p1/p2/p3), and filter (include / exclude).
  • Output: tracking/telegram_inbox.json — filtered messages, overwritten each run.
  • Trigger: manual only — run when Oleg explicitly asks (e.g. "забери свежее из Jobs"). No background polling.

After a run, triage telegram_inbox.json and promote promising postings to tracking/applications.md. For the triage itself, use the triage-jobs skill — it stratifies by priority and delegates p2/p3 (the bulk) to a Haiku subagent so the main session stays lean.


Bootstrap on a new machine

This project runs on multiple machines (VPS, laptops) with different filesystem paths. The committed config is fully portable; everything machine-specific lives in .env and .secrets/ (both gitignored). When Oleg sets up a new device, or when something Telegram-related breaks ("MCP not connecting", "session revoked", "TELEGRAM_MCP_BIN not set", etc.), walk through this.

One-time setup on a fresh checkout

  1. .env — copy from the template, then fill in:

    cp .env.example .env
    

    Required keys:

    • TRELLO_API_KEY, TRELLO_TOKEN — same across machines (copy from another device's .env).
    • TELEGRAM_API_ID, TELEGRAM_API_HASH — same across machines (one app registration).
    • TELEGRAM_MCP_DIRper-machine absolute path to the telegram-mcp checkout on THIS machine.
    • TELEGRAM_MCP_BINper-machine absolute path to the telegram-mcp executable, typically $TELEGRAM_MCP_DIR/.venv/bin/telegram-mcp.
    • TELEGRAM_SESSION_STRING, TELEGRAM_SESSION_STRING_HELPER — leave empty; generated in step 3.
  2. Node deps:

    pnpm install
    
  3. Generate Telegram sessions on THIS machine (one per account):

    pnpm tg:session:usulsu     # QR-login as @usulpro (main)
    pnpm tg:session:helper     # QR-login as @samuishka (helper)
    

    Each command opens a QR code in the terminal. Scan it from the corresponding Telegram account on Oleg's phone. The script writes the session string into .env and a copy into .secrets/session_<account>.txt.

    Critical rule — sessions are per-device: never copy TELEGRAM_SESSION_STRING* between machines. A Telegram auth key used from two IPs gets permanently revoked, and both devices lose access until re-authorized. Always run the regen script on each new machine.

  4. Reconnect MCP servers in Claude Code (/mcp → reconnect both telegram-usulpro and telegram-helper), or restart Claude Code.

How paths stay portable

  • .mcp.json uses a relative path (scripts/telegram-mcp.sh) — Claude Code launches project-scoped MCP servers with the project root as CWD, so this resolves correctly on any machine.
  • scripts/telegram-mcp.sh and scripts/regen_telegram_session.sh read TELEGRAM_MCP_BIN / TELEGRAM_MCP_DIR from .env. They fail with an explicit error message if the variable is missing or points at a non-existent path — never guess a default.

Troubleshooting cheat sheet

Symptom Cause Fix
MCP server telegram-usulpro / telegram-helper won't start .env missing, or TELEGRAM_MCP_BIN not set / not executable on this machine Check the server's stderr in Claude Code logs; the script prints exactly what's missing. Fill in .env.
session string for 'usulsu' is empty in .env Sessions weren't generated on this machine yet pnpm tg:session:usulsu (and/or :helper)
generator not found at .../.venv/bin/python TELEGRAM_MCP_DIR points at the wrong place, or .venv not created in the telegram-mcp checkout Fix the path in .env; in the telegram-mcp checkout run its venv setup
Telegram suddenly logs us out / revokes the key A session string got reused across two machines/IPs Regen on each affected machine separately with pnpm tg:session:<account>; never copy session strings
scripts/telegram-mcp.sh: No such file or directory from Claude Code .mcp.json got hardcoded back to an absolute path Restore relative form: "args": ["scripts/telegram-mcp.sh", "usulsu"]
list_telegram_channels.py / fetch_telegram_jobs.py fail to auth TELEGRAM_SESSION_STRING in .env doesn't match the usulsu account (these direct Telethon scripts always use usulsu) Regen with pnpm tg:session:usulsu

Folder layout

base/
  oleg_proskurin_<role>_cv.md   # master CV(s) in Markdown, one per target role
  reference/
    Oleg_Proskurin_*.pdf        # canonical visual reference (do not edit)
templates/
  cv-style.css                  # shared stylesheet — used by every CV
  cv-template.html              # skeleton HTML (use as a starting point)
tailored/
  <company-slug>/
    cv.md                       # tailored Markdown
    cv.html                     # generated HTML
    cv.pdf                      # final PDF
    notes.md                    # job posting, contact, decisions log
tracking/
  applications.md               # one row per application
  outreach.md                   # cold outreach + recruiter conversations
scripts/
  generate-pdf.mjs              # HTML -> PDF (puppeteer)
output/
  html/                         # generated HTML for base CVs (transient)
  pdf/                          # final PDFs for base CVs (committed)

Workflow 1 — Generate base CV PDF from Markdown

Input: base/oleg_proskurin_<role>_cv.md (e.g. oleg_proskurin_ai_engineer_fullstack_cv.md) Output: output/pdf/oleg_proskurin_<role>_cv.pdf

Steps:

  1. Read the Markdown source and the reference PDF (base/reference/...) to confirm structure.
  2. Generate output/html/oleg_proskurin_<role>_cv.html using templates/cv-template.html as a starting point.
    • Link the stylesheet via <link rel="stylesheet" href="/templates/cv-style.css" /> (absolute path — the PDF script serves the project root).
    • Map MD content to HTML tags verbatim — section titles, punctuation, list structure, dates, and contact separators must match MD character-for-character. The structure below is the tag shape; the text comes from MD. See "Content vs. style — separation rule" below.
  3. Run the PDF generator:
    pnpm pdf output/html/oleg_proskurin_<role>_cv.html
    
  4. Open the resulting PDF (output/pdf/oleg_proskurin_<role>_cv.pdf) and verify single-page-ish, no orphan bullets, no overflow.

Workflow 2 — Tailor CV for a specific company

Input: latest base/cv-*.md + job description / company info Output: tailored/<company-slug>/cv.pdf

Steps:

  1. Create tailored/<company-slug>/ folder. Use a short kebab-case slug (e.g. vercel, stripe-platform, anthropic-applied).
  2. Copy the base MD into tailored/<company-slug>/cv.md.
  3. Tailor the content:
    • Summary: re-frame around the company's stack and the role's keywords. Keep facts intact, shift emphasis.
    • Skills: reorder so the most relevant categories/tools appear first. Drop categories that are irrelevant to keep density.
    • Bullets: reorder within each job so the top bullets match what the company values. Edit phrasing to use the company's vocabulary, but never invent achievements.
    • Keep length ≤ 2 pages.
  4. Save the job posting, recruiter contacts, and tailoring decisions in tailored/<company-slug>/notes.md.
  5. Generate tailored/<company-slug>/cv.html (same template/CSS).
  6. Run the PDF generator with an explicit output directory:
    pnpm pdf tailored/<company-slug>/cv.html tailored/<company-slug>
    
    That writes tailored/<company-slug>/cv.pdf.
  7. Add a row to tracking/applications.md.

Workflow 3 — Tracking

  • tracking/applications.md: one row per application. Status column drives the funnel (applied, screen, interview, offer, rejected, withdrawn). Always store absolute dates (e.g. 2026-05-24).
  • tracking/outreach.md: log cold messages, recruiter pings, and follow-ups. One row per touchpoint; group by company.
  • When asked to "log this application" or similar, append to the right file — never rewrite existing rows unless explicitly asked.

Content vs. style — separation rule

This is the load-bearing rule for MD→HTML conversion. Read it before generating any HTML.

All text in the generated HTML comes verbatim from the source Markdown — section titles, contact-row separators, date formats, punctuation, bullet leads, list structure. The reference PDF supplies only visual conventions: typography, sizes, margins, colors, spacing, alignment, two-column header rows. The only allowed transformation is mapping MD markup to HTML tags:

  • # x<h1>x</h1>
  • ## x<h2>x</h2> (exact text — do not rename)
  • ### x → article header (exact text — do not reword)
  • **x**<strong>x</strong>
  • *x* / _x_<em>x</em>
  • `x`<code>x</code>
  • [text](url)<a href="url">text</a>
  • - item<li>item</li>
  • --- (horizontal rule) → drop (section separators are visual via <h2> underline)

Forbidden rewrites (examples — do not do these):

  • ## SummaryProfessional Summary (rename)
  • ## ExperienceWork History (rename)
  • · separators in contacts → | (substitution)
  • Feb 2025 Present02/2025 — Current (date reformatting)
  • Skills bullet list collapsed into a dense paragraph (structure change)
  • Job-description paragraph truncated to a short label (content loss)

If the visual style of the reference PDF appears to conflict with what's in MD, the MD wins. Update MD if you want different content; never silently rewrite it during HTML generation.


Visual format rules (must match the reference PDF)

The CSS in templates/cv-style.css already encodes these — when generating HTML, just use the classes and the format will follow. Reference for human review:

  • Page: A4, ~12mm margins, single column.
  • Font: Times New Roman (serif), body 10.5pt, line-height 1.25.
  • Header: centered name in 14pt bold caps, role in 11pt bold, single contact row with | separators.
  • Section headings: ALL CAPS, bold, 10.5pt, with a thin bottom rule.
  • Job entry:
    • Header row: company (bold, left) — role (bold, right).
    • Meta row: short company descriptor (italic, left) — dates (italic, right).
    • Bulleted list with disc markers, hanging indent.
  • Links: blue #1155cc, underlined.
  • Length: target 2 pages.

HTML structure to use

<header class="cv-header">
  <h1 class="name">OLEG PROSKURIN</h1>
  <p class="role">Role line</p>
  <p class="contacts">email | LinkedIn | Telegram | Github | ...</p>
</header>

<section class="cv-section">
  <h2><!-- exact ## heading from MD, e.g. "Summary" --></h2>
  <p>...</p>
</section>

<section class="cv-section cv-skills">
  <h2>Skills</h2>
  <!-- For each `**Category**` block in MD followed by a `-` list,
       render the category as <strong> then a <ul> of bullets verbatim. -->
  <p><strong>Category</strong></p>
  <ul>
    <li>Item from MD bullet (text verbatim, including ":" and punctuation)</li>
  </ul>
</section>

<section class="cv-section">
  <h2><!-- e.g. "Experience" --></h2>
  <article class="cv-job">
    <!-- ### Company — Role  →  split on first " — " -->
    <div class="cv-job-header">
      <span class="company">Company</span>
      <span class="role">Role</span>
    </div>
    <!-- "**Dates** · Location" line  →  location left, dates right -->
    <div class="cv-job-meta">
      <span>Location</span>
      <span class="dates">Dates verbatim from MD (e.g. "Feb 2025  Present")</span>
    </div>
    <!-- Full description paragraph from MD (do not truncate). Italic. -->
    <p class="cv-job-desc">Full descriptor paragraph from MD...</p>
    <ul>
      <li>Bullet text verbatim from MD (keep <strong>...</strong>: punctuation as-is)</li>
    </ul>
  </article>
</section>

<section class="cv-section">
  <h2><!-- e.g. "Education" --></h2>
  <p>...</p>
</section>

<section class="cv-section">
  <h2><!-- e.g. "Languages" --></h2>
  <p>...</p>
</section>

Conventions

  • Language: every artifact (HTML, MD, PDF, tracking rows, card notes) is in English. Always.
  • Filenames: snake_case for base CVs (oleg_proskurin_<role>_cv.md), kebab-case for tailored slugs. Tailored CV: cv.md inside tailored/<slug>/.
  • Dates: absolute (2026-05-24), never "last Thursday".
  • Scripts: all scripts in this project must be written in Node.js or bash — no Python. Use Node.js for anything with logic (parsing, transformation, linting, generation); use bash for simple shell glue (piping, file ops, invoking other tools).
  • Don't edit the reference PDF. It is the immutable visual anchor.
  • Don't auto-regenerate PDFs unless Oleg asks — show diffs first when content changes.
  • Verify before claiming "done": after pnpm pdf, confirm the PDF exists and looks correct (open it, check page count). If you can't visually verify, say so.