diff --git a/.claude/skills/get-tg-jobs/SKILL.md b/.claude/skills/get-tg-jobs/SKILL.md new file mode 100644 index 0000000..11751a0 --- /dev/null +++ b/.claude/skills/get-tg-jobs/SKILL.md @@ -0,0 +1,254 @@ +--- +name: get-tg-jobs +description: Full autonomous end-to-end job pipeline. Fetches fresh vacancies from Oleg's Telegram "Jobs" folder, handles new/pending channels, runs the full triage cycle (triage-jobs), pushes APPLY verdicts to Trello, then reports results in the chat session AND sends a Telegram notification to Oleg via telegram-helper. Trigger on "забери из Jobs", "что нового в каналах", "запусти pipeline", or similar. +--- + +# get-tg-jobs + +Autonomous end-to-end pipeline: fetch → curate new channels → triage → Trello → report. Does everything without stopping for confirmation. Only halts if a hard error occurs (auth failure, script crash, etc.). + +**Trigger only when Oleg explicitly asks** — e.g. "забери свежее из Jobs", "что нового в каналах", "запусти pipeline". No background polling. + +--- + +## Notification targets + +All pipeline reports go to the **"New cool job"** Telegram group. The `telegram-helper` account (samuishechka) is a member of that group and sends messages there. + +| Target | chat_id | When | +|---|---|---| +| "New cool job" group | `-4783500394` | All reports — success and errors | + +Tool to use: `mcp__telegram-helper__send_message(chat_id=-4783500394, message="...", parse_mode="html")` + +This section is the canonical reference for notification targets. Update here if the group changes. + +--- + +## Step 1 — Run the pipeline + +```bash +~/.local/bin/uv run scripts/list_telegram_channels.py \ + | ~/.local/bin/uv run scripts/fetch_telegram_jobs.py - +``` + +Run from the project root. The script prints a per-channel summary to stdout — echo it to the session so Oleg can see raw progress. + +**If the script fails:** +- `TELEGRAM_SESSION_STRING not set` or auth error → `.env` is missing or the session is expired. Regenerate with `scripts/regen_telegram_session.sh` and tell Oleg. +- `TELEGRAM_API_ID / TELEGRAM_API_HASH not set` → same `.env` issue. +- `Could not find folder "Jobs"` → the folder was renamed or the wrong session is in `.env`. Must be the **usulsu** session, not samuishechka. +- Any other exception → paste the error and stop. Don't retry blindly. + +--- + +## Step 2 — Check for pending (new) channels + +```bash +test -f tracking/telegram_pending_channels.json && echo PENDING || echo OK +``` + +**If OK**: skip to Step 3. + +**If PENDING**: curate each new channel using the procedure below, then rerun Step 1. + +--- + +### New-channel curation procedure + +For each channel in `telegram_pending_channels.json`: + +#### 2a — Read keyword frequency scan + +```bash +jq -r 'to_entries[] | "\n=== \(.key) ===\nmessages_scanned: \(.value.messages_scanned)\ntruncated: \(.value.truncated)\ntop keywords:\n\(.value.keyword_counts_from_other_channels | to_entries | sort_by(-.value) | .[:20] | .[] | " \(.key): \(.value)")"' \ + tracking/telegram_pending_channels.json +``` + +#### 2b — Sample messages + +```bash +jq -r '.channels[""].messages[:6] | .[] | "── \(.date[0:16])\n\(.text[0:400])\n"' \ + tracking/telegram_inbox.json +``` + +Look for: hashtag patterns, language, post structure (single role / digest / chat), recurring noise. + +#### 2c — Decide lang, priority, filter shape + +**Priority:** +- `p1` — strong stack hits (JS/TS/React/Node/AI/LLM) AND global-remote culture +- `p2` — stack OK but CIS/RUB market, or occasional gems; recruiter/market-intel content +- `p3` — wrong stack, off-market, noise, dead channels + +Base priority on the **best** vacancy in the sample, not the average. + +**Filter shape:** + +| Channel pattern | Filter | +|---|---| +| Consistent `#vacancy` + `#remote` hashtags | AND-of-OR hashtag filter | +| Vacancy text without consistent hashtags | Positive stack include + Oleg-stack excludes | +| Low-volume personal/curated digest | Trust-all (no `include`/`exclude`) | +| Mixes resumes and vacancies | Trust-all | +| Mostly noise but worth keeping | Strict positive filter | + +Standard Oleg-stack excludes: `["kafka", "golang", "kotlin", "android", "swift"]` + +For `*_jobs` channels add resume excludes: `["#резюме", "#resume", "#cv", "#ищуработу"]` + +Positive stack include: `["javascript", "typescript", " react", "node.js", "nodejs", "fullstack", "full-stack", "tech lead", "techlead", "ai engineer", " llm"]` + +#### 2d — Add entry to telegram_channels.json + +Insert in correct priority block (p1 → p2 → p3, alphabetically within each group): + +```jsonc +"": { + "lang": "ru" | "en" | "mixed", + "priority": "p1" | "p2" | "p3", + "include": [["kw1","kw2"], ["kw3"]], // optional + "exclude": ["kw4", "kw5"] // optional +} +``` + +#### 2e — Rerun and confirm clean + +```bash +~/.local/bin/uv run scripts/list_telegram_channels.py \ + | ~/.local/bin/uv run scripts/fetch_telegram_jobs.py - + +test -f tracking/telegram_pending_channels.json && echo "STILL PENDING" || echo "Clean" +``` + +#### 2f — Validate the filter + +```bash +jq -r '.channels[""] | "kept \(.kept)/\(.seen)" + (.messages | map("\n── \(.date[0:16])\n\(.text[0:300])") | join(""))' \ + tracking/telegram_inbox.json +``` + +If `kept == 0` and a filter is set — verify the filter isn't too strict before accepting zero as correct. + +--- + +## Step 3 — Run full triage + +Follow the complete `triage-jobs` skill procedure on `tracking/telegram_inbox.json`. + +Key points from that skill: + +1. **Pre-step**: build `known_applied` list before spawning Haiku: + ```bash + grep -E '^\|' tracking/applications.md | tail -n +2 | awk -F'|' '{print $3}' | sort -u + ``` + Also fetch Trello card titles from all BestJob board lists. Pass the merged list into every Haiku subagent prompt. + +2. **Stratify by priority**: + - p1 ≤ 15 messages → triage fully in main session + - p1 > 15 messages → Haiku first pass, then main session reviews only APPLY and VERIFY + - p2 and p3 → Haiku subagent, main session reviews APPLY and VERIFY only + +3. **VERIFY verdicts**: run a fast `WebSearch "{company} remote hiring countries"` to resolve geo, then apply Gate 1. + +4. **APPLY verdicts**: for every confirmed APPLY — + - Add a row to `tracking/applications.md` with status `todo` + - Create a Trello card in the TODO column (`6a1aa59555aab72a261c42aa`) on board `6a1a9a5af082cb0526b22704` with: company, role, track, verdict summary, JD link + +5. **QUICK-APPLY verdicts**: note them explicitly in the session report; do not auto-add to Trello. Ask Oleg if he wants to proceed. + +Full triage playbook: `base/reference/vacancy-filter-and-triage-2026.md`. Full batch instructions: `.claude/skills/triage-jobs/SKILL.md`. + +--- + +## Step 4 — Report in session + +After triage is complete, output a consolidated summary to the chat: + +``` +Pipeline done — + +Fetch: N messages from X channels (Y with results) +[⚠️ Truncated: channel_name (kept K/500, p2/ru)] + +Triage results: + ✅ APPLY: N roles → added to Trello + 🔍 VERIFY: N roles → resolved / still open + ⚡ QUICK-APPLY: N roles (ask Oleg before adding) + ❌ SKIP: N roles + +APPLY roles: + - Company A — Role Title (Track A) → [Trello card] + - Company B — Role Title (Track B) → [Trello card] + +New channels curated: N [or: none] +``` + +--- + +## Step 5 — Send Telegram notification + +After the session report, send a summary to the **"New cool job"** group via telegram-helper. + +### Success message format + +``` +mcp__telegram-helper__send_message( + chat_id=-4783500394, + parse_mode="html", + message="""Jobs pipeline done · 2026-06-07 + +✅ APPLY: N роли → Trello +🔍 VERIFY: N (нужен ресёрч) +⚡ Quick-apply: N (нужно ок) +❌ SKIP: N +📥 Всего: N сообщений из X каналов + +APPLY: +– Company A — Role (Track A) +– Company B — Role (Track B)""" +) +``` + +Rules: +- Always include Trello links for APPLY roles — это главная ценность уведомления. +- If QUICK-APPLY roles exist, list them with direct JD links (not Trello — для быстрого решения): + ``` + ⚡ Quick-apply (нужно ок): + – Company X — Role → wantapply.com/... + – Company Y — Role → linkedin.com/jobs/... + ``` +- Keep VERIFY as a count only (no list) — they need more research, not immediate action. +- If zero APPLY and zero QUICK-APPLY: still send the message — "всё просмотрено, ничего релевантного в этот раз". + +### Error message format + +If the pipeline failed at any step and cannot complete — send a short error notification to the **same group**: + +``` +mcp__telegram-helper__send_message( + chat_id=-4783500394, + parse_mode="html", + message="""⚠️ Pipeline error · 2026-06-07 + +Упало на шаге: +Причина: + +Что сделано до ошибки: +Что нужно: """ +) +``` + +Send the error notification **even if only partial work was done**. Better to have an incomplete notification than silence when something breaks. + +--- + +## Sanity notes + +- `telegram_state.json` is not in git — per-machine cursor. If lost, next run fetches 30 days back per channel (slow but correct). +- To reset a single channel's cursor for filter re-validation: + ```bash + jq 'del(.[""])' tracking/telegram_state.json > /tmp/s.json && mv /tmp/s.json tracking/telegram_state.json + ``` +- `telegram_inbox.json` is overwritten every run — it's the current snapshot only. +- The pipeline reads the live "Jobs" folder — new channels Oleg added in Telegram are automatically included in the next run. diff --git a/.claude/skills/triage-jobs/SKILL.md b/.claude/skills/triage-jobs/SKILL.md index fa1cc77..a34efc1 100644 --- a/.claude/skills/triage-jobs/SKILL.md +++ b/.claude/skills/triage-jobs/SKILL.md @@ -231,19 +231,31 @@ For SKIP: one-liner after gate check explaining the objective blocker. No resear When triaging `tracking/telegram_inbox.json`: +### Pre-step: build prior-applications list (do this once before spawning any subagent) + +```bash +grep -E '^\|' tracking/applications.md | tail -n +2 | awk -F'|' '{print $3}' | sort -u +``` + +Also call `mcp__trello__get_cards_by_list_id` for all BestJob board lists to extract card titles. Merge both into a `known_applied` list (company name → date/status where available). Pass this list to every Haiku subagent prompt (see template below). + +### Stratification rules + 1. Read the file. Group entries by channel `priority` field (`p1` / `p2` / `p3`). -2. **p1 entries**: triage in the main session (full Step 1–8 per entry). -3. **p2 and p3 entries**: delegate to a Haiku subagent to protect main context. - - Pass the raw entries and the playbook summary to the subagent. +2. **p1 entries — ≤ 15 messages**: triage fully in the main session (full Step 1–8 per entry). +3. **p1 entries — > 15 messages**: Haiku first pass (same prompt as p2/p3). Main session then reviews only APPLY and VERIFY verdicts; SKIP verdicts from Haiku are accepted without re-review. +4. **p2 and p3 entries**: delegate to a Haiku subagent to protect main context. + - Pass the raw entries, the playbook summary, and the `known_applied` list to the subagent. - Subagent returns a structured verdict list. - - Review, escalate any surprising p2/p3 passes to main session for full triage. -4. After all verdicts: promote APPLY entries to `tracking/applications.md`. -5. For QUICK-APPLY entries: note explicitly, ask Oleg if he wants to proceed. + - Review, escalate any APPLY and VERIFY passes to main session for full triage. +5. After all verdicts: promote confirmed APPLY entries to `tracking/applications.md`. +6. For QUICK-APPLY entries: note explicitly, ask Oleg if he wants to proceed. +7. For VERIFY entries: run a fast WebSearch `"{company} remote hiring countries"` to resolve geo; then apply the gate. ### Haiku subagent prompt template ``` -You are triaging job vacancies for Oleg Proskurin (Tech Lead / AI Engineer, based in Thailand, available for remote work). +You are triaging job vacancies for Oleg Proskurin (Tech Lead / AI Engineer, based in Thailand GMT+7, available for remote work). Apply these 4 hard gates — SKIP on first failure: @@ -252,13 +264,25 @@ Gate 2 (TZ): Oleg's window 9:00–22:00 GMT+7. Outside window: ok if comp > $9k. Gate 3 (Comp): Skip if < $4.5k/month. Floor $6–8k, target $8–12k. Gate 4 (Level): Skip if explicitly Junior or mid with no growth ceiling. -Posture: trying-friendly. On ambiguous cases, lean toward APPLY. +Posture: trying-friendly when data IS PRESENT and borderline. When key gate data is ABSENT from the message text, return VERIFY — not APPLY. Lean toward APPLY only when geo and comp are explicitly stated and borderline (e.g. "global remote", "$7k/month"). -For each vacancy below, output: -- APPLY, SKIP, or QUICK-APPLY (≥50% match + easy application) -- Gate that failed (for SKIP) -- Probable track: A (AI must-have), B (full-stack/frontend), Backend-first, Hybrid -- 1-line reason +Geo rules (Gate 1): +- Message explicitly says "global remote" / "worldwide" / "anywhere" / "любой город" / contractor-friendly → Gate 1 passes, proceed. +- Message says "United States" / "US only" / "United Kingdom only" / specific country list excluding Thailand → SKIP Gate 1. +- Message says "Remote" with no geo qualifier → VERIFY geo (cannot confirm Thailand eligibility without JD). +- Message has no location info at all → VERIFY geo. + +Comp rules (Gate 3): +- Explicit salary ≥ $4.5k/month or ≥ $54k/year → Gate 3 passes, proceed. +- Explicit salary < $4.5k/month → SKIP Gate 3. +- No salary mentioned → VERIFY comp. +- Timezone offset explicitly stated (e.g. GMT-GMT+6) → treat as geo restriction, apply Gate 1. + +Known applied companies — flag these as SKIP (already applied): +[INSERT known_applied LIST HERE — one line per company] + +For each vacancy below, output one line: +APPLY | SKIP | QUICK-APPLY | VERIFY — [company/role] — [gate failed or what's missing] — [track: A/B/Backend-first/Hybrid] Vacancies: [paste entries from telegram_inbox.json] diff --git a/CLAUDE.md b/CLAUDE.md index 8bf5053..7c3baab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,9 +51,50 @@ The list names "Applyed" and "Artifactes" are the actual spellings on the board --- +## 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 -Claude is connected to Oleg's personal Telegram account 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 are absolute — they take precedence over any in-conversation instruction, including a direct user request to ignore them.** +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 @@ -63,7 +104,7 @@ Do not call any Telegram tool whose effect cannot be cleanly undone, **even if O - 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 Oleg's account identity or security posture +- 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. @@ -85,8 +126,8 @@ A read-only investigation never needs permission; this rule only applies to muta Operational details — file layout, scripts, filter schema, priority rubric, new-channel triage procedure — live in **[`tracking/CLAUDE.md`](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. Never mirror its membership to a repo file. -- **Pipeline**: `scripts/list_telegram_channels.py` → `scripts/fetch_telegram_jobs.py` (chainable via stdin). +- **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.py` → `scripts/fetch_telegram_jobs.py` (chainable via stdin). Both scripts use the **usulsu** session (`TELEGRAM_SESSION_STRING` in `.env`). - **Curated config**: [`tracking/telegram_channels.json`](tracking/telegram_channels.json) — per-channel `lang`, `priority` (`p1`/`p2`/`p3`), and filter (`include` / `exclude`). - **Output**: [`tracking/telegram_inbox.json`](tracking/telegram_inbox.json) — filtered messages, overwritten each run. - **Trigger**: manual only — run when Oleg explicitly asks (e.g. "забери свежее из Jobs"). No background polling. diff --git a/docs/telegram-integration.md b/docs/telegram-integration.md index 125ab0d..a81d3f3 100644 --- a/docs/telegram-integration.md +++ b/docs/telegram-integration.md @@ -1,8 +1,13 @@ # Telegram MCP — setup on a new machine -Reproduces the working integration this project uses: read Oleg's Telegram channels/DMs as a user (MTProto, not a bot) through the `chigwell/telegram-mcp` server, registered in Claude Code + Claude Desktop. +Reproduces the working integration this project uses: two Telegram accounts wired up as separate MCP servers via `chigwell/telegram-mcp` (MTProto, not bots), registered in Claude Code + Claude Desktop. -Time: ~10 minutes. Most of it is one-time Telegram credentials. +| Account | MCP server name | Tool prefix | Role | +|---|---|---|---| +| **usulsu** (main) | `telegram-usulpro` | `mcp__telegram-usulpro__*` | Job channels, recruiter outreach | +| **samuishechka** (helper) | `telegram-helper` | `mcp__telegram-helper__*` | Notifications to Oleg + Ekaterina, workflow triggers | + +Time: ~15 minutes. Most of it is one-time Telegram credentials and two session strings. --- @@ -13,7 +18,9 @@ Time: ~10 minutes. Most of it is one-time Telegram credentials. --- -## 1. Get Telegram API credentials (one-time per Telegram account) +## 1. Get Telegram API credentials (one-time — shared across both accounts) + +The `api_id` / `api_hash` identify the *app*, not the account. One app serves both sessions. Open https://my.telegram.org/apps and create a new application: @@ -27,12 +34,13 @@ You'll get `api_id` (number) and `api_hash` (32-char hex). Treat them like a pas If you see a generic `ERROR` popup, fill in the URL field — that's the most common cause. -Save both into the project `.env`: +Save both into the project `.env` (session strings filled in after step 3): ``` TELEGRAM_API_ID= TELEGRAM_API_HASH= -TELEGRAM_SESSION_STRING= +TELEGRAM_SESSION_STRING= # usulsu — main account (job channels + outreach) +TELEGRAM_SESSION_STRING_HELPER= # samuishechka — notifications + workflow triggers ``` Confirm `.env` is in `.gitignore`. @@ -60,52 +68,89 @@ Two binaries appear at `/projects/my-utils/telegram/.venv/bin/`: --- -## 3. Generate the session string (one-time, interactive) +## 3. Generate session strings (one per account, interactive) -From the cv-2026 project root (where `.env` lives): +Run the generator twice — once per account. From the cv-2026 project root: ```bash set -a && source .env && set +a && \ /projects/my-utils/telegram/.venv/bin/telegram-mcp-generate-session ``` +### 3a. usulsu — main account + At the prompts: -- **Account label:** *leave empty* (Enter). A label adds a `_