claude-code-arcane 1.3.0 → 2.0.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 (67) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +5 -4
  3. package/agents/ecommerce/ecommerce-architect.md +60 -0
  4. package/dist/cli.js +2 -0
  5. package/docs/SKILLS-CATALOG.md +25 -2
  6. package/docs/USER-GUIDE.md +2 -2
  7. package/docs/coding-standards.md +26 -0
  8. package/package.json +2 -1
  9. package/profiles/ecommerce.yaml +48 -0
  10. package/profiles/job-hunt.yaml +12 -2
  11. package/rules/drafter-reviewer.md +39 -0
  12. package/skills/cart-checkout/SKILL.md +85 -0
  13. package/skills/cart-checkout/references/checkout-flow-spec.md +50 -0
  14. package/skills/cold-outreach/SKILL.md +10 -10
  15. package/skills/cold-outreach/references/recruiter-playbook.md +5 -5
  16. package/skills/commerce-analytics/SKILL.md +85 -0
  17. package/skills/commerce-analytics/references/ga4-ecommerce-events.md +63 -0
  18. package/skills/commerce-analytics/scripts/ga4_ecommerce_validate.py +134 -0
  19. package/skills/commerce-data-model/SKILL.md +84 -0
  20. package/skills/commerce-data-model/references/data-model-spec.md +79 -0
  21. package/skills/cover-letter/SKILL.md +8 -8
  22. package/skills/cv-ats-export/SKILL.md +19 -6
  23. package/skills/cv-ats-export/scripts/cv_export.py +49 -4
  24. package/skills/cv-ats-export/scripts/verify_pdf.py +90 -0
  25. package/skills/cv-tailor/SKILL.md +7 -7
  26. package/skills/cv-tailor/references/ats-keywords.md +9 -9
  27. package/skills/interview-prep/SKILL.md +7 -7
  28. package/skills/interview-prep/references/star-framework.md +10 -10
  29. package/skills/inventory-stock/SKILL.md +86 -0
  30. package/skills/inventory-stock/scripts/restock_forecast.py +173 -0
  31. package/skills/job-aplicar/SKILL.md +88 -0
  32. package/skills/job-hunt/SKILL.md +20 -13
  33. package/skills/job-hunt/references/templates/Aplicacion.md +6 -2
  34. package/skills/job-hunt/references/templates/CV Custom.md +1 -1
  35. package/skills/job-hunt/references/templates/Contacto.md +1 -1
  36. package/skills/job-hunt/references/templates/Dashboard.md +19 -15
  37. package/skills/job-hunt/references/templates/Perfil.md +2 -2
  38. package/skills/job-outcome/SKILL.md +55 -0
  39. package/skills/job-scrape/SKILL.md +95 -0
  40. package/skills/job-scrape/references/dedup-playbook.md +25 -0
  41. package/skills/job-scrape/references/getonbrd-cli.md +62 -0
  42. package/skills/job-scrape/references/getonbrd-urls.md +37 -0
  43. package/skills/job-scrape/references/linkedin-cli.md +64 -0
  44. package/skills/job-scrape/references/linkedin-urls.md +42 -0
  45. package/skills/job-scrape/references/seen_jobs.schema.json +68 -0
  46. package/skills/job-scrape/scripts/getonbrd_search.py +501 -0
  47. package/skills/job-scrape/scripts/linkedin_search.py +428 -0
  48. package/skills/job-search/SKILL.md +22 -19
  49. package/skills/job-search/references/scoring-rubric.md +8 -8
  50. package/skills/job-upskill/SKILL.md +58 -0
  51. package/skills/linkedin-optimize/SKILL.md +5 -5
  52. package/skills/master-profile/SKILL.md +17 -17
  53. package/skills/merchant-feed/SKILL.md +85 -0
  54. package/skills/merchant-feed/references/feed-attribute-map.md +47 -0
  55. package/skills/merchant-feed/scripts/feed_validate.py +140 -0
  56. package/skills/network-map/SKILL.md +7 -7
  57. package/skills/order-lifecycle/SKILL.md +86 -0
  58. package/skills/order-lifecycle/references/order-state-machine.md +53 -0
  59. package/skills/payments-architecture/SKILL.md +80 -0
  60. package/skills/personal-brand/SKILL.md +10 -10
  61. package/skills/personal-brand/references/post-pillars.md +14 -14
  62. package/skills/portfolio-site/SKILL.md +11 -11
  63. package/skills/product-listing/SKILL.md +84 -0
  64. package/skills/promotions-discounts/SKILL.md +80 -0
  65. package/skills/shipping-tax/SKILL.md +85 -0
  66. package/skills/storefront-architecture/SKILL.md +80 -0
  67. package/skills/subscription-billing/SKILL.md +81 -0
@@ -7,9 +7,9 @@ user-invocable: true
7
7
  allowed-tools: Read, Glob, Grep, Bash, Write, Edit
8
8
  ---
9
9
 
10
- # Job Hunt — Career Workspace Orchestrator
10
+ # Job Hunt - Career Workspace Orchestrator
11
11
 
12
- Eres el punto de entrada de la suite de búsqueda laboral. Tu trabajo es: detectar (o crear) el **career workspace**, entender en qué etapa está el usuario, y rutear al skill correcto. No hacés todo vos — orquestás.
12
+ Eres el punto de entrada de la suite de búsqueda laboral. Tu trabajo es: detectar (o crear) el **career workspace**, entender en qué etapa está el usuario, y rutear al skill correcto. No hacés todo vos - orquestás.
13
13
 
14
14
  ## Idioma
15
15
 
@@ -32,26 +32,29 @@ career-workspace/
32
32
  07-Recursos/ cover letters reutilizables, links
33
33
  portfolio/ datos source-of-truth del portfolio web
34
34
  Templates/ plantillas con frontmatter
35
- tools/ cv_export.py (lo instala /cv-ats-export)
35
+ tools/ cv_export.py + verify_pdf.py (los instala /cv-ats-export)
36
+ y job_scraper/seen_jobs.json (memoria de dedup de /job-scrape)
36
37
  ```
37
38
 
38
39
  Es **portable y agnóstico de Obsidian**: usa `[[wikilinks]]` (funcionan en Obsidian si el usuario lo usa, y son legibles en texto plano), frontmatter YAML, y prefijos numéricos para ordenar carpetas. No requiere Obsidian instalado.
39
40
 
41
+ Estados de una aplicación (frontmatter `estado`): `interesado → aplicado → entrevista → oferta`, con cierres `contratado | rechazado | sin_respuesta | declinada | descartado`. Los mueve `/job-outcome`; ninguna otra skill marca `aplicado`.
42
+
40
43
  ## Modos
41
44
 
42
- ### `setup` — Crear el workspace
45
+ ### `setup` - Crear el workspace
43
46
  1. Preguntar dónde crear el workspace (default `./career-workspace/`) y el/los rol(es) objetivo.
44
47
  2. Crear el árbol de carpetas completo.
45
48
  3. Copiar los 7 templates desde `references/templates/` a `Templates/` del workspace.
46
49
  4. Crear `00-Dashboard.md` desde `references/templates/Dashboard.md`.
47
50
  5. Sugerir el siguiente paso: `/master-profile` para construir el perfil maestro.
48
51
 
49
- ### `status` — Dónde estoy
52
+ ### `status` - Dónde estoy
50
53
  1. Leer `00-Dashboard.md` y escanear `03-Aplicaciones/`.
51
54
  2. Resumir: aplicaciones activas por prioridad, próximas entrevistas, ofertas que cierran pronto, gaps (aplicaciones sin CV/cover letter).
52
55
  3. Recomendar la acción de mayor impacto.
53
56
 
54
- ### `next` — Qué hago ahora
57
+ ### `next` - Qué hago ahora
55
58
  Según el estado del workspace, recomendar el skill siguiente (ver routing).
56
59
 
57
60
  ## Routing
@@ -61,6 +64,10 @@ Según el estado del workspace, recomendar el skill siguiente (ver routing).
61
64
  | No hay workspace | `/job-hunt setup` |
62
65
  | Falta perfil maestro | `/master-profile` |
63
66
  | Encontrar/scorear ofertas, plan de búsqueda | `/job-search` |
67
+ | Corrida de búsqueda con CLIs y dedup entre corridas | `/job-scrape` |
68
+ | Postulación completa (nota + CV + cover + review + PDF) | `/job-aplicar` |
69
+ | Registrar resultado/avance de una postulación | `/job-outcome` |
70
+ | Gaps de skills agregados + plan de estudio | `/job-upskill` |
64
71
  | Adaptar CV a una oferta | `/cv-tailor` |
65
72
  | Exportar CV a PDF ATS | `/cv-ats-export` |
66
73
  | Escribir cover letter / mensaje de aplicación | `/cover-letter` |
@@ -71,17 +78,17 @@ Según el estado del workspace, recomendar el skill siguiente (ver routing).
71
78
 
72
79
  ## Workflow estándar para una nueva oportunidad
73
80
 
74
- 1. `/job-search` → encontrar y scorear la oferta, crear nota en `03-Aplicaciones/` + `04-Empresas/`.
81
+ 1. `/job-scrape` → corrida de búsqueda con dedup (o `/job-search` para una oferta/búsqueda puntual): nota en `03-Aplicaciones/` + `04-Empresas/`.
75
82
  2. Identificar contacto objetivo → nota en `05-Contactos/`.
76
- 3. `/cv-tailor` → CV custom partiendo del perfil base + research.
77
- 4. `/cv-ats-export` → PDF ATS a `02-CVs/exports/`.
78
- 5. `/cover-letter` → mensaje de aplicación custom.
79
- 6. `/cold-outreach` → si hay contacto al que escribirle directo.
80
- 7. Cuando haya entrevista → `/interview-prep`.
83
+ 3. `/job-aplicar` → pipeline completo de la postulación: CV custom + cover + review (regla drafter-reviewer) + PDF ATS verificado. (A mano: `/cv-tailor` → `/cv-ats-export` → `/cover-letter`.)
84
+ 4. `/cold-outreach` → si hay contacto al que escribirle directo.
85
+ 5. Enviar la postulación (**siempre manual**) → `/job-outcome <empresa>` para marcar `aplicado`; registrar ahí cada avance.
86
+ 6. Cuando haya entrevista → `/interview-prep`.
87
+ 7. Con rechazos acumulados → `/job-upskill` para el plan de estudio según gaps reales.
81
88
 
82
89
  ## Reglas
83
90
 
84
- - Nunca reorganizar carpetas ni renombrar notas sin preguntar — rompe `[[wikilinks]]` en silencio.
91
+ - Nunca reorganizar carpetas ni renombrar notas sin preguntar - rompe `[[wikilinks]]` en silencio.
85
92
  - Nunca commitear datos sensibles (CV con teléfono/dirección, salarios, emails de recruiters) a un remoto sin confirmación explícita.
86
93
  - Si una categoría necesita un template que no existe, proponerlo antes de inventarlo.
87
94
  - Preservar los prefijos numéricos al crear archivos dentro de las carpetas ordenadas.
@@ -4,7 +4,8 @@ empresa:
4
4
  perfil:
5
5
  cv_usado:
6
6
  estado: interesado
7
- score:
7
+ match_score:
8
+ fuente:
8
9
  fecha_aplicacion:
9
10
  fecha_actualizacion: {{date:YYYY-MM-DD}}
10
11
  link_oferta:
@@ -14,6 +15,7 @@ modalidad:
14
15
  ubicacion:
15
16
  prioridad: media
16
17
  ---
18
+ %% estado: interesado -> aplicado -> entrevista -> oferta | cierres: contratado / rechazado / sin_respuesta / declinada / descartado (los mueve /job-outcome) %%
17
19
 
18
20
  # {{title}}
19
21
 
@@ -29,6 +31,8 @@ prioridad: media
29
31
  - Perfil base: [[]]
30
32
  - Qué de mi experiencia alinea con lo que piden:
31
33
  -
34
+ - Gaps reales (alimentan /job-upskill):
35
+ -
32
36
 
33
37
  ## A quién escribirle / contacto principal
34
38
  - [[]]
@@ -39,7 +43,7 @@ prioridad: media
39
43
  - [[]]
40
44
 
41
45
  ## Timeline / seguimiento
42
- - {{date:YYYY-MM-DD}} — creada la nota
46
+ - {{date:YYYY-MM-DD}} - creada la nota
43
47
 
44
48
  ## Entrevistas
45
49
  -
@@ -9,7 +9,7 @@ archivo_pdf:
9
9
  archivo_final:
10
10
  ---
11
11
 
12
- # CV custom — {{title}}
12
+ # CV custom - {{title}}
13
13
 
14
14
  ## Contexto de la postulación
15
15
  - **Aplicación:** [[]]
@@ -21,7 +21,7 @@ ultima_interaccion: {{date:YYYY-MM-DD}}
21
21
 
22
22
 
23
23
  ## Historial de interacciones
24
- - {{date:YYYY-MM-DD}} — creada la nota
24
+ - {{date:YYYY-MM-DD}} - creada la nota
25
25
 
26
26
  ## Mensajes intercambiados
27
27
 
@@ -2,7 +2,7 @@
2
2
  tipo: dashboard
3
3
  ---
4
4
 
5
- # Dashboard — Búsqueda laboral
5
+ # Dashboard - Búsqueda laboral
6
6
 
7
7
  > Índice del workspace. Cuando haya datos (aplicaciones, entrevistas), estas secciones pueden convertirse en queries dinámicas con Bases/Dataview si usás Obsidian.
8
8
 
@@ -16,6 +16,9 @@ tipo: dashboard
16
16
  **Cerradas / descartadas:**
17
17
  -
18
18
 
19
+ ## Leyenda de estados
20
+ ⬜ interesado · ✅ aplicado · 📅 entrevista/oferta · ❌ rechazado/sin_respuesta/declinada · 🚫 descartado/cerrada
21
+
19
22
  ## Próximas entrevistas
20
23
  -
21
24
 
@@ -26,20 +29,21 @@ tipo: dashboard
26
29
  -
27
30
 
28
31
  ## Accesos rápidos por carpeta
29
- - `01-Perfiles/` — perfiles profesionales base + perfil maestro
30
- - `02-CVs/` — versiones de CV (incluye CVs custom por postulación)
31
- - `03-Aplicaciones/` — una nota por postulación
32
- - `04-Empresas/` — research de empresas
33
- - `05-Contactos/` — recruiters, referidos, hiring managers
34
- - `06-Entrevistas/` — prep y feedback por ronda
35
- - `07-Recursos/` — cover letters reutilizables, links útiles
36
- - `portfolio/` — source-of-truth del portfolio web
37
- - `Templates/` — plantillas con frontmatter
32
+ - `01-Perfiles/` - perfiles profesionales base + perfil maestro
33
+ - `02-CVs/` - versiones de CV (incluye CVs custom por postulación)
34
+ - `03-Aplicaciones/` - una nota por postulación
35
+ - `04-Empresas/` - research de empresas
36
+ - `05-Contactos/` - recruiters, referidos, hiring managers
37
+ - `06-Entrevistas/` - prep y feedback por ronda
38
+ - `07-Recursos/` - cover letters reutilizables, links útiles
39
+ - `portfolio/` - source-of-truth del portfolio web
40
+ - `Templates/` - plantillas con frontmatter
41
+ - `tools/` - cv_export.py, verify_pdf.py y job_scraper/seen_jobs.json
38
42
 
39
43
  ## Workflow estándar para una nueva oportunidad
40
- 1. `/job-search` — encontrar y scorear la oferta → nota en `03-Aplicaciones/` + `04-Empresas/`.
44
+ 1. `/job-scrape` - corrida de búsqueda con dedup (o `/job-search` para una oferta puntual) → nota en `03-Aplicaciones/` + `04-Empresas/`.
41
45
  2. Identificar contacto objetivo → nota en `05-Contactos/`.
42
- 3. `/cv-tailor` — CV custom desde el perfil base + research.
43
- 4. `/cv-ats-export` — PDF ATS a `02-CVs/exports/`.
44
- 5. `/cover-letter` — mensaje de aplicación custom.
45
- 6. `/interview-prep` — cuando haya entrevista.
46
+ 3. `/job-aplicar` - pipeline completo: CV custom + cover + review + PDF ATS verificado.
47
+ 4. Enviar (siempre manual) → `/job-outcome <empresa>` para marcar `aplicado` y registrar avances.
48
+ 5. `/interview-prep` - cuando haya entrevista.
49
+ 6. `/job-upskill` - con rechazos acumulados, plan de estudio según gaps reales.
@@ -13,10 +13,10 @@ regla_seniority:
13
13
  fuentes: []
14
14
  ---
15
15
 
16
- # Perfil — {{title}}
16
+ # Perfil - {{title}}
17
17
 
18
18
  ## Resumen
19
- > 2–3 líneas: quién sos para este rol y qué te hace fuerte.
19
+ > 2-3 líneas: quién sos para este rol y qué te hace fuerte.
20
20
 
21
21
  ## Stack principal
22
22
  -
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: job-outcome
3
+ description: "Record the outcome or progress of a job application (interview, offer, hired, rejected, no reply) updating the application note and the dashboard. Triggers: me respondieron de, registrar resultado, me rechazaron, tengo entrevista con, recibi una oferta, no me contestaron, actualizar estado de la postulacion."
4
+ argument-hint: "[empresa]"
5
+ category: "career"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Write, Edit
8
+ ---
9
+
10
+ # /job-outcome - Registrar resultado de una aplicación
11
+
12
+ Registrá qué pasó con una postulación: avances (entrevista agendada/hecha, oferta recibida) o resoluciones (contratado, rechazado, sin respuesta, oferta declinada, descartado). Adaptado del `/outcome` de MadsLorentzen/ai-job-search al esquema de notas del career workspace (rutas relativas al workspace: `--workspace`, env `CAREER_WORKSPACE`, o `./career-workspace/`).
13
+
14
+ Empresa/oferta indicada: `$ARGUMENTS`
15
+
16
+ ## Pasos
17
+
18
+ 1. **Localizar la nota.** Buscar en `03-Aplicaciones/` la nota cuyo nombre o frontmatter `empresa` matchee (case-insensitive) con `$ARGUMENTS`. Si no se pasó argumento o hay ambigüedad, listar las aplicaciones con estado abierto (`interesado`, `aplicado`, `entrevista`, `oferta`) y preguntar cuál.
19
+
20
+ 2. **Recolectar qué pasó.** Preguntar al usuario y clasificar:
21
+ - **Progreso:** entrevista agendada / entrevista realizada / oferta recibida → estados `entrevista` u `oferta`.
22
+ - **Resolución:** `contratado` · `rechazado` · `sin_respuesta` · `declinada` (la oferta la rechazó el usuario) · `descartado` (el usuario perdió interés / la oferta cerró).
23
+ Registrar fecha, feedback textual recibido y lecciones aprendidas. **Registrar datos, no interpretarlos:** citar el feedback tal como llegó, sin suavizarlo ni analizarlo acá.
24
+
25
+ 3. **Actualizar la nota de aplicación**, con approval del usuario sobre lo recolectado (idempotente - agregar, nunca duplicar ni sobrescribir):
26
+ - Frontmatter: `estado` al nuevo valor y `fecha_actualizacion` a hoy. Compatibilidad: si la nota vieja usa `score:` en vez de `match_score:`, no renombrar el campo, solo tocar `estado`.
27
+ - `## Timeline / seguimiento`: agregar línea `- YYYY-MM-DD - <qué pasó>`.
28
+ - Si hubo entrevista: entrada en `## Entrevistas` (fecha, etapa, entrevistador si se sabe, y link a la nota de `06-Entrevistas/` si existe).
29
+ - Feedback y lecciones van en `## Notas`.
30
+
31
+ 4. **Actualizar el dashboard** (`00-Dashboard.md`): buscar la fila de la oferta y actualizar la columna **Estado** con el nuevo estado (usar la leyenda de emojis de abajo, la misma del template Dashboard) y la columna de nota si aporta (ej. "Rechazada 2026-07-13").
32
+
33
+ 5. **Sugerir siguiente paso** según el caso:
34
+ - `entrevista` → ofrecer `/interview-prep`.
35
+ - `rechazado` con feedback → anotar la lección en la nota; si se acumulan 3+ resoluciones, ofrecer `/job-upskill` y revisar los criterios de búsqueda contra los resultados reales.
36
+ - `sin_respuesta` a los 10-14 días → ofrecer follow-up con `/cold-outreach`.
37
+ - `contratado` → 🎉 y actualizar el resto de las aplicaciones abiertas (¿retirarse de procesos?).
38
+
39
+ 6. **Confirmar** al usuario qué se registró (nota, campos tocados, fila del dashboard).
40
+
41
+ ## Vocabulario de `estado` (frontmatter)
42
+
43
+ `interesado → aplicado → entrevista → oferta` y cierra en: `contratado | rechazado | sin_respuesta | declinada | descartado`
44
+
45
+ Emojis del dashboard: ⬜ interesado · ✅ aplicado · 📅 entrevista/oferta · ❌ rechazado/sin_respuesta/declinada · 🚫 descartado/cerrada
46
+
47
+ ## Reglas
48
+
49
+ - No editar en masa notas existentes: solo la nota de la aplicación en cuestión y su fila del dashboard.
50
+ - No inventar datos: si falta información (fecha, feedback), preguntar o dejar el campo como está.
51
+ - Sin guiones largos en todo texto agregado.
52
+
53
+ ## Handoff
54
+
55
+ Registro COMPLETE cuando nota y dashboard quedaron consistentes. Según el resultado, el siguiente paso es `/interview-prep` (entrevista agendada), `/cold-outreach` (follow-up) o `/job-upskill` (rechazos acumulados).
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: job-scrape
3
+ description: "Search job boards (bundled LinkedIn + GetOnBoard CLIs) with dedup memory across runs, score new openings against a role profile and offer to create application notes. Triggers: buscar ofertas nuevas, scrape de ofertas, correr los CLIs de busqueda, buscar trabajo en LinkedIn o GetOnBoard, nueva corrida de busqueda, ofertas con dedup."
4
+ argument-hint: "[perfil] [--jobage N]"
5
+ category: "career"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Bash, Write, Edit
8
+ ---
9
+
10
+ # /job-scrape - Búsqueda de ofertas con memoria
11
+
12
+ Corre los CLIs de búsqueda bundleados (LinkedIn + GetOnBoard, en `scripts/`) con los criterios de un perfil, deduplica contra `seen_jobs.json` y contra las notas existentes, scorea lo nuevo y ofrece crear notas de aplicación. Adaptado del skill job-scraper de MadsLorentzen/ai-job-search.
13
+
14
+ Flags y datos que devuelve cada script: `references/linkedin-cli.md` y `references/getonbrd-cli.md`.
15
+
16
+ Argumentos: `$ARGUMENTS` (perfil, opcional; `--jobage N` para override de recencia, default 7 días).
17
+
18
+ Todas las rutas de notas son relativas al **career workspace** (flag `--workspace`, env `CAREER_WORKSPACE`, o `./career-workspace/`; lo crea `/job-hunt setup`). El estado de dedup vive en `<workspace>/tools/job_scraper/seen_jobs.json`.
19
+
20
+ ## Requisitos
21
+
22
+ - **Python 3** en el PATH (los scripts usan solo la stdlib: cero dependencias, sin build, sin venv).
23
+ - Career workspace con al menos un perfil en `01-Perfiles/`.
24
+ - Opcional: skill `job-search` instalado (aporta la rúbrica de scoring completa).
25
+
26
+ ## Pasos
27
+
28
+ 1. **Perfil y criterios.** Si `$ARGUMENTS` no matchea una nota de `01-Perfiles/` (excluir `_index`), listar los perfiles y preguntar cuál usar. Leer del frontmatter: `keywords`, `keywords_excluir`, `match_score_minimo`, `salario_minimo_usd_mes`, `modalidad`, `seniority`, `regla_seniority`, `mercado_objetivo`.
29
+
30
+ 2. **Cargar estado.** Leer `<workspace>/tools/job_scraper/seen_jobs.json` (si no existe, crearlo con el esqueleto de `references/seen_jobs.schema.json`; si hay notas previas en `03-Aplicaciones/`, ofrecer backfillearlas como `nota_creada`). Validar que toda entrada `nota_creada` apunte a una nota existente en `03-Aplicaciones/`; reportar punteros rotos sin frenar.
31
+
32
+ 3. **Derivar queries.** 3-5 queries con forma de rol desde las keywords del perfil (ej. "fullstack developer", "backend developer"), no fragmentos de skill sueltos.
33
+
34
+ 4. **CONFIRMACIÓN: correr los CLIs.** Antes de la primera corrida de la sesión, mostrar el warning y pedir approval explícito (mismo criterio que `/job-search` con Apify):
35
+
36
+ > ⚠️ El CLI de LinkedIn lee páginas públicas de LinkedIn. El acceso automatizado va **contra sus Terms of Service**, y el uso es a tu propia responsabilidad: personal, volumen bajo, nunca comercial ni barridos masivos. GetOnBoard usa su API pública v0 y no tiene esta restricción.
37
+ > ¿Corro ambos CLIs, solo GetOnBoard, o ninguno?
38
+
39
+ Si el usuario elige solo GetOnBoard, saltear LinkedIn y declarar la cobertura reducida en el resumen final.
40
+
41
+ 5. **Ejecutar los CLIs** (tolerar el fallo de uno y reportar la degradación):
42
+ ```
43
+ python .claude/skills/job-scrape/scripts/linkedin_search.py search -q "<query>" -l "<location>" --remote remote --jobage <N> --format json
44
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "<query>" --jobage <N> --brief
45
+ ```
46
+ La `<location>` de LinkedIn sale del `mercado_objetivo` del perfil; si es worldwide, usar además `-l "Remote"`. Volumen bajo: máximo ~5 queries por CLI por corrida.
47
+
48
+ **`--brief` no es opcional en el barrido:** los JD completos son ~80% del payload de GetOnBoard (~58 KB por query), y el quick score del paso 8 solo necesita los campos estructurados + el arranque de la description. El JD completo se trae en el paso 9, solo para las `high`. Si el CLI de LinkedIn emite un `warning` `NO_CARDS_PARSED` en stderr, la respuesta traía contenido pero el markup cambió: reportarlo como degradación de esa fuente, no como "no hay ofertas".
49
+
50
+ 6. **Normalizar y dedup intra-batch.** Canonicalizar URLs (sin utm/tracking; LinkedIn → `jobs/view/<id>`; GetOnBoard `/empleos/` → `/jobs/`), calcular `job_key = slug(empresa)|slug(titulo)` (reglas exactas en `references/dedup-playbook.md`). Misma oferta en ambas fuentes = una entrada con dos `sources[]`.
51
+
52
+ 7. **Dedup contra historial:**
53
+ - `seen_jobs.json`: entradas `nota_creada` o `descartado` se omiten de la tabla (contarlas aparte); entradas `new` re-vistas actualizan `last_seen` y se muestran marcadas "vista antes, sin decisión".
54
+ - `03-Aplicaciones/`: Grep por `link_oferta` con las URLs canónicas y por nombre `<Empresa> - <Rol>`. Si hay match no registrado en seen_jobs, registrarlo como `nota_creada` (autocuración).
55
+ - Matching blando (misma empresa + títulos muy parecidos): mostrar warning "posible duplicado de [[nota]]", no dedup automático.
56
+
57
+ 8. **Quick score (todas las nuevas)** usando solo datos del listing:
58
+ - `low`: keyword de `keywords_excluir` en el título (o en la description truncada de GetOnBoard); seniority fuera de banda (aplicando `regla_seniority` del perfil si existe); `salary_max < salario_minimo_usd_mes`; o elegibilidad bloqueada (countries/remote_zone excluye el país del usuario según `mercado_objetivo`).
59
+ - `high`: título matchea keywords del perfil + seniority en banda + sin bloqueos.
60
+ - `medium`: el resto. Campos null = desconocido, no penalizar.
61
+
62
+ 9. **Score completo solo para las `high`**, con la rúbrica de `.claude/skills/job-search/references/scoring-rubric.md` (si el skill job-search no está instalado, quedarse con el quick score y avisar). El JD completo se trae con `detail` de la fuente que corresponda (`detail <slug>` en GetOnBoard, `detail <id>` en LinkedIn), con un **cap de ~10 fetches por corrida sumando ambas fuentes** (ToS, volumen bajo), priorizando por quick score y recencia. Si las `high` exceden el cap, scorear las que entren y listar el resto con su quick score, declarando el corte.
63
+
64
+ 10. **Presentar tabla** ordenada por score: Título | Empresa | Fuente(s) | Salario | Score | Veredicto. Preguntar si alguna se descarta ya (registrar `status: descartado` + `descarte_motivo`).
65
+
66
+ 11. **Persistir** todas las entradas nuevas/actualizadas en `seen_jobs.json` y actualizar el campo `actualizado`. JSON válido siempre (es un data file con schema).
67
+
68
+ 12. **Ofrecer crear notas** para las de score ≥ `match_score_minimo`, con approval explícito **por oferta** (o "todas") antes de escribir. Por cada confirmada:
69
+ - Nota en `03-Aplicaciones/<Empresa> - <Rol>.md` desde el template `Templates/Aplicacion.md` del workspace, frontmatter completo (`estado: interesado`, `match_score`, `fuente`, `link_oferta`, `salario_rango`, `modalidad`, `ubicacion`, `prioridad`) y sección "Match con mi perfil" con alineaciones y **gaps reales** (alimentan `/job-upskill`).
70
+ - Fila en `00-Dashboard.md`, sección de aplicaciones activas, respetando las columnas exactas del header y escapando `\|` en wikilinks con alias.
71
+ - Entrada seen_jobs → `status: nota_creada` + `nota`.
72
+
73
+ 13. **Resumen final:** contadores (encontradas / ya vistas / nuevas / descartadas / notas creadas), fuentes efectivamente consultadas y cortes aplicados (cap de `detail`, fuente degradada), y sugerencia de `/job-aplicar` para las top.
74
+
75
+ ## Qué NO hace
76
+
77
+ - No corre el CLI de LinkedIn sin la confirmación del paso 4.
78
+ - No crea notas sin confirmación individual.
79
+ - No genera CVs ni covers (eso es `/job-aplicar`).
80
+ - No marca `estado: aplicado` nunca.
81
+ - No edita notas existentes ni re-scorea ofertas ya con nota.
82
+ - No borra filas del dashboard ni entradas de seen_jobs.
83
+ - No se corre en loop ni excede los caps de volumen.
84
+
85
+ ## Reglas
86
+
87
+ - Respetar la matriz de fuente de verdad de `references/dedup-playbook.md`: seen_jobs → nota → dashboard, unidireccional.
88
+ - Notas nuevas en el idioma del workspace, sin guiones largos, con wikilinks.
89
+ - Volumen bajo siempre en LinkedIn (el acceso automatizado va contra sus ToS; uso personal y puntual), y con el warning + approval del paso 4 en la primera corrida de la sesión. El profile pre-aprueba el permiso de Bash, no la decisión: la confirmación la pide el skill.
90
+ - `--brief` en el barrido de GetOnBoard; el JD completo solo vía `detail` para las `high`.
91
+ - Si ambos CLIs fallan, frenar y reportar (no degradar a scraping manual sin avisar).
92
+
93
+ ## Handoff
94
+
95
+ Al cerrar la corrida, las ofertas con nota creada quedan READY para el pipeline de postulación: correr `/job-aplicar <nota|url>` con la mejor. Para gaps recurrentes detectados en los scores, `/job-upskill`.
@@ -0,0 +1,25 @@
1
+ # seen_jobs - estado de deduplicacion de /job-scrape
2
+
3
+ `seen_jobs.json` registra todas las ofertas vistas por `/job-scrape` para no volver a presentarlas en corridas siguientes. Schema formal en `seen_jobs.schema.json` (junto a este archivo). Vive en `<workspace>/tools/job_scraper/` (no entre las notas) porque es dato de maquina, no nota: el vault/Obsidian no lo indexa.
4
+
5
+ ## Matriz de fuente de verdad (regla anti-divergencia)
6
+
7
+ | Dato | Fuente de verdad | Quien escribe |
8
+ |---|---|---|
9
+ | "Ya vi esta oferta" + triage + descartes pre-nota | `seen_jobs.json` | `/job-scrape` (y `/job-aplicar` al crear o descartar) |
10
+ | Estado del pipeline (`interesado -> aplicado -> ...`), match_score definitivo | Frontmatter de la nota en `03-Aplicaciones/` | `/job-aplicar` crea; `/job-outcome` actualiza |
11
+ | Vista humana de la cola | `00-Dashboard.md` | Derivado de las notas; `/job-aplicar` y `/job-outcome` fila a fila |
12
+
13
+ El flujo es **unidireccional**: seen_jobs -> nota -> dashboard. Cuando una entrada pasa a `status: nota_creada` se congela (solo conserva el puntero `nota`); el pipeline sigue en la nota y NUNCA se sincroniza de vuelta. Por eso no existe el estado `aplicado` en este archivo.
14
+
15
+ ## Claves de dedup
16
+
17
+ 1. **URL canonica** (por fuente): sin utm/tracking params; LinkedIn reducido a `linkedin.com/jobs/view/<id>`; GetOnBoard `/empleos/` -> `/jobs/`.
18
+ 2. **`job_key`** = `slug(empresa)|slug(titulo)`: lowercase, sin acentos, sin puntuacion (conserva `.` interno: coderslab.io, .net), sin sufijos legales (inc, llc, sa...), sin calificadores del titulo entre parentesis ("(remote)", "(latam)"). Detecta la misma oferta publicada en dos fuentes con URLs distintas; se agrega a `sources[]` de la entrada existente en vez de duplicar.
19
+ 3. Matching blando (misma empresa + alto solapamiento de tokens del titulo) = **warning** en `/job-scrape`, nunca dedup automatico (una empresa puede tener dos roles parecidos legitimos, o el mismo rol con titulo traducido).
20
+
21
+ ## Mantenimiento
22
+
23
+ - **Punteros rotos:** `/job-scrape` valida al arrancar que toda entrada `nota_creada` apunte a una nota existente y reporta las rotas.
24
+ - **Poda (opt-in):** entradas `new`/`descartado` con `last_seen` de mas de 90 dias se pueden borrar. Solo a pedido del usuario, nunca automatico. Las `nota_creada` no se podan (son el historial de dedup contra aplicaciones reales).
25
+ - **Primer run:** el archivo arranca vacio (esqueleto del schema). Si ya existen notas en `03-Aplicaciones/`, `/job-scrape` ofrece backfillearlas como `nota_creada` (con `quick_score: null` y `url: "nota://<archivo>"` si la nota no tiene `link_oferta`).
@@ -0,0 +1,62 @@
1
+ # GetOnBoard Search CLI
2
+
3
+ Busca ofertas en GetOnBoard (job board tech de LATAM) via su API pública v0. Sin auth, sin API key, **cero dependencias**: solo la stdlib de Python 3. Espejo del contrato de `linkedin_search.py` para que `/job-scrape` pueda usar ambos de forma intercambiable.
4
+
5
+ El script es `scripts/getonbrd_search.py`. Construido a medida siguiendo el patrón `/add-portal` de MadsLorentzen/ai-job-search (investigar endpoint → scaffold con el mismo contrato → test en vivo), y portado de TypeScript a Python por la regla de lenguaje por capa (`docs/coding-standards.md`); la salida de las dos versiones se comparó campo por campo sobre las mismas queries: identica.
6
+
7
+ ## Uso
8
+
9
+ ### Buscar ofertas
10
+
11
+ ```bash
12
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "<keywords>" [flags]
13
+ ```
14
+
15
+ Flags:
16
+ - `-q / --query <texto>` - **requerido**, minimo 3 chars. La API hace AND de todos los terminos.
17
+ - `--jobage <dias>` - publicada en los ultimos N dias (filtro client-side sobre `published_at`).
18
+ - `--remote <modo>` - `remote` | `hybrid` | `onsite` (filtro client-side).
19
+ - `--page <n>` - paginacion (20 resultados por pagina).
20
+ - `-n / --limit <n>` - tope de resultados (`0` no emite ninguno).
21
+ - `--brief` - solo en `json`: trunca cada `description` a 300 chars y marca `description_truncated`. Los JD completos son ~80% del payload, asi que para triage conviene `--brief` y despues `detail` del shortlist.
22
+ - `--format json|table|plain` - default `json`.
23
+
24
+ Sin flag `--location`: GetOnBoard es LATAM/remoto por naturaleza; filtrar con `--remote` y leer `countries` en el JSON.
25
+
26
+ ### Detalle de una oferta
27
+
28
+ ```bash
29
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py detail <slug|url> [--format json|plain]
30
+ ```
31
+
32
+ Acepta el `ID` (slug) de los resultados de search o cualquier URL de getonbrd.com (`/jobs/...` o `/empleos/...`, con o sin categoria). Dos capas:
33
+ 1. **Search API** (queries progresivas derivadas del slug): datos completos, incluye `applications_count`.
34
+ 2. **Microdata fallback** (pagina SSR, schema.org JobPosting): para ofertas viejas o fuera del indice. Detecta y marca ofertas **CERRADAS**.
35
+
36
+ ## Datos que devuelve (JSON)
37
+
38
+ `id` (slug), `title`, `company`, `location`, `date` (ISO), `url`, `seniority` (Junior/Semi Senior/Senior/Expert), `remote`, `remote_modality` (`remote_local`/`hybrid`/`no_remote`...), `remote_zone`, `countries`, `salary_min_usd_month`/`salary_max_usd_month` (USD/mes cuando esta publicado), `applications_count` (util para la dimension "calidad de la oferta" de la rubrica), `description` (texto plano).
39
+
40
+ ## Ejemplos
41
+
42
+ ```bash
43
+ # Fullstack, ultimas 2 semanas
44
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "fullstack developer" --jobage 14 --format table
45
+
46
+ # Triage barato: estructurado completo, JD truncado
47
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q "fullstack developer" --jobage 14 --brief
48
+
49
+ # .NET remoto
50
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py search -q ".NET developer" --remote remote --format table
51
+
52
+ # JD completo de una oferta
53
+ python .claude/skills/job-scrape/scripts/getonbrd_search.py detail "https://www.getonbrd.com/jobs/programming/full-stack-developer-buildwithin-remote-b1ef" --format plain
54
+ ```
55
+
56
+ ## Notas
57
+
58
+ - API publica en beta (`api/v0`); si cambia, el fallback de microdata sigue funcionando.
59
+ - Errores a **stderr** como `{ "error": "...", "code": "..." }` con exit code `1`.
60
+ - Volumen bajo por cortesia; la API es publica pero no esta pensada para bulk scraping.
61
+ - Referencia de endpoints: `getonbrd-urls.md`.
62
+ - Tests de los parsers: `tests/job_scrape/test_getonbrd_search.py` en el repo de Arcane.
@@ -0,0 +1,37 @@
1
+ # GetOnBoard API v0 - referencia de endpoints (verificada 2026-07-13)
2
+
3
+ Base: `https://www.getonbrd.com/api/v0` - sin autenticacion para endpoints publicos. Formato JSON:API.
4
+
5
+ ## Search de ofertas
6
+
7
+ ```
8
+ GET /search/jobs?query=<q>&per_page=<n>&page=<n>&expand[]=seniority&expand[]=company
9
+ ```
10
+
11
+ - `query`: requerido, min 3 chars. Hace AND de todos los terminos (queries largas sobre-restringen).
12
+ - `per_page` / `page`: paginacion; `meta: { page, per_page, total_pages }` en la respuesta.
13
+ - `expand[]`: resuelve las referencias `seniority` y `company` inline (sin expand vienen solo como `{id, type}`).
14
+
15
+ Respuesta por job (`data[]`): `id` = slug, `links.public_url`, y en `attributes`:
16
+ `title`, `description` (HTML), `functions` (HTML), `desirable` (HTML), `benefits` (HTML), `remote` (bool), `remote_modality` (`remote_local` | `hybrid` | `no_remote` | ...), `remote_zone`, `countries[]`, `min_salary`/`max_salary` (USD/mes, null si no publican), `published_at` (unix), `applications_count`, `seniority.data.attributes.name`, `company.data.attributes.name`, `lang`, `perks[]`.
17
+
18
+ ## Catalogos publicos
19
+
20
+ ```
21
+ GET /seniorities -> Junior=2, Semi Senior=3, Senior=4, Expert=5, Sin experiencia=1
22
+ GET /categories -> categorias (programming, design-ux, etc.)
23
+ ```
24
+
25
+ ## Lo que NO es publico
26
+
27
+ - `GET /jobs/<slug>` devuelve **401** (endpoint privado, requiere API key de cuenta).
28
+ Por eso `detail` del script usa search + filtro por id exacto, con fallback a microdata.
29
+
30
+ ## Paginas SSR (fallback de detail)
31
+
32
+ - `https://www.getonbrd.com/jobs/<slug>` redirige 301 a `/jobs/<categoria>/<slug>`.
33
+ - La pagina renderiza microdata schema.org JobPosting via atributos `itemprop`:
34
+ `title`, `hiringOrganization > name`, `datePosted`, `baseSalary > minValue/maxValue`,
35
+ `description`, `qualifications` (seniority), `address`.
36
+ - Ofertas cerradas: pierden el `itemtype="http://schema.org/JobPosting"` pero conservan
37
+ los `itemprop`; la pagina contiene el texto "Closed job" / "No longer accepting".
@@ -0,0 +1,64 @@
1
+ # LinkedIn Search CLI
2
+
3
+ Busca ofertas en el job board publico de LinkedIn (endpoints `jobs-guest`) para cualquier pais/region y remoto. Sin autenticacion, sin API key, **cero dependencias**: solo la stdlib de Python 3.
4
+
5
+ El script es `scripts/linkedin_search.py`. Portado del CLI en TypeScript vendoreado de [MadsLorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search) (`.agents/skills/linkedin-search/`): los scripts de skill corren en el proyecto del usuario sin build step, y TypeScript ahi implica exigir Node 24+ para el type stripping nativo (ver `docs/coding-standards.md`). La traduccion se verifico comparando la salida de las dos versiones sobre el mismo HTML y el mismo detalle: identica.
6
+
7
+ Diferencias respecto del upstream, todas deliberadas:
8
+ - `normalize_id` acepta el slash final: la URL del boton de compartir es `/jobs/view/<id>/?refId=...` y el regex original solo anclaba en `?`.
9
+ - Warning `NO_CARDS_PARSED` a stderr cuando la respuesta trae contenido pero se parsean 0 cards (cambio de markup vs "no hay ofertas").
10
+ - `&nbsp;` se normaliza a espacio comun: U+00A0 es invisible y rompe el matching de keywords en el JD.
11
+ - Salida UTF-8 forzada: en Windows `stdout` es cp1252 y cualquier acento del JD reventaba al redirigir.
12
+
13
+ ## ⚠️ Uso personal, volumen bajo
14
+
15
+ Lee paginas publicas de LinkedIn; el acceso automatizado va contra sus Terms of Service. **Mantener volumen bajo** (busquedas puntuales, no barridos masivos ni uso comercial). El script reintenta 429/5xx con backoff exponencial. `/job-scrape` pide confirmacion explicita antes de la primera corrida de la sesion.
16
+
17
+ ## Comandos
18
+
19
+ ### Buscar ofertas
20
+
21
+ ```bash
22
+ python .claude/skills/job-scrape/scripts/linkedin_search.py search -l "<lugar>" [flags]
23
+ ```
24
+
25
+ Flags:
26
+ - `-l / --location <texto>` - **requerido.** Place string de LinkedIn: `"Spain"`, `"European Union"`, `"Remote"`, `"Berlin, Germany"`.
27
+ - `-q / --query <texto>` - keywords (titulo, skill, rol). Recomendado.
28
+ - `--jobage <dias>` - publicado en los ultimos `1`, `7`, `14` o `30` dias.
29
+ - `--remote <modo>` - `remote`, `hybrid` u `onsite`.
30
+ - `--page <n>` - paginacion (10 resultados por pagina).
31
+ - `-n / --limit <n>` - tope de resultados emitidos (`0` no emite ninguno).
32
+ - `--format json|table|plain` - default `json`.
33
+
34
+ ### Detalle completo de una oferta
35
+
36
+ ```bash
37
+ python .claude/skills/job-scrape/scripts/linkedin_search.py detail <id|url> [--format json|plain]
38
+ ```
39
+
40
+ Acepta el ID numerico del search, una URL `jobs/view/...` (con o sin slash final y query params, como la que da el boton de compartir) o un URN. Devuelve descripcion completa, seniority, tipo de empleo, industrias y link.
41
+
42
+ ## Ejemplos
43
+
44
+ ```bash
45
+ # Fullstack remoto en un pais, ultima semana
46
+ python .claude/skills/job-scrape/scripts/linkedin_search.py search -q "full stack developer" -l "Spain" --remote remote --jobage 7 --format table
47
+
48
+ # Backend .NET remoto en una region
49
+ python .claude/skills/job-scrape/scripts/linkedin_search.py search -q ".NET developer" -l "European Union" --remote remote --jobage 14 --format table
50
+
51
+ # Game developer remoto global
52
+ python .claude/skills/job-scrape/scripts/linkedin_search.py search -q "game developer" -l "Remote" --jobage 30 --format table
53
+
54
+ # JD completo de una oferta
55
+ python .claude/skills/job-scrape/scripts/linkedin_search.py detail 4434569000 --format plain
56
+ ```
57
+
58
+ ## Notas
59
+
60
+ - Errores van a **stderr** como `{ "error": "...", "code": "..." }` con exit code `1`. Los diagnosticos no fatales usan la misma forma con clave `warning` (ej. `NO_CARDS_PARSED`) y no cambian el exit code.
61
+ - El search de LinkedIn **no es determinista**: dos corridas seguidas con los mismos parametros pueden devolver ofertas distintas. No asumir que "no aparece" significa "no existe"; el dedup de `/job-scrape` esta justamente para eso.
62
+ - `--format json` sirve para encadenar: sacar IDs y pasarlos a `detail`, o scorear ofertas contra los criterios (`match_score`) antes de crear notas en `03-Aplicaciones/` del career workspace.
63
+ - Referencia de como se construyen las URLs upstream: `linkedin-urls.md`.
64
+ - Tests de los parsers: `tests/job_scrape/test_linkedin_search.py` en el repo de Arcane.
@@ -0,0 +1,42 @@
1
+ # LinkedIn Jobs URL Reference
2
+
3
+ Public, unauthenticated `jobs-guest` endpoints used by this skill. Global — the same
4
+ endpoints serve every market; only the `location` value changes.
5
+
6
+ > Personal use only — automated access is against LinkedIn's Terms of Service; keep volume low.
7
+
8
+ ## Search
9
+
10
+ ```
11
+ GET https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search
12
+ ```
13
+
14
+ Query params:
15
+
16
+ | Param | Meaning | Example |
17
+ |-------|---------|---------|
18
+ | `keywords` | Free-text query | `data engineer` |
19
+ | `location` | Place string | `Mumbai, Maharashtra, India` · `Berlin, Germany` · `Remote` |
20
+ | `f_TPR` | Posted-within window (seconds) | `r604800` (7d), `r2592000` (30d) |
21
+ | `f_WT` | Workplace type | `1` on-site · `2` remote · `3` hybrid |
22
+ | `start` | Pagination offset (10/page) | `0`, `10`, `20`, … |
23
+
24
+ Returns an HTML list of job cards (one `<li>` per posting). The script parses each card by
25
+ its `data-entity-urn="urn:li:jobPosting:<id>"` and extracts title, company, location, date, URL.
26
+
27
+ ## Detail
28
+
29
+ ```
30
+ GET https://www.linkedin.com/jobs-guest/jobs/api/jobPosting/<jobId>
31
+ ```
32
+
33
+ Returns a single job's HTML: title (`top-card-layout__title`), company
34
+ (`topcard__org-name-link`), location (`topcard__flavor--bullet`), the rich description
35
+ (`show-more-less-html__markup` / `description__text`), and job-criteria items
36
+ (seniority, employment type, job function, industries).
37
+
38
+ ## Notes
39
+
40
+ - No authentication required.
41
+ - Respect rate limits: the script backs off on 429/5xx.
42
+ - Country-agnostic: pass any `--location` (city, region, country, or "Remote").