@saulwade/swl-ses 1.3.7 → 1.4.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 (129) hide show
  1. package/CLAUDE.md +12 -4
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-webhook-server.js +198 -0
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +21 -1
  7. package/comandos/swl/claudemd.md +14 -1
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/exportar-vault.md +207 -7
  10. package/comandos/swl/nuevo-proyecto.md +24 -2
  11. package/gateway/adapters/base.js +109 -0
  12. package/gateway/adapters/discord.js +167 -0
  13. package/gateway/adapters/email.js +221 -0
  14. package/gateway/adapters/slack.js +192 -0
  15. package/gateway/adapters/telegram.js +183 -0
  16. package/gateway/adapters/webhook.js +113 -0
  17. package/gateway/adapters/whatsapp.js +214 -0
  18. package/gateway/agent-executor.js +322 -0
  19. package/gateway/command-relay.js +271 -0
  20. package/gateway/cron/jobs.js +263 -0
  21. package/gateway/cron/scheduler.js +322 -0
  22. package/gateway/cron/store.js +335 -0
  23. package/gateway/index.js +320 -0
  24. package/gateway/lib/event-channel.js +191 -0
  25. package/gateway/session.js +131 -0
  26. package/gateway/webhook-server.js +324 -0
  27. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  28. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  29. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  30. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  31. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  32. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  33. package/habilidades/eval-framework/SKILL.md +212 -212
  34. package/habilidades/extractor-de-aprendizajes/SKILL.md +24 -10
  35. package/habilidades/harness-claude-code/SKILL.md +299 -299
  36. package/habilidades/infra-github-actions/SKILL.md +166 -166
  37. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  38. package/habilidades/manejo-errores/.evolved.json +8 -8
  39. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  40. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  41. package/habilidades/nextjs-testing/SKILL.md +89 -5
  42. package/habilidades/node-experto/SKILL.md +37 -1
  43. package/habilidades/patrones-python/SKILL.md +229 -229
  44. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  45. package/habilidades/planear-fase/SKILL.md +319 -319
  46. package/habilidades/react-experto/SKILL.md +45 -4
  47. package/habilidades/release-semver/.evolved.json +8 -8
  48. package/habilidades/swl-claudemd/SKILL.md +15 -1
  49. package/habilidades/tdd-workflow/SKILL.md +36 -4
  50. package/habilidades/testing-python/SKILL.md +340 -340
  51. package/hooks/claudemd-bloat-detector.js +161 -161
  52. package/hooks/inyeccion-contexto.js +8 -3
  53. package/hooks/lib/agent-routing.js +107 -107
  54. package/hooks/lib/auto-consolidator.js +335 -335
  55. package/hooks/lib/error-classifier.js +308 -308
  56. package/hooks/lib/merkle-audit.js +96 -96
  57. package/hooks/lib/provenance-tracker.js +191 -191
  58. package/hooks/lib/rate-limit-ip.js +177 -0
  59. package/hooks/lib/rate-limit-tracker.js +253 -253
  60. package/hooks/lib/resource-quota.js +122 -122
  61. package/hooks/lib/retry-jitter.js +165 -165
  62. package/hooks/lib/skill-auditor.js +588 -588
  63. package/hooks/lib/sync-status.js +228 -228
  64. package/hooks/lib/taint-tracker.js +107 -107
  65. package/hooks/lib/text-similarity.js +241 -241
  66. package/hooks/lib/toon-compressor.js +245 -245
  67. package/hooks/lib/webhook-dedup.js +184 -0
  68. package/hooks/lib/webhook-verify.js +123 -0
  69. package/hooks/proteccion-rutas.js +120 -15
  70. package/hooks/registro-turnos.js +209 -209
  71. package/hooks/sugerir-regenerar-inventario.js +170 -170
  72. package/hooks/validar-formato-post-subagente.js +140 -140
  73. package/hooks/validar-memoria-hook.js +218 -218
  74. package/instintos/prompt-appendices.yaml +57 -57
  75. package/manifiestos/agent-output-schemas.json +57 -57
  76. package/manifiestos/modulos.json +1 -0
  77. package/manifiestos/skills-lock.json +37 -37
  78. package/package.json +5 -3
  79. package/plantillas/auditor-veto-template.md +105 -105
  80. package/plantillas/github-workflows/README.md +47 -47
  81. package/plantillas/github-workflows/release-please.yml +44 -44
  82. package/plantillas/github-workflows/swl-ci.yml +107 -107
  83. package/plantillas/github-workflows/swl-security.yml +51 -51
  84. package/plugin.json +1 -1
  85. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  86. package/reglas/arreglar-al-detectar.md +147 -147
  87. package/reglas/fragmentos-compartidos.md +152 -152
  88. package/reglas/harness-claude-code.md +213 -213
  89. package/reglas/usar-context7.md +226 -226
  90. package/reglas/usar-sistema-swl.md +251 -0
  91. package/schemas/diary-entry.schema.json +80 -80
  92. package/scripts/benchmark-memoria.js +167 -167
  93. package/scripts/comandos/skills.js +251 -2
  94. package/scripts/configurar-branch-protection.js +418 -418
  95. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  96. package/scripts/field-report.js +199 -199
  97. package/scripts/generar-checklists-consolidados.js +273 -273
  98. package/scripts/generar-inventario.js +420 -420
  99. package/scripts/generar-matriz-lenguajes.js +271 -271
  100. package/scripts/lib/artefactos-python.js +43 -43
  101. package/scripts/lib/benchmark-metrics.js +160 -160
  102. package/scripts/lib/budget-enforcer.js +252 -252
  103. package/scripts/lib/configurar-ci.js +380 -380
  104. package/scripts/lib/contadores-inventario.js +217 -217
  105. package/scripts/lib/detectar-stack-detallado.js +307 -307
  106. package/scripts/lib/diary-entry.js +234 -234
  107. package/scripts/lib/eval-metrics-store.js +218 -218
  108. package/scripts/lib/eval-quality.js +171 -171
  109. package/scripts/lib/eval-schemas.js +144 -144
  110. package/scripts/lib/eval-self-correct.js +106 -106
  111. package/scripts/lib/eval-validator.js +185 -185
  112. package/scripts/lib/jaccard-similarity.js +98 -98
  113. package/scripts/lib/longmemeval-runner.js +125 -125
  114. package/scripts/lib/npm-version.js +261 -261
  115. package/scripts/lib/paquetes-conocidos.js +50 -50
  116. package/scripts/lib/prompt-builder.js +264 -264
  117. package/scripts/lib/rrf-fusion.js +175 -175
  118. package/scripts/lib/scoring-instintos.js +277 -277
  119. package/scripts/lib/semantic-search.js +252 -252
  120. package/scripts/limpiar-artefactos-python.js +131 -131
  121. package/scripts/mcp-server/README.md +128 -128
  122. package/scripts/mcp-server/handlers.js +206 -206
  123. package/scripts/migrar-csv-a-array.js +168 -168
  124. package/scripts/migrar-fase-dominio.js +201 -201
  125. package/scripts/publicar.js +511 -511
  126. package/scripts/run-eval.js +141 -141
  127. package/scripts/validar-manifest.js +195 -195
  128. package/scripts/validar-userland-vacio.js +110 -110
  129. package/scripts/verificar-release.js +110 -0
@@ -17,9 +17,12 @@ Este comando es **complementario a `/swl:compactar`**, no lo reemplaza. Compacta
17
17
  - Saul lo pide explícitamente.
18
18
  - La sesión produjo una decisión arquitectural que afecta a otros proyectos del ecosistema.
19
19
 
20
- ## Paso 0 — Validación del destino
20
+ ## Paso 0 — Validación del destino y canal de escritura
21
21
 
22
- Verifica que el vault existe y es accesible:
22
+ Hay **dos canales** posibles para escribir al vault. El canal preferido es el
23
+ MCP de Obsidian; el filesystem directo es fallback documentado.
24
+
25
+ ### 0a — Validar el vault en filesystem (para lectura y detección)
23
26
 
24
27
  ```bash
25
28
  test -d "F:\Google Drive\Developer\Obsidian\Vault\SWL\00-Inbox" \
@@ -29,6 +32,89 @@ test -d "F:\Google Drive\Developer\Obsidian\Vault\SWL\00-Inbox" \
29
32
 
30
33
  Si la ruta no es accesible (por ejemplo, Google Drive no sincronizado o letra de unidad distinta), **abortar con mensaje claro**. No intentes rutas alternativas sin permiso explícito.
31
34
 
35
+ ### 0b — Detectar disponibilidad del MCP de Obsidian
36
+
37
+ Verifica si los tools `mcp__obsidian__obsidian_append_content` o
38
+ `mcp__obsidian__obsidian_patch_content` están cargados o deferidos:
39
+
40
+ - Si aparecen como **deferred** en `<system-reminder>`, cargar el schema con
41
+ `ToolSearch(query="select:mcp__obsidian__obsidian_append_content", ...)`.
42
+ - Si tras `ToolSearch` siguen sin estar disponibles, el MCP server no está
43
+ corriendo (Obsidian cerrado o plugin Local REST API desactivado). En ese
44
+ caso usar canal de fallback (filesystem directo) — ver Paso 4.
45
+
46
+ **Por qué MCP-first**: el hook `proteccion-rutas.js` bloquea la herramienta
47
+ `Write` con destino fuera del CWD del proyecto. La ruta del vault
48
+ (`F:\Google Drive\...`) siempre cae fuera del CWD si trabajas en
49
+ `D:\Python\<proyecto>\`. El MCP de Obsidian opera vía HTTPS al puerto 27124
50
+ del plugin Local REST API — **no pasa por el hook de filesystem**, así que
51
+ nunca dispara el bloqueo.
52
+
53
+ Defaultear a filesystem cuando el MCP está disponible es un anti-patrón
54
+ documentado en `reglas/consultar-vault-primero.md § Workflow forzoso para
55
+ escritura al vault`. El bloqueo por `proteccion-rutas.js` no es una falla a
56
+ esquivar — es una señal de que el canal correcto es el MCP.
57
+
58
+ ### 0c — Verificar que el vault activo de Obsidian apunta a `Vault\SWL\` (CRÍTICO)
59
+
60
+ > Origen del paso: sesión 2026-05-13 v1.4.0. El MCP de Obsidian resuelve paths
61
+ > relativos contra el **vault que Obsidian tiene abierto**, no contra una
62
+ > ruta hardcodeada en el plugin. Si Obsidian abrió un vault distinto del
63
+ > esperado (ej. `F:\Google Drive\Developer\Obsidian\Vault\` raíz en lugar
64
+ > de `Vault\SWL\`), todas las escrituras del MCP irán al vault equivocado.
65
+ > El plugin **crea carpetas inexistentes** sobre la marcha, así que ni
66
+ > siquiera obtendrás error — los archivos terminan en una ubicación huérfana.
67
+
68
+ Verifica antes de cualquier llamada MCP:
69
+
70
+ ```bash
71
+ # Path canónico de la config global de Obsidian en Windows
72
+ APP="$APPDATA/obsidian/obsidian.json"
73
+ node -e "
74
+ const fs = require('fs');
75
+ const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
76
+ const abierto = Object.values(data.vaults).find(v => v.open === true);
77
+ console.log('Vault abierto:', abierto ? abierto.path : 'NINGUNO');
78
+ console.log('Esperado: F:\\\\Google Drive\\\\Developer\\\\Obsidian\\\\Vault\\\\SWL');
79
+ console.log('OK:', abierto && abierto.path.endsWith('SWL') ? 'sí' : 'NO');
80
+ " "$APP"
81
+ ```
82
+
83
+ Resultado esperado:
84
+
85
+ ```
86
+ Vault abierto: F:\Google Drive\Developer\Obsidian\Vault\SWL
87
+ Esperado: F:\Google Drive\Developer\Obsidian\Vault\SWL
88
+ OK: sí
89
+ ```
90
+
91
+ Si `OK: NO` o `Vault abierto: NINGUNO`, **NO usar MCP** — caer al filesystem
92
+ para esta sesión y reportar al usuario que debe abrir el vault `SWL` en
93
+ Obsidian (File → Open vault → seleccionar `F:\Google Drive\Developer\Obsidian\Vault\SWL`).
94
+
95
+ ### 0d — Verificar que la apiKey del MCP coincide con la del plugin activo
96
+
97
+ > Origen: cuando el vault activo cambia, la apiKey del plugin Local REST API
98
+ > cambia (cada vault tiene su propio `.obsidian/plugins/obsidian-local-rest-api/data.json`
99
+ > con apiKey, cert y privateKey distintos). El MCP server registrado en
100
+ > Claude.ai tiene una apiKey hardcodeada — si no se actualiza tras cambiar de
101
+ > vault, todas las llamadas retornan `40101 Authorization required`.
102
+
103
+ Si tras `ToolSearch` el MCP carga sus tools pero la primera llamada (ej.
104
+ `obsidian_list_files_in_dir`) retorna `Error 40101: Authorization required`,
105
+ el síntoma es claro: apiKey desincronizada.
106
+
107
+ Acción:
108
+ 1. Leer la apiKey actual del plugin con
109
+ `grep '"apiKey"' "F:/Google Drive/Developer/Obsidian/Vault/SWL/.obsidian/plugins/obsidian-local-rest-api/data.json"`
110
+ 2. Reportar al usuario que actualice en **claude.ai → Settings → Connectors
111
+ → Obsidian → API Key**. Disconnect + Reconnect tras pegar el valor.
112
+ 3. Mientras tanto, usar filesystem como fallback para escribir al Inbox.
113
+
114
+ Si hay procesos zombie de Obsidian o `mcp-obsidian` cliente con apiKey
115
+ cacheada, requieren terminación + reinicio de Claude Code para tomar la
116
+ nueva apiKey.
117
+
32
118
  ## Paso 1 — Identificación del proyecto actual
33
119
 
34
120
  Detecta qué proyecto eres leyendo:
@@ -205,23 +291,104 @@ Ejecutar `/sync-projects {proyecto-slug}` en el vault para integrar los cambios
205
291
 
206
292
  ## Paso 4 — Escritura en el vault
207
293
 
208
- Escribe el archivo en:
294
+ El archivo destino es:
209
295
 
210
296
  ```
211
297
  F:\Google Drive\Developer\Obsidian\Vault\SWL\00-Inbox\YYYY-MM-DD_HHMM_export-{proyecto-slug}.md
212
298
  ```
213
299
 
214
- Usar UTF-8 sin BOM. En PowerShell:
300
+ Con UTF-8 sin BOM. Usar **uno de dos canales** según disponibilidad:
301
+
302
+ ### Canal A (preferido) — MCP de Obsidian
303
+
304
+ Si el Paso 0b confirmó que `mcp__obsidian__obsidian_append_content` está
305
+ disponible (cargado directamente o tras `ToolSearch`), usar este canal:
306
+
307
+ ```jsonc
308
+ // La ruta es RELATIVA al vault root (no incluye F:\...\SWL\)
309
+ mcp__obsidian__obsidian_append_content({
310
+ filepath: "00-Inbox/YYYY-MM-DD_HHMM_export-{proyecto-slug}.md",
311
+ content: "<contenido completo del export>"
312
+ })
313
+ ```
314
+
315
+ Ventajas frente al filesystem:
316
+ - **No pasa por `proteccion-rutas.js`** — el MCP opera vía HTTPS al puerto
317
+ 27124, fuera del flujo de Bash/Write/Edit.
318
+ - **Sin staging intermedio**: escribe directamente al archivo final del vault.
319
+ - **Auditable**: el plugin Local REST API de Obsidian registra el acceso.
320
+ - **Cross-OS**: el wrapper funciona idéntico en Windows/macOS/Linux sin
321
+ manejar separators de path.
322
+
323
+ Notas operativas:
324
+ - Si el archivo ya existe con ese timestamp (raro), agregar sufijo `_b`
325
+ al nombre antes de invocar `append_content`. El MCP de obsidian no
326
+ sobreescribe si el archivo existe — `append` agrega al final.
327
+ - Si se necesita escribir secciones específicas en lugar de un archivo
328
+ completo, usar `mcp__obsidian__obsidian_patch_content` con
329
+ `target_type: "heading"`.
330
+
331
+ ### Canal B (fallback) — Filesystem directo
332
+
333
+ Solo cuando el MCP no responde tras `ToolSearch` (Obsidian cerrado, plugin
334
+ desactivado, sin red local). **Esto va a chocar con `proteccion-rutas.js`**
335
+ si el CWD es el directorio del proyecto, así que requiere ejecución vía
336
+ Bash con redirección o vía un comando del CLI nativo del SO:
215
337
 
216
338
  ```powershell
339
+ # PowerShell — UTF-8 sin BOM
217
340
  $encoding = New-Object System.Text.UTF8Encoding($false)
218
341
  [System.IO.File]::WriteAllText($path, $content, $encoding)
219
342
  ```
220
343
 
221
- En Node.js hook o script:
344
+ ```bash
345
+ # Bash + heredoc — UTF-8 sin BOM por default en Node 18+
346
+ cat > "$path" <<'EOF'
347
+ <contenido del export>
348
+ EOF
349
+ ```
350
+
351
+ **No usar `Write` directo** desde la herramienta del agente — `proteccion-rutas.js`
352
+ lo bloquea. Si por algún motivo el agente necesita usar `Write`, primero
353
+ escribe a `_userland/staging/<timestamp>.md` dentro del CWD, luego mueve con
354
+ `Bash` (`mv` o PowerShell `Move-Item`).
355
+
356
+ Reportar al usuario qué canal se usó:
357
+ - Canal A → `[OK] Vía: MCP Obsidian (puerto 27124)`
358
+ - Canal B → `[OK] Vía: filesystem directo (MCP no disponible)`
359
+ - Canal Híbrido → `[OK] Vía: filesystem para Inbox, MCP para promociones`
360
+
361
+ ### Modo HÍBRIDO (validado en sesión 2026-05-13)
362
+
363
+ Cuando la sesión incluye **autorización ampliada para promover documentos**
364
+ a `02-Projects/`, `07-Decisions/` y `04-Resources/`, el flujo óptimo combina
365
+ ambos canales:
366
+
367
+ 1. **Inbox** (`00-Inbox/YYYY-MM-DD_HHMM_export-{slug}.md`): usar siempre
368
+ **filesystem directo via Bash heredoc con ruta absoluta** (canal B). Razón:
369
+ el Bash heredoc bypassa cualquier desalineación del vault activo de
370
+ Obsidian — escribe directamente al disco en la ruta exacta.
371
+
372
+ 2. **Promociones** (a `02-Projects/`, `07-Decisions/`, `04-Resources/`):
373
+ usar **MCP append_content** (canal A) tras validar Pasos 0c y 0d. Razón:
374
+ las promociones requieren enlaces bidireccionales con `[[...]]` que el
375
+ plugin de Obsidian indexa al detectar la escritura. El filesystem directo
376
+ crea el archivo pero no dispara la reindexación hasta que Obsidian
377
+ detecte el cambio.
378
+
379
+ 3. **Aprobación de revisiones** (cambiar `status: pending-review` → `reviewed`
380
+ en frontmatter): si el archivo está en `00-Inbox/`, usar `sed` via Bash;
381
+ si está en `02-Projects/` etc., usar `mcp__obsidian__obsidian_patch_content`
382
+ con `target_type: "frontmatter"`. Validado que `patch_content` puede
383
+ responder 404 en algunos archivos pre-existentes — caer al filesystem si
384
+ falla.
385
+
386
+ Reportar al usuario el desglose final del modo híbrido:
222
387
 
223
- ```javascript
224
- fs.writeFileSync(path, content, { encoding: 'utf8' });
388
+ ```
389
+ [OK] Inbox creado via filesystem: F:\...\00-Inbox\...md
390
+ [OK] 3 promociones via MCP: 02-Projects, 07-Decisions, 04-Resources
391
+ [OK] Revisión aprobada (reviewed: true) en el Inbox
225
392
  ```
226
393
 
227
394
  ## Paso 5 — Confirmación
@@ -267,6 +434,12 @@ Próximo paso: al abrir el vault, ejecuta:
267
434
  - Duplicar con el `COMPACTACION.md` del proyecto (copiar pega tal cual). El export es una **síntesis para vault**, no un espejo.
268
435
  - Intentar escribir directamente en `02-Projects/` del vault. Eso es zona ⚠️ en el vault — solo Saul decide si promoverlo.
269
436
  - Usar rutas con backslash no escapadas en código generado.
437
+ - **Defaultear a filesystem cuando el MCP de Obsidian está disponible**.
438
+ El bloqueo por `proteccion-rutas.js` no es una falla a esquivar con Bash
439
+ staging — es señal de que el canal correcto es el MCP. Ver Paso 0b.
440
+ - **Recurrir al workaround del filesystem antes de cargar el schema del
441
+ MCP con `ToolSearch`**. Tools deferred ≠ tools ausentes — el MCP server
442
+ está corriendo, solo falta cargar el schema. Cargar antes de defaultear.
270
443
 
271
444
  ## Relación con otros comandos SWL
272
445
 
@@ -285,7 +458,34 @@ Produce:
285
458
 
286
459
  ```
287
460
  [OK] Export creado: F:\Google Drive\Developer\Obsidian\Vault\SWL\00-Inbox\2026-04-16_2130_export-sigaf.md (487 palabras)
461
+ [OK] Vía: MCP Obsidian (puerto 27124)
462
+ [OK] Enlace canónico: [[DEV - SIGAF]]
288
463
 
289
464
  Próximo paso: al abrir el vault, ejecuta:
290
465
  /sync-projects sigaf
291
466
  ```
467
+
468
+ ## Historial de cambios del comando
469
+
470
+ - **v1.4.0** (2026-05-13) — Agregados Pasos 0c y 0d con verificación previa
471
+ del vault activo de Obsidian y de la apiKey del MCP. Origen: sesión
472
+ 2026-05-13 con autorización ampliada del usuario para promover docs;
473
+ el MCP escribió `02-Projects/DEV - swl-ses.md` en `Vault\` raíz en lugar
474
+ de `Vault\SWL\` porque Obsidian tenía abierto el vault un nivel arriba
475
+ del esperado. Hallazgo crítico: el plugin Local REST API resuelve paths
476
+ relativos contra el vault que Obsidian tiene abierto en `obsidian.json`
477
+ global, no contra una ruta hardcodeada. Si el vault activo está mal,
478
+ el MCP escribe a la ubicación equivocada sin error (el plugin crea
479
+ carpetas inexistentes). Agregado también Paso 0d para apiKey
480
+ desincronizada (síntoma `40101 Authorization required`). Y nuevo
481
+ modo HÍBRIDO en Paso 4: filesystem directo para Inbox + MCP para
482
+ promociones, validado en la misma sesión.
483
+
484
+ - **v1.3.8** (2026-05-11) — Agregado flujo MCP-first en Paso 4 con detección
485
+ en Paso 0b. Origen: en la sesión v1.3.4 → v1.3.8 el comando defaultó a
486
+ `Write` directo al filesystem que fue bloqueado por `proteccion-rutas.js`.
487
+ El fallback al MCP funcionó pero quedó manual. Esta versión documenta
488
+ el orden de preferencia (MCP primero, filesystem como fallback explícito)
489
+ y agrega la nota de "tools deferred ≠ tools ausentes" en anti-patrones.
490
+ Aplicación de la regla `consultar-vault-primero.md § Workflow forzoso
491
+ para escritura al vault`.
@@ -135,11 +135,33 @@ Estructura:
135
135
  - Si el usuario no definió fases claras, propón una división lógica basada en las respuestas del Bloque C
136
136
  - Estado inicial de todas las fases: "Pendiente"
137
137
 
138
- ## Paso 6 — Reporte al usuario
138
+ ## Paso 6 — Generar CLAUDE.md inicial del proyecto
139
+
140
+ Si NO existe `CLAUDE.md` en la raíz del proyecto, generarlo con la estructura
141
+ mínima definida en `/swl:claudemd init-project`. **OBLIGATORIO** incluir como
142
+ primera sección bajo el título:
143
+
144
+ ```markdown
145
+ ## Reglas obligatorias
146
+
147
+ @reglas/usar-sistema-swl.md
148
+ ```
149
+
150
+ Esta referencia carga la matriz operacional del sistema SWL al inicio de cada
151
+ sesión del proyecto y previene que el agente haga trabajo directo cuando
152
+ existe un componente especializado. Sin ella, el proyecto pierde el contrato
153
+ de uso del sistema SWL.
154
+
155
+ Si ya existe `CLAUDE.md` (verificado en Paso 1), revisar que incluya
156
+ `@reglas/usar-sistema-swl.md` en la sección de reglas obligatorias. Si NO
157
+ lo incluye, agregarlo en este paso preservando el resto del contenido.
158
+
159
+ ## Paso 7 — Reporte al usuario
139
160
 
140
161
  Al terminar, reporta:
141
162
 
142
- 1. Lista de archivos creados con sus rutas absolutas
163
+ 1. Lista de archivos creados con sus rutas absolutas (incluyendo CLAUDE.md
164
+ si se generó o se modificó)
143
165
  2. Resumen de la investigación del agente (cuando esté disponible)
144
166
  3. Próximo paso recomendado: "Para comenzar a trabajar en la Fase 1, usa `/swl:discutir-fase 1`"
145
167
  4. Si encontraste riesgos o ambigüedades durante la entrevista, listarlos aquí como "Puntos a resolver"
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Base Adapter — Clase base abstracta para adaptadores de plataforma.
5
+ *
6
+ * Cada plataforma (Telegram, Discord, Webhook) extiende esta clase
7
+ * e implementa los métodos abstractos. El gateway/index.js orquesta
8
+ * múltiples adaptadores de forma uniforme.
9
+ *
10
+ * Patrón adoptado de Hermes Agent (gateway/platforms/base.py).
11
+ *
12
+ * @module gateway/adapters/base
13
+ */
14
+
15
+ /**
16
+ * @abstract
17
+ */
18
+ class BaseAdapter {
19
+ /**
20
+ * @param {string} name - Nombre de la plataforma (ej: 'telegram').
21
+ * @param {object} config - Configuración específica de la plataforma.
22
+ */
23
+ constructor(name, config) {
24
+ if (new.target === BaseAdapter) {
25
+ throw new Error('BaseAdapter es abstracto — usar un adaptador concreto.');
26
+ }
27
+ this.name = name;
28
+ this.config = config;
29
+ this.running = false;
30
+ this._messageHandler = null;
31
+ }
32
+
33
+ /**
34
+ * Inicia la conexión con la plataforma.
35
+ * @abstract
36
+ * @returns {Promise<void>}
37
+ */
38
+ async start() {
39
+ throw new Error(`${this.name}: start() no implementado`);
40
+ }
41
+
42
+ /**
43
+ * Detiene la conexión con la plataforma.
44
+ * @abstract
45
+ * @returns {Promise<void>}
46
+ */
47
+ async stop() {
48
+ this.running = false;
49
+ }
50
+
51
+ /**
52
+ * Envía un mensaje formateado a la plataforma.
53
+ * @abstract
54
+ * @param {object} message
55
+ * @param {string} message.text - Texto del mensaje (Markdown).
56
+ * @param {string} [message.chatId] - Destino específico.
57
+ * @param {string} [message.type] - Tipo: 'notification', 'alert', 'response'.
58
+ * @returns {Promise<boolean>} true si se envió correctamente.
59
+ */
60
+ async send(message) {
61
+ throw new Error(`${this.name}: send() no implementado`);
62
+ }
63
+
64
+ /**
65
+ * Registra un handler para mensajes entrantes de la plataforma.
66
+ * @param {function} handler - Callback (message) => void.
67
+ */
68
+ onMessage(handler) {
69
+ this._messageHandler = handler;
70
+ }
71
+
72
+ /**
73
+ * Emite un mensaje entrante al handler registrado.
74
+ * @protected
75
+ * @param {object} message
76
+ */
77
+ _emitMessage(message) {
78
+ if (typeof this._messageHandler === 'function') {
79
+ this._messageHandler({
80
+ platform: this.name,
81
+ ...message,
82
+ });
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Formatea un mensaje SWL para la plataforma específica.
88
+ * Override en cada adaptador para formato nativo (embeds, markdown, etc.).
89
+ *
90
+ * @param {object} swlMessage - Mensaje del sistema SWL.
91
+ * @returns {string} Texto formateado para la plataforma.
92
+ */
93
+ formatMessage(swlMessage) {
94
+ const { jobName, status, output, type } = swlMessage.payload || swlMessage;
95
+ const lines = [];
96
+
97
+ if (type === 'gateway_notification' || jobName) {
98
+ lines.push(`*${jobName || 'SWL Notificación'}*`);
99
+ if (status) lines.push(`Estado: ${status}`);
100
+ if (output) lines.push(`\`\`\`\n${output.substring(0, 500)}\n\`\`\``);
101
+ } else {
102
+ lines.push(swlMessage.text || JSON.stringify(swlMessage));
103
+ }
104
+
105
+ return lines.join('\n');
106
+ }
107
+ }
108
+
109
+ module.exports = BaseAdapter;
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Discord Adapter — Adaptador de plataforma para Discord.
5
+ *
6
+ * Usa discord.js para conectar un bot. Soporta:
7
+ * - Recepción de mensajes en canal dedicado
8
+ * - Envío con embeds para notificaciones estructuradas
9
+ * - Comandos slash nativos
10
+ * - Filtro por guildId y channelId
11
+ *
12
+ * Dependencia: discord.js (npm install discord.js)
13
+ * Si no está instalada, el adaptador se desactiva silenciosamente.
14
+ *
15
+ * Inspirado en Hermes Agent (gateway/platforms/discord.py).
16
+ *
17
+ * @module gateway/adapters/discord
18
+ */
19
+
20
+ const BaseAdapter = require('./base');
21
+
22
+ /** Límite de caracteres por mensaje de Discord. */
23
+ const MAX_MSG_LENGTH = 2000;
24
+
25
+ class DiscordAdapter extends BaseAdapter {
26
+ constructor(config) {
27
+ super('discord', config);
28
+ this._client = null;
29
+ }
30
+
31
+ async start() {
32
+ let Discord;
33
+ try {
34
+ Discord = require('discord.js');
35
+ } catch (_) {
36
+ console.log('[discord] discord.js no instalado. Ejecutar: npm install discord.js');
37
+ return;
38
+ }
39
+
40
+ const token = this.config.token || process.env.DISCORD_BOT_TOKEN;
41
+ if (!token) {
42
+ console.log('[discord] Token no configurado. Establecer DISCORD_BOT_TOKEN.');
43
+ return;
44
+ }
45
+
46
+ const { Client, GatewayIntentBits } = Discord;
47
+
48
+ this._client = new Client({
49
+ intents: [
50
+ GatewayIntentBits.Guilds,
51
+ GatewayIntentBits.GuildMessages,
52
+ GatewayIntentBits.MessageContent,
53
+ ],
54
+ });
55
+
56
+ this._client.on('ready', () => {
57
+ this.running = true;
58
+ console.log(`[discord] Bot conectado como ${this._client.user.tag}`);
59
+ });
60
+
61
+ this._client.on('messageCreate', (msg) => {
62
+ // Ignorar mensajes del bot
63
+ if (msg.author.bot) return;
64
+
65
+ // Filtrar por guild y canal si están configurados
66
+ if (this.config.guildId && msg.guildId !== this.config.guildId) return;
67
+ if (this.config.channelId && msg.channelId !== this.config.channelId) return;
68
+
69
+ const text = msg.content || '';
70
+ const parts = text.split(/\s+/);
71
+ const command = parts[0].startsWith('/') ? parts[0] : null;
72
+ const args = command ? parts.slice(1).join(' ') : '';
73
+
74
+ this._emitMessage({
75
+ chatId: msg.channelId,
76
+ userId: msg.author.id,
77
+ userName: msg.author.username,
78
+ text: text,
79
+ command: command,
80
+ args: args,
81
+ guildId: msg.guildId,
82
+ raw: msg,
83
+ });
84
+ });
85
+
86
+ this._client.on('error', (err) => {
87
+ console.error(`[discord] Error: ${err.message}`);
88
+ });
89
+
90
+ await this._client.login(token);
91
+ console.log('[discord] Adaptador iniciado.');
92
+ }
93
+
94
+ async stop() {
95
+ if (this._client) {
96
+ try { await this._client.destroy(); } catch (_) {}
97
+ this._client = null;
98
+ }
99
+ this.running = false;
100
+ console.log('[discord] Adaptador detenido.');
101
+ }
102
+
103
+ async send(message) {
104
+ if (!this._client) return false;
105
+
106
+ const channelId = message.chatId || this.config.channelId;
107
+ if (!channelId) return false;
108
+
109
+ try {
110
+ const channel = await this._client.channels.fetch(channelId);
111
+ if (!channel || !channel.isTextBased()) return false;
112
+
113
+ const embed = this._buildEmbed(message);
114
+ if (embed) {
115
+ await channel.send({ embeds: [embed] });
116
+ } else {
117
+ const text = this.formatMessage(message);
118
+ // Split si excede límite
119
+ if (text.length > MAX_MSG_LENGTH) {
120
+ const chunks = [];
121
+ for (let i = 0; i < text.length; i += MAX_MSG_LENGTH) {
122
+ chunks.push(text.substring(i, i + MAX_MSG_LENGTH));
123
+ }
124
+ for (const chunk of chunks) await channel.send(chunk);
125
+ } else {
126
+ await channel.send(text);
127
+ }
128
+ }
129
+ return true;
130
+ } catch (err) {
131
+ console.error(`[discord] Error enviando: ${err.message}`);
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Construye un embed de Discord para notificaciones estructuradas.
138
+ * @private
139
+ */
140
+ _buildEmbed(message) {
141
+ const payload = message.payload || message;
142
+ if (!payload.jobName && !payload.title) return null;
143
+
144
+ let Discord;
145
+ try { Discord = require('discord.js'); } catch (_) { return null; }
146
+
147
+ const { EmbedBuilder } = Discord;
148
+ const isError = payload.status === 'error';
149
+
150
+ const embed = new EmbedBuilder()
151
+ .setTitle(payload.jobName || payload.title || 'SWL Notificación')
152
+ .setColor(isError ? 0xFF0000 : 0x00CC66)
153
+ .setTimestamp();
154
+
155
+ if (payload.status) {
156
+ embed.addFields({ name: 'Estado', value: `\`${payload.status}\``, inline: true });
157
+ }
158
+ if (payload.output) {
159
+ const output = payload.output.substring(0, 1000);
160
+ embed.addFields({ name: 'Output', value: `\`\`\`\n${output}\n\`\`\`` });
161
+ }
162
+
163
+ return embed;
164
+ }
165
+ }
166
+
167
+ module.exports = DiscordAdapter;