@saulwade/swl-ses 1.6.3 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/release.md +62 -2
- package/comandos/swl/salud.md +32 -0
- package/comandos/swl/verificar.md +116 -2
- package/habilidades/agent-browser/SKILL.md +111 -4
- package/habilidades/agent-deep-links/SKILL.md +148 -0
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
- package/habilidades/backend-error-design/SKILL.md +221 -0
- package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
- package/habilidades/browser-research-domains/SKILL.md +635 -0
- package/habilidades/changelog-generator/SKILL.md +172 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/tdd-workflow/SKILL.md +12 -5
- package/hooks/extraccion-aprendizajes.js +8 -0
- package/hooks/lib/deep-links.js +185 -0
- package/hooks/lib/evolution-tracker.js +148 -20
- package/hooks/lib/gateway-notify.js +70 -7
- package/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +92 -92
- package/plugin.json +371 -362
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/registro-componentes-nuevos.md +14 -0
- package/reglas/tests-cleanup.md +220 -0
- package/scripts/instalador.js +72 -4
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/lib/notificaciones-telegram.js +14 -0
- package/scripts/lib/transformadores/codex.js +4 -0
- package/scripts/lib/transformadores/cursor.js +5 -0
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- 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
|
+
};
|
|
@@ -46,6 +46,35 @@ const EVOLUTION_FIELDS = {
|
|
|
46
46
|
/** Nombre del archivo sidecar para componentes sin frontmatter (reglas). */
|
|
47
47
|
const SIDECAR_FILENAME = '.evolved.json';
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Patrones de nombre de archivo que NUNCA deben tratarse como componentes
|
|
51
|
+
* evolucionados aunque tengan extensión `.md` y frontmatter `evolved: true`.
|
|
52
|
+
* Cubren residuos de herramientas externas (Syncthing, merge tools, editores).
|
|
53
|
+
*
|
|
54
|
+
* Cuando un dispositivo sincroniza vía Syncthing y hay un conflicto, se crea
|
|
55
|
+
* `SKILL.sync-conflict-YYYYMMDD-HHMMSS-XXXX.md` que es copia del original con
|
|
56
|
+
* el mismo frontmatter. Sin este filtro, scanEvolved los reportaba como
|
|
57
|
+
* "evoluciones" independientes y el output mostraba 4 SKILL.md duplicados sin
|
|
58
|
+
* forma de distinguirlos (v1.6.5 release sesión 2026-05-21).
|
|
59
|
+
*/
|
|
60
|
+
const EXCLUDED_FILENAME_PATTERNS = [
|
|
61
|
+
/\.sync-conflict-/, // Syncthing
|
|
62
|
+
/\.orig$/, // git merge backup
|
|
63
|
+
/\.bak$/, // backups varios
|
|
64
|
+
/\.rej$/, // patch reject
|
|
65
|
+
/\.merge_file_/, // merge tools (kdiff3, etc.)
|
|
66
|
+
/~$/, // editores tipo Emacs/Vim
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decide si un nombre de archivo debe ignorarse al escanear evoluciones.
|
|
71
|
+
* @param {string} filename
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function isExcludedFilename(filename) {
|
|
75
|
+
return EXCLUDED_FILENAME_PATTERNS.some((re) => re.test(filename));
|
|
76
|
+
}
|
|
77
|
+
|
|
49
78
|
/** Comandos que generan evoluciones. */
|
|
50
79
|
const EVOLUTION_SOURCES = [
|
|
51
80
|
'evolucionar',
|
|
@@ -345,6 +374,30 @@ function _stripEvolutionFields(content) {
|
|
|
345
374
|
.join('\n');
|
|
346
375
|
}
|
|
347
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Separa frontmatter YAML del body en un archivo markdown SWL.
|
|
379
|
+
*
|
|
380
|
+
* Cuando se calcula el diff de mutaciones de un archivo evolucionado, el
|
|
381
|
+
* frontmatter SIEMPRE diverge (el destino tiene campos `evolved-*` que el
|
|
382
|
+
* origen no tiene, y viceversa con campos nuevos del paquete). Contar esas
|
|
383
|
+
* diferencias como "mutaciones del usuario" genera ruido masivo por
|
|
384
|
+
* desplazamiento de líneas. Esta función permite comparar solo el body.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} content - Contenido completo del archivo .md.
|
|
387
|
+
* @returns {{ frontmatter: string, body: string }}
|
|
388
|
+
* - `frontmatter`: bloque YAML entre `---` (vacío si no hay frontmatter).
|
|
389
|
+
* - `body`: todo lo que viene después del frontmatter cerrado.
|
|
390
|
+
* @private
|
|
391
|
+
*/
|
|
392
|
+
function _splitFrontmatterAndBody(content) {
|
|
393
|
+
const m = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
|
|
394
|
+
if (!m) return { frontmatter: '', body: content };
|
|
395
|
+
return { frontmatter: m[1], body: m[2] };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Umbral defensivo: tras este número de diffs el archivo pasa a modo resumen. */
|
|
399
|
+
const DIFF_NOISY_THRESHOLD = 50;
|
|
400
|
+
|
|
348
401
|
// ---------------------------------------------------------------------------
|
|
349
402
|
// Merge de evoluciones
|
|
350
403
|
// ---------------------------------------------------------------------------
|
|
@@ -356,10 +409,29 @@ function _stripEvolutionFields(content) {
|
|
|
356
409
|
* evolución (frontmatter evolved-*). Las mutaciones de contenido se preservan
|
|
357
410
|
* generando un archivo .evolved-diff.md que Claude puede re-aplicar.
|
|
358
411
|
*
|
|
412
|
+
* Comparación: solo el body (post-frontmatter) se compara línea-a-línea.
|
|
413
|
+
* El frontmatter SIEMPRE diverge (el destino tiene campos `evolved-*` que el
|
|
414
|
+
* origen no tiene, y viceversa con campos nuevos del paquete), por lo que
|
|
415
|
+
* contarlo como mutación genera ruido por desplazamiento.
|
|
416
|
+
*
|
|
417
|
+
* Limpieza: cuando un merge posterior elimina la divergencia (diffs vacíos),
|
|
418
|
+
* borra el `.evolved-diff.md` huérfano de sesiones previas si existe.
|
|
419
|
+
*
|
|
420
|
+
* Cap defensivo: si tras alinear correctamente el body aún hay más de
|
|
421
|
+
* `DIFF_NOISY_THRESHOLD` líneas distintas, genera un resumen estadístico
|
|
422
|
+
* con muestra (primeras 20 + últimas 5) en lugar del dump completo.
|
|
423
|
+
*
|
|
359
424
|
* @param {string} destino - Ruta del archivo evolucionado (local).
|
|
360
425
|
* @param {string} origen - Ruta del archivo nuevo del paquete.
|
|
361
426
|
* @param {string} versionNueva - Versión del paquete nuevo.
|
|
362
|
-
* @returns {{
|
|
427
|
+
* @returns {{
|
|
428
|
+
* merged: boolean,
|
|
429
|
+
* diffPath?: string,
|
|
430
|
+
* diffsCount?: number,
|
|
431
|
+
* cleanedDiff?: boolean,
|
|
432
|
+
* truncated?: boolean,
|
|
433
|
+
* error?: string
|
|
434
|
+
* }}
|
|
363
435
|
*/
|
|
364
436
|
function mergeEvolved(destino, origen, versionNueva) {
|
|
365
437
|
try {
|
|
@@ -375,11 +447,14 @@ function mergeEvolved(destino, origen, versionNueva) {
|
|
|
375
447
|
const destinoContent = fs.readFileSync(destino, 'utf8');
|
|
376
448
|
const origenContent = fs.readFileSync(origen, 'utf8');
|
|
377
449
|
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
const
|
|
450
|
+
// Comparar SOLO el body, no el frontmatter. El frontmatter del destino
|
|
451
|
+
// tiene los campos `evolved-*` que el origen no tiene — contarlos como
|
|
452
|
+
// mutaciones desplaza todas las líneas siguientes y genera ruido.
|
|
453
|
+
const { body: destinoBody } = _splitFrontmatterAndBody(destinoContent);
|
|
454
|
+
const { body: origenBody } = _splitFrontmatterAndBody(origenContent);
|
|
455
|
+
|
|
456
|
+
const origenLines = origenBody.split(/\r?\n/);
|
|
457
|
+
const destinoLines = destinoBody.split(/\r?\n/);
|
|
383
458
|
|
|
384
459
|
const diffs = [];
|
|
385
460
|
const maxLen = Math.max(origenLines.length, destinoLines.length);
|
|
@@ -395,34 +470,85 @@ function mergeEvolved(destino, origen, versionNueva) {
|
|
|
395
470
|
}
|
|
396
471
|
}
|
|
397
472
|
|
|
473
|
+
const diffPath = destino.replace(/\.md$/, '.evolved-diff.md');
|
|
474
|
+
|
|
398
475
|
if (diffs.length === 0) {
|
|
399
|
-
// Sin diferencias reales —
|
|
476
|
+
// Sin diferencias reales — limpiar diff huérfano si existe (de sesión
|
|
477
|
+
// previa donde sí hubo divergencia que ya quedó resuelta) y re-aplicar
|
|
478
|
+
// campos evolved al destino.
|
|
479
|
+
let cleanedDiff = false;
|
|
480
|
+
if (fs.existsSync(diffPath)) {
|
|
481
|
+
try {
|
|
482
|
+
fs.unlinkSync(diffPath);
|
|
483
|
+
cleanedDiff = true;
|
|
484
|
+
} catch {
|
|
485
|
+
// Best-effort: si el unlink falla por permisos/locks, dejarlo —
|
|
486
|
+
// el merge sigue siendo válido.
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// force: true — `mergeEvolved` solo se invoca en contexto de update
|
|
491
|
+
// intencional. El skip de isPackageRoot() aplica a la primera marca
|
|
492
|
+
// del mantenedor, no a re-aplicar campos tras un merge resuelto.
|
|
400
493
|
const marked = markAsEvolved(destino, {
|
|
401
494
|
from: versionNueva,
|
|
402
495
|
by: evo.metadata.evolvedBy || 'auto-evolución',
|
|
403
496
|
rounds: evo.metadata.evolvedRounds ? parseInt(evo.metadata.evolvedRounds, 10) : undefined,
|
|
404
497
|
score: evo.metadata.evolvedScore,
|
|
405
498
|
note: `Re-aplicado desde v${evo.metadata.evolvedFrom || '?'} tras actualización a v${versionNueva}`,
|
|
499
|
+
force: true,
|
|
406
500
|
});
|
|
407
|
-
return { merged: marked.marked };
|
|
501
|
+
return { merged: marked.marked, cleanedDiff };
|
|
408
502
|
}
|
|
409
503
|
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
504
|
+
// Cap defensivo: tras alinear correctamente sigue habiendo más de N diffs.
|
|
505
|
+
// En lugar de dumpear cada línea (puede explotar a miles), generar resumen
|
|
506
|
+
// con muestra acotada.
|
|
507
|
+
const truncated = diffs.length > DIFF_NOISY_THRESHOLD;
|
|
508
|
+
const diffsParaMostrar = truncated
|
|
509
|
+
? [...diffs.slice(0, 20), ...diffs.slice(-5)]
|
|
510
|
+
: diffs;
|
|
511
|
+
|
|
512
|
+
const header = [
|
|
413
513
|
`# Diff de evolución — ${path.basename(destino)}`,
|
|
414
514
|
``,
|
|
415
515
|
`**Archivo evolucionado por**: ${evo.metadata.evolvedBy || 'auto-evolución'}`,
|
|
416
516
|
`**Versión base original**: ${evo.metadata.evolvedFrom || '?'}`,
|
|
417
517
|
`**Versión nueva**: ${versionNueva}`,
|
|
418
518
|
`**Fecha**: ${new Date().toISOString().split('T')[0]}`,
|
|
519
|
+
`**Diferencias detectadas (body)**: ${diffs.length}`,
|
|
419
520
|
``,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
521
|
+
];
|
|
522
|
+
|
|
523
|
+
if (truncated) {
|
|
524
|
+
header.push(
|
|
525
|
+
`## ⚠ Resumen (truncado)`,
|
|
526
|
+
``,
|
|
527
|
+
`El diff excede el umbral defensivo de ${DIFF_NOISY_THRESHOLD} líneas`,
|
|
528
|
+
`(${diffs.length} diferencias detectadas). Esto suele indicar:`,
|
|
529
|
+
``,
|
|
530
|
+
`- El archivo fue reescrito completo entre versiones (rebrand, refactor).`,
|
|
531
|
+
`- El alineamiento línea-a-línea no es útil aquí — usar \`git diff\`.`,
|
|
532
|
+
``,
|
|
533
|
+
`Se muestran las primeras 20 + últimas 5 diferencias como muestra.`,
|
|
534
|
+
`Para diff completo: \`diff <(sed '1,/^---$/d; 1,/^---$/d' archivo) <(...)\`.`,
|
|
535
|
+
``,
|
|
536
|
+
`## Muestra de mutaciones (primeras 20 + últimas 5)`,
|
|
537
|
+
``,
|
|
538
|
+
);
|
|
539
|
+
} else {
|
|
540
|
+
header.push(
|
|
541
|
+
`## Mutaciones locales a re-aplicar`,
|
|
542
|
+
``,
|
|
543
|
+
`Estas son las líneas del body que difieren entre la versión`,
|
|
544
|
+
`evolucionada y la nueva. Re-aplicar con \`/swl:autoresearch\` o manualmente.`,
|
|
545
|
+
``,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const diffContent = [
|
|
550
|
+
...header,
|
|
551
|
+
...diffsParaMostrar.map(d => [
|
|
426
552
|
`### Línea ${d.line}`,
|
|
427
553
|
`- **Nueva (base)**: \`${d.origen}\``,
|
|
428
554
|
`- **Evolucionada**: \`${d.destino}\``,
|
|
@@ -432,7 +558,7 @@ function mergeEvolved(destino, origen, versionNueva) {
|
|
|
432
558
|
|
|
433
559
|
atomicWriteSync(diffPath, diffContent, 'utf8');
|
|
434
560
|
|
|
435
|
-
return { merged: true, diffPath, diffsCount: diffs.length };
|
|
561
|
+
return { merged: true, diffPath, diffsCount: diffs.length, truncated };
|
|
436
562
|
} catch (err) {
|
|
437
563
|
return { merged: false, error: err.message };
|
|
438
564
|
}
|
|
@@ -463,7 +589,7 @@ function scanEvolved(dir, opts = {}) {
|
|
|
463
589
|
|
|
464
590
|
if (entry.isDirectory() && recursive) {
|
|
465
591
|
results.push(...scanEvolved(fullPath, opts));
|
|
466
|
-
} else if (entry.name.endsWith('.md')) {
|
|
592
|
+
} else if (entry.name.endsWith('.md') && !isExcludedFilename(entry.name)) {
|
|
467
593
|
const evo = readEvolutionMeta(fullPath);
|
|
468
594
|
if (evo.evolved) {
|
|
469
595
|
results.push({ path: fullPath, ...evo.metadata });
|
|
@@ -478,7 +604,7 @@ function scanEvolved(dir, opts = {}) {
|
|
|
478
604
|
try {
|
|
479
605
|
const data = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
480
606
|
for (const [filename, meta] of Object.entries(data)) {
|
|
481
|
-
if (meta.evolved) {
|
|
607
|
+
if (meta.evolved && !isExcludedFilename(filename)) {
|
|
482
608
|
results.push({ path: path.join(dir, filename), ...meta });
|
|
483
609
|
}
|
|
484
610
|
}
|
|
@@ -498,8 +624,10 @@ module.exports = {
|
|
|
498
624
|
decideUpdateStrategy,
|
|
499
625
|
mergeEvolved,
|
|
500
626
|
scanEvolved,
|
|
627
|
+
isExcludedFilename,
|
|
501
628
|
isPackageRoot,
|
|
502
629
|
EVOLUTION_FIELDS,
|
|
503
630
|
EVOLUTION_SOURCES,
|
|
631
|
+
EXCLUDED_FILENAME_PATTERNS,
|
|
504
632
|
SIDECAR_FILENAME,
|
|
505
633
|
};
|
|
@@ -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:
|
|
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',
|
package/manifiestos/modulos.json
CHANGED
|
@@ -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",
|