@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.
- 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 +115 -18
- package/hooks/lib/gateway-notify.js +70 -7
- package/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +3 -3
- package/plugin.json +11 -2
- 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/lib/mcp_config.py +29 -14
- 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
|
+
};
|
|
@@ -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 {{
|
|
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
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
const
|
|
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 —
|
|
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
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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:
|
|
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",
|