claude-code-arcane 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/agents/engineering/dotnet-engineer.md +78 -0
- package/dist/cli.js +284 -72
- package/docs/RELEASE-SETUP.md +99 -0
- package/docs/SKILLS-CATALOG.md +26 -3
- package/docs/presentations/arcane-overview-12.pptx +0 -0
- package/docs/presentations/arcane-overview.pptx +0 -0
- package/docs/presentations/build_arcane_deck.py +310 -0
- package/docs/presentations/build_arcane_deck_12.py +399 -0
- package/package.json +1 -1
- package/profiles/backend-dotnet.yaml +54 -0
- package/profiles/job-hunt.yaml +35 -0
- package/profiles/unity-design.yaml +1 -0
- package/profiles/unity-dev.yaml +1 -0
- package/rules/dotnet-code.md +64 -0
- package/skills/cold-outreach/SKILL.md +65 -0
- package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
- package/skills/cover-letter/SKILL.md +66 -0
- package/skills/cv-ats-export/SKILL.md +64 -0
- package/skills/cv-ats-export/scripts/cv_export.py +306 -0
- package/skills/cv-tailor/SKILL.md +70 -0
- package/skills/cv-tailor/references/ats-keywords.md +46 -0
- package/skills/dotnet-architecture/SKILL.md +66 -0
- package/skills/dotnet-architecture/references/anti-patterns.md +12 -0
- package/skills/dotnet-architecture/references/checklist.md +19 -0
- package/skills/dotnet-architecture/references/patterns.md +118 -0
- package/skills/dotnet-architecture/references/project-structure.md +78 -0
- package/skills/dotnet-best-practices/SKILL.md +76 -0
- package/skills/dotnet-best-practices/references/api-design.md +75 -0
- package/skills/dotnet-best-practices/references/architecture.md +62 -0
- package/skills/dotnet-best-practices/references/async.md +62 -0
- package/skills/dotnet-best-practices/references/database.md +69 -0
- package/skills/dotnet-best-practices/references/dependency-injection.md +73 -0
- package/skills/dotnet-best-practices/references/devops.md +76 -0
- package/skills/dotnet-best-practices/references/error-handling.md +72 -0
- package/skills/dotnet-best-practices/references/performance.md +63 -0
- package/skills/dotnet-best-practices/references/security.md +73 -0
- package/skills/dotnet-best-practices/references/testing.md +76 -0
- package/skills/dotnet-scaffold/SKILL.md +99 -0
- package/skills/install-mcp/SKILL.md +107 -0
- package/skills/install-mcp/references/manual-setup.md +92 -0
- package/skills/interview-prep/SKILL.md +69 -0
- package/skills/interview-prep/references/star-framework.md +42 -0
- package/skills/job-hunt/SKILL.md +92 -0
- package/skills/job-hunt/references/templates/Aplicacion.md +48 -0
- package/skills/job-hunt/references/templates/CV Custom.md +53 -0
- package/skills/job-hunt/references/templates/Contacto.md +30 -0
- package/skills/job-hunt/references/templates/Dashboard.md +45 -0
- package/skills/job-hunt/references/templates/Empresa.md +36 -0
- package/skills/job-hunt/references/templates/Entrevista.md +44 -0
- package/skills/job-hunt/references/templates/Perfil.md +38 -0
- package/skills/job-search/SKILL.md +83 -0
- package/skills/job-search/references/scoring-rubric.md +43 -0
- package/skills/linkedin-optimize/SKILL.md +79 -0
- package/skills/master-profile/SKILL.md +69 -0
- package/skills/network-map/SKILL.md +61 -0
- package/skills/network-map/scripts/network_map.py +109 -0
- package/skills/personal-brand/SKILL.md +54 -0
- package/skills/personal-brand/references/post-pillars.md +66 -0
- package/skills/portfolio-site/SKILL.md +59 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Recruiter / Hiring Manager Playbook
|
|
2
|
+
|
|
3
|
+
Plantillas y psicología para outreach de candidato. Adaptá siempre la primera línea — las plantillas son esqueleto, no copy-paste.
|
|
4
|
+
|
|
5
|
+
## Por qué responden (y por qué no)
|
|
6
|
+
|
|
7
|
+
Responden cuando: el mensaje es corto, claro sobre el fit, fácil de responder, y se nota que es para *ellos*. No responden cuando: es genérico, largo, pide demasiado de entrada, o suena desesperado. El recruiter recibe decenas por día — el filtro es "¿esto me ahorra trabajo o me lo agrega?".
|
|
8
|
+
|
|
9
|
+
## Plantillas
|
|
10
|
+
|
|
11
|
+
### 1. Recruiter que posteó un rol
|
|
12
|
+
```
|
|
13
|
+
Hola {nombre}, vi que están buscando {rol} en {empresa}.
|
|
14
|
+
Soy {rol/seniority} con {logro relevante en 1 línea, con número}.
|
|
15
|
+
Me interesa especialmente {algo real del rol/empresa}.
|
|
16
|
+
¿Te sirve si te paso mi CV o coordinamos 15 min?
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Hiring manager / futuro lead (mensaje de valor)
|
|
20
|
+
```
|
|
21
|
+
Hola {nombre}, sigo {producto/equipo de la empresa} y me llamó {detalle específico}.
|
|
22
|
+
Vengo de {contexto}: {logro mapeado a lo que su equipo necesita}.
|
|
23
|
+
Apliqué a {rol}; si te viene bien, encantado de darte contexto en una charla corta.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 3. Pedir un referido (a alguien del equipo)
|
|
27
|
+
```
|
|
28
|
+
Hola {nombre}, vi que trabajás en {empresa} como {rol}.
|
|
29
|
+
Estoy aplicando a {rol} ahí y me encantaría tu perspectiva sobre el equipo.
|
|
30
|
+
Si te parece y te hace sentido, ¿estarías abierto a referirme? Sin problema si no.
|
|
31
|
+
Te dejo mi perfil para que veas si encaja: {link}.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 4. Connection note (<300 chars)
|
|
35
|
+
```
|
|
36
|
+
Hola {nombre}, me interesa mucho {empresa/área}. Vengo de {1 cosa}.
|
|
37
|
+
Me encantaría conectar y, si surge, charlar sobre {rol/equipo}.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 5. Follow-up 1 (día 4–5) — con valor nuevo
|
|
41
|
+
```
|
|
42
|
+
Hola {nombre}, te escribo para reafirmar mi interés en {rol}.
|
|
43
|
+
Sumo un dato: {proyecto/idea relevante para su producto}.
|
|
44
|
+
Quedo a disposición para lo que necesiten del proceso.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 6. Follow-up 2 (día 10–12) — cierre digno
|
|
48
|
+
```
|
|
49
|
+
Hola {nombre}, entiendo que están con mucho.
|
|
50
|
+
Dejo la puerta abierta por si avanza {rol}; me encantaría sumar.
|
|
51
|
+
¡Gracias por el tiempo!
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Reglas de cadencia
|
|
55
|
+
|
|
56
|
+
- Máximo 2 follow-ups. Después, soltar.
|
|
57
|
+
- Cada follow-up agrega algo (no "¿novedades?").
|
|
58
|
+
- Espaciar 4–5 días. Nunca el mismo día ni días consecutivos.
|
|
59
|
+
- Registrar cada toque en la nota de `05-Contactos/`.
|
|
60
|
+
|
|
61
|
+
## Tono
|
|
62
|
+
|
|
63
|
+
- Cálido y profesional, nunca servil ni agresivo.
|
|
64
|
+
- Confiado: estás ofreciendo valor, no mendigando.
|
|
65
|
+
- Humano: escribí como hablás, sin jerga corporativa vacía.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cover-letter
|
|
3
|
+
description: "Write concise, confident cover letters and application messages tailored to the company and the person who will read them. Triggers: cover letter, carta de presentacion, mensaje de aplicacion, application message, carta de motivacion, why I'm a fit."
|
|
4
|
+
argument-hint: "[application-note | company + role]"
|
|
5
|
+
category: "career"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Write, Edit, WebFetch
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Cover Letter — Mensaje de aplicación de élite
|
|
11
|
+
|
|
12
|
+
Escribís cover letters y mensajes de aplicación **concisos, seguros y custom** — que suenan a una persona que entendió el rol y la empresa, no a una plantilla. El objetivo: que el lector quiera leer el CV.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- **Rol y empresa** + el job description.
|
|
17
|
+
- **Perfil base / maestro** (`01-Perfiles/`) para los logros y el ángulo.
|
|
18
|
+
- **Research** de `04-Empresas/` y del contacto en `05-Contactos/` (a quién le escribís cambia el tono).
|
|
19
|
+
- El **canal**: cover letter formal (campo del ATS), mensaje corto de LinkedIn/email, o párrafo de "why you" en un form.
|
|
20
|
+
|
|
21
|
+
## Principios
|
|
22
|
+
|
|
23
|
+
- **Una página máximo; idealmente 150–250 palabras** para mensajes, 250–350 para carta formal.
|
|
24
|
+
- **El primer renglón gana o pierde.** Nada de "I am writing to apply for…". Abrí con un gancho específico: un logro relevante, un punto de conexión con la empresa, o por qué este rol te mueve.
|
|
25
|
+
- **Mostrá fit, no historia.** 1–2 logros que mapean directo a lo que piden, con número.
|
|
26
|
+
- **Específico de la empresa.** Una frase que demuestre que investigaste (producto, valor, problema que resuelven). Si la podés pegar en otra aplicación, no sirve.
|
|
27
|
+
- **Confiado, no arrogante; cálido, no servil.** Ni "would be honored", ni "I'm the best candidate".
|
|
28
|
+
- **CTA simple al final.** Disponibilidad / ganas de charlar, sin rogar.
|
|
29
|
+
|
|
30
|
+
## Estructura (carta formal)
|
|
31
|
+
|
|
32
|
+
1. **Hook** — gancho específico (logro o conexión real).
|
|
33
|
+
2. **Fit** — 1–2 logros mapeados a must-haves del JD, con métrica.
|
|
34
|
+
3. **Empresa** — por qué *esta* empresa/rol (research real).
|
|
35
|
+
4. **Cierre** — CTA breve + gracias.
|
|
36
|
+
|
|
37
|
+
## Estructura (mensaje corto / LinkedIn / email)
|
|
38
|
+
|
|
39
|
+
- Saludo personal (nombre del contacto si lo tenés).
|
|
40
|
+
- 1 oración: quién sos + el logro más relevante.
|
|
41
|
+
- 1 oración: por qué esta empresa/rol.
|
|
42
|
+
- 1 oración: CTA (charlar / te dejo el CV).
|
|
43
|
+
|
|
44
|
+
## Proceso
|
|
45
|
+
|
|
46
|
+
1. Leer JD + research. Identificar los 2 must-haves donde más brillás.
|
|
47
|
+
2. Elegir formato según canal.
|
|
48
|
+
3. Draftear. Después **cortar sin piedad** todo lo genérico.
|
|
49
|
+
4. Generar 2 variantes de apertura si el usuario quiere elegir.
|
|
50
|
+
5. Guardar en la nota de aplicación (`03-Aplicaciones/`, sección cover letter) o en `07-Recursos/` si es reutilizable.
|
|
51
|
+
|
|
52
|
+
## Idioma
|
|
53
|
+
|
|
54
|
+
Inglés para roles internacionales; español si la empresa es hispanohablante. Espejá el idioma del JD ante la duda.
|
|
55
|
+
|
|
56
|
+
## Anti-patrones (cortar siempre)
|
|
57
|
+
|
|
58
|
+
- "I am writing to express my interest in…" / "Por medio de la presente…".
|
|
59
|
+
- Recitar el CV en prosa.
|
|
60
|
+
- Adjetivos vacíos sin evidencia (passionate, hardworking, team player).
|
|
61
|
+
- Párrafos genéricos que sirven para cualquier empresa.
|
|
62
|
+
- Pedir disculpas o sonar desesperado.
|
|
63
|
+
|
|
64
|
+
## Handoff
|
|
65
|
+
|
|
66
|
+
Pedí aprobación (approval) antes de guardar la carta en la nota de aplicación o en `07-Recursos/`. Cuando la cover letter está READY, el siguiente paso es `/cold-outreach` si hay un contacto al que escribirle directo.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cv-ats-export
|
|
3
|
+
description: "Export a Markdown CV to an ATS-compliant PDF (single column, selectable text, A4) using headless Chrome/Edge — no Obsidian or design tool needed. Triggers: exportar CV a PDF, generar PDF del CV, CV ATS PDF, convertir CV markdown a PDF, imprimir CV."
|
|
4
|
+
argument-hint: "[cv-name | all] [--workspace <path>]"
|
|
5
|
+
category: "career"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# CV ATS Export — Markdown → PDF ATS-compliant
|
|
11
|
+
|
|
12
|
+
Convertís un CV en Markdown a un **PDF que pasa filtros ATS**: una sola columna, texto seleccionable (no imagen), A4, tipografía limpia. Usa Chrome o Edge en modo headless — no requiere Obsidian, LaTeX ni herramienta de diseño.
|
|
13
|
+
|
|
14
|
+
El motor es `scripts/cv_export.py`. La primera vez, instalalo en el workspace (`tools/cv_export.py`) para que quede versionado con los CVs; o corrélo directo desde el skill.
|
|
15
|
+
|
|
16
|
+
## Requisitos
|
|
17
|
+
|
|
18
|
+
- **Python 3** en el PATH.
|
|
19
|
+
- **Google Chrome o Microsoft Edge** instalado (el script autodetecta rutas estándar en Windows/macOS/Linux).
|
|
20
|
+
- CVs en Markdown dentro de `02-CVs/` del career workspace, con headers (`# Nombre`, `## Sección`), bullets y `---` como separadores.
|
|
21
|
+
|
|
22
|
+
## Uso
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Todos los CVs base (CV - *.md) del workspace
|
|
26
|
+
python scripts/cv_export.py
|
|
27
|
+
|
|
28
|
+
# Uno solo (con o sin .md)
|
|
29
|
+
python scripts/cv_export.py "CV - Acme - Backend"
|
|
30
|
+
|
|
31
|
+
# Workspace explícito
|
|
32
|
+
python scripts/cv_export.py --workspace ./career-workspace "CV - Acme - Backend"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
El script:
|
|
36
|
+
1. Localiza el workspace (flag `--workspace`, env `CAREER_WORKSPACE`, o sube directorios buscando una carpeta `02-CVs`, o usa el cwd).
|
|
37
|
+
2. Lee el `.md`, separa frontmatter, convierte el cuerpo a HTML con el CSS ATS embebido.
|
|
38
|
+
3. Imprime a PDF con Chrome/Edge headless (`--headless=new --print-to-pdf`).
|
|
39
|
+
4. Guarda en `02-CVs/exports/`. El nombre sale del frontmatter `archivo_pdf` si existe; si no, del nombre del `.md` (sin el prefijo `CV - `).
|
|
40
|
+
|
|
41
|
+
## Markdown soportado
|
|
42
|
+
|
|
43
|
+
`# ## ### títulos` · `**negrita**` · `*itálica*` · `[texto](url)` · `- bullets` · `---` (regla horizontal). Un salto de línea simple dentro de un párrafo se renderiza como `<br>` (comportamiento tipo Obsidian).
|
|
44
|
+
|
|
45
|
+
## Por qué este pipeline (y no Word/Canva)
|
|
46
|
+
|
|
47
|
+
- **Texto seleccionable garantizado** → el ATS puede parsearlo (a diferencia de un PDF exportado como imagen).
|
|
48
|
+
- **Una columna, sin tablas** → no se rompe el parseo.
|
|
49
|
+
- **Reproducible y versionable** → el CV vive en Markdown (diffeable, en git), el PDF es un artefacto generado.
|
|
50
|
+
- **Mismo estilo siempre** → el CSS ATS está embebido en el script.
|
|
51
|
+
|
|
52
|
+
## Personalizar el estilo
|
|
53
|
+
|
|
54
|
+
El CSS ATS está en la constante `CSS` dentro de `scripts/cv_export.py` (tamaño de fuente, márgenes A4, estilos de h1/h2). Editá ahí si querés ajustar tipografía o espaciado — mantené una sola columna y texto real.
|
|
55
|
+
|
|
56
|
+
## Reglas
|
|
57
|
+
|
|
58
|
+
- No metas tablas, columnas ni imágenes en el CV: rompen el parseo ATS.
|
|
59
|
+
- Verificá el PDF generado: que el texto sea seleccionable y entre en 1–2 páginas.
|
|
60
|
+
- No commitees PDFs con datos sensibles a un remoto sin confirmación.
|
|
61
|
+
|
|
62
|
+
## Handoff
|
|
63
|
+
|
|
64
|
+
Confirmá (approval) antes de instalar `cv_export.py` en el workspace o sobrescribir PDFs. Cuando el PDF está READY y verificado, el siguiente paso suele ser `/cover-letter` para el mensaje de aplicación.
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cv_export.py — Export Markdown CVs to ATS-compliant PDF.
|
|
3
|
+
|
|
4
|
+
Converts a Markdown CV to styled HTML and prints it with headless Chrome/Edge
|
|
5
|
+
(selectable text, single column, A4). No Obsidian, LaTeX or design tool needed.
|
|
6
|
+
|
|
7
|
+
Usage (from anywhere; the script locates the career workspace):
|
|
8
|
+
python cv_export.py # all base CVs (CV - *.md)
|
|
9
|
+
python cv_export.py "CV - Acme - Backend" # a single CV (with/without .md)
|
|
10
|
+
python cv_export.py --workspace ./career-workspace "CV - Acme"
|
|
11
|
+
|
|
12
|
+
Workspace resolution order:
|
|
13
|
+
1. --workspace <path> flag
|
|
14
|
+
2. CAREER_WORKSPACE env var
|
|
15
|
+
3. walk up from the cwd looking for a folder containing "02-CVs" (or "02 - CVs")
|
|
16
|
+
4. the current working directory
|
|
17
|
+
|
|
18
|
+
Output path:
|
|
19
|
+
- If frontmatter has `archivo_pdf`, the PDF is written to that path
|
|
20
|
+
relative to the CVs folder (<workspace>/02-CVs/<archivo_pdf>).
|
|
21
|
+
- Otherwise it goes to <workspace>/02-CVs/exports/<name>.pdf, where
|
|
22
|
+
<name> is the frontmatter field `archivo_final` if present, else the
|
|
23
|
+
.md filename with the "CV - " prefix stripped.
|
|
24
|
+
|
|
25
|
+
Supported Markdown (what CVs use):
|
|
26
|
+
# ## ### headings · **bold** · *italic* · [text](url) · - bullets · ---
|
|
27
|
+
A single line break inside a paragraph becomes <br> (Obsidian-like).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import html
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import subprocess
|
|
34
|
+
import sys
|
|
35
|
+
import tempfile
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
# Possible names for the CVs folder (portable + Obsidian-style with spaces).
|
|
39
|
+
CV_DIR_NAMES = ["02-CVs", "02 - CVs"]
|
|
40
|
+
|
|
41
|
+
BROWSERS = [
|
|
42
|
+
# Windows
|
|
43
|
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
44
|
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
45
|
+
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
46
|
+
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
|
47
|
+
# macOS
|
|
48
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
49
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
50
|
+
# Linux
|
|
51
|
+
"/usr/bin/google-chrome",
|
|
52
|
+
"/usr/bin/google-chrome-stable",
|
|
53
|
+
"/usr/bin/chromium",
|
|
54
|
+
"/usr/bin/chromium-browser",
|
|
55
|
+
"/usr/bin/microsoft-edge",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# ATS-friendly print style: single column, selectable text, A4.
|
|
59
|
+
CSS = """
|
|
60
|
+
@page { size: A4; margin: 18mm 16mm 18mm 16mm; }
|
|
61
|
+
body {
|
|
62
|
+
font-family: "Calibri", "Arial", "Helvetica", sans-serif;
|
|
63
|
+
font-size: 10.5pt; line-height: 1.35; color: #000; background: #fff;
|
|
64
|
+
margin: 0; padding: 0;
|
|
65
|
+
}
|
|
66
|
+
h1 { font-size: 18pt; font-weight: 700; margin: 0 0 2pt 0; padding: 0; }
|
|
67
|
+
h2 {
|
|
68
|
+
font-size: 12pt; font-weight: 700; text-transform: uppercase;
|
|
69
|
+
letter-spacing: 0.5pt; border-bottom: 0.5pt solid #000;
|
|
70
|
+
padding: 0 0 1pt 0; margin: 10pt 0 5pt 0;
|
|
71
|
+
page-break-after: avoid;
|
|
72
|
+
}
|
|
73
|
+
h3 { font-size: 11pt; font-weight: 700; margin: 6pt 0 2pt 0; padding: 0; page-break-after: avoid; }
|
|
74
|
+
p { margin: 2pt 0; page-break-inside: avoid; }
|
|
75
|
+
ul, ol { margin: 2pt 0 4pt 0; padding-left: 16pt; }
|
|
76
|
+
li { margin: 1pt 0; page-break-inside: avoid; }
|
|
77
|
+
strong { font-weight: 700; }
|
|
78
|
+
a { color: #000; text-decoration: none; }
|
|
79
|
+
hr { border: none; border-top: 0.5pt solid #000; margin: 6pt 0; }
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def find_cv_dir(start):
|
|
84
|
+
"""Walk up from `start` looking for a directory containing a CVs folder."""
|
|
85
|
+
cur = Path(start).resolve()
|
|
86
|
+
for base in [cur, *cur.parents]:
|
|
87
|
+
for name in CV_DIR_NAMES:
|
|
88
|
+
cand = base / name
|
|
89
|
+
if cand.is_dir():
|
|
90
|
+
return cand
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_cv_dir(workspace_arg):
|
|
95
|
+
# 1. explicit --workspace
|
|
96
|
+
if workspace_arg:
|
|
97
|
+
ws = Path(workspace_arg).resolve()
|
|
98
|
+
for name in CV_DIR_NAMES:
|
|
99
|
+
if (ws / name).is_dir():
|
|
100
|
+
return ws / name
|
|
101
|
+
# maybe they passed the CVs dir directly
|
|
102
|
+
if ws.name in CV_DIR_NAMES and ws.is_dir():
|
|
103
|
+
return ws
|
|
104
|
+
print(f"[ERROR] No CVs folder ({' / '.join(CV_DIR_NAMES)}) under: {ws}")
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
# 2. env var
|
|
107
|
+
env = os.environ.get("CAREER_WORKSPACE")
|
|
108
|
+
if env:
|
|
109
|
+
for name in CV_DIR_NAMES:
|
|
110
|
+
if (Path(env) / name).is_dir():
|
|
111
|
+
return Path(env).resolve() / name
|
|
112
|
+
# 3. walk up from cwd
|
|
113
|
+
found = find_cv_dir(Path.cwd())
|
|
114
|
+
if found:
|
|
115
|
+
return found
|
|
116
|
+
# 4. cwd fallback
|
|
117
|
+
print("[ERROR] Could not locate a CVs folder. Use --workspace <path>.")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def strip_frontmatter(text):
|
|
122
|
+
"""Return (frontmatter_dict, body). Flat key: value frontmatter."""
|
|
123
|
+
fm = {}
|
|
124
|
+
if text.startswith("---"):
|
|
125
|
+
end = text.find("\n---", 3)
|
|
126
|
+
if end != -1:
|
|
127
|
+
for line in text[3:end].strip().splitlines():
|
|
128
|
+
if ":" in line:
|
|
129
|
+
k, _, v = line.partition(":")
|
|
130
|
+
fm[k.strip()] = v.strip().strip('"')
|
|
131
|
+
text = text[end + 4:]
|
|
132
|
+
return fm, text.lstrip("\n")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def inline(s):
|
|
136
|
+
"""Inline markdown -> HTML (escape first, then links/bold/italic)."""
|
|
137
|
+
s = html.escape(s, quote=False)
|
|
138
|
+
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
|
|
139
|
+
s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
|
|
140
|
+
s = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", s)
|
|
141
|
+
return s
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def md_to_html(md):
|
|
145
|
+
out = []
|
|
146
|
+
paragraph = []
|
|
147
|
+
in_list = False
|
|
148
|
+
|
|
149
|
+
def flush_paragraph():
|
|
150
|
+
if paragraph:
|
|
151
|
+
out.append("<p>" + "<br>".join(inline(l) for l in paragraph) + "</p>")
|
|
152
|
+
paragraph.clear()
|
|
153
|
+
|
|
154
|
+
def close_list():
|
|
155
|
+
nonlocal in_list
|
|
156
|
+
if in_list:
|
|
157
|
+
out.append("</ul>")
|
|
158
|
+
in_list = False
|
|
159
|
+
|
|
160
|
+
for raw in md.splitlines():
|
|
161
|
+
line = raw.rstrip()
|
|
162
|
+
stripped = line.strip()
|
|
163
|
+
|
|
164
|
+
if not stripped:
|
|
165
|
+
flush_paragraph()
|
|
166
|
+
close_list()
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
m = re.match(r"^(#{1,3})\s+(.*)", stripped)
|
|
170
|
+
if m:
|
|
171
|
+
flush_paragraph()
|
|
172
|
+
close_list()
|
|
173
|
+
level = len(m.group(1))
|
|
174
|
+
out.append(f"<h{level}>{inline(m.group(2))}</h{level}>")
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if re.match(r"^(-{3,}|\*{3,})$", stripped):
|
|
178
|
+
flush_paragraph()
|
|
179
|
+
close_list()
|
|
180
|
+
out.append("<hr>")
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
m = re.match(r"^[-*]\s+(.*)", stripped)
|
|
184
|
+
if m:
|
|
185
|
+
flush_paragraph()
|
|
186
|
+
if not in_list:
|
|
187
|
+
out.append("<ul>")
|
|
188
|
+
in_list = True
|
|
189
|
+
out.append(f"<li>{inline(m.group(1))}</li>")
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
close_list()
|
|
193
|
+
paragraph.append(stripped)
|
|
194
|
+
|
|
195
|
+
flush_paragraph()
|
|
196
|
+
close_list()
|
|
197
|
+
return "\n".join(out)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def find_browser():
|
|
201
|
+
for path in BROWSERS:
|
|
202
|
+
if Path(path).exists():
|
|
203
|
+
return path
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def export(md_path, browser, cv_dir, export_dir):
|
|
208
|
+
text = md_path.read_text(encoding="utf-8")
|
|
209
|
+
fm, body = strip_frontmatter(text)
|
|
210
|
+
|
|
211
|
+
if fm.get("archivo_pdf"):
|
|
212
|
+
pdf_path = cv_dir / fm["archivo_pdf"]
|
|
213
|
+
else:
|
|
214
|
+
name = fm.get("archivo_final") or md_path.stem.replace("CV - ", "")
|
|
215
|
+
pdf_path = export_dir / f"{name}.pdf"
|
|
216
|
+
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
|
|
218
|
+
doc = (
|
|
219
|
+
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
|
220
|
+
f"<title>{html.escape(md_path.stem)}</title>"
|
|
221
|
+
f"<style>{CSS}</style></head><body>"
|
|
222
|
+
+ md_to_html(body)
|
|
223
|
+
+ "</body></html>"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
with tempfile.NamedTemporaryFile(
|
|
227
|
+
"w", suffix=".html", delete=False, encoding="utf-8"
|
|
228
|
+
) as tmp:
|
|
229
|
+
tmp.write(doc)
|
|
230
|
+
tmp_path = Path(tmp.name)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
[
|
|
235
|
+
browser,
|
|
236
|
+
"--headless=new",
|
|
237
|
+
"--disable-gpu",
|
|
238
|
+
"--no-pdf-header-footer",
|
|
239
|
+
f"--print-to-pdf={pdf_path}",
|
|
240
|
+
tmp_path.as_uri(),
|
|
241
|
+
],
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
timeout=120,
|
|
245
|
+
)
|
|
246
|
+
if result.returncode != 0 or not pdf_path.exists():
|
|
247
|
+
print(f"[ERROR] {md_path.name}: browser exit {result.returncode}")
|
|
248
|
+
print(result.stderr[-500:] if result.stderr else "(no stderr)")
|
|
249
|
+
return False
|
|
250
|
+
finally:
|
|
251
|
+
tmp_path.unlink(missing_ok=True)
|
|
252
|
+
|
|
253
|
+
size_kb = pdf_path.stat().st_size // 1024
|
|
254
|
+
print(f"[OK] {md_path.name} -> {pdf_path} ({size_kb} KB)")
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def parse_args(argv):
|
|
259
|
+
workspace = None
|
|
260
|
+
names = []
|
|
261
|
+
i = 0
|
|
262
|
+
while i < len(argv):
|
|
263
|
+
a = argv[i]
|
|
264
|
+
if a == "--workspace":
|
|
265
|
+
i += 1
|
|
266
|
+
workspace = argv[i] if i < len(argv) else None
|
|
267
|
+
elif a.startswith("--workspace="):
|
|
268
|
+
workspace = a.split("=", 1)[1]
|
|
269
|
+
else:
|
|
270
|
+
names.append(a)
|
|
271
|
+
i += 1
|
|
272
|
+
return workspace, names
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def main():
|
|
276
|
+
workspace, names = parse_args(sys.argv[1:])
|
|
277
|
+
|
|
278
|
+
browser = find_browser()
|
|
279
|
+
if not browser:
|
|
280
|
+
print("[ERROR] Chrome/Edge not found in standard paths. Install one or edit BROWSERS.")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
cv_dir = resolve_cv_dir(workspace)
|
|
284
|
+
export_dir = cv_dir / "exports"
|
|
285
|
+
|
|
286
|
+
if names:
|
|
287
|
+
targets = []
|
|
288
|
+
for n in names:
|
|
289
|
+
p = cv_dir / (n if n.endswith(".md") else n + ".md")
|
|
290
|
+
if not p.exists():
|
|
291
|
+
print(f"[ERROR] Not found: {p}")
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
targets.append(p)
|
|
294
|
+
else:
|
|
295
|
+
targets = sorted(cv_dir.glob("CV - *.md"))
|
|
296
|
+
|
|
297
|
+
if not targets:
|
|
298
|
+
print(f"[ERROR] No CVs (CV - *.md) found in {cv_dir}.")
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
|
|
301
|
+
ok = all([export(t, browser, cv_dir, export_dir) for t in targets])
|
|
302
|
+
sys.exit(0 if ok else 1)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
if __name__ == "__main__":
|
|
306
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cv-tailor
|
|
3
|
+
description: "Tailor a CV to a specific job description: extract ATS keywords, decide what to emphasize vs downplay, and rewrite achievement bullets in the offer's vocabulary, starting from a role profile. Triggers: adaptar CV, custom CV, tailor resume, CV para esta oferta, optimizar CV para ATS, matchear CV con job description."
|
|
4
|
+
argument-hint: "[job-url | path-to-jd | application-note]"
|
|
5
|
+
category: "career"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Write, Edit, WebFetch
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# CV Tailor — Adaptar el CV a la oferta
|
|
11
|
+
|
|
12
|
+
Tomás un job description y el perfil base del usuario, y producís un **CV custom** que matchea con los filtros automáticos (ATS) y con la lectura humana del hiring manager. No reescribís la verdad — la reorganizás y la traducís al vocabulario de la oferta.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
- **El job description** (URL → usá WebFetch, o texto pegado, o una nota de `03-Aplicaciones/`).
|
|
17
|
+
- **El perfil base**: `01-Perfiles/<Role>.md` (derivado del maestro vía `/master-profile derive`). Si no existe, sugerir crearlo.
|
|
18
|
+
- **Research del contacto/empresa** si existe en `04-Empresas/` y `05-Contactos/`.
|
|
19
|
+
|
|
20
|
+
## Proceso
|
|
21
|
+
|
|
22
|
+
### 1. Analizar la oferta
|
|
23
|
+
- Extraer **keywords literales** del JD: tecnologías, metodologías, responsabilidades, soft skills, seniority. Literal importa — el ATS hace matching de strings (ej: si piden "React.js" no asumas que "React" matchea igual).
|
|
24
|
+
- Detectar **must-have vs nice-to-have**.
|
|
25
|
+
- Detectar **señales de cultura / ángulo** (startup vs enterprise, code-forward, ownership, etc.).
|
|
26
|
+
- Ver `references/ats-keywords.md` para cómo extraer y mapear keywords.
|
|
27
|
+
|
|
28
|
+
### 2. Mapear contra el perfil
|
|
29
|
+
- Por cada must-have: ¿qué experiencia/proyecto del perfil lo cubre? Marcar gaps honestos.
|
|
30
|
+
- Decidir **qué resaltar** (lo que más alinea, arriba y con más espacio) y **qué bajar de volumen** (lo irrelevante para este rol).
|
|
31
|
+
|
|
32
|
+
### 3. Reescribir
|
|
33
|
+
- Reordenar experiencia y proyectos hacia el rol.
|
|
34
|
+
- Reescribir bullets de logros usando el vocabulario de la oferta, manteniendo número→contexto→resultado.
|
|
35
|
+
- Inyectar keywords de forma natural (en bullets reales, no en un "keyword stuffing" detectable).
|
|
36
|
+
- Ajustar el headline/summary al rol exacto.
|
|
37
|
+
|
|
38
|
+
### 4. Score before/after
|
|
39
|
+
- Calcular un **match rate** aproximado: % de keywords/requisitos del JD presentes en el CV, **antes** vs **después** del tailoring.
|
|
40
|
+
- Reportar el delta ("match 48% → 81%") y qué keywords se cubrieron.
|
|
41
|
+
- **Flag de keyword stuffing:** marcar cualquier inserción que a un revisor humano le suene forzada o poco natural — el objetivo es pasar el ATS *y* leer bien para una persona.
|
|
42
|
+
|
|
43
|
+
### 5. Producir el CV custom
|
|
44
|
+
- Crear/actualizar la nota en `02-CVs/CV - <Empresa> - <Rol>.md` usando el template `CV Custom` (`Templates/CV Custom.md`).
|
|
45
|
+
- Completar frontmatter (`perfil_base`, `aplicacion`, `empresa`, `archivo_pdf`).
|
|
46
|
+
- Documentar las decisiones (keywords, qué se resaltó/bajó) en las secciones del template — sirve para la entrevista y para iterar.
|
|
47
|
+
- El cuerpo del CV en sí debe quedar listo para `/cv-ats-export` (markdown limpio: `# nombre`, `## secciones`, bullets, `---`).
|
|
48
|
+
|
|
49
|
+
## Formato del CV (ATS-friendly)
|
|
50
|
+
|
|
51
|
+
- **Una columna.** Sin tablas, sin cajas de texto, sin columnas — los ATS las leen mal.
|
|
52
|
+
- Secciones estándar con headers claros: Summary, Experience, Skills, Projects, Education.
|
|
53
|
+
- Fechas consistentes (MM/YYYY).
|
|
54
|
+
- Sin gráficos ni imágenes para el contenido crítico.
|
|
55
|
+
- Verbos de acción + métricas.
|
|
56
|
+
|
|
57
|
+
## Idioma
|
|
58
|
+
|
|
59
|
+
CV en **inglés** para roles internacionales/remotos globales; **español** si la oferta/empresa es hispanohablante. Ante la duda, preguntar.
|
|
60
|
+
|
|
61
|
+
## Reglas
|
|
62
|
+
|
|
63
|
+
- Nunca inventar experiencia, fechas o métricas. Tailoring = reorganizar y traducir, no mentir.
|
|
64
|
+
- No keyword-stuffing: cada keyword debe vivir en un bullet verdadero.
|
|
65
|
+
- Un CV custom por postulación — no pisar el perfil base ni el maestro.
|
|
66
|
+
- Después de generar el `.md`, recordar al usuario correr `/cv-ats-export` para el PDF.
|
|
67
|
+
|
|
68
|
+
## Handoff
|
|
69
|
+
|
|
70
|
+
Pedí aprobación (approval) antes de escribir el CV custom. Cuando el `.md` está READY, el siguiente paso es `/cv-ats-export` para generar el PDF ATS y `/cover-letter` para el mensaje.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ATS Keywords — Cómo extraer y mapear
|
|
2
|
+
|
|
3
|
+
Los Applicant Tracking Systems (Greenhouse, Lever, Workday, Workable, etc.) hacen un primer filtro automático antes de que un humano vea el CV. Entender cómo "leen" cambia el resultado.
|
|
4
|
+
|
|
5
|
+
## Cómo funciona un ATS (modelo mental)
|
|
6
|
+
|
|
7
|
+
- Parsea el CV a texto plano y lo indexa.
|
|
8
|
+
- Hace matching de **strings** y variantes contra la oferta y/o contra criterios del recruiter.
|
|
9
|
+
- Rankea o filtra candidatos por densidad/relevancia de keywords y por campos estructurados (título, fechas, ubicación).
|
|
10
|
+
- **Lo que rompe el parseo:** tablas, columnas, text boxes, headers/footers con datos, gráficos, iconos en lugar de texto, PDFs escaneados (imagen).
|
|
11
|
+
|
|
12
|
+
## Extracción de keywords del JD
|
|
13
|
+
|
|
14
|
+
1. **Hard skills / tecnologías** — lenguajes, frameworks, DBs, cloud, herramientas. Copiá la forma **literal** (React.js ≠ React ≠ ReactJS para un match exacto; cuando puedas, incluí la variante que usa la oferta).
|
|
15
|
+
2. **Responsabilidades / verbos** — "design REST APIs", "lead a team", "CI/CD pipelines".
|
|
16
|
+
3. **Metodologías** — Agile, Scrum, TDD, clean architecture.
|
|
17
|
+
4. **Seniority y años** — "5+ years", "senior", "lead".
|
|
18
|
+
5. **Soft skills explícitos** — communication, ownership, cross-functional.
|
|
19
|
+
6. **Señales de cultura / frases gatillo** — ej. "code-forward", "startup mentality", "high autonomy". Suelen indicar qué valoran y dan ángulo para el summary y la cover letter.
|
|
20
|
+
|
|
21
|
+
## Mapeo contra el perfil
|
|
22
|
+
|
|
23
|
+
| Keyword del JD | ¿Lo tengo? | Dónde lo demuestro (bullet/proyecto) | Acción |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| React.js | Sí | "Built React + TS frontends @ X" | resaltar arriba |
|
|
26
|
+
| Kubernetes | Parcial | exposición en CI/CD | mencionar honesto |
|
|
27
|
+
| Go | No | — | gap; no inventar |
|
|
28
|
+
|
|
29
|
+
- **Must-have que tenés** → arriba, con espacio, con la palabra exacta.
|
|
30
|
+
- **Must-have parcial** → mencionar con honestidad ("exposure to…").
|
|
31
|
+
- **Must-have que no tenés** → gap real; no lo inventes. Decidí si igual aplicás.
|
|
32
|
+
|
|
33
|
+
## Inyección natural (no stuffing)
|
|
34
|
+
|
|
35
|
+
- Cada keyword vive en un **bullet verdadero** con contexto y resultado.
|
|
36
|
+
- Una sección "Skills" lista las tecnologías (bien para el ATS), pero las importantes también deben aparecer en la experiencia (bien para el humano y para el ranking).
|
|
37
|
+
- Evitá listas de 40 tecnologías sin contexto: lee como relleno y diluye el ranking.
|
|
38
|
+
|
|
39
|
+
## Checklist de formato ATS-safe
|
|
40
|
+
|
|
41
|
+
- [ ] Una sola columna, sin tablas/text boxes.
|
|
42
|
+
- [ ] Headers de sección estándar (Experience, Skills, Education…).
|
|
43
|
+
- [ ] Fechas consistentes MM/YYYY.
|
|
44
|
+
- [ ] Texto seleccionable (no imagen) — el export de `/cv-ats-export` ya lo garantiza.
|
|
45
|
+
- [ ] Nombre del archivo profesional (`Nombre_Apellido_Empresa.pdf`).
|
|
46
|
+
- [ ] Contacto en el cuerpo, no en el header/footer del documento.
|