@saulwade/swl-ses 1.6.3 → 1.6.5

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 (42) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/agentes/gh-fix-ci-swl.md +275 -0
  4. package/agentes/nemesis-auditor-swl.md +90 -1
  5. package/comandos/swl/exportar-vault.md +106 -14
  6. package/comandos/swl/nemesis.md +70 -3
  7. package/comandos/swl/release.md +62 -2
  8. package/comandos/swl/salud.md +32 -0
  9. package/comandos/swl/verificar.md +116 -2
  10. package/habilidades/agent-browser/SKILL.md +111 -4
  11. package/habilidades/agent-deep-links/SKILL.md +148 -0
  12. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
  13. package/habilidades/backend-error-design/SKILL.md +221 -0
  14. package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
  15. package/habilidades/browser-research-domains/SKILL.md +635 -0
  16. package/habilidades/changelog-generator/SKILL.md +172 -0
  17. package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
  18. package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
  19. package/habilidades/fastapi-experto/SKILL.md +49 -4
  20. package/habilidades/harness-claude-code/SKILL.md +4 -1
  21. package/habilidades/postgresql-experto/SKILL.md +80 -4
  22. package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
  23. package/habilidades/proceso-modular-split/SKILL.md +256 -0
  24. package/habilidades/tdd-workflow/SKILL.md +12 -5
  25. package/hooks/extraccion-aprendizajes.js +8 -0
  26. package/hooks/lib/deep-links.js +185 -0
  27. package/hooks/lib/evolution-tracker.js +115 -18
  28. package/hooks/lib/gateway-notify.js +70 -7
  29. package/manifiestos/modulos.json +13 -3
  30. package/manifiestos/skills-lock.json +1247 -1191
  31. package/package.json +3 -3
  32. package/plugin.json +11 -2
  33. package/reglas/arquitectura.md +38 -0
  34. package/reglas/arreglar-al-detectar.md +93 -0
  35. package/reglas/auditorias-documentales-estructurales.md +38 -0
  36. package/reglas/registro-componentes-nuevos.md +14 -0
  37. package/reglas/tests-cleanup.md +220 -0
  38. package/scripts/lib/mcp_config.py +29 -14
  39. package/scripts/mcp-orchestrator.py +153 -131
  40. package/scripts/mcp-pool-manager.py +132 -107
  41. package/scripts/mcp-telemetry.py +139 -120
  42. package/scripts/verificar-release.js +199 -1
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Deep Links — Helper zero-deps para construir URLs que abren archivos en
5
+ * IDEs y editores directamente desde notificaciones.
6
+ *
7
+ * Cubre VS Code (vscode://), VS Code Insiders, Cursor (cursor://), JetBrains
8
+ * (jetbrains://), Codex Desktop (codex://) y fallbacks para apps sin esquema
9
+ * oficial (Visual Studio en Windows usa CLI fallback).
10
+ *
11
+ * Documentación funcional y matriz de soporte en:
12
+ * habilidades/agent-deep-links/SKILL.md
13
+ *
14
+ * Documentado en ADR-0029 (integración parcial awesome-codex-skills).
15
+ *
16
+ * @module hooks/lib/deep-links
17
+ */
18
+
19
+ const path = require('node:path');
20
+
21
+ /**
22
+ * IDEs soportados con esquema oficial confiable.
23
+ * Cualquier valor fuera de esta lista retorna null en construirDeepLink.
24
+ */
25
+ const IDES_SOPORTADOS = Object.freeze([
26
+ 'vscode',
27
+ 'vscode-insiders',
28
+ 'cursor',
29
+ 'codex',
30
+ 'jetbrains-idea',
31
+ 'jetbrains-pycharm',
32
+ 'jetbrains-webstorm',
33
+ 'jetbrains-goland',
34
+ 'jetbrains-rubymine',
35
+ 'jetbrains-clion',
36
+ 'jetbrains-rider',
37
+ 'jetbrains-phpstorm',
38
+ ]);
39
+
40
+ /**
41
+ * Map de IDE → esquema base. JetBrains usa formato distinto (manejado en
42
+ * función separada).
43
+ */
44
+ const ESQUEMAS = Object.freeze({
45
+ 'vscode': 'vscode://file',
46
+ 'vscode-insiders': 'vscode-insiders://file',
47
+ 'cursor': 'cursor://file',
48
+ });
49
+
50
+ /**
51
+ * Indica si un IDE soporta deep links a archivo:línea.
52
+ *
53
+ * @param {string} ide - Identificador del IDE.
54
+ * @returns {boolean}
55
+ */
56
+ function soportaDeepLinks(ide) {
57
+ if (typeof ide !== 'string') return false;
58
+ return IDES_SOPORTADOS.includes(ide);
59
+ }
60
+
61
+ /**
62
+ * Normaliza un path absoluto para uso en deep links.
63
+ * - Resuelve a absoluto (rechaza relativos).
64
+ * - Normaliza separadores (Windows backslash → forward slash; VS Code/Cursor
65
+ * aceptan forward slashes incluso en Windows).
66
+ * - Encoding URI para espacios y caracteres especiales.
67
+ *
68
+ * @param {string} rutaAbsoluta
69
+ * @returns {string|null}
70
+ * @private
71
+ */
72
+ function _normalizarPath(rutaAbsoluta) {
73
+ if (typeof rutaAbsoluta !== 'string' || rutaAbsoluta.length === 0) {
74
+ return null;
75
+ }
76
+ if (!path.isAbsolute(rutaAbsoluta)) {
77
+ return null;
78
+ }
79
+ // path.resolve normaliza .., ., dobles separadores.
80
+ const resuelto = path.resolve(rutaAbsoluta);
81
+ // Forward slashes en todos los SO — los IDEs los aceptan en Windows también.
82
+ const forwardSlashes = resuelto.split(path.sep).join('/');
83
+ // encodeURI preserva : y / (los necesitamos), encodea espacios y caracteres especiales.
84
+ return encodeURI(forwardSlashes);
85
+ }
86
+
87
+ /**
88
+ * Valida que línea y columna sean enteros positivos si están definidos.
89
+ * @private
90
+ */
91
+ function _validarPosicion(linea, columna) {
92
+ if (linea !== undefined && linea !== null) {
93
+ if (!Number.isInteger(linea) || linea < 1) return false;
94
+ }
95
+ if (columna !== undefined && columna !== null) {
96
+ if (!Number.isInteger(columna) || columna < 1) return false;
97
+ }
98
+ return true;
99
+ }
100
+
101
+ /**
102
+ * Construye un deep link para abrir un archivo en el IDE especificado.
103
+ *
104
+ * Retorna null cuando:
105
+ * - El IDE no soporta deep links a archivos (Codex Desktop, Xcode, etc.).
106
+ * - La ruta no es absoluta o está vacía.
107
+ * - Línea/columna están definidas pero no son enteros positivos.
108
+ * - El IDE es JetBrains pero falta `proyecto` (requerido).
109
+ *
110
+ * @param {object} params
111
+ * @param {string} params.ide - 'vscode' | 'vscode-insiders' | 'cursor' | 'jetbrains-<ide-id>'
112
+ * @param {string} params.rutaAbsoluta - Path absoluto al archivo. Forward o backslashes.
113
+ * @param {number} [params.linea] - Línea 1-indexed.
114
+ * @param {number} [params.columna] - Columna 1-indexed.
115
+ * @param {string} [params.proyecto] - Solo para JetBrains: nombre del proyecto.
116
+ * @returns {string|null} URL del deep link, o null si no se puede construir.
117
+ */
118
+ function construirDeepLink(params) {
119
+ const { ide, rutaAbsoluta, linea, columna, proyecto } = params || {};
120
+
121
+ if (!soportaDeepLinks(ide)) return null;
122
+ if (!_validarPosicion(linea, columna)) return null;
123
+
124
+ const pathNormalizado = _normalizarPath(rutaAbsoluta);
125
+ if (!pathNormalizado) return null;
126
+
127
+ // JetBrains usa un formato distinto al resto.
128
+ if (ide.startsWith('jetbrains-')) {
129
+ if (!proyecto || typeof proyecto !== 'string') return null;
130
+ const ideId = ide.slice('jetbrains-'.length);
131
+ // jetbrains://idea/navigate/reference?project=<name>&path=<rel>:<line>
132
+ const query = new URLSearchParams({
133
+ project: proyecto,
134
+ path: linea ? `${pathNormalizado}:${linea}` : pathNormalizado,
135
+ }).toString();
136
+ return `jetbrains://${ideId}/navigate/reference?${query}`;
137
+ }
138
+
139
+ // VS Code / Cursor / VS Code Insiders.
140
+ const base = ESQUEMAS[ide];
141
+ let url = `${base}${pathNormalizado.startsWith('/') ? '' : '/'}${pathNormalizado}`;
142
+ if (linea) {
143
+ url += `:${linea}`;
144
+ if (columna) {
145
+ url += `:${columna}`;
146
+ }
147
+ }
148
+ return url;
149
+ }
150
+
151
+ /**
152
+ * Formato según receptor — envuelve un deep link en la sintaxis correcta del
153
+ * canal destino. Devuelve texto plano (con sufijo "(abrir manualmente)") si
154
+ * el deep link es null.
155
+ *
156
+ * @param {string|null} deepLink - URL del deep link, o null para fallback.
157
+ * @param {string} etiqueta - Texto visible para el usuario.
158
+ * @param {string} receptor - 'telegram' | 'discord' | 'slack' | 'email' | 'plain'
159
+ * @returns {string}
160
+ */
161
+ function formatearEnlace(deepLink, etiqueta, receptor) {
162
+ if (!deepLink) {
163
+ return `${etiqueta} (abrir manualmente)`;
164
+ }
165
+ switch (receptor) {
166
+ case 'slack':
167
+ return `<${deepLink}|${etiqueta}>`;
168
+ case 'telegram':
169
+ case 'discord':
170
+ // Ambos usan Markdown estándar para enlaces.
171
+ return `[${etiqueta}](${deepLink})`;
172
+ case 'email':
173
+ return `<a href="${deepLink}">${etiqueta}</a>`;
174
+ case 'plain':
175
+ default:
176
+ return `${etiqueta}: ${deepLink}`;
177
+ }
178
+ }
179
+
180
+ module.exports = {
181
+ construirDeepLink,
182
+ formatearEnlace,
183
+ soportaDeepLinks,
184
+ IDES_SOPORTADOS,
185
+ };
@@ -345,6 +345,30 @@ function _stripEvolutionFields(content) {
345
345
  .join('\n');
346
346
  }
347
347
 
348
+ /**
349
+ * Separa frontmatter YAML del body en un archivo markdown SWL.
350
+ *
351
+ * Cuando se calcula el diff de mutaciones de un archivo evolucionado, el
352
+ * frontmatter SIEMPRE diverge (el destino tiene campos `evolved-*` que el
353
+ * origen no tiene, y viceversa con campos nuevos del paquete). Contar esas
354
+ * diferencias como "mutaciones del usuario" genera ruido masivo por
355
+ * desplazamiento de líneas. Esta función permite comparar solo el body.
356
+ *
357
+ * @param {string} content - Contenido completo del archivo .md.
358
+ * @returns {{ frontmatter: string, body: string }}
359
+ * - `frontmatter`: bloque YAML entre `---` (vacío si no hay frontmatter).
360
+ * - `body`: todo lo que viene después del frontmatter cerrado.
361
+ * @private
362
+ */
363
+ function _splitFrontmatterAndBody(content) {
364
+ const m = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
365
+ if (!m) return { frontmatter: '', body: content };
366
+ return { frontmatter: m[1], body: m[2] };
367
+ }
368
+
369
+ /** Umbral defensivo: tras este número de diffs el archivo pasa a modo resumen. */
370
+ const DIFF_NOISY_THRESHOLD = 50;
371
+
348
372
  // ---------------------------------------------------------------------------
349
373
  // Merge de evoluciones
350
374
  // ---------------------------------------------------------------------------
@@ -356,10 +380,29 @@ function _stripEvolutionFields(content) {
356
380
  * evolución (frontmatter evolved-*). Las mutaciones de contenido se preservan
357
381
  * generando un archivo .evolved-diff.md que Claude puede re-aplicar.
358
382
  *
383
+ * Comparación: solo el body (post-frontmatter) se compara línea-a-línea.
384
+ * El frontmatter SIEMPRE diverge (el destino tiene campos `evolved-*` que el
385
+ * origen no tiene, y viceversa con campos nuevos del paquete), por lo que
386
+ * contarlo como mutación genera ruido por desplazamiento.
387
+ *
388
+ * Limpieza: cuando un merge posterior elimina la divergencia (diffs vacíos),
389
+ * borra el `.evolved-diff.md` huérfano de sesiones previas si existe.
390
+ *
391
+ * Cap defensivo: si tras alinear correctamente el body aún hay más de
392
+ * `DIFF_NOISY_THRESHOLD` líneas distintas, genera un resumen estadístico
393
+ * con muestra (primeras 20 + últimas 5) en lugar del dump completo.
394
+ *
359
395
  * @param {string} destino - Ruta del archivo evolucionado (local).
360
396
  * @param {string} origen - Ruta del archivo nuevo del paquete.
361
397
  * @param {string} versionNueva - Versión del paquete nuevo.
362
- * @returns {{ merged: boolean, diffPath?: string, error?: string }}
398
+ * @returns {{
399
+ * merged: boolean,
400
+ * diffPath?: string,
401
+ * diffsCount?: number,
402
+ * cleanedDiff?: boolean,
403
+ * truncated?: boolean,
404
+ * error?: string
405
+ * }}
363
406
  */
364
407
  function mergeEvolved(destino, origen, versionNueva) {
365
408
  try {
@@ -375,11 +418,14 @@ function mergeEvolved(destino, origen, versionNueva) {
375
418
  const destinoContent = fs.readFileSync(destino, 'utf8');
376
419
  const origenContent = fs.readFileSync(origen, 'utf8');
377
420
 
378
- // Extraer las líneas que son diferentes entre destino (sin evolved fields) y origen.
379
- // Normalizar CRLF a LF para comparar independientemente del SO de origen.
380
- const destinoSinEvo = _stripEvolutionFields(destinoContent);
381
- const origenLines = origenContent.split(/\r?\n/);
382
- const destinoLines = destinoSinEvo.split(/\r?\n/);
421
+ // Comparar SOLO el body, no el frontmatter. El frontmatter del destino
422
+ // tiene los campos `evolved-*` que el origen no tiene contarlos como
423
+ // mutaciones desplaza todas las líneas siguientes y genera ruido.
424
+ const { body: destinoBody } = _splitFrontmatterAndBody(destinoContent);
425
+ const { body: origenBody } = _splitFrontmatterAndBody(origenContent);
426
+
427
+ const origenLines = origenBody.split(/\r?\n/);
428
+ const destinoLines = destinoBody.split(/\r?\n/);
383
429
 
384
430
  const diffs = [];
385
431
  const maxLen = Math.max(origenLines.length, destinoLines.length);
@@ -395,34 +441,85 @@ function mergeEvolved(destino, origen, versionNueva) {
395
441
  }
396
442
  }
397
443
 
444
+ const diffPath = destino.replace(/\.md$/, '.evolved-diff.md');
445
+
398
446
  if (diffs.length === 0) {
399
- // Sin diferencias reales — solo re-aplicar campos evolved al nuevo
447
+ // Sin diferencias reales — limpiar diff huérfano si existe (de sesión
448
+ // previa donde sí hubo divergencia que ya quedó resuelta) y re-aplicar
449
+ // campos evolved al destino.
450
+ let cleanedDiff = false;
451
+ if (fs.existsSync(diffPath)) {
452
+ try {
453
+ fs.unlinkSync(diffPath);
454
+ cleanedDiff = true;
455
+ } catch {
456
+ // Best-effort: si el unlink falla por permisos/locks, dejarlo —
457
+ // el merge sigue siendo válido.
458
+ }
459
+ }
460
+
461
+ // force: true — `mergeEvolved` solo se invoca en contexto de update
462
+ // intencional. El skip de isPackageRoot() aplica a la primera marca
463
+ // del mantenedor, no a re-aplicar campos tras un merge resuelto.
400
464
  const marked = markAsEvolved(destino, {
401
465
  from: versionNueva,
402
466
  by: evo.metadata.evolvedBy || 'auto-evolución',
403
467
  rounds: evo.metadata.evolvedRounds ? parseInt(evo.metadata.evolvedRounds, 10) : undefined,
404
468
  score: evo.metadata.evolvedScore,
405
469
  note: `Re-aplicado desde v${evo.metadata.evolvedFrom || '?'} tras actualización a v${versionNueva}`,
470
+ force: true,
406
471
  });
407
- return { merged: marked.marked };
472
+ return { merged: marked.marked, cleanedDiff };
408
473
  }
409
474
 
410
- // Guardar diff para revisión/re-aplicación por Claude
411
- const diffPath = destino.replace(/\.md$/, '.evolved-diff.md');
412
- const diffContent = [
475
+ // Cap defensivo: tras alinear correctamente sigue habiendo más de N diffs.
476
+ // En lugar de dumpear cada línea (puede explotar a miles), generar resumen
477
+ // con muestra acotada.
478
+ const truncated = diffs.length > DIFF_NOISY_THRESHOLD;
479
+ const diffsParaMostrar = truncated
480
+ ? [...diffs.slice(0, 20), ...diffs.slice(-5)]
481
+ : diffs;
482
+
483
+ const header = [
413
484
  `# Diff de evolución — ${path.basename(destino)}`,
414
485
  ``,
415
486
  `**Archivo evolucionado por**: ${evo.metadata.evolvedBy || 'auto-evolución'}`,
416
487
  `**Versión base original**: ${evo.metadata.evolvedFrom || '?'}`,
417
488
  `**Versión nueva**: ${versionNueva}`,
418
489
  `**Fecha**: ${new Date().toISOString().split('T')[0]}`,
490
+ `**Diferencias detectadas (body)**: ${diffs.length}`,
419
491
  ``,
420
- `## Mutaciones locales a re-aplicar`,
421
- ``,
422
- `Estas son las líneas que difieren entre la versión evolucionada y la nueva.`,
423
- `Re-aplicar con \`/swl:autoresearch\` o manualmente.`,
424
- ``,
425
- ...diffs.map(d => [
492
+ ];
493
+
494
+ if (truncated) {
495
+ header.push(
496
+ `## ⚠ Resumen (truncado)`,
497
+ ``,
498
+ `El diff excede el umbral defensivo de ${DIFF_NOISY_THRESHOLD} líneas`,
499
+ `(${diffs.length} diferencias detectadas). Esto suele indicar:`,
500
+ ``,
501
+ `- El archivo fue reescrito completo entre versiones (rebrand, refactor).`,
502
+ `- El alineamiento línea-a-línea no es útil aquí — usar \`git diff\`.`,
503
+ ``,
504
+ `Se muestran las primeras 20 + últimas 5 diferencias como muestra.`,
505
+ `Para diff completo: \`diff <(sed '1,/^---$/d; 1,/^---$/d' archivo) <(...)\`.`,
506
+ ``,
507
+ `## Muestra de mutaciones (primeras 20 + últimas 5)`,
508
+ ``,
509
+ );
510
+ } else {
511
+ header.push(
512
+ `## Mutaciones locales a re-aplicar`,
513
+ ``,
514
+ `Estas son las líneas del body que difieren entre la versión`,
515
+ `evolucionada y la nueva. Re-aplicar con \`/swl:autoresearch\` o manualmente.`,
516
+ ``,
517
+ );
518
+ }
519
+
520
+ const diffContent = [
521
+ ...header,
522
+ ...diffsParaMostrar.map(d => [
426
523
  `### Línea ${d.line}`,
427
524
  `- **Nueva (base)**: \`${d.origen}\``,
428
525
  `- **Evolucionada**: \`${d.destino}\``,
@@ -432,7 +529,7 @@ function mergeEvolved(destino, origen, versionNueva) {
432
529
 
433
530
  atomicWriteSync(diffPath, diffContent, 'utf8');
434
531
 
435
- return { merged: true, diffPath, diffsCount: diffs.length };
532
+ return { merged: true, diffPath, diffsCount: diffs.length, truncated };
436
533
  } catch (err) {
437
534
  return { merged: false, error: err.message };
438
535
  }
@@ -21,9 +21,23 @@ const fs = require('fs');
21
21
  const path = require('path');
22
22
  const { atomicWriteJSON } = require('./atomic-write');
23
23
 
24
+ // Deep links opt-in (ADR-0029). Si el módulo no carga (instalación incompleta),
25
+ // el enriquecimiento se omite silenciosamente — comportamiento default sin
26
+ // cambios respecto a versiones previas.
27
+ let construirDeepLink, formatearEnlace;
28
+ try {
29
+ ({ construirDeepLink, formatearEnlace } = require('./deep-links'));
30
+ } catch {
31
+ construirDeepLink = () => null;
32
+ formatearEnlace = (_, etiqueta) => etiqueta;
33
+ }
34
+
24
35
  const COMMS_DIR = '.planning/comms';
25
36
  const CONFIG_PATH = 'manifiestos/gateway-config.json';
26
37
 
38
+ /** Receptores reconocidos para formato de deep link. */
39
+ const RECEPTORES = new Set(['telegram', 'discord', 'slack', 'email', 'plain']);
40
+
27
41
  /**
28
42
  * Verifica si el gateway está habilitado en configuración.
29
43
  * @returns {boolean}
@@ -66,10 +80,52 @@ function tipoHabilitado(tipo) {
66
80
  }
67
81
  }
68
82
 
83
+ /**
84
+ * Si el payload incluye `fileRef` + `idePreferido`, retorna el enlace
85
+ * formateado al receptor; en otro caso retorna null. No modifica el payload.
86
+ *
87
+ * Estructura esperada del payload (opt-in, ADR-0029):
88
+ * payload.fileRef = {
89
+ * archivo: '/abs/path/al/archivo.js',
90
+ * linea?: 42,
91
+ * columna?: 8,
92
+ * etiqueta?: 'Abrir archivo.js:42', // default: archivo:linea
93
+ * proyecto?: 'mi-proyecto', // requerido solo para JetBrains
94
+ * }
95
+ * payload.idePreferido = 'vscode' | 'cursor' | ...
96
+ *
97
+ * @param {object} payload
98
+ * @param {string} receptor - 'telegram' | 'discord' | 'slack' | 'email' | 'plain'
99
+ * @returns {string|null}
100
+ * @private
101
+ */
102
+ function _construirEnlaceFileRef(payload, receptor) {
103
+ if (!payload || !payload.fileRef || !payload.idePreferido) return null;
104
+ const { archivo, linea, columna, etiqueta, proyecto } = payload.fileRef;
105
+ if (!archivo) return null;
106
+
107
+ const url = construirDeepLink({
108
+ ide: payload.idePreferido,
109
+ rutaAbsoluta: archivo,
110
+ linea,
111
+ columna,
112
+ proyecto,
113
+ });
114
+
115
+ const label = etiqueta || (linea ? `${path.basename(archivo)}:${linea}` : path.basename(archivo));
116
+ const formato = RECEPTORES.has(receptor) ? receptor : 'plain';
117
+ return formatearEnlace(url, label, formato);
118
+ }
119
+
69
120
  /**
70
121
  * Encola una notificación para el gateway.
71
122
  * No bloquea ni lanza. Retorna true si se encoló, false si fue descartada.
72
123
  *
124
+ * Enriquecimiento opt-in (ADR-0029): si `params.payload.fileRef` +
125
+ * `params.payload.idePreferido` están presentes, el mensaje agrega el campo
126
+ * `enlace` con un deep link formateado al receptor. Sin esos campos, el
127
+ * comportamiento es idéntico al previo (compatibilidad hacia atrás).
128
+ *
73
129
  * @param {object} params
74
130
  * @param {string} params.tipo - session-stop, checkpoint, error, release, build-fail, custom
75
131
  * @param {string} [params.titulo] - Título corto.
@@ -89,17 +145,24 @@ function notificarGateway(params) {
89
145
  }
90
146
 
91
147
  const id = `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
148
+ const receptor = params.to || 'all';
149
+ const payloadBase = {
150
+ tipo: params.tipo,
151
+ titulo: params.titulo || '',
152
+ texto: params.texto || '',
153
+ ...(params.payload || {}),
154
+ };
155
+
156
+ // Enriquecimiento opt-in con deep link (ADR-0029).
157
+ const enlace = _construirEnlaceFileRef(payloadBase, receptor);
158
+ if (enlace) payloadBase.enlace = enlace;
159
+
92
160
  const msg = {
93
161
  id,
94
162
  type: 'gateway_notification',
95
163
  from: 'swl-system',
96
- to: params.to || 'all',
97
- payload: {
98
- tipo: params.tipo,
99
- titulo: params.titulo || '',
100
- texto: params.texto || '',
101
- ...(params.payload || {}),
102
- },
164
+ to: receptor,
165
+ payload: payloadBase,
103
166
  text: params.texto || '',
104
167
  timestamp: new Date().toISOString(),
105
168
  status: 'pending',
@@ -18,6 +18,7 @@
18
18
  "agentes/depurador-swl.md",
19
19
  "agentes/documentador-swl.md",
20
20
  "agentes/resolutor-build-swl.md",
21
+ "agentes/gh-fix-ci-swl.md",
21
22
  "agentes/perfilador-usuario-swl.md"
22
23
  ],
23
24
  "targets": [
@@ -241,7 +242,11 @@
241
242
  "habilidades/reducir-entropia",
242
243
  "habilidades/proceso-intent-engineering",
243
244
  "habilidades/proceso-ddia-fundamentos",
244
- "habilidades/proceso-ddia-streaming"
245
+ "habilidades/proceso-ddia-streaming",
246
+ "habilidades/proceso-discovery-machote",
247
+ "habilidades/proceso-modular-split",
248
+ "habilidades/agent-deep-links",
249
+ "habilidades/changelog-generator"
245
250
  ],
246
251
  "targets": [
247
252
  "claude",
@@ -264,7 +269,9 @@
264
269
  "habilidades/testing-python",
265
270
  "habilidades/manejo-errores",
266
271
  "habilidades/auth-patrones",
267
- "habilidades/build-errors-python"
272
+ "habilidades/build-errors-python",
273
+ "habilidades/backend-error-design",
274
+ "habilidades/backend-async-postgres-testing"
268
275
  ],
269
276
  "targets": [
270
277
  "claude",
@@ -709,6 +716,8 @@
709
716
  "habilidades/prompt-engineering",
710
717
  "habilidades/structured-outputs",
711
718
  "habilidades/agent-browser",
719
+ "habilidades/browser-interaction-patterns",
720
+ "habilidades/browser-research-domains",
712
721
  "habilidades/wiki-conocimiento",
713
722
  "habilidades/orquestacion-async",
714
723
  "habilidades/diagrama-arquitectura",
@@ -895,7 +904,8 @@
895
904
  "reglas/analisis-previo-tareas-grandes.md",
896
905
  "reglas/registro-componentes-nuevos.md",
897
906
  "reglas/auditorias-documentales-estructurales.md",
898
- "reglas/intent-engineering.md"
907
+ "reglas/intent-engineering.md",
908
+ "reglas/tests-cleanup.md"
899
909
  ],
900
910
  "targets": [
901
911
  "claude",