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.
Files changed (61) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -0
  3. package/agents/engineering/dotnet-engineer.md +78 -0
  4. package/dist/cli.js +284 -72
  5. package/docs/RELEASE-SETUP.md +99 -0
  6. package/docs/SKILLS-CATALOG.md +26 -3
  7. package/docs/presentations/arcane-overview-12.pptx +0 -0
  8. package/docs/presentations/arcane-overview.pptx +0 -0
  9. package/docs/presentations/build_arcane_deck.py +310 -0
  10. package/docs/presentations/build_arcane_deck_12.py +399 -0
  11. package/package.json +1 -1
  12. package/profiles/backend-dotnet.yaml +54 -0
  13. package/profiles/job-hunt.yaml +35 -0
  14. package/profiles/unity-design.yaml +1 -0
  15. package/profiles/unity-dev.yaml +1 -0
  16. package/rules/dotnet-code.md +64 -0
  17. package/skills/cold-outreach/SKILL.md +65 -0
  18. package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
  19. package/skills/cover-letter/SKILL.md +66 -0
  20. package/skills/cv-ats-export/SKILL.md +64 -0
  21. package/skills/cv-ats-export/scripts/cv_export.py +306 -0
  22. package/skills/cv-tailor/SKILL.md +70 -0
  23. package/skills/cv-tailor/references/ats-keywords.md +46 -0
  24. package/skills/dotnet-architecture/SKILL.md +66 -0
  25. package/skills/dotnet-architecture/references/anti-patterns.md +12 -0
  26. package/skills/dotnet-architecture/references/checklist.md +19 -0
  27. package/skills/dotnet-architecture/references/patterns.md +118 -0
  28. package/skills/dotnet-architecture/references/project-structure.md +78 -0
  29. package/skills/dotnet-best-practices/SKILL.md +76 -0
  30. package/skills/dotnet-best-practices/references/api-design.md +75 -0
  31. package/skills/dotnet-best-practices/references/architecture.md +62 -0
  32. package/skills/dotnet-best-practices/references/async.md +62 -0
  33. package/skills/dotnet-best-practices/references/database.md +69 -0
  34. package/skills/dotnet-best-practices/references/dependency-injection.md +73 -0
  35. package/skills/dotnet-best-practices/references/devops.md +76 -0
  36. package/skills/dotnet-best-practices/references/error-handling.md +72 -0
  37. package/skills/dotnet-best-practices/references/performance.md +63 -0
  38. package/skills/dotnet-best-practices/references/security.md +73 -0
  39. package/skills/dotnet-best-practices/references/testing.md +76 -0
  40. package/skills/dotnet-scaffold/SKILL.md +99 -0
  41. package/skills/install-mcp/SKILL.md +107 -0
  42. package/skills/install-mcp/references/manual-setup.md +92 -0
  43. package/skills/interview-prep/SKILL.md +69 -0
  44. package/skills/interview-prep/references/star-framework.md +42 -0
  45. package/skills/job-hunt/SKILL.md +92 -0
  46. package/skills/job-hunt/references/templates/Aplicacion.md +48 -0
  47. package/skills/job-hunt/references/templates/CV Custom.md +53 -0
  48. package/skills/job-hunt/references/templates/Contacto.md +30 -0
  49. package/skills/job-hunt/references/templates/Dashboard.md +45 -0
  50. package/skills/job-hunt/references/templates/Empresa.md +36 -0
  51. package/skills/job-hunt/references/templates/Entrevista.md +44 -0
  52. package/skills/job-hunt/references/templates/Perfil.md +38 -0
  53. package/skills/job-search/SKILL.md +83 -0
  54. package/skills/job-search/references/scoring-rubric.md +43 -0
  55. package/skills/linkedin-optimize/SKILL.md +79 -0
  56. package/skills/master-profile/SKILL.md +69 -0
  57. package/skills/network-map/SKILL.md +61 -0
  58. package/skills/network-map/scripts/network_map.py +109 -0
  59. package/skills/personal-brand/SKILL.md +54 -0
  60. package/skills/personal-brand/references/post-pillars.md +66 -0
  61. 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.