init
This commit is contained in:
commit
d295c7f139
1527 files changed
+270726
No files matched your search
@@ -0,0 +1,95 @@
|
||||
# Batch Processing
|
||||
|
||||
Process multiple job offers in parallel via headless workers. Each worker runs the full evaluation pipeline (A-F report + PDF + tracker line) autonomously. See the **Headless / Batch Mode** table in `AGENTS.md` for the correct command per CLI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Add offers** to `batch-input.tsv` (tab-separated: `id`, `url`, `source`, `notes`):
|
||||
|
||||
```tsv
|
||||
id url source notes
|
||||
1 https://jobs.example.com/role-a LinkedIn
|
||||
2 https://greenhouse.io/company/role-b Greenhouse priority
|
||||
```
|
||||
|
||||
2. **Dry run** to preview what will be processed:
|
||||
|
||||
```bash
|
||||
./batch/batch-runner.sh --dry-run
|
||||
```
|
||||
|
||||
3. **Run the batch**:
|
||||
|
||||
```bash
|
||||
./batch/batch-runner.sh
|
||||
```
|
||||
|
||||
4. **Results** are automatically merged into `data/applications.md`, processed offers are reconciled out of the `data/pipeline.md` inbox, and integrity is verified with `verify-pipeline.mjs` at the end of the run.
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--parallel N` | `1` | Number of concurrent headless workers |
|
||||
| `--dry-run` | off | Preview pending offers without processing |
|
||||
| `--retry-failed` | off | Only retry offers marked as `failed` in state |
|
||||
| `--resume-paused` | off | Resume offers paused after a Claude session/rate limit |
|
||||
| `--start-from N` | `0` | Skip offers with ID below N |
|
||||
| `--limit N` | `0` | Max number of offers to process in this run (0 = no limit) |
|
||||
| `--max-retries N` | `2` | Max retry attempts per offer before giving up |
|
||||
| `--rate-limit-sleep N` | `300` | Seconds to wait before retrying a transient rate-limited worker; use `0` to pause the batch immediately |
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
batch/
|
||||
batch-runner.sh # Orchestrator script
|
||||
batch-prompt.md # Prompt template sent to each worker
|
||||
batch-input.tsv # Input offers (you create this)
|
||||
batch-state.tsv # Processing state (auto-managed, resumable)
|
||||
logs/ # Per-offer worker logs ({report_num}-{id}.log)
|
||||
tracker-additions/ # TSV lines produced by workers
|
||||
merged/ # TSVs already merged into applications.md
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **batch-runner.sh** reads `batch-input.tsv` and `batch-state.tsv` to determine which offers need processing.
|
||||
2. For each pending offer, it assigns a report number and launches a headless worker with `batch-prompt.md` as the system prompt (placeholders like `{{URL}}`, `{{REPORT_NUM}}` are resolved).
|
||||
3. Each worker evaluates the offer, writes a report to `reports/`, generates a PDF to `output/`, and writes a tracker TSV to `tracker-additions/`.
|
||||
4. After all workers finish, batch-runner calls `merge-tracker.mjs` to merge TSVs into `data/applications.md`, `reconcile-pipeline.mjs` to move processed offers out of the `data/pipeline.md` inbox, and `verify-pipeline.mjs` to check integrity.
|
||||
|
||||
## Tracker Merge
|
||||
|
||||
Workers write one TSV per offer to `batch/tracker-additions/`. The merge script (`npm run merge`) handles:
|
||||
|
||||
- Deduplication by company + role fuzzy match and report number
|
||||
- Column order conversion (TSV has status before score; applications.md has score before status)
|
||||
- In-place updates when a re-evaluation scores higher than the existing entry
|
||||
- Moving processed TSVs to `tracker-additions/merged/`
|
||||
|
||||
Run `npm run merge` manually if you need to merge outside of a batch run.
|
||||
|
||||
## Pipeline Reconcile
|
||||
|
||||
Batch mode reads offers from `batch-input.tsv`, but the `data/pipeline.md` inbox is a separate list. Without reconciliation, an offer evaluated by a batch run stays in the pipeline "Pendientes" section and gets surfaced again on the next scan or `/career-ops pipeline` run -- producing duplicate reports.
|
||||
|
||||
`reconcile-pipeline.mjs` (run as `npm run reconcile`) closes that gap: after the tracker merge, every `completed` or `skipped` offer in `batch-state.tsv` whose URL is still in pipeline "Pendientes" is moved to "Procesadas" with its report link and score (entries without a report file on disk are left in place). It is idempotent -- safe to run after every batch, or manually.
|
||||
|
||||
## Resumability
|
||||
|
||||
`batch-state.tsv` tracks the status of every offer (`pending`, `processing`, `completed`, `failed`, `skipped`, `rate_limited`, `paused_rate_limit`). If the batch is interrupted, re-running `batch-runner.sh` picks up where it left off -- completed offers are skipped automatically. `rate_limited` is a non-completed state used while the runner waits before retrying, so interrupted rate-limited jobs are eligible on the next normal run.
|
||||
|
||||
`paused_rate_limit` is different: it means a worker hit a Claude session/usage limit, so the runner stopped scheduling new offers and preserved the retry count. Resume those rows explicitly after the limit resets:
|
||||
|
||||
```bash
|
||||
./batch/batch-runner.sh --resume-paused
|
||||
```
|
||||
|
||||
A PID-based lock file (`batch-runner.pid`) prevents concurrent batch runs. If a previous run crashed, the stale lock is detected and removed automatically.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Your CLI in PATH (see **Headless / Batch Mode** table in `AGENTS.md`)
|
||||
- Node.js >= 18, Playwright chromium installed (`npm run doctor` to verify)
|
||||
- `batch-input.tsv` with at least one offer
|
||||
@@ -0,0 +1,11 @@
|
||||
id url source notes
|
||||
1 https://getmatch.ru/vacancies/34414-starshii-frontend-razrabotchik?s=offers Javascript jobs (Vue/React/Angular) — Telegram Старший frontend-разработчик | note: via Telegram: https://t.me/s/jsdevjob/2410
|
||||
2 https://hirehi.ru/development/frontend-razrabotchik-55966 Javascript jobs (Vue/React/Angular) — Telegram Middle Frontend | note: via Telegram: https://t.me/s/jsdevjob/2413
|
||||
3 https://hh.ru/vacancy/133613060?query=frontend&hhtmFrom=vacancy_search_list Javascript jobs (Vue/React/Angular) — Telegram Frontend-разработчик | note: via Telegram: https://t.me/s/jsdevjob/2416
|
||||
4 https://hh.ru/vacancy/133056126?query=frontend&hhtmFrom=vacancy_search_list Javascript jobs (Vue/React/Angular) — Telegram Frontend developer (React) | note: via Telegram: https://t.me/s/jsdevjob/2419
|
||||
5 https://getmatch.ru/vacancies/35037-frontend-software-engineer-bootcamp?s=offers Javascript jobs (Vue/React/Angular) — Telegram Frontend Software Engineer (Bootcamp) | note: via Telegram: https://t.me/s/jsdevjob/2423
|
||||
6 https://career.habr.com/vacancies/1000167021 Javascript jobs (Vue/React/Angular) — Telegram Ведущий фронтенд-разработчик (React) | note: via Telegram: https://t.me/s/jsdevjob/2425
|
||||
7 https://hh.ru/vacancy/119867578?query=frontend&hhtmFrom=vacancy_search_list Javascript jobs (Vue/React/Angular) — Telegram Frontend-разработчик React (junior) | note: via Telegram: https://t.me/s/jsdevjob/2429
|
||||
8 https://t.me/s/runello_rus_javascript/1443 SiteAnalyzer Frontend на React
|
||||
9 https://t.me/s/runello_rus_javascript/1445 JavaScript Job Offers — Telegram Fullstack React Developer
|
||||
10 https://t.me/s/runello_rus_javascript/1448 ЗАО КонсультантПлюс Веб разработчик Vue/Js
|
||||
|
@@ -0,0 +1,458 @@
|
||||
# career-ops Batch Worker — Evaluación Completa + PDF + Tracker Line
|
||||
|
||||
Eres un worker de evaluación de ofertas de empleo for the candidate (read name from config/profile.yml). Recibes una oferta (URL + JD text) y produces:
|
||||
|
||||
1. Evaluación completa A-G (report .md)
|
||||
2. PDF personalizado ATS-optimizado
|
||||
3. Línea de tracker para merge posterior
|
||||
|
||||
**IMPORTANTE**: Este prompt es self-contained. Tienes TODO lo necesario aquí. No dependes de ningún otro skill ni sistema.
|
||||
|
||||
---
|
||||
|
||||
## Fuentes de Verdad (LEER antes de evaluar)
|
||||
|
||||
| Archivo | Ruta absoluta | Cuándo |
|
||||
|---------|---------------|--------|
|
||||
| cv.md | `cv.md (project root)` | SIEMPRE |
|
||||
| _profile.md | `modes/_profile.md (if exists)` | SIEMPRE (user customizations: archetypes, role_shape, location policy, comp targets) |
|
||||
| profile.yml | `config/profile.yml (if exists)` | SIEMPRE (candidate identity, comp range, role_shape rules) |
|
||||
| llms.txt | `llms.txt (if exists)` | SIEMPRE |
|
||||
| article-digest.md | `article-digest.md (project root)` | SIEMPRE (proof points) |
|
||||
| i18n.ts | `i18n.ts (if exists, optional)` | Solo entrevistas/deep |
|
||||
| cv-template.html | `templates/cv-template.html` | Para PDF |
|
||||
| generate-pdf.mjs | `generate-pdf.mjs` | Para PDF |
|
||||
|
||||
**REGLA: NUNCA escribir en cv.md ni i18n.ts.** Son read-only.
|
||||
**REGLA: NUNCA hardcodear métricas.** Leerlas de cv.md + article-digest.md en el momento.
|
||||
**REGLA: Para métricas de artículos, article-digest.md prevalece sobre cv.md.** cv.md puede tener números más antiguos — es normal.
|
||||
**REGLA: Antes de evaluar, cargar `modes/_profile.md` y `config/profile.yml` si existen.** Contienen las preferencias del candidato Y reglas concretas de scoring que **sobrescriben** los defaults del sistema.
|
||||
|
||||
Tipos de patrones que estos archivos pueden incluir:
|
||||
- **Caps de bloque** — ej: "cap Block A at 3.0/5 if title contains 'Lead'/'Head'/'Principal'"
|
||||
- **Overrides de recomendación** — ej: "force SKIP if comp ceiling below $120K" o "force SKIP if role_shape signals broad ownership"
|
||||
- **Scoring por dimensión** — ej: "Remote: full credit on remote-first; score 2.0 on full on-site outside [region]"
|
||||
- **Framing adaptativo por archetype** — mappings entre arquetipos detectados y proof points a priorizar
|
||||
|
||||
Aplicación durante la evaluación A-G:
|
||||
- **Bloque A:** aplicar caps de role-shape ANTES de calcular el score del bloque
|
||||
- **Bloques B-D:** aplicar adaptive framing por archetype y reglas de dimension scoring (location, comp, etc.)
|
||||
- **Bloque F:** aplicar recommendation overrides (SKIP forzado, etc.) — `_profile.md` puede convertir un score técnicamente alto en un SKIP por shape o por comp
|
||||
|
||||
**En conflicto, las reglas de `_profile.md` ganan sobre los defaults de `_shared.md`.** Esto es intencional: `_profile.md` es la capa de personalización del usuario.
|
||||
|
||||
---
|
||||
|
||||
## Placeholders (sustituidos por el orquestador)
|
||||
|
||||
| Placeholder | Descripción |
|
||||
|-------------|-------------|
|
||||
| `{{URL}}` | URL de la oferta |
|
||||
| `{{JD_FILE}}` | Ruta al archivo con el texto del JD |
|
||||
| `{{REPORT_NUM}}` | Número de report (3 dígitos, zero-padded: 001, 002...) |
|
||||
| `{{DATE}}` | Fecha actual YYYY-MM-DD |
|
||||
| `{{ID}}` | ID único de la oferta en batch-input.tsv |
|
||||
|
||||
---
|
||||
|
||||
## Pipeline (ejecutar en orden)
|
||||
|
||||
### Paso 1 — Obtener JD
|
||||
|
||||
1. Lee el archivo JD en `{{JD_FILE}}`
|
||||
2. Si el archivo está vacío o no existe, intenta obtener el JD desde `{{URL}}` con WebFetch
|
||||
3. Si ambos fallan, reporta error y termina
|
||||
|
||||
### Paso 2 — Evaluación A-G
|
||||
|
||||
Read `cv.md`. Ejecuta TODOS los bloques:
|
||||
|
||||
#### Paso 0 — Detección de Arquetipo
|
||||
|
||||
Clasifica la oferta en uno de los 6 arquetipos. Si es híbrido, indica los 2 más cercanos.
|
||||
|
||||
**Los 6 arquetipos (todos igual de válidos):**
|
||||
|
||||
| Arquetipo | Ejes temáticos | Qué compran |
|
||||
|-----------|----------------|-------------|
|
||||
| **AI Platform / LLMOps Engineer** | Evaluation, observability, reliability, pipelines | Alguien que ponga AI en producción con métricas |
|
||||
| **Agentic Workflows / Automation** | HITL, tooling, orchestration, multi-agent | Alguien que construya sistemas de agentes fiables |
|
||||
| **Technical AI Product Manager** | GenAI/Agents, PRDs, discovery, delivery | Alguien que traduzca negocio → producto AI |
|
||||
| **AI Solutions Architect** | Hyperautomation, enterprise, integrations | Alguien que diseñe arquitecturas AI end-to-end |
|
||||
| **AI Forward Deployed Engineer** | Client-facing, fast delivery, prototyping | Alguien que entregue soluciones AI a clientes rápido |
|
||||
| **AI Transformation Lead** | Change management, adoption, org enablement | Alguien que lidere el cambio AI en una organización |
|
||||
|
||||
**Framing adaptativo:**
|
||||
|
||||
> **Las métricas concretas se leen de `cv.md` + `article-digest.md` en cada evaluación. NUNCA hardcodear números aquí.**
|
||||
|
||||
| Si el rol es... | Emphasize about the candidate... | Fuentes de proof points |
|
||||
|-----------------|--------------------------|--------------------------|
|
||||
| Platform / LLMOps | Builder de sistemas en producción, observability, evals, closed-loop | article-digest.md + cv.md |
|
||||
| Agentic / Automation | Orquestación multi-agente, HITL, reliability, cost | article-digest.md + cv.md |
|
||||
| Technical AI PM | Product discovery, PRDs, métricas, stakeholder mgmt | cv.md + article-digest.md |
|
||||
| Solutions Architect | Diseño de sistemas, integrations, enterprise-ready | article-digest.md + cv.md |
|
||||
| Forward Deployed Engineer | Fast delivery, client-facing, prototype → prod | cv.md + article-digest.md |
|
||||
| AI Transformation Lead | Change management, team enablement, adoption | cv.md + article-digest.md |
|
||||
|
||||
**Ventaja transversal**: Enmarcar perfil como **"Technical builder"** que adapta su framing al rol:
|
||||
- Para PM: "builder que reduce incertidumbre con prototipos y luego productioniza con disciplina"
|
||||
- Para FDE: "builder que entrega fast con observability y métricas desde día 1"
|
||||
- Para SA: "builder que diseña sistemas end-to-end con experiencia real en integrations"
|
||||
- Para LLMOps: "builder que pone AI en producción con closed-loop quality systems — leer métricas de article-digest.md"
|
||||
|
||||
Convertir "builder" en señal profesional, no en "hobby maker". El framing cambia, la verdad es la misma.
|
||||
|
||||
#### Bloque A — Resumen del Rol
|
||||
|
||||
Tabla con: Arquetipo detectado, Domain, Function, Seniority, Remote, Team size, TL;DR.
|
||||
|
||||
#### Bloque B — Match con CV
|
||||
|
||||
Read `cv.md`. Tabla con cada requisito del JD mapeado a líneas exactas del CV o keys de i18n.ts.
|
||||
|
||||
**Adaptado al arquetipo:**
|
||||
- FDE → priorizar delivery rápida y client-facing
|
||||
- SA → priorizar diseño de sistemas e integrations
|
||||
- PM → priorizar product discovery y métricas
|
||||
- LLMOps → priorizar evals, observability, pipelines
|
||||
- Agentic → priorizar multi-agent, HITL, orchestration
|
||||
- Transformation → priorizar change management, adoption, scaling
|
||||
|
||||
Sección de **gaps** con estrategia de mitigación para cada uno:
|
||||
1. ¿Es hard blocker o nice-to-have?
|
||||
2. Can the candidate demonstrate experiencia adyacente?
|
||||
3. ¿Hay un proyecto portfolio que cubra este gap?
|
||||
4. Plan de mitigación concreto
|
||||
|
||||
#### Bloque C — Nivel y Estrategia
|
||||
|
||||
1. **Nivel detectado** en el JD vs **candidate's natural level**
|
||||
2. **Plan "vender senior sin mentir"**: frases específicas, logros concretos, founder como ventaja
|
||||
3. **Plan "si me downlevelan"**: aceptar si comp justa, review a 6 meses, criterios claros
|
||||
|
||||
#### Bloque D — Comp y Demanda
|
||||
|
||||
Usar WebSearch para salarios actuales (Glassdoor, Levels.fyi, Blind), reputación comp de la empresa, tendencia demanda. Tabla con datos y fuentes citadas. Si no hay datos, decirlo.
|
||||
|
||||
Score de comp (1-5): 5=top quartile, 4=above market, 3=median, 2=slightly below, 1=well below.
|
||||
|
||||
#### Bloque E — Plan de Personalización
|
||||
|
||||
| # | Sección | Estado actual | Cambio propuesto | Por qué |
|
||||
|---|---------|---------------|------------------|---------|
|
||||
|
||||
Top 5 cambios al CV + Top 5 cambios a LinkedIn.
|
||||
|
||||
#### Bloque F — Plan de Entrevistas
|
||||
|
||||
6-10 historias STAR mapeadas a requisitos del JD:
|
||||
|
||||
| # | Requisito del JD | Historia STAR | S | T | A | R |
|
||||
|
||||
**Selección adaptada al arquetipo.** Incluir también:
|
||||
- 1 case study recomendado (cuál proyecto presentar y cómo)
|
||||
- Preguntas red-flag y cómo responderlas
|
||||
|
||||
#### Bloque G — Posting Legitimacy
|
||||
|
||||
Analyze posting signals to assess whether this is a real, active opening.
|
||||
|
||||
**Batch mode limitations:** Playwright is not available, so posting freshness signals (exact days posted, apply button state) cannot be directly verified. Mark these as "unverified (batch mode)."
|
||||
|
||||
**What IS available in batch mode:**
|
||||
1. **Description quality analysis** -- Full JD text is available. Analyze specificity, requirements realism, salary transparency, boilerplate ratio.
|
||||
2. **Company hiring signals** -- WebSearch queries for layoff/freeze news (combine with Block D comp research).
|
||||
3. **Reposting detection** -- Read `data/scan-history.tsv` to check for prior appearances.
|
||||
4. **Role market context** -- Qualitative assessment from JD content.
|
||||
|
||||
**Output format:** Same as interactive mode (Assessment tier + Signals table + Context Notes), but with a note that posting freshness is unverified.
|
||||
|
||||
**Assessment:** Apply the same three tiers (High Confidence / Proceed with Caution / Suspicious), weighting available signals more heavily. If insufficient signals are available to make a determination, default to "Proceed with Caution" with a note about limited data.
|
||||
|
||||
#### Score Global
|
||||
|
||||
| Dimensión | Score |
|
||||
|-----------|-------|
|
||||
| Match con CV | X/5 |
|
||||
| Alineación North Star | X/5 |
|
||||
| Comp | X/5 |
|
||||
| Señales culturales | X/5 |
|
||||
| Red flags | -X (si hay) |
|
||||
| **Global** | **X/5** |
|
||||
|
||||
#### Machine Summary
|
||||
|
||||
Create a machine-readable summary from the completed A-G evaluation and global score. This block is for downstream scripts; keep field names exact, use YAML, and do not add prose inside the fence.
|
||||
|
||||
```yaml
|
||||
company: "{empresa}"
|
||||
role: "{rol}"
|
||||
score: {X.X}
|
||||
legitimacy_tier: "{High Confidence | Proceed with Caution | Suspicious}"
|
||||
archetype: "{detectado}"
|
||||
final_decision: "{Apply | Consider | Research first | Skip}"
|
||||
hard_stops:
|
||||
- "{blocking gap or risk}"
|
||||
soft_gaps:
|
||||
- "{non-blocking gap}"
|
||||
top_strengths:
|
||||
- "{strength most relevant to this role}"
|
||||
risk_level: "{Low | Medium | High}"
|
||||
confidence: "{Low | Medium | High}"
|
||||
next_action: "{one concrete next step}"
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Use `[]` for `hard_stops`, `soft_gaps`, or `top_strengths` when empty.
|
||||
- `score` is numeric only, without `/5`.
|
||||
- `final_decision` must reflect the full evaluation, not only the CV match.
|
||||
- Do not invent missing data. If confidence is limited, set `confidence: "Low"` and explain the limitation in the human-readable sections.
|
||||
|
||||
### Paso 3 — Guardar Report .md
|
||||
|
||||
Guardar evaluación completa en:
|
||||
```
|
||||
reports/{{REPORT_NUM}}-{company-slug}-{{DATE}}.md
|
||||
```
|
||||
|
||||
Donde `{company-slug}` es el nombre de empresa en lowercase, sin espacios, con guiones.
|
||||
|
||||
**Formato del report:**
|
||||
|
||||
```markdown
|
||||
# Evaluación: {Empresa} — {Rol}
|
||||
|
||||
**Fecha:** {{DATE}}
|
||||
**Arquetipo:** {detectado}
|
||||
**Score:** {X/5}
|
||||
**Legitimacy:** {High Confidence | Proceed with Caution | Suspicious}
|
||||
**URL:** {URL de la oferta original}
|
||||
**PDF:** {output/cv-candidate-{company-slug}-{{DATE}}.pdf if score ≥ the resolved `auto_pdf_score_threshold` from Paso 4, else `not generated — run /career-ops pdf {company-slug} to create on demand`}
|
||||
**Batch ID:** {{ID}}
|
||||
|
||||
---
|
||||
|
||||
## Machine Summary
|
||||
|
||||
```yaml
|
||||
company: "{empresa}"
|
||||
role: "{rol}"
|
||||
score: {X.X}
|
||||
legitimacy_tier: "{High Confidence | Proceed with Caution | Suspicious}"
|
||||
archetype: "{detectado}"
|
||||
final_decision: "{Apply | Consider | Research first | Skip}"
|
||||
hard_stops:
|
||||
- "{blocking gap or risk}"
|
||||
soft_gaps:
|
||||
- "{non-blocking gap}"
|
||||
top_strengths:
|
||||
- "{strength most relevant to this role}"
|
||||
risk_level: "{Low | Medium | High}"
|
||||
confidence: "{Low | Medium | High}"
|
||||
next_action: "{one concrete next step}"
|
||||
```
|
||||
|
||||
## A) Resumen del Rol
|
||||
(contenido completo)
|
||||
|
||||
## B) Match con CV
|
||||
(contenido completo)
|
||||
|
||||
## C) Nivel y Estrategia
|
||||
(contenido completo)
|
||||
|
||||
## D) Comp y Demanda
|
||||
(contenido completo)
|
||||
|
||||
## E) Plan de Personalización
|
||||
(contenido completo)
|
||||
|
||||
## F) Plan de Entrevistas
|
||||
(contenido completo)
|
||||
|
||||
## G) Posting Legitimacy
|
||||
(contenido completo)
|
||||
|
||||
---
|
||||
|
||||
## Keywords extraídas
|
||||
(15-20 keywords del JD para ATS)
|
||||
```
|
||||
|
||||
### Paso 4 — Generar PDF (configurable)
|
||||
|
||||
**Gate:** Read `config/profile.yml` → `auto_pdf_score_threshold`. If the key is absent, default to **`3.0`** (the original gate of Path A). This step ONLY runs when the score from Paso 2 is **≥ the resolved threshold**. For everything below it, skip this entire step — the user can generate a tailored PDF on demand later via `/career-ops pdf {company-slug}` using the report from Paso 3 as input.
|
||||
|
||||
**Rationale:** Generating a tailored PDF costs ~30–60s per offer (Playwright launch + HTML render) and produces files that often go unused — most roles score 2.x/3.x and never reach application. The `3.0` default matches Path A's original behavior; raise `auto_pdf_score_threshold` (e.g. `4.0`) to pre-generate fewer PDFs, or set `0` to generate one for every offer. Both Path A (`/career-ops pipeline`) and Path B (this batch worker) read the same config key for consistency.
|
||||
|
||||
**If score < threshold:**
|
||||
- Skip steps 1–14 below.
|
||||
- In the report header use: `**PDF:** not generated — run /career-ops pdf {company-slug} to create on demand`.
|
||||
- In Paso 5 (tracker line) use `pdf_emoji` = `❌`.
|
||||
- In Paso 6 (output JSON) set `"pdf": null`.
|
||||
- Done — move to Paso 5.
|
||||
|
||||
**If score ≥ threshold**, generate the tailored PDF:
|
||||
|
||||
1. Lee `cv.md` + `i18n.ts`
|
||||
2. Extrae 15-20 keywords del JD
|
||||
3. Detecta idioma del JD → idioma del CV (EN default)
|
||||
4. Detecta ubicación empresa → formato papel: US/Canada → `letter`, resto → `a4`
|
||||
5. Detecta arquetipo → adapta framing
|
||||
6. Reescribe Professional Summary inyectando keywords
|
||||
7. Selecciona top 3-4 proyectos más relevantes
|
||||
8. Reordena bullets de experiencia por relevancia al JD
|
||||
9. Construye competency grid (6-8 keyword phrases)
|
||||
10. Inyecta keywords en logros existentes (**NUNCA inventa**)
|
||||
11. Genera HTML completo desde template (lee `templates/cv-template.html`)
|
||||
12. Escribe HTML a `output/cv-candidate-{company-slug}.html` (NO en /tmp — el HTML registrado es la fuente de regeneración del dashboard)
|
||||
13. Ejecuta:
|
||||
```bash
|
||||
node generate-pdf.mjs \
|
||||
output/cv-candidate-{company-slug}.html \
|
||||
output/cv-candidate-{company-slug}-{{DATE}}.pdf \
|
||||
--format={letter|a4} \
|
||||
--report={{REPORT_NUM}}
|
||||
```
|
||||
14. Reporta: ruta PDF, nº páginas, % cobertura keywords
|
||||
|
||||
On success, in Paso 5 use `pdf_emoji` = `✅` and in Paso 6 set `"pdf"` to the output path.
|
||||
|
||||
**Reglas ATS:**
|
||||
- Single-column (sin sidebars)
|
||||
- Headers estándar: "Professional Summary", "Work Experience", "Education", "Skills", "Certifications", "Projects"
|
||||
- Sin texto en imágenes/SVGs
|
||||
- Sin info crítica en headers/footers
|
||||
- UTF-8, texto seleccionable
|
||||
- Keywords distribuidas: Summary (top 5), primer bullet de cada rol, Skills section
|
||||
|
||||
**Diseño:**
|
||||
- Fonts: Space Grotesk (headings, 600-700) + DM Sans (body, 400-500)
|
||||
- Fonts self-hosted: `fonts/`
|
||||
- Header: Space Grotesk 24px bold + gradiente cyan→purple 2px + contacto
|
||||
- Section headers: Space Grotesk 13px uppercase, color cyan `hsl(187,74%,32%)`
|
||||
- Body: DM Sans 11px, line-height 1.5
|
||||
- Company names: purple `hsl(270,70%,45%)`
|
||||
- Márgenes: 0.6in
|
||||
- Background: blanco
|
||||
|
||||
**Estrategia keyword injection (ético):**
|
||||
- Reformular experiencia real con vocabulario exacto del JD
|
||||
- NUNCA añadir skills the candidate doesn't have
|
||||
- Ejemplo: JD dice "RAG pipelines" y CV dice "LLM workflows with retrieval" → "RAG pipeline design and LLM orchestration workflows"
|
||||
|
||||
**Template placeholders (en cv-template.html):**
|
||||
|
||||
| Placeholder | Contenido |
|
||||
|-------------|-----------|
|
||||
| `{{LANG}}` | `en` o `es` |
|
||||
| `{{PAGE_WIDTH}}` | `8.5in` (letter) o `210mm` (A4) |
|
||||
| `{{NAME}}` | (from profile.yml) |
|
||||
| `{{EMAIL}}` | (from profile.yml) |
|
||||
| `{{LINKEDIN_URL}}` | (from profile.yml) |
|
||||
| `{{LINKEDIN_DISPLAY}}` | (from profile.yml) |
|
||||
| `{{PORTFOLIO_URL}}` | (from profile.yml) |
|
||||
| `{{PORTFOLIO_DISPLAY}}` | (from profile.yml) |
|
||||
| `{{LOCATION}}` | (from profile.yml) |
|
||||
| `{{SECTION_SUMMARY}}` | Professional Summary / Resumen Profesional |
|
||||
| `{{SUMMARY_TEXT}}` | Summary personalizado con keywords |
|
||||
| `{{SECTION_COMPETENCIES}}` | Core Competencies / Competencias Core |
|
||||
| `{{COMPETENCIES}}` | `<span class="competency-tag">keyword</span>` × 6-8 |
|
||||
| `{{SECTION_EXPERIENCE}}` | Work Experience / Experiencia Laboral |
|
||||
| `{{EXPERIENCE}}` | HTML de cada trabajo con bullets reordenados |
|
||||
| `{{SECTION_PROJECTS}}` | Projects / Proyectos |
|
||||
| `{{PROJECTS}}` | HTML de top 3-4 proyectos |
|
||||
| `{{SECTION_EDUCATION}}` | Education / Formación |
|
||||
| `{{EDUCATION}}` | HTML de educación |
|
||||
| `{{SECTION_CERTIFICATIONS}}` | Certifications / Certificaciones |
|
||||
| `{{CERTIFICATIONS}}` | HTML de certificaciones |
|
||||
| `{{SECTION_SKILLS}}` | Skills / Competencias |
|
||||
| `{{SKILLS}}` | HTML de skills |
|
||||
|
||||
### Paso 5 — Tracker Line
|
||||
|
||||
Escribir una línea TSV a:
|
||||
```
|
||||
batch/tracker-additions/{{ID}}.tsv
|
||||
```
|
||||
|
||||
Formato TSV (una sola línea, sin header, 9 columnas tab-separated):
|
||||
```
|
||||
{next_num}\t{{DATE}}\t{empresa}\t{rol}\t{status}\t{score}/5\t{pdf_emoji}\t[{{REPORT_NUM}}](reports/{{REPORT_NUM}}-{company-slug}-{{DATE}}.md)\t{nota_1_frase}
|
||||
```
|
||||
|
||||
**Columnas TSV (orden exacto):**
|
||||
|
||||
| # | Campo | Tipo | Ejemplo | Validación |
|
||||
|---|-------|------|---------|------------|
|
||||
| 1 | num | int | `647` | Secuencial, max existente + 1 |
|
||||
| 2 | date | YYYY-MM-DD | `2026-03-14` | Fecha de evaluación |
|
||||
| 3 | company | string | `Datadog` | Nombre corto de empresa |
|
||||
| 4 | role | string | `Staff AI Engineer` | Título del rol |
|
||||
| 5 | status | canonical | `Evaluada` | DEBE ser canónico (ver states.yml) |
|
||||
| 6 | score | X.XX/5 | `4.55/5` | O `N/A` si no evaluable |
|
||||
| 7 | pdf | emoji | `✅` o `❌` | Si se generó PDF |
|
||||
| 8 | report | md link | `[647](reports/647-...)` | Link root-relative; merge-tracker.mjs lo normaliza relativo al tracker (ej. `../reports/...`, #760) |
|
||||
| 9 | notes | string | `APPLY HIGH...` | Resumen 1 frase |
|
||||
|
||||
**IMPORTANTE:** El orden TSV tiene status ANTES de score (col 5→status, col 6→score). En applications.md el orden es inverso (col 5→score, col 6→status). merge-tracker.mjs maneja la conversión.
|
||||
|
||||
**Estados canónicos válidos:** `Evaluada`, `Aplicado`, `Respondido`, `Entrevista`, `Oferta`, `Rechazado`, `Descartado`, `NO APLICAR`
|
||||
|
||||
Donde `{next_num}` se calcula leyendo la última línea de `data/applications.md`.
|
||||
|
||||
### Paso 6 — Output final
|
||||
|
||||
Al terminar, imprime por stdout un resumen JSON para que el orquestador lo parsee:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "{{ID}}",
|
||||
"report_num": "{{REPORT_NUM}}",
|
||||
"company": "{empresa}",
|
||||
"role": "{rol}",
|
||||
"score": {score_num},
|
||||
"legitimacy": "{High Confidence|Proceed with Caution|Suspicious}",
|
||||
"pdf": "{ruta_pdf}",
|
||||
"report": "{ruta_report}",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
Si algo falla:
|
||||
```json
|
||||
{
|
||||
"status": "failed",
|
||||
"id": "{{ID}}",
|
||||
"report_num": "{{REPORT_NUM}}",
|
||||
"company": "{empresa_o_unknown}",
|
||||
"role": "{rol_o_unknown}",
|
||||
"score": null,
|
||||
"pdf": null,
|
||||
"report": "{ruta_report_si_existe}",
|
||||
"error": "{descripción_del_error}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reglas Globales
|
||||
|
||||
### NUNCA
|
||||
1. Inventar experiencia o métricas
|
||||
2. Modificar cv.md, i18n.ts ni archivos del portfolio
|
||||
3. Compartir el teléfono en mensajes generados
|
||||
4. Recomendar comp por debajo de mercado
|
||||
5. Generar PDF sin leer primero el JD
|
||||
6. Usar corporate-speak
|
||||
|
||||
### SIEMPRE
|
||||
1. Leer cv.md, llms.txt y article-digest.md antes de evaluar
|
||||
2. Detectar el arquetipo del rol y adaptar el framing
|
||||
3. Citar líneas exactas del CV cuando haga match
|
||||
4. Usar WebSearch para datos de comp y empresa
|
||||
5. Generar contenido en el idioma del JD (EN default)
|
||||
6. Ser directo y accionable — sin fluff
|
||||
7. Cuando generes texto en inglés (PDF summaries, bullets, STAR stories), usa inglés nativo de tech: frases cortas, verbos de acción, sin passive voice innecesaria, sin "in order to" ni "utilized"
|
||||
@@ -0,0 +1,932 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# career-ops batch runner — standalone orchestrator for claude -p workers
|
||||
# Reads batch-input.tsv, delegates each offer to a claude -p worker,
|
||||
# tracks state in batch-state.tsv for resumability.
|
||||
#
|
||||
# NOTE: This script is Claude Code-specific. It uses claude -p with
|
||||
# --dangerously-skip-permissions and --append-system-prompt-file flags
|
||||
# that are not available in other CLIs. Multi-CLI support is out of scope
|
||||
# for now — contributions welcome.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BATCH_DIR="$SCRIPT_DIR"
|
||||
INPUT_FILE="$BATCH_DIR/batch-input.tsv"
|
||||
STATE_FILE="$BATCH_DIR/batch-state.tsv"
|
||||
PROMPT_FILE="$BATCH_DIR/batch-prompt.md"
|
||||
LOGS_DIR="$BATCH_DIR/logs"
|
||||
TRACKER_DIR="$BATCH_DIR/tracker-additions"
|
||||
REPORTS_DIR="$PROJECT_DIR/reports"
|
||||
APPLICATIONS_FILE="$PROJECT_DIR/data/applications.md"
|
||||
LOCK_FILE="$BATCH_DIR/batch-runner.pid"
|
||||
PAUSE_FILE="$BATCH_DIR/batch-runner.paused"
|
||||
STATE_LOCK_DIR="$BATCH_DIR/.batch-state.lock"
|
||||
STATE_LOCK_PID_FILE="$STATE_LOCK_DIR/pid"
|
||||
STATE_LOCK_TIMEOUT_SECONDS=30
|
||||
MAIN_PID="${BASHPID:-$$}"
|
||||
|
||||
# Defaults
|
||||
PARALLEL=1
|
||||
DRY_RUN=false
|
||||
RETRY_FAILED=false
|
||||
RESUME_PAUSED=false
|
||||
START_FROM=0
|
||||
MAX_RETRIES=2
|
||||
MIN_SCORE=0
|
||||
SKIP_PDF=false
|
||||
MODEL="" # empty = let claude -p use the Claude Max default
|
||||
RATE_LIMIT_SLEEP=300
|
||||
BATCH_PAUSED=false
|
||||
STATUS_ONLY=false
|
||||
WATCH_MODE=false
|
||||
LIMIT=0
|
||||
|
||||
# Return success for non-negative integer or decimal strings.
|
||||
is_decimal_number() {
|
||||
[[ "$1" =~ ^[0-9]+([.][0-9]+)?$ ]]
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
career-ops batch runner — process job offers in batch via claude -p workers
|
||||
Uses your default Claude model (Claude Max subscription).
|
||||
|
||||
Usage: batch-runner.sh [OPTIONS]
|
||||
|
||||
Options:
|
||||
--parallel N Number of parallel workers (default: 1)
|
||||
--dry-run Show what would be processed, don't execute
|
||||
--retry-failed Only retry offers marked as "failed" in state
|
||||
--resume-paused Resume offers paused by a Claude session/rate limit
|
||||
--start-from N Start from offer ID N (skip earlier IDs)
|
||||
--limit N Max number of offers to process in this run
|
||||
--max-retries N Max retry attempts per offer (default: 2)
|
||||
--min-score N Skip PDF/tracker for offers scoring below N (default: 0 = off)
|
||||
--skip-pdf Skip PDF generation entirely (write ❌ in tracker PDF column)
|
||||
--rate-limit-sleep N Seconds to wait before retrying a rate-limited worker
|
||||
(default: 300)
|
||||
--model NAME Claude model passed to `claude -p --model` (default:
|
||||
unset = Claude Max default). Use a cheaper model for
|
||||
large batches, e.g. `--model claude-sonnet-4-6`.
|
||||
--status Show batch progress and a per-job table, then exit
|
||||
--watch Live-refresh progress until the run completes
|
||||
-h, --help Show this help
|
||||
|
||||
Files:
|
||||
batch-input.tsv Input offers (id, url, source, notes)
|
||||
batch-state.tsv Processing state (auto-managed)
|
||||
batch-prompt.md Prompt template for workers
|
||||
logs/ Per-offer logs
|
||||
tracker-additions/ Tracker lines for post-batch merge
|
||||
|
||||
Examples:
|
||||
# Dry run to see pending offers
|
||||
./batch-runner.sh --dry-run
|
||||
|
||||
# Process all pending
|
||||
./batch-runner.sh
|
||||
|
||||
# Retry only failed offers
|
||||
./batch-runner.sh --retry-failed
|
||||
|
||||
# Process 2 at a time starting from ID 10
|
||||
./batch-runner.sh --parallel 2 --start-from 10
|
||||
USAGE
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--parallel) PARALLEL="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--retry-failed) RETRY_FAILED=true; shift ;;
|
||||
--resume-paused) RESUME_PAUSED=true; shift ;;
|
||||
--start-from) START_FROM="$2"; shift 2 ;;
|
||||
--limit) LIMIT="$2"; shift 2 ;;
|
||||
--max-retries) MAX_RETRIES="$2"; shift 2 ;;
|
||||
--min-score) MIN_SCORE="$2"; shift 2 ;;
|
||||
--skip-pdf) SKIP_PDF=true; shift ;;
|
||||
--rate-limit-sleep)
|
||||
[[ $# -ge 2 ]] || { echo "ERROR: --rate-limit-sleep requires an argument"; exit 1; }
|
||||
RATE_LIMIT_SLEEP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--model) MODEL="$2"; shift 2 ;;
|
||||
--status) STATUS_ONLY=true; shift ;;
|
||||
--watch) WATCH_MODE=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1"; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$RATE_LIMIT_SLEEP" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: --rate-limit-sleep must be a non-negative integer (seconds)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! is_decimal_number "$MIN_SCORE"; then
|
||||
echo "ERROR: --min-score must be a non-negative number."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$LIMIT" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: --limit must be a non-negative integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Lock file to prevent double execution
|
||||
acquire_lock() {
|
||||
if [[ -f "$LOCK_FILE" ]]; then
|
||||
local old_pid
|
||||
old_pid=$(cat "$LOCK_FILE")
|
||||
if kill -0 "$old_pid" 2>/dev/null; then
|
||||
echo "ERROR: Another batch-runner is already running (PID $old_pid)"
|
||||
echo "If this is stale, remove $LOCK_FILE"
|
||||
exit 1
|
||||
else
|
||||
echo "WARN: Stale lock file found (PID $old_pid not running). Removing."
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
fi
|
||||
echo "$MAIN_PID" > "$LOCK_FILE"
|
||||
}
|
||||
|
||||
release_lock() {
|
||||
if [[ "${BASHPID:-$$}" != "$MAIN_PID" ]]; then
|
||||
return
|
||||
fi
|
||||
rm -f "$LOCK_FILE"
|
||||
}
|
||||
|
||||
trap release_lock EXIT
|
||||
|
||||
# Validate prerequisites
|
||||
check_prerequisites() {
|
||||
if [[ ! -f "$INPUT_FILE" ]]; then
|
||||
echo "ERROR: $INPUT_FILE not found. Add offers first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PROMPT_FILE" ]]; then
|
||||
echo "ERROR: $PROMPT_FILE not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v claude &>/dev/null; then
|
||||
echo "ERROR: 'claude' CLI not found in PATH."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LOGS_DIR" "$TRACKER_DIR" "$REPORTS_DIR"
|
||||
}
|
||||
|
||||
# Status/watch mode only needs prior batch state, not worker prerequisites.
|
||||
check_status_prerequisites() {
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "No state file found at $STATE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize state file if it doesn't exist
|
||||
init_state() {
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
printf 'id\turl\tstatus\tstarted_at\tcompleted_at\treport_num\tscore\terror\tretries\n' > "$STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
acquire_state_lock() {
|
||||
if [[ "${STATE_LOCK_DISABLED:-0}" -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local waited=0
|
||||
local max_waits=$((STATE_LOCK_TIMEOUT_SECONDS * 10))
|
||||
|
||||
while true; do
|
||||
if mkdir "$STATE_LOCK_DIR" 2>/dev/null; then
|
||||
if printf '%s\n' "${BASHPID:-$$}" > "$STATE_LOCK_PID_FILE"; then
|
||||
STATE_LOCK_OWNED=1
|
||||
return 0
|
||||
fi
|
||||
rm -f "$STATE_LOCK_PID_FILE" 2>/dev/null || true
|
||||
rmdir "$STATE_LOCK_DIR" 2>/dev/null || true
|
||||
echo "ERROR: Failed to initialize state lock metadata at $STATE_LOCK_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$STATE_LOCK_DIR" ]]; then
|
||||
if (( PARALLEL <= 1 )); then
|
||||
echo "WARN: State lock creation failed. Falling back to lock-free operation (single-worker mode)." >&2
|
||||
STATE_LOCK_DISABLED=1
|
||||
STATE_LOCK_OWNED=0
|
||||
return 0
|
||||
fi
|
||||
echo "ERROR: Failed to create state lock directory $STATE_LOCK_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f "$STATE_LOCK_PID_FILE" ]]; then
|
||||
local lock_pid
|
||||
lock_pid=$(cat "$STATE_LOCK_PID_FILE" 2>/dev/null || true)
|
||||
if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
||||
rm -f "$STATE_LOCK_PID_FILE"
|
||||
if rmdir "$STATE_LOCK_DIR" 2>/dev/null; then
|
||||
echo "WARN: Recovered stale state lock (PID $lock_pid not running)."
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( waited >= max_waits )); then
|
||||
echo "ERROR: Timed out waiting for state lock at $STATE_LOCK_DIR"
|
||||
echo "If no batch-runner worker is active, remove the stale lock directory."
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 0.1
|
||||
((waited += 1))
|
||||
done
|
||||
}
|
||||
|
||||
release_state_lock() {
|
||||
if [[ "${STATE_LOCK_OWNED:-0}" -ne 1 ]]; then
|
||||
return
|
||||
fi
|
||||
rm -f "$STATE_LOCK_PID_FILE" 2>/dev/null || true
|
||||
rmdir "$STATE_LOCK_DIR" 2>/dev/null || true
|
||||
STATE_LOCK_OWNED=0
|
||||
}
|
||||
|
||||
run_with_state_lock() {
|
||||
acquire_state_lock || return $?
|
||||
|
||||
local status=0
|
||||
if "$@"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
|
||||
release_state_lock
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# Get status of an offer from state file
|
||||
get_status() {
|
||||
local id="$1"
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "none"
|
||||
return
|
||||
fi
|
||||
local status
|
||||
status=$(awk -F'\t' -v id="$id" '$1 == id { print $3 }' "$STATE_FILE")
|
||||
echo "${status:-none}"
|
||||
}
|
||||
|
||||
# Get retry count for an offer
|
||||
get_retries() {
|
||||
local id="$1"
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "0"
|
||||
return
|
||||
fi
|
||||
local retries
|
||||
retries=$(awk -F'\t' -v id="$id" '$1 == id { print $9 }' "$STATE_FILE")
|
||||
echo "${retries:-0}"
|
||||
}
|
||||
|
||||
# Calculate next report number.
|
||||
# Caller must hold STATE_LOCK_DIR while this runs.
|
||||
next_report_num_unlocked() {
|
||||
local max_num=0
|
||||
if [[ -d "$REPORTS_DIR" ]]; then
|
||||
for f in "$REPORTS_DIR"/*.md; do
|
||||
[[ -f "$f" ]] || continue
|
||||
local basename
|
||||
basename=$(basename "$f")
|
||||
local num="${basename%%-*}"
|
||||
num=$((10#$num)) # Remove leading zeros for arithmetic
|
||||
if (( num > max_num )); then
|
||||
max_num=$num
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Also check state file for assigned report numbers
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
while IFS=$'\t' read -r _ _ _ _ _ rnum _ _ _; do
|
||||
[[ "$rnum" == "report_num" || "$rnum" == "-" || -z "$rnum" ]] && continue
|
||||
local n=$((10#$rnum))
|
||||
if (( n > max_num )); then
|
||||
max_num=$n
|
||||
fi
|
||||
done < "$STATE_FILE"
|
||||
fi
|
||||
printf '%03d' $((max_num + 1))
|
||||
}
|
||||
|
||||
# Update or insert state for an offer.
|
||||
# Caller must hold STATE_LOCK_DIR while this runs.
|
||||
update_state_unlocked() {
|
||||
local id="$1" url="$2" status="$3" started="$4" completed="$5" report_num="$6" score="$7" error="$8" retries="$9"
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
init_state
|
||||
fi
|
||||
|
||||
local tmp="$STATE_FILE.tmp"
|
||||
local found=false
|
||||
|
||||
# Write header
|
||||
head -1 "$STATE_FILE" > "$tmp"
|
||||
|
||||
# Process existing lines
|
||||
while IFS=$'\t' read -r sid surl sstatus sstarted scompleted sreport sscore serror sretries; do
|
||||
[[ "$sid" == "id" ]] && continue # skip header
|
||||
if [[ "$sid" == "$id" ]]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$id" "$url" "$status" "$started" "$completed" "$report_num" "$score" "$error" "$retries" >> "$tmp"
|
||||
found=true
|
||||
else
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$sid" "$surl" "$sstatus" "$sstarted" "$scompleted" "$sreport" "$sscore" "$serror" "$sretries" >> "$tmp"
|
||||
fi
|
||||
done < "$STATE_FILE"
|
||||
|
||||
if [[ "$found" == "false" ]]; then
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$id" "$url" "$status" "$started" "$completed" "$report_num" "$score" "$error" "$retries" >> "$tmp"
|
||||
fi
|
||||
|
||||
mv "$tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
update_state() {
|
||||
run_with_state_lock update_state_unlocked "$@"
|
||||
}
|
||||
|
||||
is_rate_limit_log() {
|
||||
local log_file="$1"
|
||||
grep -Eiq '(rate limit|rate_limit|too many requests|429|quota exceeded|try again later|temporarily unavailable)' "$log_file"
|
||||
}
|
||||
|
||||
is_session_limit_log() {
|
||||
local log_file="$1"
|
||||
grep -Eiq '(session limit|resets [0-9:]+[ap]m|usage limit|limit[[:space:]]+reached)' "$log_file"
|
||||
}
|
||||
|
||||
mark_paused_rate_limit() {
|
||||
local id="$1" url="$2" started_at="$3" report_num="$4" retries="$5" log_file="$6"
|
||||
local completed_at
|
||||
completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
local error_msg
|
||||
error_msg=$(tail -5 "$log_file" 2>/dev/null | tr '\n' ' ' | cut -c1-200 || echo "session/rate limit reached")
|
||||
update_state "$id" "$url" "paused_rate_limit" "$started_at" "$completed_at" "$report_num" "-" "$error_msg" "$retries"
|
||||
printf '%s\t%s\t%s\n' "$id" "$report_num" "$error_msg" > "$PAUSE_FILE"
|
||||
BATCH_PAUSED=true
|
||||
}
|
||||
|
||||
reserve_report_num_unlocked() {
|
||||
local id="$1" url="$2" started="$3" retries="$4"
|
||||
|
||||
local report_num=""
|
||||
if report_num=$(next_report_num_unlocked); then
|
||||
update_state_unlocked "$id" "$url" "processing" "$started" "-" "$report_num" "-" "-" "$retries"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$report_num"
|
||||
}
|
||||
|
||||
reserve_report_num() {
|
||||
run_with_state_lock reserve_report_num_unlocked "$@"
|
||||
}
|
||||
|
||||
# Process a single offer
|
||||
process_offer() {
|
||||
local id="$1" url="$2" source="$3" notes="$4"
|
||||
|
||||
local started_at
|
||||
started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
local retries
|
||||
retries=$(get_retries "$id")
|
||||
local report_num
|
||||
report_num=$(reserve_report_num "$id" "$url" "$started_at" "$retries")
|
||||
local date
|
||||
date=$(date +%Y-%m-%d)
|
||||
# Use mktemp instead of a predictable /tmp path: a fixed name like
|
||||
# /tmp/batch-jd-${id}.txt is guessable, so an attacker on a shared machine
|
||||
# could pre-create it as a symlink and redirect or clobber the write.
|
||||
local jd_file
|
||||
jd_file="$(mktemp "${TMPDIR:-/tmp}/batch-jd-${id}.XXXXXX")"
|
||||
|
||||
echo "--- Processing offer #$id: $url (report $report_num, attempt $((retries + 1)))"
|
||||
|
||||
# Build the prompt with placeholders replaced
|
||||
local prompt
|
||||
if [[ "$SKIP_PDF" == "true" ]]; then
|
||||
prompt="Procesa esta oferta de empleo. Ejecuta el pipeline: evaluación A-F + report .md + tracker line. NO generes PDF; en el tracker escribe ❌ en la columna PDF y en el JSON final establece \"pdf\": null."
|
||||
echo " ⏭️ --skip-pdf set — skipping PDF generation for #$id ($url)"
|
||||
else
|
||||
prompt="Procesa esta oferta de empleo. Ejecuta el pipeline completo: evaluación A-F + report .md + PDF + tracker line."
|
||||
fi
|
||||
prompt="$prompt URL: $url"
|
||||
prompt="$prompt JD file: $jd_file"
|
||||
prompt="$prompt Report number: $report_num"
|
||||
prompt="$prompt Date: $date"
|
||||
prompt="$prompt Batch ID: $id"
|
||||
|
||||
local log_file="$LOGS_DIR/${report_num}-${id}.log"
|
||||
|
||||
# Prepare system prompt with placeholders resolved
|
||||
local resolved_prompt="$BATCH_DIR/.resolved-prompt-${id}.md"
|
||||
# Escape sed delimiter characters in variables to prevent substitution breakage
|
||||
local esc_url esc_jd_file esc_report_num esc_date esc_id
|
||||
esc_url="${url//\\/\\\\}"
|
||||
esc_url="${esc_url//|/\\|}"
|
||||
esc_jd_file="${jd_file//\\/\\\\}"
|
||||
esc_jd_file="${esc_jd_file//|/\\|}"
|
||||
esc_report_num="${report_num//|/\\|}"
|
||||
esc_date="${date//|/\\|}"
|
||||
esc_id="${id//|/\\|}"
|
||||
sed \
|
||||
-e "s|{{URL}}|${esc_url}|g" \
|
||||
-e "s|{{JD_FILE}}|${esc_jd_file}|g" \
|
||||
-e "s|{{REPORT_NUM}}|${esc_report_num}|g" \
|
||||
-e "s|{{DATE}}|${esc_date}|g" \
|
||||
-e "s|{{ID}}|${esc_id}|g" \
|
||||
"$PROMPT_FILE" > "$resolved_prompt"
|
||||
|
||||
# Inject user-layer personalization into the temporary worker prompt.
|
||||
# The resolved prompt is gitignored runtime state, so user profile data stays
|
||||
# out of the system layer while batch scoring matches interactive scoring.
|
||||
for context_file in "$PROJECT_DIR/modes/_profile.md" "$PROJECT_DIR/config/profile.yml"; do
|
||||
if [[ -f "$context_file" ]]; then
|
||||
{
|
||||
printf '\n\n---\n\n'
|
||||
printf '## Runtime personalization: %s\n\n' "${context_file#$PROJECT_DIR/}"
|
||||
sed 's/^/ /' "$context_file"
|
||||
printf '\n'
|
||||
} >> "$resolved_prompt"
|
||||
fi
|
||||
done
|
||||
|
||||
# Launch claude -p worker.
|
||||
# Model defaults to the Claude Max subscription default unless --model was
|
||||
# passed. Building the command in an array keeps quoting safe regardless.
|
||||
# --strict-mcp-config (with no --mcp-config) starts workers with no MCP
|
||||
# servers: they only evaluate offers and need none. Without it each parallel
|
||||
# worker inherits the parent session's MCP (e.g. Playwright) and they deadlock
|
||||
# fighting over the single shared browser when --parallel > 1 (issue #506).
|
||||
local -a claude_args=(-p --dangerously-skip-permissions --strict-mcp-config)
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
claude_args+=(--model "$MODEL")
|
||||
fi
|
||||
claude_args+=(--append-system-prompt-file "$resolved_prompt" "$prompt")
|
||||
|
||||
local exit_code=0
|
||||
local terminal_failure_recorded=false
|
||||
local shim_retries=0
|
||||
local max_shim_retries=4
|
||||
while true; do
|
||||
exit_code=0
|
||||
claude "${claude_args[@]}" > "$log_file" 2>&1 || exit_code=$?
|
||||
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Check for Claude Code npm shim swap (exit code 127 + command not found)
|
||||
if [[ $exit_code -eq 127 ]] && grep -qE "(claude: command not found|claude:.*not found|cannot find.*claude)" "$log_file" && (( shim_retries < max_shim_retries )); then
|
||||
shim_retries=$((shim_retries + 1))
|
||||
echo " ⏳ Claude command not found (shim swap detected). Retrying in 30s (attempt $shim_retries/$max_shim_retries)..."
|
||||
sleep 30
|
||||
continue
|
||||
fi
|
||||
|
||||
if is_session_limit_log "$log_file"; then
|
||||
mark_paused_rate_limit "$id" "$url" "$started_at" "$report_num" "$retries" "$log_file"
|
||||
echo " ⏸️ Session/rate limit reached; pausing batch without consuming retry budget."
|
||||
terminal_failure_recorded=true
|
||||
break
|
||||
fi
|
||||
|
||||
if is_rate_limit_log "$log_file" && (( retries < MAX_RETRIES )); then
|
||||
if (( RATE_LIMIT_SLEEP <= 0 )); then
|
||||
mark_paused_rate_limit "$id" "$url" "$started_at" "$report_num" "$retries" "$log_file"
|
||||
echo " ⏸️ Rate limited and --rate-limit-sleep is 0; pausing batch without consuming retry budget."
|
||||
terminal_failure_recorded=true
|
||||
break
|
||||
fi
|
||||
retries=$((retries + 1))
|
||||
local retry_completed_at
|
||||
retry_completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
update_state "$id" "$url" "rate_limited" "$started_at" "$retry_completed_at" "$report_num" "-" "rate-limit; retrying after ${RATE_LIMIT_SLEEP}s" "$retries"
|
||||
echo " ⏳ Rate limited (attempt $retries/$MAX_RETRIES). Waiting ${RATE_LIMIT_SLEEP}s before retry..."
|
||||
sleep "$RATE_LIMIT_SLEEP"
|
||||
continue
|
||||
fi
|
||||
|
||||
break
|
||||
done
|
||||
|
||||
# Cleanup resolved prompt
|
||||
rm -f "$resolved_prompt"
|
||||
|
||||
local completed_at
|
||||
completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
# Try to extract score from worker output
|
||||
local score="-"
|
||||
local score_match
|
||||
score_match=$(sed -nE 's/.*"score":[[:space:]]*([0-9.]+).*/\1/p' "$log_file" 2>/dev/null | head -1 || true)
|
||||
if [[ -n "$score_match" ]]; then
|
||||
score="$score_match"
|
||||
fi
|
||||
|
||||
# Check min-score gate
|
||||
if is_decimal_number "$score" && awk -v min="$MIN_SCORE" 'BEGIN{exit !(min > 0)}'; then
|
||||
if awk -v score="$score" -v min="$MIN_SCORE" 'BEGIN{exit !(score < min)}'; then
|
||||
update_state "$id" "$url" "skipped" "$started_at" "$completed_at" "$report_num" "$score" "below-min-score" "$retries"
|
||||
echo " ⏭️ Skipped (score: $score < min-score: $MIN_SCORE)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
update_state "$id" "$url" "completed" "$started_at" "$completed_at" "$report_num" "$score" "-" "$retries"
|
||||
echo " ✅ Completed (score: $score, report: $report_num)"
|
||||
elif [[ "$terminal_failure_recorded" == "false" ]]; then
|
||||
if (( retries < MAX_RETRIES )); then
|
||||
retries=$((retries + 1))
|
||||
fi
|
||||
local error_msg
|
||||
error_msg=$(tail -5 "$log_file" 2>/dev/null | tr '\n' ' ' | cut -c1-200 || echo "Unknown error (exit code $exit_code)")
|
||||
update_state "$id" "$url" "failed" "$started_at" "$completed_at" "$report_num" "-" "$error_msg" "$retries"
|
||||
echo " ❌ Failed (attempt $retries, exit code $exit_code)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Merge tracker additions into applications.md
|
||||
merge_tracker() {
|
||||
echo ""
|
||||
echo "=== Merging tracker additions ==="
|
||||
node "$PROJECT_DIR/merge-tracker.mjs"
|
||||
echo ""
|
||||
echo "=== Reconciling pipeline.md ==="
|
||||
node "$PROJECT_DIR/reconcile-pipeline.mjs" || echo "⚠️ Pipeline reconcile had issues (see above)"
|
||||
echo ""
|
||||
echo "=== Verifying pipeline integrity ==="
|
||||
node "$PROJECT_DIR/verify-pipeline.mjs" || echo "⚠️ Verification found issues (see above)"
|
||||
}
|
||||
|
||||
# Print summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "=== Batch Summary ==="
|
||||
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "No state file found."
|
||||
return
|
||||
fi
|
||||
|
||||
local total=0 completed=0 skipped=0 failed=0 pending=0
|
||||
local score_sum=0 score_count=0
|
||||
|
||||
while IFS=$'\t' read -r sid _ sstatus _ _ _ sscore _ _; do
|
||||
[[ "$sid" == "id" ]] && continue
|
||||
total=$((total + 1))
|
||||
case "$sstatus" in
|
||||
completed) completed=$((completed + 1))
|
||||
if is_decimal_number "$sscore"; then
|
||||
score_sum=$(awk -v sum="$score_sum" -v score="$sscore" 'BEGIN{print sum + score}' 2>/dev/null || echo "$score_sum")
|
||||
score_count=$((score_count + 1))
|
||||
fi
|
||||
;;
|
||||
skipped) skipped=$((skipped + 1)) ;;
|
||||
failed) failed=$((failed + 1)) ;;
|
||||
*) pending=$((pending + 1)) ;;
|
||||
esac
|
||||
done < "$STATE_FILE"
|
||||
|
||||
echo "Total: $total | Completed: $completed | Skipped: $skipped | Failed: $failed | Pending: $pending"
|
||||
|
||||
if (( score_count > 0 )); then
|
||||
local avg
|
||||
avg=$(awk -v sum="$score_sum" -v count="$score_count" 'BEGIN{printf "%.1f", sum / count}' 2>/dev/null || echo "N/A")
|
||||
echo "Average score: $avg/5 ($score_count scored)"
|
||||
fi
|
||||
}
|
||||
|
||||
print_status_table() {
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
echo "No state file found at $STATE_FILE"
|
||||
return
|
||||
fi
|
||||
|
||||
local total=0 completed=0 processing=0 failed=0 pending=0 skipped=0 rate_limited=0 paused_rate_limit=0
|
||||
local score_sum=0 score_count=0
|
||||
|
||||
# Read first line to skip header
|
||||
local header=true
|
||||
while IFS=$'\t' read -r sid surl sstatus sstarted scompleted sreport sscore serror sretries || [[ -n "$sid" ]]; do
|
||||
if [[ "$header" == "true" ]]; then
|
||||
header=false
|
||||
continue
|
||||
fi
|
||||
[[ -z "$sid" ]] && continue
|
||||
sstatus="${sstatus%$'\r'}"
|
||||
sscore="${sscore%$'\r'}"
|
||||
serror="${serror%$'\r'}"
|
||||
sreport="${sreport%$'\r'}"
|
||||
total=$((total + 1))
|
||||
case "$sstatus" in
|
||||
completed)
|
||||
completed=$((completed + 1))
|
||||
if is_decimal_number "$sscore"; then
|
||||
score_sum=$(awk -v sum="$score_sum" -v score="$sscore" 'BEGIN{print sum + score}' 2>/dev/null || echo "$score_sum")
|
||||
score_count=$((score_count + 1))
|
||||
fi
|
||||
;;
|
||||
processing) processing=$((processing + 1)) ;;
|
||||
failed) failed=$((failed + 1)) ;;
|
||||
skipped) skipped=$((skipped + 1)) ;;
|
||||
rate_limited) rate_limited=$((rate_limited + 1)) ;;
|
||||
paused_rate_limit) paused_rate_limit=$((paused_rate_limit + 1)) ;;
|
||||
*) pending=$((pending + 1)) ;;
|
||||
esac
|
||||
done < "$STATE_FILE"
|
||||
|
||||
echo "=== Batch Progress ==="
|
||||
echo "Total: $total | Completed: $completed | Processing: $processing | Failed: $failed | Pending: $pending | Skipped: $skipped | Rate Limited: $rate_limited | Paused: $paused_rate_limit"
|
||||
if (( score_count > 0 )); then
|
||||
local avg
|
||||
avg=$(awk -v sum="$score_sum" -v count="$score_count" 'BEGIN{printf "%.1f", sum / count}' 2>/dev/null || echo "N/A")
|
||||
echo "Average score: $avg/5 ($score_count scored)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Format the per-job table:
|
||||
# Columns: ID, Status, Report, Score, Target (URL or Error Message)
|
||||
printf "%-4s | %-17s | %-6s | %-5s | %-40s\n" "ID" "Status" "Report" "Score" "URL / Error"
|
||||
printf "%-4s+%-19s+%-8s+%-7s+%-42s\n" "----" "-------------------" "--------" "-------" "------------------------------------------"
|
||||
|
||||
header=true
|
||||
while IFS=$'\t' read -r sid surl sstatus sstarted scompleted sreport sscore serror sretries || [[ -n "$sid" ]]; do
|
||||
if [[ "$header" == "true" ]]; then
|
||||
header=false
|
||||
continue
|
||||
fi
|
||||
[[ -z "$sid" ]] && continue
|
||||
sstatus="${sstatus%$'\r'}"
|
||||
sscore="${sscore%$'\r'}"
|
||||
serror="${serror%$'\r'}"
|
||||
sreport="${sreport%$'\r'}"
|
||||
local target="$surl"
|
||||
if [[ "$sstatus" == "failed" && -n "$serror" && "$serror" != "-" ]]; then
|
||||
target="Error: $serror"
|
||||
fi
|
||||
# Trim target to fit nicely (e.g. 50 chars)
|
||||
if (( ${#target} > 50 )); then
|
||||
target="${target:0:47}..."
|
||||
fi
|
||||
printf "%-4s | %-17s | %-6s | %-5s | %-50s\n" "$sid" "$sstatus" "$sreport" "$sscore" "$target"
|
||||
done < "$STATE_FILE"
|
||||
}
|
||||
|
||||
watch_status() {
|
||||
local active_pid=""
|
||||
if [[ -f "$LOCK_FILE" ]]; then
|
||||
active_pid=$(cat "$LOCK_FILE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -n "$active_pid" ]] && kill -0 "$active_pid" 2>/dev/null; then
|
||||
echo "Watching batch-runner (PID $active_pid)... Press Ctrl+C to stop."
|
||||
while kill -0 "$active_pid" 2>/dev/null; do
|
||||
clear || printf "\033[c"
|
||||
echo "=== Watching Batch Progress (PID $active_pid) ==="
|
||||
print_status_table
|
||||
sleep 2
|
||||
done
|
||||
echo ""
|
||||
echo "=== Batch runner process (PID $active_pid) has finished ==="
|
||||
else
|
||||
echo "No active batch-runner detected."
|
||||
fi
|
||||
|
||||
echo "Showing final status:"
|
||||
print_status_table
|
||||
|
||||
# Chain verify-pipeline.mjs
|
||||
if [[ -f "$PROJECT_DIR/verify-pipeline.mjs" ]]; then
|
||||
echo ""
|
||||
echo "=== Running pipeline verification ==="
|
||||
node "$PROJECT_DIR/verify-pipeline.mjs" || echo "⚠️ Verification found issues"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
if [[ "$STATUS_ONLY" == "true" ]]; then
|
||||
check_status_prerequisites
|
||||
print_status_table
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$WATCH_MODE" == "true" ]]; then
|
||||
check_status_prerequisites
|
||||
watch_status
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_prerequisites
|
||||
|
||||
if [[ "$DRY_RUN" == "false" ]]; then
|
||||
acquire_lock
|
||||
rm -f "$PAUSE_FILE"
|
||||
fi
|
||||
|
||||
init_state
|
||||
|
||||
# Count input offers (skip header, ignore blank lines)
|
||||
local total_input
|
||||
total_input=$(tail -n +2 "$INPUT_FILE" | grep -c '[^[:space:]]' 2>/dev/null || true)
|
||||
total_input="${total_input:-0}"
|
||||
|
||||
if (( total_input == 0 )); then
|
||||
echo "No offers in $INPUT_FILE. Add offers first."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "=== career-ops batch runner ==="
|
||||
if (( LIMIT > 0 )); then
|
||||
echo "Parallel: $PARALLEL | Max retries: $MAX_RETRIES | Limit: $LIMIT"
|
||||
else
|
||||
echo "Parallel: $PARALLEL | Max retries: $MAX_RETRIES"
|
||||
fi
|
||||
echo "Input: $total_input offers"
|
||||
echo ""
|
||||
|
||||
# Build list of offers to process
|
||||
local -a pending_ids=()
|
||||
local -a pending_urls=()
|
||||
local -a pending_sources=()
|
||||
local -a pending_notes=()
|
||||
|
||||
while IFS=$'\t' read -r id url source notes; do
|
||||
[[ "$id" == "id" ]] && continue # skip header
|
||||
[[ -z "$id" || -z "$url" ]] && continue
|
||||
|
||||
# Guard against non-numeric id values
|
||||
[[ "$id" =~ ^[0-9]+$ ]] || continue
|
||||
|
||||
# Skip if before start-from
|
||||
if (( id < START_FROM )); then
|
||||
continue
|
||||
fi
|
||||
|
||||
local status
|
||||
status=$(get_status "$id")
|
||||
|
||||
if [[ "$RESUME_PAUSED" == "true" ]]; then
|
||||
if [[ "$status" != "paused_rate_limit" ]]; then
|
||||
continue
|
||||
fi
|
||||
elif [[ "$RETRY_FAILED" == "true" ]]; then
|
||||
# Only process failed offers
|
||||
if [[ "$status" != "failed" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Check retry limit
|
||||
local retries
|
||||
retries=$(get_retries "$id")
|
||||
if (( retries >= MAX_RETRIES )); then
|
||||
echo "SKIP #$id: max retries ($MAX_RETRIES) reached"
|
||||
continue
|
||||
fi
|
||||
else
|
||||
# Skip terminal offers
|
||||
if [[ "$status" == "completed" || "$status" == "skipped" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Paused rate-limit offers resume explicitly with --resume-paused.
|
||||
if [[ "$status" == "paused_rate_limit" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Skip failed offers that hit retry limit (unless --retry-failed)
|
||||
if [[ "$status" == "failed" ]]; then
|
||||
local retries
|
||||
retries=$(get_retries "$id")
|
||||
if (( retries >= MAX_RETRIES )); then
|
||||
echo "SKIP #$id: failed and max retries reached (use --retry-failed to force)"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( LIMIT > 0 )) && (( ${#pending_ids[@]} >= LIMIT )); then
|
||||
break
|
||||
fi
|
||||
|
||||
pending_ids+=("$id")
|
||||
pending_urls+=("$url")
|
||||
pending_sources+=("$source")
|
||||
pending_notes+=("$notes")
|
||||
done < "$INPUT_FILE"
|
||||
|
||||
local pending_count=${#pending_ids[@]}
|
||||
|
||||
if (( pending_count == 0 )); then
|
||||
echo "No offers to process."
|
||||
print_summary
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Pending: $pending_count offers"
|
||||
echo ""
|
||||
|
||||
# Dry run: just list
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "=== DRY RUN (no processing) ==="
|
||||
for i in "${!pending_ids[@]}"; do
|
||||
local status
|
||||
status=$(get_status "${pending_ids[$i]}")
|
||||
echo " #${pending_ids[$i]}: ${pending_urls[$i]} [${pending_sources[$i]}] (status: $status)"
|
||||
done
|
||||
echo ""
|
||||
echo "Would process $pending_count offers"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Process offers
|
||||
if (( PARALLEL <= 1 )); then
|
||||
# Sequential processing
|
||||
for i in "${!pending_ids[@]}"; do
|
||||
process_offer "${pending_ids[$i]}" "${pending_urls[$i]}" "${pending_sources[$i]}" "${pending_notes[$i]}"
|
||||
if [[ "$BATCH_PAUSED" == "true" || -f "$PAUSE_FILE" ]]; then
|
||||
echo "=== Batch paused: session/rate limit reached. Resume later with --resume-paused. ==="
|
||||
break
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Parallel processing with job control
|
||||
local running=0
|
||||
local -a pids=()
|
||||
local -a pid_ids=()
|
||||
|
||||
for i in "${!pending_ids[@]}"; do
|
||||
if [[ "$BATCH_PAUSED" == "true" || -f "$PAUSE_FILE" ]]; then
|
||||
echo "=== Batch paused: session/rate limit reached. Waiting for running workers, not scheduling new offers. ==="
|
||||
break
|
||||
fi
|
||||
|
||||
# Wait if we're at parallel limit
|
||||
while (( running >= PARALLEL )); do
|
||||
# Wait for any child to finish
|
||||
for j in "${!pids[@]}"; do
|
||||
if ! kill -0 "${pids[$j]}" 2>/dev/null; then
|
||||
wait "${pids[$j]}" 2>/dev/null || true
|
||||
unset 'pids[j]'
|
||||
unset 'pid_ids[j]'
|
||||
running=$((running - 1))
|
||||
fi
|
||||
done
|
||||
# Compact arrays
|
||||
pids=("${pids[@]}")
|
||||
pid_ids=("${pid_ids[@]}")
|
||||
if [[ "$BATCH_PAUSED" == "true" || -f "$PAUSE_FILE" ]]; then
|
||||
echo "=== Batch paused: session/rate limit reached. Waiting for running workers, not scheduling new offers. ==="
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "$BATCH_PAUSED" == "true" || -f "$PAUSE_FILE" ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Launch worker in background
|
||||
process_offer "${pending_ids[$i]}" "${pending_urls[$i]}" "${pending_sources[$i]}" "${pending_notes[$i]}" &
|
||||
pids+=($!)
|
||||
pid_ids+=("${pending_ids[$i]}")
|
||||
running=$((running + 1))
|
||||
done
|
||||
|
||||
# Wait for remaining workers
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Merge tracker additions
|
||||
merge_tracker
|
||||
|
||||
# Print summary
|
||||
print_summary
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
id url status started_at completed_at report_num score error retries
|
||||
1 https://getmatch.ru/vacancies/34414-starshii-frontend-razrabotchik?s=offers completed 2026-07-08T04:20:29Z 2026-07-08T04:23:49Z 066 - - 0
|
||||
3 https://hh.ru/vacancy/133613060?query=frontend&hhtmFrom=vacancy_search_list completed 2026-07-08T04:20:29Z 2026-07-08T04:22:50Z 067 1.0 - 0
|
||||
2 https://hirehi.ru/development/frontend-razrabotchik-55966 completed 2026-07-08T04:20:29Z 2026-07-08T04:23:38Z 068 1.2 - 0
|
||||
4 https://hh.ru/vacancy/133056126?query=frontend&hhtmFrom=vacancy_search_list completed 2026-07-08T04:22:52Z 2026-07-08T04:25:32Z 069 1.2 - 0
|
||||
5 https://getmatch.ru/vacancies/35037-frontend-software-engineer-bootcamp?s=offers completed 2026-07-08T04:23:40Z 2026-07-08T04:26:23Z 070 1.0 - 0
|
||||
6 https://career.habr.com/vacancies/1000167021 completed 2026-07-08T04:23:51Z 2026-07-08T04:26:50Z 071 1.8 - 0
|
||||
7 https://hh.ru/vacancy/119867578?query=frontend&hhtmFrom=vacancy_search_list completed 2026-07-08T04:25:34Z 2026-07-08T04:29:23Z 072 1.7 - 0
|
||||
8 https://t.me/s/runello_rus_javascript/1443 completed 2026-07-08T04:26:24Z 2026-07-08T04:27:23Z 073 - - 0
|
||||
9 https://t.me/s/runello_rus_javascript/1445 completed 2026-07-08T04:26:51Z 2026-07-08T04:29:25Z 074 - - 0
|
||||
10 https://t.me/s/runello_rus_javascript/1448 completed 2026-07-08T04:27:24Z 2026-07-08T04:31:35Z 075 3.0 - 0
|
||||
|
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
||||
**VK — Старший Frontend-разработчик (React): Score 1.0/5, Skip.**
|
||||
|
||||
Hallazgo clave: al hacer WebFetch de la URL, la fuente indica explícitamente que **la vacante está archivada** (VK ya no busca candidatos) — el JD file estaba vacío, así que este dato solo salió al intentar el fallback. Además de eso, dos bloqueadores estructurales independientes del estado de la vacante:
|
||||
- Stack **React** puro (hooks, context, state management) — sin coincidencia con la especialización Vue 3/Flutter del perfil.
|
||||
- Nivel **Senior 5+ años** vs ~1.5-2 años de experiencia real de la candidata.
|
||||
|
||||
Comp era en realidad excelente (300k+₽, por encima del rango objetivo), pero irrelevante dado lo anterior. Report guardado en `reports/066-vk-2026-07-08.md`, línea de tracker en `batch/tracker-additions/066-vk.tsv` (próximo número: 16), PDF no generado (score bajo el umbral de 3.0).
|
||||
@@ -0,0 +1,32 @@
|
||||
## Resultado: oferta duplicada, no se generó nueva evaluación
|
||||
|
||||
Esta URL (`https://hh.ru/vacancy/133613060?query=frontend&hhtmFrom=vacancy_search_list`) ya fue evaluada ayer como **report 053** — es la vacante exacta de **Студия Перцы** (Ярославль), programa de trainee para principiantes:
|
||||
|
||||
- Стек: React + Sass (verstka/animación para juegos/promo-sitios), no Vue
|
||||
- Nivel: sin experiencia requerida (programa para estudiantes/graduados) — la candidata ya es Middle con experiencia comercial
|
||||
- Formato: **solo presencial en Ярославль**, sin remoto (bloqueador duro según `modes/_profile.md`: candidata en Тула, sin reubicación)
|
||||
- Salario: 40 000–50 000 ₽/mes (etapa de stажировка: 20 000 ₽) — muy por debajo del mínimo objetivo (100 000–120 000+ ₽)
|
||||
- **Score:** 1.0/5 · **Legitimacy:** High Confidence (empresa real, activa desde 2006, reseñas verificadas)
|
||||
|
||||
Confirmé que:
|
||||
- El TSV de tracker (`batch/tracker-additions/053-studiya-percy.tsv`) ya existe para esta misma empresa+rol, pendiente de `merge-tracker.mjs`.
|
||||
- Report `053-studiya-percy-2026-07-07.md` cubre exactamente el mismo JD/URL.
|
||||
|
||||
Por la regla de integridad del pipeline (nunca duplicar entradas de company+role ya evaluadas), **no generé el report 067 ni un nuevo tracker line** — sería una duplicación exacta del mismo posting evaluado ayer.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "skipped_duplicate",
|
||||
"id": "3",
|
||||
"report_num": "067",
|
||||
"company": "Студия Перцы",
|
||||
"role": "Frontend-разработчик",
|
||||
"score": 1.0,
|
||||
"legitimacy": "High Confidence",
|
||||
"pdf": null,
|
||||
"report": "reports/053-studiya-percy-2026-07-07.md",
|
||||
"error": "Duplicate of existing report 053 (same hh.ru vacancy URL, already evaluated 2026-07-07). No new report or tracker line created per pipeline integrity rule."
|
||||
}
|
||||
```
|
||||
|
||||
Cuando ejecutes `node merge-tracker.mjs`, la entrada #053 (score 1.0/5, no aplicar) se fusionará en `data/applications.md` normalmente — no requiere ninguna acción adicional de esta corrida.
|
||||
@@ -0,0 +1,18 @@
|
||||
Evaluación completa. Este es un **SKIP** claro: la posting está marcada por la fuente como archivada/cerrada, el formato es híbrido en Moscú (bloqueo duro de ubicación — la candidata no se muda de Тула), el stack es React/Redux (no Vue.js, su arquetipo primario), y pide iOS nativo en Swift (solo tiene Flutter cross-platform).
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "2",
|
||||
"report_num": "068",
|
||||
"company": "Сбер (СберТех)",
|
||||
"role": "Frontend-разработчик (Middle)",
|
||||
"score": 1.2,
|
||||
"legitimacy": "Suspicious",
|
||||
"pdf": null,
|
||||
"report": "reports/068-sber-frontend-2026-07-08.md",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
Resumen: score 1.2/5, PDF no generado (por debajo del umbral 3.0), report guardado, línea de tracker escrita en `batch/tracker-additions/2.tsv` (pendiente de `node merge-tracker.mjs`).
|
||||
@@ -0,0 +1,18 @@
|
||||
Tracker line and report are done. Final JSON summary for the orchestrator:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "4",
|
||||
"report_num": "069",
|
||||
"company": "Т-Банк",
|
||||
"role": "Frontend Developer (React)",
|
||||
"score": 1.2,
|
||||
"legitimacy": "High Confidence",
|
||||
"pdf": null,
|
||||
"report": "reports/069-t-bank-2026-07-08.md",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
**Resumen:** Т-Банк, Frontend Developer (React) en Kazan — score 1.2/5, SKIP claro. Tres bloqueadores duros: stack React (no Vue, el arquetipo principal de la candidata), seniority requerida de 3-6 años (vs. ~1.5-2 años reales), y ubicación en Kazan que exige reubicación (política explícita de no-reubicación). No se generó PDF (por debajo del umbral 3.0). Report guardado en `reports/069-t-bank-2026-07-08.md`, línea de tracker en `batch/tracker-additions/4.tsv` (pendiente de `node merge-tracker.mjs`).
|
||||
@@ -0,0 +1,18 @@
|
||||
Now the final JSON summary for the orchestrator:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "5",
|
||||
"report_num": "070",
|
||||
"company": "Exness",
|
||||
"role": "Frontend Software Engineer (Bootcamp)",
|
||||
"score": 1.0,
|
||||
"legitimacy": "High Confidence",
|
||||
"pdf": null,
|
||||
"report": "reports/070-exness-2026-07-08.md",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
**Resumen:** Score 1.0/5 — SKIP. Esta oferta (Exness, Limassol/Chipre, oficina) es un **duplicado exacto** del reporte 055 (2026-07-07, mismo ID de vacante 35037), confirmado en `data/scan-history.tsv`. Bloqueadores duros sin cambios: puesto presencial en Chipre que exige residencia previa allí (política de no reubicación de la candidata) + stack React (la candidata trabaja en Vue 3). No se generó PDF (score bajo el umbral 3.0). Report y línea de tracker guardados; falta correr `node merge-tracker.mjs` para fusionar el batch completo.
|
||||
@@ -0,0 +1,19 @@
|
||||
Report saved. Score is well below the 3.0 PDF auto-generation threshold anyway (and it's a duplicate), so no PDF was generated, and no tracker line was written for batch ID 6 to prevent a duplicate row in `applications.md`.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "6",
|
||||
"report_num": "071",
|
||||
"company": "РТК-ЦОД",
|
||||
"role": "Ведущий фронтенд-разработчик (React)",
|
||||
"score": 1.8,
|
||||
"legitimacy": "High Confidence",
|
||||
"pdf": null,
|
||||
"report": "reports/071-rtk-tsod-2026-07-08.md",
|
||||
"error": null,
|
||||
"note": "Duplicate of report 056 (same URL/JD, evaluated 2026-07-07). Tracker line intentionally skipped to avoid a duplicate applications.md row — company+role already covered by batch/tracker-additions/056-rtk-tsod.tsv."
|
||||
}
|
||||
```
|
||||
|
||||
**Resumen:** РТК-ЦОД — Ведущий фронтенд-разработчик (React), 1.8/5, no recomendado. Es un duplicado exacto de la oferta ya evaluada ayer (reporte 056) — mismo URL, misma JD. Bloqueadores: stack React/Next.js/GraphQL sin ningún proyecto React en el CV (todo el perfil es Vue 3 + Flutter), y nivel Senior/Lead vs. Middle del candidato. No se generó PDF ni línea de tracker nueva para evitar duplicar la entrada de РТК-ЦОД que ya está pendiente de merge desde el reporte 056.
|
||||
@@ -0,0 +1,16 @@
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "7",
|
||||
"report_num": "072",
|
||||
"company": "Т-Банк",
|
||||
"role": "Frontend-разработчик React (Junior)",
|
||||
"score": 1.7,
|
||||
"legitimacy": "Proceed with Caution",
|
||||
"pdf": null,
|
||||
"report": "reports/072-t-bank-react-junior-perm-2026-07-08.md",
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
Evaluated Т-Банк's Junior React frontend role (Perm, remote-friendly) — scored 1.7/5, recommendation SKIP. Main blockers: role requires React (candidate's primary stack is Vue.js, no commercial React experience) and the Junior scope/comp sits below her actual level of ownership (~100k+ ₽/mes minimum vs. Junior market rates of ~55-80k). No PDF generated (below 3.0 threshold). Report saved to `reports/072-t-bank-react-junior-perm-2026-07-08.md`, tracker line queued at `batch/tracker-additions/7.tsv` — run `node merge-tracker.mjs` when the batch finishes.
|
||||
@@ -0,0 +1,17 @@
|
||||
No report, PDF, or tracker line was created for this ID since there is no actual job content to evaluate.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "failed",
|
||||
"id": "8",
|
||||
"report_num": "073",
|
||||
"company": "unknown",
|
||||
"role": "unknown",
|
||||
"score": null,
|
||||
"pdf": null,
|
||||
"report": null,
|
||||
"error": "JD file /tmp/batch-jd-8.okc7U0 is empty (0 bytes), and the fallback URL https://t.me/s/runello_rus_javascript/1443 does not contain a job posting — message #1443 in that channel is a Runello bot promotional/service message ('upload your resume to @RunelloBot'), not an actual JD. No evaluable content exists for this offer."
|
||||
}
|
||||
```
|
||||
|
||||
Report #073 was not generated — this batch ID has no real job description to score. Recommend removing this URL from the batch input or re-scraping the correct message ID for that channel entry.
|
||||
@@ -0,0 +1 @@
|
||||
**Resumen:** Esta oferta (`t.me/s/runello_rus_javascript/1445`) es un duplicado exacto del post ya evaluado como reporte 059 el 2026-07-07 — mismo empleador anónimo, mismo stack (React + Python/LLM/LangChain/RAG), mismo bot de Runello como único canal de aplicación. Score 1.0/5, legitimidad **Suspicious** (empleador no revelado + aplicación forzada vía bot de terceros), sin match real con el perfil Vue/Flutter de la candidata. No se generó PDF (por debajo del umbral 3.0). Reporte guardado en `reports/074-unknown-employer-runello-2026-07-08.md` y línea de tracker en `batch/tracker-additions/9.tsv`. Recomendación: **no aplicar**.
|
||||
@@ -0,0 +1,25 @@
|
||||
**Duplicate detected — no new report/PDF created.**
|
||||
|
||||
URL `https://t.me/s/runello_rus_javascript/1448` (batch ID 10) is identical to a posting already fully evaluated yesterday as **report 060** (`reports/060-konsultantplyus-2026-07-07.md`, ЗАО КонсультантПлюс — Веб разработчик Vue/Js, score 3.0/5, Proceed with Caution). The JD file was empty and WebFetch returned the same thin Telegram/Runello preview (only title + company blurb — no requirements, salary, or location), so there is no new information that would change that evaluation.
|
||||
|
||||
Per the pipeline-integrity rule ("never create duplicate entries for the same company+role"), I did not generate report 075 or a new PDF. Instead I found that report 060's tracker line had never been written (a pre-existing gap — its score never made it into `data/applications.md`), so I closed that gap:
|
||||
|
||||
- Wrote `batch/tracker-additions/10.tsv` pointing to the existing report 060 (score 3.0/5, ❌ PDF — the PDF was never actually compiled despite the report claiming it hit the 3.0 threshold; only the HTML exists in `output/`).
|
||||
- Run `node merge-tracker.mjs` when convenient to fold this into `data/applications.md`.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "completed",
|
||||
"id": "10",
|
||||
"report_num": "075",
|
||||
"company": "ЗАО КонсультантПлюс",
|
||||
"role": "Веб разработчик Vue/Js",
|
||||
"score": 3.0,
|
||||
"legitimacy": "Proceed with Caution",
|
||||
"pdf": null,
|
||||
"report": "reports/060-konsultantplyus-2026-07-07.md",
|
||||
"error": "duplicate_of_report_060: identical URL/JD already evaluated 2026-07-07; no new report or PDF generated; wrote missing tracker line for existing report 060 instead"
|
||||
}
|
||||
```
|
||||
|
||||
If the scanner is re-feeding already-processed URLs into the batch inbox, it's worth checking `data/scan-history.tsv` dedup logic for why this one resurfaced.
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
001 2026-07-06 Яндекс (Лавка) Разработчик фронтенда (Vue/React) в Лавку Evaluated 1.0/5 ❌ [001](reports/001-yandex-lavka-2026-07-06.md) Сильный стек-мэтч (Vue3+тесты+legacy+ритейл), но офис/гибрид в Москве/СПб без удалёнки — жёсткий локационный блокер
|
||||
|
@@ -0,0 +1 @@
|
||||
002 2026-07-06 VK Frontend-разработчик (MAX) Evaluated 2.3/5 ❌ [002](reports/002-vk-max-2026-07-06.md) Удалёнка доступна и уровень Middle совпадает, но ядро роли — React-экосистема, которой нет в CV (стек кандидата — Vue)
|
||||
|
@@ -0,0 +1 @@
|
||||
003 2026-07-06 VK Старший Frontend-разработчик сообщества (ВКонтакте) Evaluated 1.8/5 ❌ [003](reports/003-vk-vkontakte-senior-2026-07-06.md) Двойной разрыв: требуется Senior/4+ лет и React, у кандидата Middle/~2.5 года и стек Vue; не рекомендуется откликаться
|
||||
|
@@ -0,0 +1 @@
|
||||
004 2026-07-06 Ozon Frontend-разработчик (ERP и учётные системы) Evaluated 4.2/5 ✅ [004](reports/004-ozon-2026-07-06.md) Лучший фит пакета: Middle Vue3/TS, удалённо, formal "3+ года JS" порог выше факта (~2 года)
|
||||
|
Can't render this file because it contains an unexpected character in line 1 and column 225.
|
@@ -0,0 +1 @@
|
||||
005 2026-07-06 Ozon Стажёр Frontend-разработчик (Маркетинговая платформа) Evaluated 2.5/5 ❌ [005](reports/005-ozon-2026-07-06.md) Уровень "стажёр" ниже Middle-профиля кандидата
|
||||
|
Can't render this file because it contains an unexpected character in line 1 and column 187.
|
@@ -0,0 +1 @@
|
||||
006 2026-07-06 Wildberries Frontend-разработчик (Платформа) Evaluated 2.8/5 ❌ [006](reports/006-wildberries-2026-07-06.md) Стек React, не Vue — adjacent архетип, не primary
|
||||
|
@@ -0,0 +1 @@
|
||||
007 2026-07-06 Wildberries (RWB) Senior Frontend developer (Angular) в WB Job Evaluated 1.7/5 ❌ [007](reports/007-wildberries-2026-07-06.md) Удалённо, но нет Angular и стаж вдвое короче требуемых 4 лет — строго не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
008 2026-07-06 Т-Банк Frontend-разработчик — React Evaluated 2.0/5 ❌ [008](reports/008-tbank-react-2026-07-06.md) Удалённо/гибко, но нет коммерческого React (обязательно от 4 лет) — строго не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
009 2026-07-06 Т-Банк Frontend-разработчик — Angular Evaluated 2.3/5 ❌ [009](reports/009-tbank-angular-2026-07-06.md) Удалённо/гибко, Middle/Senior, но нет Angular (обязательно от 2 лет) — строго не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
010 2026-07-06 МТС Senior Frontend разработчик [NBA.Банк] Evaluated 1.6/5 ❌ [010](reports/010-mts-senior-frontend-nba-bank-2026-07-06.md) Сильное совпадение по Vue3/TS, но гибрид в Москве нарушает жёсткое правило локации (Тула, без переезда) — балл тонет до 1.6
|
||||
|
@@ -0,0 +1 @@
|
||||
011 2026-07-06 МТС Intern Frontend dev [Governance] Evaluated 2.0/5 ❌ [011](reports/011-mts-intern-frontend-governance-2026-07-06.md) Полностью удалённо (ок для локации), но стек React (не Vue) и уровень "стажёр без опыта" не подходят Middle-кандидату
|
||||
|
Can't render this file because it contains an unexpected character in line 1 and column 255.
|
@@ -0,0 +1 @@
|
||||
012 2026-07-06 МТС Middle Frontend Developer (Vue) [GARNET] Evaluated 1.9/5 ❌ [012](reports/012-mts-middle-frontend-vue-garnet-2026-07-06.md) Лучшее стековое совпадение в пакете (Vue3+Composition API+TS), но тот же блокер: гибрид в Москве, не Тула
|
||||
|
@@ -0,0 +1 @@
|
||||
013 2026-07-06 МТС Senior Frontend (ON Медиа) Evaluated 1.7/5 ❌ [013](reports/013-mts-on-media-2026-07-06.md) Стек Angular не совпадает с primary-архетипом (Vue.js)
|
||||
|
@@ -0,0 +1 @@
|
||||
014 2026-07-06 МТС Senior Frontend разработчик Evaluated 1.6/5 ❌ [014](reports/014-mts-frontend-razrabotchik-2026-07-06.md) Стек Vue совпадает, но гибрид/офис в Москве — жёсткий локационный блокер
|
||||
|
@@ -0,0 +1 @@
|
||||
015 2026-07-06 Kaspersky JavaScript Developer (KES) Evaluated 3.2/5 ✅ [015](reports/015-kaspersky-kes-2026-07-06.md) React+Node enterprise-роль, удалённо по всей России, комп сильный, стек — умеренный разрыв
|
||||
|
@@ -0,0 +1 @@
|
||||
016 2026-07-07 КСК ЭЛКОМ Разработчик Flutter (Dart) Evaluated 1.0/5 ❌ [016](reports/016-ksk-elkom-2026-07-07.md) Офис в СПб без удалёнки — блокер по location-политике
|
||||
|
@@ -0,0 +1 @@
|
||||
017 2026-07-07 АО "ЦФТ" Frontend-разработчик Vue Evaluated 2.1/5 ❌ [017](reports/017-tsft-2026-07-07.md) Сильное совпадение по Vue3/TS/Pinia, но стажевый пробел (3-6 лет vs ~1.7), формат работы не подтверждён (Новосибирск), зарплата не раскрыта
|
||||
|
Can't render this file because it contains an unexpected character in line 1 and column 21.
|
@@ -0,0 +1 @@
|
||||
018 2026-07-07 Яндекс Fullstack-разработчик в команду frontend-инфраструктуры Evaluated 1.0/5 ❌ [018](reports/018-yandex-frontend-infra-2026-07-07.md) Екатеринбург/Москва, гибрид/офис без удалёнки + роль platform-tooling, не UI
|
||||
|
@@ -0,0 +1 @@
|
||||
019 2026-07-07 Яндекс Разработчик фронтенда в команду Яндекс Кит Evaluated 1.0/5 ❌ [019](reports/019-yandex-kit-2026-07-07.md) СПб/Москва без удалёнки + стек React вместо Vue
|
||||
|
@@ -0,0 +1 @@
|
||||
020 2026-07-07 Яндекс Мобильный разработчик (Android/iOS) Payment SDK в Финтех Evaluated 1.0/5 ❌ [020](reports/020-yandex-payment-sdk-2026-07-07.md) СПб/Москва без удалёнки + нативный Kotlin/Swift вместо Flutter
|
||||
|
@@ -0,0 +1 @@
|
||||
021 2026-07-07 Яндекс Стажёр в команду разработки AI-инфраструктуры фронтенда Яндекс 360 Evaluated 1.0/5 ❌ [021](reports/021-yandex-360-intern-2026-07-07.md) Москва, только офис, стажировка на React — не подходит по уровню и локации
|
||||
|
@@ -0,0 +1 @@
|
||||
022 2026-07-07 Яндекс Фронтенд-разработчик в еком-сценарии Поиска Evaluated 1.0/5 ❌ [022](reports/022-yandex-ecom-search-2026-07-07.md) Хороший содержательный фит, но СПб/Москва без удалёнки — блокер
|
||||
|
@@ -0,0 +1 @@
|
||||
023 2026-07-07 Яндекс Разработчик фронтенда в Яндекс 360 Evaluated 1.0/5 ❌ [023](reports/023-yandex-360-frontend-2026-07-07.md) СПб/Москва без удалёнки + React вместо Vue
|
||||
|
@@ -0,0 +1 @@
|
||||
024 2026-07-07 Бизнес Технологии Senior/Tech Lead frontend-разработчик Evaluated 1.5/5 ❌ [024](reports/024-biznes-tehnologii-2026-07-07.md) Отличная компенсация (300-350к на руки), но требование 10 лет опыта vs 1.7 года у кандидата - критический разрыв
|
||||
|
@@ -0,0 +1 @@
|
||||
025 2026-07-07 ИП Каржева М.К. Frontend-разработчик Evaluated 2.6/5 ❌ [025](reports/025-karzheva-2026-07-07.md) WebFetch дважды не смог извлечь JD; компенсация (~113-130к на руки расч.) в целевом диапазоне, но легитимность/детали не проверены
|
||||
|
@@ -0,0 +1 @@
|
||||
026 2026-07-07 Dev Company 23 Frontend разработчик Senior Evaluated 2.4/5 ❌ [026](reports/026-dev-company-23-2026-07-07.md) Сильнейшее совпадение стека (Vue3/TS/Pinia) и отличная компенсация (от 270к gross), но заявлено 4-6 лет опыта vs 1.7 года у кандидата
|
||||
|
@@ -0,0 +1 @@
|
||||
027 2026-07-07 Sakura Games Fullstack Developer (Node.js/TS + React/Next.js) Evaluated 1.9/5 ❌ [027](reports/027-sakura-games-2026-07-07.md) Стек React/Next.js (не Vue) - нет production опыта; заявлено 6+ лет vs 1.7 года; зарплата не раскрыта; контракт проектный 3-6 мес
|
||||
|
@@ -0,0 +1 @@
|
||||
028 2026-07-07 Selecty Senior Frontend Developer (Vue.js/Nuxt.js) Evaluated 2.0/5 ❌ [028](reports/028-selecty-2026-07-07.md) Удалёнка ок, но 5 лет опыта + английский B2+ mandatory — два hard-блокера
|
||||
|
@@ -0,0 +1 @@
|
||||
029 2026-07-07 Bnovo Senior Frontend разработчик Evaluated 2.5/5 ❌ [029](reports/029-bnovo-2026-07-07.md) Удалёнка подтверждена, отличная репутация, но 4+ года опыта + tech-lead трек выше уровня
|
||||
|
@@ -0,0 +1 @@
|
||||
030 2026-07-07 ООО Бизнес Технологии Middle frontend-разработчик Evaluated 4.2/5 ✅ [030](reports/030-biznes-tehnologii-middle-2026-07-07.md) Гибкий формат (вкл. удалёнку), 150-200к на руки, стек совпадает — рекомендуется откликнуться
|
||||
|
@@ -0,0 +1 @@
|
||||
032 2026-07-07 ИП Шевченко М.Ю. Flutter-разработчик Evaluated 2.7/5 ❌ [032](reports/032-ip-shevchenko-2026-07-07.md) Проектный ГПХ-контракт, заявлен стаж 3-6 лет vs фактический ~1.5-2 года, агрессивный дедлайн (10% готовности за 8 недель)
|
||||
|
@@ -0,0 +1 @@
|
||||
033 2026-07-07 МЕДИА ПОТОК Fullstack-разработчик мобильных приложений Evaluated 4.1/5 ✅ [033](reports/033-media-potok-2026-07-07.md) Стаж 1-3 года и стек (Flutter+Node.js) совпадают почти идеально, полностью гибкий формат, вилка 100-200к на руки
|
||||
|
@@ -0,0 +1 @@
|
||||
034 2026-07-07 Playneta Senior Flutter Developer Evaluated 1.8/5 ❌ [034](reports/034-playneta-2026-07-07.md) Senior-геймдев роль (5+ лет, рендеринг/анимация 60-120fps) vs фактический опыт ~1.5-2 года без игрового бэкграунда; 2 дублирующих объявления замечены
|
||||
|
@@ -0,0 +1 @@
|
||||
035 2026-07-07 Noroots Fullstack-разработчик Evaluated N/A ❌ [035](reports/035-noroots-2026-07-07.md) Вакансия недоступна для просмотра (403/ограничен доступ) — оценка невозможна
|
||||
|
@@ -0,0 +1 @@
|
||||
036 2026-07-07 МТС Senior Frontend developer [МТС Маркетолог] Evaluated 1.8/5 ❌ [036](reports/036-mts-marketolog-2026-07-07.md) React vs Vue stack mismatch + Senior 6+ лет vs ~2 года опыта — не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
037 2026-07-07 МТС Middle Flutter Developer [GOODOK] Evaluated 3.6/5 ✅ [037](reports/037-mts-goodok-2026-07-07.md) Лучший фит партии: BLoC/iOS-портирование совпадают, remote-опция, но ниже порога 4.0
|
||||
|
@@ -0,0 +1 @@
|
||||
038 2026-07-07 МТС Fullstack developer React/Node.JS [MWS Tables] Evaluated 1.9/5 ❌ [038](reports/038-mts-mwstables-2026-07-07.md) React + тяжёлый DevOps (K8s/Helm) далеко от Vue + ознакомительного Node — не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
039 2026-07-07 МТС Стажер Fullstack QA [MWS Octapi] Evaluated 1.0/5 ❌ [039](reports/039-mts-octapi-2026-07-07.md) Hard location blocker (Москва гибрид, без remote) + программа для студентов, не для выпускников
|
||||
|
@@ -0,0 +1 @@
|
||||
040 2026-07-07 МТС Junior Fullstack-аналитик [CICADA8] Evaluated 1.0/5 ❌ [040](reports/040-mts-cicada8-2026-07-07.md) Hard location blocker (офис Москва, без remote) + роль аналитика не входит в архетипы кандидата
|
||||
|
@@ -0,0 +1 @@
|
||||
051 2026-07-07 VK Старший Frontend-разработчик (React, мессенджер) Evaluated 1.5/5 ❌ [051](reports/051-vk-messenger-frontend-2026-07-07.md) Архивная вакансия, стек React (не Vue) и требование 5+ лет — тройной hard blocker, не рекомендуется откликаться
|
||||
|
@@ -0,0 +1 @@
|
||||
052 2026-07-07 Сбер Frontend Developer Evaluated 1.0/5 ❌ [052](reports/052-sber-2026-07-07.md) Стек React/Redux/Swift/AI-моделей не совпадает с Vue/Flutter кандидата; роль в Москве требует переезда (hard blocker); листинг архивирован по данным hirehi.ru
|
||||
|
@@ -0,0 +1 @@
|
||||
053 2026-07-07 Студия Перцы Frontend-разработчик Evaluated 1.0/5 ❌ [053](reports/053-studiya-percy-2026-07-07.md) Стажёрская программа для новичков, офис только в Ярославле без удалёнки, ЗП 40-50к — не совпадает по формату работы и уровню
|
||||
|
@@ -0,0 +1 @@
|
||||
054 2026-07-07 Т-Банк Frontend Developer (React) Evaluated 1.0/5 ❌ [054](reports/054-t-bank-2026-07-07.md) Hard location blocker — гибрид офис Казань, без переезда; стек React vs Vue у кандидата
|
||||
|
@@ -0,0 +1 @@
|
||||
055 2026-07-07 Exness Frontend Software Engineer (Bootcamp) Evaluated 1.0/5 ❌ [055](reports/055-exness-2026-07-07.md) Офис в Лимасоле (Кипр), требует уже жить на Кипре, стек React не Vue — hard location blocker, не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
056 2026-07-07 РТК-ЦОД Ведущий фронтенд-разработчик (React) Evaluated 1.8/5 ❌ [056](reports/056-rtk-tsod-2026-07-07.md) Стек React/Next.js/GraphQL и уровень Lead не совпадают с профилем (Vue 3, Middle) — не рекомендуется откликаться
|
||||
|
@@ -0,0 +1 @@
|
||||
057 2026-07-07 Т-Банк Frontend-разработчик React (junior) Evaluated 2.4/5 ❌ [057](reports/057-t-bank-2026-07-07.md) Junior-грейд vs Middle кандидата, стек React не совпадает с Vue, зарплата не раскрыта — не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
059 2026-07-07 Unknown (Runello aggregator) Fullstack React Developer Evaluated 1.0/5 ❌ [059](reports/059-unknown-employer-runello-2026-07-07.md) Работодатель не назван, отклик только через сторонний бот; стек React+Python/LLM/RAG не совпадает с профилем (Vue+Flutter) — Suspicious legitimacy, не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
061 2026-07-07 TOOLUM Fullstack разработчик Next.js Evaluated 2.3/5 ❌ [061](reports/061-toolum-2026-07-07.md) Next.js/React hard req vs Vue-only CV; JD terse (no salary/location/format); TOOLUM no public footprint
|
||||
|
@@ -0,0 +1 @@
|
||||
16 2026-07-08 VK Старший Frontend-разработчик (React) Evaluated 1.0/5 ❌ [066](reports/066-vk-2026-07-08.md) Вакансия архивирована (компания не ищет кандидатов) + стек React вместо Vue + Senior 5+ лет vs ~1.5 года опыта — SKIP
|
||||
|
@@ -0,0 +1 @@
|
||||
075 2026-07-08 СПБ ИАЦ Программист Front-end Evaluated 1.0/5 ❌ [075](reports/075-spb-iac-2026-07-08.md) Vue 3/TS отлично совпадает, но офис только в Санкт-Петербурге без удалёнки — блокер по локационной политике
|
||||
|
@@ -0,0 +1 @@
|
||||
076 2026-07-08 Bellerage Стажер Frontend-разработчик Evaluated 1.0/5 ❌ [076](reports/076-bellerage-2026-07-08.md) React (не Vue) + стажёрский уровень + офис в Москве без удалёнки — блокер по локации
|
||||
|
@@ -0,0 +1 @@
|
||||
077 2026-07-08 Bioniq Младший фронтенд-разработчик Evaluated 1.0/5 ❌ [077](reports/077-bioniq-2026-07-08.md) Angular (не Vue) + вёрстка лендингов + офис в Москве — блокер по стеку и локации
|
||||
|
@@ -0,0 +1 @@
|
||||
078 2026-07-08 А7-Технологии Flutter-разработчик Evaluated 1.0/5 ❌ [078](reports/078-a7-tehnologii-2026-07-08.md) Хорошее совпадение по Flutter/BLoC, но офис/гибрид в Москве без полной удалёнки — блокер по локации
|
||||
|
@@ -0,0 +1 @@
|
||||
079 2026-07-08 DataLouna Backend Engineer (Node.js, TypeScript, Data, Real-time) Evaluated 1.0/5 ❌ [079](reports/079-datalouna-2026-07-08.md) Remote ок, но senior data/backend роль (ClickHouse, K8s, парсеры) без frontend — глубокое несовпадение по функции и опыту (5+ лет)
|
||||
|
@@ -0,0 +1 @@
|
||||
080 2026-07-08 Selfwork Full-stack engineer Evaluated 1.5/5 ❌ [080](reports/080-selfwork-2026-07-08.md) Remote, TS/Node пересекаются, но React (не Vue) + жёсткое требование 5+ лет senior fullstack опыта не в аутсорсе
|
||||
|
@@ -0,0 +1 @@
|
||||
081 2026-07-08 Positive Technologies Fullstack QA Engineer (RA VPN) Evaluated 1.0/5 ❌ [081](reports/081-positive-technologies-2026-07-08.md) Remote ок, но это QA-роль (тестирование VPN/NGFW), а не разработка — функциональное несовпадение
|
||||
|
@@ -0,0 +1 @@
|
||||
082 2026-07-08 Air Production Frontend-разработчик (Vue.js) Evaluated 4.0/5 ✅ [082](reports/082-air-production-2026-07-08.md) Лучшее совпадение сессии: Vue.js, удалённо, опыт 1-3 года и ЗП 90-130к точно в цель; toolchain-разрыв (Vuex/Webpack/Gulp vs Pinia/Vite) закрываем
|
||||
|
@@ -0,0 +1 @@
|
||||
083 2026-07-08 Nexters Senior Frontend Developer (Hero Wars) Evaluated 1.0/5 ❌ [083](reports/083-nexters-2026-07-08.md) Remote ок, но gamedev-стек (игровые движки Cocos/Phaser/Pixi) + Senior 3+ года — не веб-Vue разработка
|
||||
|
@@ -0,0 +1 @@
|
||||
084 2026-07-08 МТС (Маркетолог) Senior Frontend developer Evaluated 1.0/5 ❌ [084](reports/084-mts-marketolog-2026-07-08.md) Remote ок, но React/Next.js (не Vue) + Senior 6+ лет — стек и уровень не совпадают
|
||||
|
@@ -0,0 +1 @@
|
||||
085 2026-07-08 МТС (GOODOK) Middle Flutter Developer Evaluated 2.0/5 ❌ [085](reports/085-mts-goodok-2026-07-08.md) Гибрид/удалённо ок, BLoC/Dart совпадают, но глубина (Isolates, локальные БД, тесты) и стаж 3-6 лет vs ~1 год — разрыв
|
||||
|
@@ -0,0 +1 @@
|
||||
086 2026-07-08 МТС (MWS Tables) Fullstack developer React/Node.JS Evaluated 1.0/5 ❌ [086](reports/086-mts-tables-2026-07-08.md) Гибрид в Москве — локационный блокер (Тула, без релокации); плюс стек/стаж выше профиля
|
||||
|
@@ -0,0 +1 @@
|
||||
087 2026-07-08 МТС (MWS Octapi) Стажер Fullstack QA Evaluated 1.0/5 ❌ [087](reports/087-mts-octapi-2026-07-08.md) Гибрид в Москве — локационный блокер; плюс QA-функция и стажёрский уровень
|
||||
|
@@ -0,0 +1 @@
|
||||
088 2026-07-08 МТС (CICADA8) Junior Fullstack-аналитик Evaluated 1.0/5 ❌ [088](reports/088-mts-cicada8-2026-07-08.md) Офис в Москве — локационный блокер; плюс роль аналитика, не разработчика
|
||||
|
@@ -0,0 +1 @@
|
||||
089 2026-07-08 МТС (Телеком платформа) Middle QA fullstack Evaluated 1.0/5 ❌ [089](reports/089-mts-telecom-platforma-2026-07-08.md) Remote ок, но это QA-роль (тестирование), не разработка
|
||||
|
@@ -0,0 +1 @@
|
||||
090 2026-07-08 МТС Middle/Senior Fullstack разработчик Evaluated 1.0/5 ❌ [090](reports/090-mts-fullstack-2026-07-08.md) Единственная из 7 без явного remote-тега (только Москва+гибкий график) — консервативно офис, блокер
|
||||
|
@@ -0,0 +1 @@
|
||||
091 2026-07-09 Неизвестный работодатель (TG-брокер) WebView-разработчик (Unity/Flutter/Native) Evaluated 1.0/5 ❌ [091](reports/091-webview-broker-2026-07-09.md) Сдельная оплата, нет компании, не соответствует профилю — не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
092 2026-07-09 Swiftly / STA TECH Junior Flutter-разработчик Evaluated 1.2/5 ❌ [092](reports/092-swiftly-sta-tech-2026-07-09.md) Стек совпадает (Flutter), но без оплаты до релиза и монетизации — не рекомендуется
|
||||
|
@@ -0,0 +1 @@
|
||||
093 2026-07-09 АО ГК Орбита Fullstack C# Senior Evaluated 1.0/5 ❌ [093](reports/093-gk-orbita-2026-07-09.md) C# стек, не пересекается ни с одним архетипом кандидата
|
||||
|
@@ -0,0 +1 @@
|
||||
094 2026-07-09 EvaTeam Frontend разработчик Angular Evaluated 1.3/5 ❌ [094](reports/094-evateam-2026-07-09.md) Angular, не Vue.js — стековое несовпадение
|
||||
|
@@ -0,0 +1 @@
|
||||
095 2026-07-09 TOOLUM Fullstack разработчик Next.js Evaluated 1.3/5 ❌ [095](reports/095-toolum-2026-07-09.md) Next.js/React, не Vue.js — стековое несовпадение
|
||||
|
@@ -0,0 +1 @@
|
||||
096 2026-07-09 AppDepo Flutter Developer (Junior) Evaluated 3.2/5 ✅ [096](reports/096-appdepo-2026-07-09.md) Сильное совпадение по Flutter/Bloc/Firebase, но зарплата не раскрыта (почасовая) — уточнить ставку до отклика
|
||||
|
@@ -0,0 +1 @@
|
||||
097 2026-07-09 Awelite Fullstack Web разработчик Evaluated 1.3/5 ❌ [097](reports/097-awelite-2026-07-09.md) PHP-компания, бэкенд-стек не совпадает с профилем (Node.js)
|
||||
|
@@ -0,0 +1 @@
|
||||
098 2026-07-09 Young&Junior / СОФИ И ПАРТНЕРЫ Junior Frontend Разработчик Evaluated 1.6/5 ❌ [098](reports/098-young-junior-2026-07-09.md) React (не Vue), ЗП от 90к ниже минимума кандидата, несовпадение брендинга работодателя
|
||||
|
@@ -0,0 +1 @@
|
||||
099 2026-07-09 Constructor TECH Frontend Engineer (Intern) Evaluated 1.0/5 ❌ [099](reports/099-constructor-tech-2026-07-09.md) Стажировка только для студентов Constructor University/Nexademy/Nexford — формальный барьер входа, вероятно неоплачиваемая
|
||||
|
@@ -0,0 +1 @@
|
||||
60 2026-07-07 ЗАО КонсультантПлюс Веб разработчик Vue/Js Evaluated 3.0/5 ❌ [060](reports/060-konsultantplyus-2026-07-07.md) Дубликат URL из сегодняшнего batch-инпута (тот же t.me/s/runello_rus_javascript/1448); отчёт 060 от 2026-07-07 уже покрывает эту вакансию — новый отчёт не создан. PDF не был фактически сгенерирован несмотря на балл=порог 3.0 (только HTML), требует ручной генерации при необходимости.
|
||||
|
@@ -0,0 +1 @@
|
||||
100 2026-07-09 CHOICY (агентство, B2C SaaS партнёр) Senior Full-stack React Developer Evaluated 1.0/5 ❌ [100](reports/100-choicy-react-b2c-saas-2026-07-09.md) 8+ лет опыта требуется + React, не Vue — двойное несовпадение
|
||||
|
@@ -0,0 +1 @@
|
||||
101 2026-07-09 Tilda Publishing Head of Frontend-разработки Evaluated 1.0/5 ❌ [101](reports/101-tilda-publishing-2026-07-09.md) Управленческая роль (15+ подчинённых, 3+ года лидерства) + гибрид Москва/СПб — двойной блокер
|
||||
|
@@ -0,0 +1 @@
|
||||
102 2026-07-09 Data World (аутстафф Сбер) Frontend разработчик (опыт работы в банке) Evaluated 1.4/5 ❌ [102](reports/102-data-world-2026-07-09.md) React + мин. 3 года опыта — двойное несовпадение, несмотря на хорошую ЗП и удалёнку
|
||||
|
@@ -0,0 +1 @@
|
||||
103 2026-07-09 CORE TECHNOLOGY Frontend Developer (Middle) Discarded 1.0/5 ❌ [103](reports/103-core-technology-2026-07-09.md) Hard blocker: only on-site Tashkent, no remote, relocation required
|
||||
|
@@ -0,0 +1 @@
|
||||
104 2026-07-09 Lunas.pro Lead Graphics Engineer - Web 3D Discarded 1.0/5 ❌ [104](reports/104-lunas-pro-2026-07-09.md) Skill mismatch: WebGPU/Three.js/shaders, no overlap with Vue/Flutter background; also Lead vs Middle level
|
||||
|
@@ -0,0 +1 @@
|
||||
105 2026-07-09 Podscribe Senior Full-stack Engineer (Node.js) Discarded 1.5/5 ❌ [105](reports/105-podscribe-2026-07-09.md) Senior level, React not Vue, Python/ClickHouse gap, B1 English risk on intl team
|
||||
|
@@ -0,0 +1 @@
|
||||
106 2026-07-09 IDT Senior Frontend Software Engineer (React) Discarded 1.0/5 ❌ [106](reports/106-idt-2026-07-09.md) Employment tied to Georgia/Moldova/Belarus, not RU; also React/Electron/AWS CDK stack mismatch
|
||||
|
@@ -0,0 +1 @@
|
||||
107 2026-07-09 Acronis Senior Full-Stack Developer Discarded 1.0/5 ❌ [107](reports/107-acronis-2026-07-09.md) Bulgaria hybrid relocation + Senior 6+yrs gap; strong RBAC/Vue content match though
|
||||
|
Loaded 100 of 1527 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user