@saulwade/swl-ses 1.1.4 → 1.2.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 (34) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +3 -3
  3. package/bin/swl-mcp-server.js +187 -0
  4. package/habilidades/benchmark-memoria/SKILL.md +186 -0
  5. package/habilidades/contenedores-docker/SKILL.md +8 -1
  6. package/habilidades/datos-etl/SKILL.md +18 -1
  7. package/habilidades/eval-framework/SKILL.md +212 -0
  8. package/habilidades/memoria-busqueda/SKILL.md +24 -1
  9. package/habilidades/planear-fase/SKILL.md +299 -269
  10. package/habilidades/postgresql-experto/SKILL.md +24 -1
  11. package/habilidades/verificar-trabajo/SKILL.md +7 -1
  12. package/hooks/lib/evolution-tracker.js +65 -11
  13. package/hooks/lib/memory-search.js +44 -13
  14. package/hooks/sugerir-contribuir.js +226 -0
  15. package/manifiestos/hooks-config.json +9 -0
  16. package/manifiestos/modulos.json +33 -1
  17. package/manifiestos/perfiles.json +2 -1
  18. package/package.json +4 -3
  19. package/plugin.json +343 -343
  20. package/scripts/benchmark-memoria.js +167 -0
  21. package/scripts/detectar-aprendizajes-duplicados.js +151 -0
  22. package/scripts/lib/benchmark-metrics.js +160 -0
  23. package/scripts/lib/eval-metrics-store.js +218 -0
  24. package/scripts/lib/eval-quality.js +171 -0
  25. package/scripts/lib/eval-schemas.js +144 -0
  26. package/scripts/lib/eval-self-correct.js +106 -0
  27. package/scripts/lib/eval-validator.js +185 -0
  28. package/scripts/lib/jaccard-similarity.js +98 -0
  29. package/scripts/lib/longmemeval-runner.js +125 -0
  30. package/scripts/lib/rrf-fusion.js +175 -0
  31. package/scripts/lib/scoring-instintos.js +40 -3
  32. package/scripts/mcp-server/README.md +128 -0
  33. package/scripts/mcp-server/handlers.js +206 -0
  34. package/scripts/run-eval.js +141 -0
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: postgresql-experto
3
3
  description: PostgreSQL avanzado. JSONB, arrays, tipos personalizados, búsqueda de texto completo, window functions, CTEs recursivos, Row Level Security y funciones almacenadas.
4
- version: "1.0.0"
4
+ version: "1.1.0"
5
+ evolved: true
6
+ evolved-from: "1.0.0"
7
+ evolved-at: "2026-05-05"
8
+ evolved-by: "aprender"
9
+ evolved-note: "3 gotchas nuevos de la sesión SIGM 2026-05-05: RLS bypass por superusuarios, UUIDs hex en seeds, consultar enum_range antes de seedear (L2/L4/L8)"
5
10
  herramientasPermitidas: [Read, Grep]
6
11
  exclusiones:
7
12
  - "No cargar para optimización de queries SQL (EXPLAIN ANALYZE, índices, partitioning) — para optimización cargar `sql-optimizacion`."
@@ -144,6 +149,24 @@ ver [recursos/referencia-completa.md](recursos/referencia-completa.md).
144
149
 
145
150
  **`SET LOCAL app.empresa_id` para RLS no persiste fuera de una transacción**: `SET LOCAL` establece la variable solo para la transacción actual — si se ejecuta fuera de `BEGIN/COMMIT`, tiene el mismo efecto que `SET` (sesión completa). En aplicaciones con pooling (PgBouncer en transaction mode), la sesión se reutiliza entre conexiones y la variable puede persistir de una petición anterior. Causa: el pool de conexiones reutiliza sessions sin limpiar el estado. Fix: usar SIEMPRE `SET LOCAL` dentro de una transacción explícita (`async with session.begin()`) y verificar que el pool esté en transaction mode.
146
151
 
152
+ **RLS NO se aplica a superusuarios — verificar siempre con rol app-only**: `FORCE ROW LEVEL SECURITY` hace que las policies apliquen al OWNER de la tabla, pero los superusuarios SIEMPRE bypassean RLS. En imágenes oficiales (`postgis/postgis`, `postgres`), el rol que se crea por defecto (con `POSTGRES_USER`) es superusuario. Caso real: `SET app.tenant_id = COAT` mostraba 20 predios del tenant MINA porque la query corría como `sigm` superuser. Fix: para verificar aislamiento RLS o exponer al backend de producción, crear un rol app-only sin BYPASSRLS:
153
+ ```sql
154
+ CREATE ROLE sigm_app NOLOGIN;
155
+ GRANT USAGE ON SCHEMA <s> TO sigm_app;
156
+ GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA <s> TO sigm_app;
157
+ SET ROLE sigm_app; -- ahora RLS aplica
158
+ ```
159
+ NUNCA usar el rol propietario del schema (suele ser superuser) como rol de aplicación.
160
+
161
+ **UUIDs deterministas en seeds DEBEN usar SOLO chars hex (0-9, a-f)**: la convención común de "prefijo legible" (`vut00000-...` para valores unitarios terreno, `z0000000-...` para zonas, `tv000000-...` para tipos vialidad) produce strings inválidos como UUID. PostgreSQL rechaza con `invalid input syntax for type uuid: "vut00000-..."`. Detectado en sesión 2026-05-04 con 30+ UUIDs inválidos cascadeando errores en seeds dependientes. Fix: usar dígitos hex como prefijo discriminador (`30000000-` zonas, `40000000-` manzanas, `b0000000-` colonias) o usar `gen_random_uuid()` y resolver vínculos via subqueries (`(SELECT id FROM ... WHERE clave = 'X')`).
162
+
163
+ **Enum types: nunca presumir variantes — consultar `enum_range` antes de seedear**: PostgreSQL crea tipos enum con variantes específicas que pueden diferir del nombre de dominio. Ejemplos reales: `tipo_suelo` no tiene `'rústico'` (con acento) sino `'rural'`; `tipo_estatus_medidor` no tiene `'funcionando'` sino `'funcional'`. Fix: antes de incluir un valor literal en seed o INSERT, ejecutar:
164
+ ```sql
165
+ SELECT enum_range(NULL::<schema>.<tipo>);
166
+ -- {funcional,descompuesto,sin_medidor,retirado}
167
+ ```
168
+ Si el valor que necesitas no existe, agregar la variante con `ALTER TYPE ... ADD VALUE` en una migración separada — no improvisar en seeds.
169
+
147
170
  **GIN index en columna JSONB no se usa con el operador `->>` en una cláusula WHERE**: `WHERE metadata ->> 'marca' = 'Dell'` extrae texto y compara — este operador no usa el índice GIN. Solo los operadores `@>`, `?`, `?|`, `?&` usan el índice GIN. Causa: `->>` convierte JSONB a texto y la comparación es una operación de texto sin índice JSONB. Fix: para búsquedas de igualdad con índice, usar `WHERE metadata @> '{"marca": "Dell"}'` que sí usa el índice GIN, o crear un índice de expresión B-Tree sobre `(metadata ->> 'marca')`.
148
171
 
149
172
  **`FORCE ROW LEVEL SECURITY` no protege al usuario dueño de la tabla (superuser/owner)**: el propietario de la tabla y los superusuarios de PostgreSQL bypasean RLS por defecto aunque esté `FORCE` habilitado. Causa: `FORCE` aplica a todos los usuarios excepto al dueño de la tabla y a superusuarios. Fix: si la aplicación conecta como el dueño de la tabla, cambiar el rol de conexión a un rol de aplicación sin privilegios de dueño (`GRANT CONNECT ON DATABASE ... TO app_user`), no usar el usuario `postgres` para conexiones de aplicación.
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: verificar-trabajo
3
3
  description: Verificación goal-backward del trabajo ejecutado en 4 niveles progresivos — EXISTE, SUSTANTIVO, CONECTADO, DATOS_FLUYEN. Detecta stubs, componentes huérfanos, integraciones rotas y flujos incompletos. Produce veredictos estructurados JSON con clasificación de riesgo (Low/Medium/High) y evidencia verificable. Soporta loop de reparación cuando el veredicto es Fail.
4
- version: "1.1.0"
4
+ version: "1.1.1"
5
+ evolved: true
6
+ evolved-from: "1.1.0"
7
+ evolved-at: "2026-05-05"
8
+ evolved-by: "aprender"
9
+ evolved-note: "Gotcha de la sesión SIGM 2026-05-05 (L7): tests + linter no detectan schema-seed drift; cuando el alcance toca BD, Nivel 4 obligatorio con docker compose down -v && up fresco"
5
10
  herramientasPermitidas: [Read, Write, Edit, Bash, Glob, Grep]
6
11
  exclusiones:
7
12
  - "No cargar durante la implementación activa de una tarea; la verificación es posterior a la implementación, no concurrente."
@@ -290,6 +295,7 @@ estructurado JSON, ver [recursos/plantilla-verificacion.md](recursos/plantilla-v
290
295
  - **Score del veredicto calculado solo sobre artefactos verificados, no sobre todos los prometidos**: si el plan prometía 8 entregables pero solo se verificaron 5, el score 1.0 sobre los 5 no es evidencia de que los 8 están correctos. Causa: el agente omite entregables del plan en la lista de artefactos. Solución: antes de verificar, listar todos los artefactos del PLAN.md y verificar cada uno explícitamente — un artefacto no verificado es un Fail implícito.
291
296
  - **Loop de reparación sin re-verificación completa**: tras el fix, el agente re-verifica solo el item que falló en lugar del veredicto completo. Causa: optimización incorrecta para ahorrar tiempo. Solución: el paso 3 del loop de reparación dice explícitamente "re-ejecutar la verificación COMPLETA" — un fix puede resolver un fallo pero introducir otro.
292
297
  - **Hollow Component no detectado en Nivel 2**: el componente Angular tiene template pero no usa ninguna señal del componente. El agente verifica que el archivo existe (Nivel 1) y que tiene contenido (Nivel 2), pero no detecta que el contenido es decorativo. Causa: la definición de "stub" en Nivel 2 no se aplicó al caso de templates sin binding. Solución: verificar explícitamente que el template usa al menos una variable/señal del componente.
298
+ - **Verificación con tests + linter pasa pero al levantar BD fresca todo se rompe**: tests Python con mocks y `ruff check` retornan verde, pero `docker compose down -v && up` falla porque hay drift entre `database/schemas/`, seeds, funciones SQL y vistas. Caso real (SIGM 2026-05-04): primera pasada de `/swl:verificar` reportó "APROBADO 21/22" sin detectar que ~10 seeds tenían columnas obsoletas, UUIDs inválidos y un script `02-crear-buckets-minio.sh` que abortaba initdb del contenedor postgres. Causa: la verificación corre solo contra el código (que mockea BD); nunca aplica el SQL real. Solución: cuando el alcance toca cualquier archivo en `database/schemas/`, `database/seeds/`, `database/functions/` o `database/views/`, ejecutar como Nivel 4 (DATOS_FLUYEN) obligatorio: `docker compose down -v && docker compose up -d db && wait_for_health && docker logs <db_container> | grep -E "ERROR|skipped"`. Cualquier ERROR en logs de init invalida el veredicto Pass.
293
299
 
294
300
  ## Regla de oro del verificador
295
301
 
@@ -72,8 +72,8 @@ function readEvolutionMeta(filePath) {
72
72
  try {
73
73
  const content = fs.readFileSync(filePath, 'utf8');
74
74
 
75
- // Detectar frontmatter YAML (entre --- y ---)
76
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
75
+ // Detectar frontmatter YAML (entre --- y ---). Soporta LF y CRLF.
76
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
77
77
  if (!fmMatch) {
78
78
  // Sin frontmatter — verificar sidecar
79
79
  return _readSidecar(filePath);
@@ -120,6 +120,38 @@ function _readSidecar(filePath) {
120
120
  return { evolved: false, metadata: {} };
121
121
  }
122
122
 
123
+ // ---------------------------------------------------------------------------
124
+ // Detección de CWD == package root (commits del mantenedor)
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Determina si el CWD actual es la raíz del paquete swl-ses.
129
+ *
130
+ * Cuando el desarrollador del paquete edita componentes en su propio repo, no
131
+ * son evoluciones de usuario — son commits normales del mantenedor. Marcar
132
+ * `evolved: true` en esos casos contamina el frontmatter, ensucia el flujo de
133
+ * preserve/conflict en upgrades de usuario, y rompe el invariante de que
134
+ * `evolved: true` significa "este archivo fue modificado por un usuario tras
135
+ * recibirlo del paquete".
136
+ *
137
+ * Heurística: leer `package.json` del cwd; si su campo `name` es
138
+ * `"@saulwade/swl-ses"` (o `"swl-ses"` legacy), estamos en el repo fuente.
139
+ *
140
+ * @param {string} [cwd=process.cwd()]
141
+ * @returns {boolean}
142
+ */
143
+ function isPackageRoot(cwd) {
144
+ const dir = cwd || process.cwd();
145
+ try {
146
+ const pkgPath = path.join(dir, 'package.json');
147
+ if (!fs.existsSync(pkgPath)) return false;
148
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
149
+ return pkg.name === '@saulwade/swl-ses' || pkg.name === 'swl-ses';
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
123
155
  // ---------------------------------------------------------------------------
124
156
  // Escritura de metadata de evolución
125
157
  // ---------------------------------------------------------------------------
@@ -127,6 +159,11 @@ function _readSidecar(filePath) {
127
159
  /**
128
160
  * Marca un archivo como evolucionado inyectando campos en su frontmatter.
129
161
  *
162
+ * Cuando el CWD es la raíz del paquete swl-ses (commit del mantenedor), NO se
163
+ * inyecta `evolved: true` ni los campos `evolved-*`. El sistema asume que los
164
+ * cambios en el repo fuente son parte del desarrollo normal, no evoluciones
165
+ * de usuario.
166
+ *
130
167
  * @param {string} filePath - Ruta del archivo a marcar.
131
168
  * @param {object} meta
132
169
  * @param {string} meta.from - Versión base (ej: "5.1.0").
@@ -134,16 +171,26 @@ function _readSidecar(filePath) {
134
171
  * @param {number} [meta.rounds] - Rondas de autoresearch.
135
172
  * @param {string} [meta.score] - Score "baseline% → final%".
136
173
  * @param {string} [meta.note] - Descripción del cambio.
137
- * @returns {{ marked: boolean, error?: string }}
174
+ * @param {boolean} [meta.force] - Forzar marcado aunque CWD == package root (ej: tests).
175
+ * @returns {{ marked: boolean, skipped?: boolean, reason?: string, error?: string }}
138
176
  */
139
177
  function markAsEvolved(filePath, meta) {
140
178
  if (!fs.existsSync(filePath)) {
141
179
  return { marked: false, error: 'Archivo no existe' };
142
180
  }
143
181
 
182
+ if (!meta?.force && isPackageRoot()) {
183
+ return {
184
+ marked: false,
185
+ skipped: true,
186
+ reason: 'CWD es la raíz del paquete swl-ses — los cambios del mantenedor no se marcan como evolved',
187
+ };
188
+ }
189
+
144
190
  try {
145
191
  const content = fs.readFileSync(filePath, 'utf8');
146
- const fmMatch = content.match(/^(---\n)([\s\S]*?)(\n---)/);
192
+ // Soportar LF y CRLF en line endings.
193
+ const fmMatch = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
147
194
 
148
195
  if (!fmMatch) {
149
196
  // Sin frontmatter — usar sidecar
@@ -155,11 +202,14 @@ function markAsEvolved(filePath, meta) {
155
202
  const suffix = fmMatch[3];
156
203
  const rest = content.slice(fmMatch[0].length);
157
204
 
205
+ // Detectar EOL del archivo para preservarlo en la escritura.
206
+ const eol = prefix.includes('\r\n') ? '\r\n' : '\n';
207
+
158
208
  // Remover campos evolved previos
159
209
  frontmatter = frontmatter
160
- .split('\n')
210
+ .split(/\r?\n/)
161
211
  .filter(line => !line.match(/^evolved[-\w]*:/))
162
- .join('\n');
212
+ .join(eol);
163
213
 
164
214
  // Agregar campos nuevos
165
215
  const date = new Date().toISOString().split('T')[0];
@@ -173,7 +223,7 @@ function markAsEvolved(filePath, meta) {
173
223
  if (meta.score) newFields.push(`evolved-score: "${meta.score}"`);
174
224
  if (meta.note) newFields.push(`evolved-note: "${meta.note}"`);
175
225
 
176
- frontmatter = frontmatter.trimEnd() + '\n' + newFields.join('\n');
226
+ frontmatter = frontmatter.trimEnd() + eol + newFields.join(eol);
177
227
 
178
228
  const newContent = prefix + frontmatter + suffix + rest;
179
229
  atomicWriteSync(filePath, newContent, 'utf8');
@@ -284,11 +334,13 @@ function decideUpdateStrategy(destino, origen, versionNueva) {
284
334
 
285
335
  /**
286
336
  * Remueve campos de evolución del frontmatter para comparación limpia.
337
+ * Normaliza CRLF a LF para que el contenido se pueda comparar
338
+ * independientemente del sistema operativo en que se generó.
287
339
  * @private
288
340
  */
289
341
  function _stripEvolutionFields(content) {
290
342
  return content
291
- .split('\n')
343
+ .split(/\r?\n/)
292
344
  .filter(line => !line.match(/^evolved[-\w]*:/))
293
345
  .join('\n');
294
346
  }
@@ -323,10 +375,11 @@ function mergeEvolved(destino, origen, versionNueva) {
323
375
  const destinoContent = fs.readFileSync(destino, 'utf8');
324
376
  const origenContent = fs.readFileSync(origen, 'utf8');
325
377
 
326
- // Extraer las líneas que son diferentes entre destino (sin evolved fields) y origen
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.
327
380
  const destinoSinEvo = _stripEvolutionFields(destinoContent);
328
- const origenLines = origenContent.split('\n');
329
- const destinoLines = destinoSinEvo.split('\n');
381
+ const origenLines = origenContent.split(/\r?\n/);
382
+ const destinoLines = destinoSinEvo.split(/\r?\n/);
330
383
 
331
384
  const diffs = [];
332
385
  const maxLen = Math.max(origenLines.length, destinoLines.length);
@@ -445,6 +498,7 @@ module.exports = {
445
498
  decideUpdateStrategy,
446
499
  mergeEvolved,
447
500
  scanEvolved,
501
+ isPackageRoot,
448
502
  EVOLUTION_FIELDS,
449
503
  EVOLUTION_SOURCES,
450
504
  SIDECAR_FILENAME,
@@ -25,6 +25,16 @@ const path = require('path');
25
25
 
26
26
  const sessionFts = require('./session-fts');
27
27
 
28
+ // RRF fusion para combinar streams heterogéneos. Carga defensiva: si la lib no
29
+ // está disponible (ej. instalación parcial), `search()` cae al merge por
30
+ // relevancia simple — backward compatible.
31
+ let rrfFusion = null;
32
+ try {
33
+ ({ rrfFusion } = require('../../scripts/lib/rrf-fusion'));
34
+ } catch (_) {
35
+ rrfFusion = null;
36
+ }
37
+
28
38
  // ---------------------------------------------------------------------------
29
39
  // Constantes
30
40
  // ---------------------------------------------------------------------------
@@ -321,16 +331,39 @@ function search(baseDir, query, filtros = {}) {
321
331
  ? filtros.limit
322
332
  : 20;
323
333
 
324
- let resultados = [];
325
-
326
- if (!filtros.tipo || filtros.tipo === 'aprendizaje') {
327
- resultados.push(...buscarEnAprendizajes(baseDir, terminos));
328
- }
329
- if (!filtros.tipo || filtros.tipo === 'sesion') {
330
- resultados.push(...buscarEnSesiones(baseDir, terminos));
331
- }
332
- if (!filtros.tipo || filtros.tipo === 'instinto') {
333
- resultados.push(...buscarEnInstintos(baseDir, terminos));
334
+ // Cada fuente devuelve sus resultados ordenados por relevancia interna.
335
+ // Conservamos los streams separados para que RRF los combine usando rank,
336
+ // no la magnitud de la relevancia (que no es comparable entre fuentes).
337
+ const stream_aprendizajes = (!filtros.tipo || filtros.tipo === 'aprendizaje')
338
+ ? buscarEnAprendizajes(baseDir, terminos).sort((a, b) => b.relevancia - a.relevancia)
339
+ : [];
340
+ const stream_sesiones = (!filtros.tipo || filtros.tipo === 'sesion')
341
+ ? buscarEnSesiones(baseDir, terminos).sort((a, b) => b.relevancia - a.relevancia)
342
+ : [];
343
+ const stream_instintos = (!filtros.tipo || filtros.tipo === 'instinto')
344
+ ? buscarEnInstintos(baseDir, terminos).sort((a, b) => b.relevancia - a.relevancia)
345
+ : [];
346
+
347
+ let resultados;
348
+
349
+ if (rrfFusion && (stream_aprendizajes.length + stream_sesiones.length + stream_instintos.length) > 0) {
350
+ // RRF fusion: combina streams heterogéneos usando posición (rank), no
351
+ // magnitud de relevancia. Los pesos reflejan la utilidad relativa
352
+ // observada empíricamente:
353
+ // - Aprendizajes (0.4): conocimiento curado, mayor señal por entrada.
354
+ // - Sesiones (0.4): contexto operativo, alta densidad pero ruidoso.
355
+ // - Instintos (0.2): patrones consolidados, pocos pero específicos.
356
+ resultados = rrfFusion(
357
+ [stream_aprendizajes, stream_sesiones, stream_instintos],
358
+ { weights: [0.4, 0.4, 0.2] },
359
+ );
360
+ } else {
361
+ // Fallback sin RRF: merge por relevancia simple (comportamiento legado).
362
+ resultados = [
363
+ ...stream_aprendizajes,
364
+ ...stream_sesiones,
365
+ ...stream_instintos,
366
+ ].sort((a, b) => b.relevancia - a.relevancia);
334
367
  }
335
368
 
336
369
  // Filtros de fecha (solo sobre resultados con fecha real)
@@ -345,9 +378,7 @@ function search(baseDir, query, filtros = {}) {
345
378
  );
346
379
  }
347
380
 
348
- return resultados
349
- .sort((a, b) => b.relevancia - a.relevancia)
350
- .slice(0, limit);
381
+ return resultados.slice(0, limit);
351
382
  }
352
383
 
353
384
  // ---------------------------------------------------------------------------
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: sugerir-contribuir.js
6
+ * Tipo: PostToolUse (aplica a: Write, Edit, MultiEdit)
7
+ *
8
+ * Cuando el usuario edita un componente con frontmatter `evolved: true` en su
9
+ * instalación de SWL (no en el repo fuente), sugiere — sin bloquear — abrir
10
+ * `/swl:contribuir <skill>` para subir la mejora al core via PR.
11
+ *
12
+ * Origen: durante v1.2.0 detectamos que las evoluciones locales quedaban
13
+ * atrapadas en una sola máquina porque `markAsEvolved` marca pero no notifica.
14
+ * Sin canal de "subida", una mejora de un usuario nunca llega al paquete y se
15
+ * pierde para todos los demás equipos. Este nudge cierra ese ciclo.
16
+ *
17
+ * Reglas de activación:
18
+ * - El archivo está en agentes/, habilidades/, comandos/swl/ o reglas/
19
+ * - El archivo tiene `evolved: true` en frontmatter (o sidecar .evolved.json)
20
+ * - El CWD NO es la raíz del paquete swl-ses (un mantenedor editando el
21
+ * repo fuente NO debe recibir el nudge — sus cambios van por PR normal)
22
+ * - No estamos dentro del cooldown de 24h para este archivo
23
+ *
24
+ * Cooldown: 24h por archivo. Estado en
25
+ * `.planning/comms/sugerir-contribuir-cooldown.json`.
26
+ *
27
+ * Resultado:
28
+ * - Cumple condiciones → sugerencia en stdout (exit 0)
29
+ * - No cumple → sin output (exit 0)
30
+ * - Error interno → sin output (nunca bloquear)
31
+ *
32
+ * Activo por defecto. Para silenciar: SWL_SUGERIR_CONTRIBUIR=0.
33
+ */
34
+
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+
38
+ let isPackageRoot, readEvolutionMeta;
39
+ try {
40
+ ({ isPackageRoot, readEvolutionMeta } = require('./lib/evolution-tracker'));
41
+ } catch {
42
+ // Fallback si el módulo no carga: nunca bloquear, no emitir
43
+ isPackageRoot = () => false;
44
+ readEvolutionMeta = () => ({ evolved: false, metadata: {} });
45
+ }
46
+
47
+ const CARPETAS_EVOLUCIONABLES = [
48
+ 'agentes/',
49
+ 'habilidades/',
50
+ 'comandos/swl/',
51
+ 'reglas/',
52
+ ];
53
+
54
+ const STATE_FILE = path.join(
55
+ process.cwd(),
56
+ '.planning',
57
+ 'comms',
58
+ 'sugerir-contribuir-cooldown.json'
59
+ );
60
+ const COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24h
61
+
62
+ function normalizarSeparadores(p) {
63
+ return String(p).replace(/\\/g, '/');
64
+ }
65
+
66
+ function rutaRelativaAlProyecto(rutaRaw) {
67
+ if (!rutaRaw) return null;
68
+ let normalizada = normalizarSeparadores(rutaRaw);
69
+
70
+ if (path.isAbsolute(normalizada)) {
71
+ const cwd = normalizarSeparadores(process.cwd());
72
+ if (normalizada.toLowerCase().startsWith(cwd.toLowerCase())) {
73
+ normalizada = normalizada.slice(cwd.length).replace(/^\/+/, '');
74
+ } else {
75
+ return null;
76
+ }
77
+ } else {
78
+ normalizada = normalizada.replace(/^\.\//, '');
79
+ }
80
+
81
+ return normalizada;
82
+ }
83
+
84
+ function esEvolucionable(rutaRelativa) {
85
+ if (!rutaRelativa) return false;
86
+ if (!rutaRelativa.endsWith('.md')) return false;
87
+ return CARPETAS_EVOLUCIONABLES.some(c => rutaRelativa.startsWith(c));
88
+ }
89
+
90
+ function nombreSkillDesdeRuta(rutaRelativa) {
91
+ // habilidades/postgresql-experto/SKILL.md → postgresql-experto
92
+ // agentes/orquestador-swl.md → orquestador-swl
93
+ // comandos/swl/release.md → /swl:release
94
+ // reglas/seguridad.md → reglas/seguridad
95
+ if (rutaRelativa.startsWith('habilidades/')) {
96
+ const rest = rutaRelativa.slice('habilidades/'.length);
97
+ return rest.split('/')[0];
98
+ }
99
+ if (rutaRelativa.startsWith('agentes/')) {
100
+ return path.basename(rutaRelativa, '.md');
101
+ }
102
+ if (rutaRelativa.startsWith('comandos/swl/')) {
103
+ return '/swl:' + path.basename(rutaRelativa, '.md');
104
+ }
105
+ if (rutaRelativa.startsWith('reglas/')) {
106
+ return rutaRelativa.replace(/\.md$/, '');
107
+ }
108
+ return path.basename(rutaRelativa, '.md');
109
+ }
110
+
111
+ function leerEstado() {
112
+ try {
113
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ function escribirEstado(estado) {
120
+ try {
121
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
122
+ const tmp = STATE_FILE + '.tmp';
123
+ fs.writeFileSync(tmp, JSON.stringify(estado, null, 2), 'utf8');
124
+ fs.renameSync(tmp, STATE_FILE);
125
+ } catch {
126
+ // Falla silenciosa — cooldown no es crítico
127
+ }
128
+ }
129
+
130
+ function dentroDeCooldown(estado, ruta) {
131
+ const ultima = estado[ruta];
132
+ if (!ultima) return false;
133
+ return Date.now() - ultima < COOLDOWN_MS;
134
+ }
135
+
136
+ function marcarAviso(estado, ruta) {
137
+ estado[ruta] = Date.now();
138
+ escribirEstado(estado);
139
+ }
140
+
141
+ function construirMensaje(rutaRelativa, metadata) {
142
+ const skill = nombreSkillDesdeRuta(rutaRelativa);
143
+ const desde = metadata.evolvedFrom || '?';
144
+ const por = metadata.evolvedBy || 'auto-evolución';
145
+ const nota = metadata.evolvedNote ? `\n Nota: ${metadata.evolvedNote}` : '';
146
+
147
+ return (
148
+ '\n[swl-ses] Detecté una evolución local en `' + rutaRelativa + '` ' +
149
+ '(desde v' + desde + ' por ' + por + ').' + nota + '\n' +
150
+ ' Si la mejora aplica a ingeniería de software general, considera contribuirla:\n' +
151
+ ' /swl:contribuir ' + skill + '\n' +
152
+ ' Eso valida con PluginEval (≥80) y abre PR al core para que otros equipos\n' +
153
+ ' reciban el cambio en el siguiente release.\n' +
154
+ ' (silenciar con SWL_SUGERIR_CONTRIBUIR=0; cooldown 24h por archivo)\n'
155
+ );
156
+ }
157
+
158
+ function extraerRuta(toolInput) {
159
+ if (!toolInput || typeof toolInput !== 'object') return null;
160
+ return (
161
+ toolInput.file_path ||
162
+ toolInput.path ||
163
+ toolInput.notebook_path ||
164
+ null
165
+ );
166
+ }
167
+
168
+ function evaluar(toolName, toolInput) {
169
+ if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) return null;
170
+ if (isPackageRoot()) return null; // Mantenedor editando el repo: nunca sugerir
171
+
172
+ const rutaRaw = extraerRuta(toolInput);
173
+ const ruta = rutaRelativaAlProyecto(rutaRaw);
174
+ if (!ruta) return null;
175
+ if (!esEvolucionable(ruta)) return null;
176
+
177
+ const rutaAbsoluta = path.isAbsolute(rutaRaw) ? rutaRaw : path.join(process.cwd(), ruta);
178
+ if (!fs.existsSync(rutaAbsoluta)) return null;
179
+
180
+ const evo = readEvolutionMeta(rutaAbsoluta);
181
+ if (!evo.evolved) return null;
182
+
183
+ return { ruta, metadata: evo.metadata };
184
+ }
185
+
186
+ function ejecutarComoHook() {
187
+ let inputRaw = '';
188
+
189
+ process.stdin.on('data', chunk => { inputRaw += chunk; });
190
+
191
+ process.stdin.on('end', () => {
192
+ try {
193
+ if (process.env.SWL_SUGERIR_CONTRIBUIR === '0') return;
194
+
195
+ const data = JSON.parse(inputRaw || '{}');
196
+ const toolName = String(data.tool_name || data.tool?.name || '');
197
+ const toolInput = data.tool_input || data.tool?.input || {};
198
+
199
+ const r = evaluar(toolName, toolInput);
200
+ if (!r) return;
201
+
202
+ const estado = leerEstado();
203
+ if (dentroDeCooldown(estado, r.ruta)) return;
204
+
205
+ process.stdout.write(construirMensaje(r.ruta, r.metadata));
206
+ marcarAviso(estado, r.ruta);
207
+ } catch {
208
+ // Errores internos nunca bloquean
209
+ }
210
+ });
211
+ }
212
+
213
+ if (require.main === module) {
214
+ ejecutarComoHook();
215
+ }
216
+
217
+ module.exports = {
218
+ CARPETAS_EVOLUCIONABLES,
219
+ COOLDOWN_MS,
220
+ rutaRelativaAlProyecto,
221
+ esEvolucionable,
222
+ nombreSkillDesdeRuta,
223
+ dentroDeCooldown,
224
+ construirMensaje,
225
+ evaluar,
226
+ };
@@ -99,6 +99,15 @@
99
99
  "maxConsecutiveFailures": 5,
100
100
  "degradeOnFailure": "skip"
101
101
  },
102
+ "sugerir-contribuir.js": {
103
+ "event": "PostToolUse",
104
+ "matcher": "Write|Edit|MultiEdit",
105
+ "description": "Cuando el usuario edita un componente con frontmatter `evolved: true` en su instalación (no en el repo fuente), sugiere abrir /swl:contribuir para subir la mejora al core. Cooldown 24h por archivo. Activo por defecto; silenciar con SWL_SUGERIR_CONTRIBUIR=0.",
106
+ "blocking": false,
107
+ "async": true,
108
+ "maxConsecutiveFailures": 5,
109
+ "degradeOnFailure": "skip"
110
+ },
102
111
  "degradacion-instintos.js": {
103
112
  "event": "PostToolUse",
104
113
  "matcher": "Bash|Write|Edit",
@@ -727,6 +727,8 @@
727
727
  "habilidades/brainstorming",
728
728
  "habilidades/context-builder",
729
729
  "habilidades/memoria-busqueda",
730
+ "habilidades/eval-framework",
731
+ "habilidades/benchmark-memoria",
730
732
  "habilidades/prevencion-racionalizacion",
731
733
  "habilidades/prevencion-sobreingenieria",
732
734
  "habilidades/privacy-memoria",
@@ -954,6 +956,7 @@
954
956
  "hooks/inbox-aviso.js",
955
957
  "hooks/aiisms-detector.js",
956
958
  "hooks/sugerir-regenerar-inventario.js",
959
+ "hooks/sugerir-contribuir.js",
957
960
  "hooks/_run-hook.sh",
958
961
  "hooks/lib/fingerprint-id.js",
959
962
  "hooks/lib/token-budget.js",
@@ -1001,7 +1004,19 @@
1001
1004
  "scripts/lib/scoring-instintos.js",
1002
1005
  "scripts/lib/budget-enforcer.js",
1003
1006
  "scripts/lib/semantic-search.js",
1004
- "scripts/lib/diary-entry.js"
1007
+ "scripts/lib/diary-entry.js",
1008
+ "scripts/lib/rrf-fusion.js",
1009
+ "scripts/lib/jaccard-similarity.js",
1010
+ "scripts/detectar-aprendizajes-duplicados.js",
1011
+ "scripts/lib/eval-schemas.js",
1012
+ "scripts/lib/eval-validator.js",
1013
+ "scripts/lib/eval-quality.js",
1014
+ "scripts/lib/eval-self-correct.js",
1015
+ "scripts/lib/eval-metrics-store.js",
1016
+ "scripts/run-eval.js",
1017
+ "scripts/lib/benchmark-metrics.js",
1018
+ "scripts/lib/longmemeval-runner.js",
1019
+ "scripts/benchmark-memoria.js"
1005
1020
  ],
1006
1021
  "targets": [
1007
1022
  "claude",
@@ -1120,6 +1135,23 @@
1120
1135
  "gemini"
1121
1136
  ]
1122
1137
  },
1138
+ "mcp-server-swl": {
1139
+ "descripcion": "MCP server stub experimental que expone memoria SWL (aprendizajes, sesiones, instintos) a clientes MCP externos (Cursor, Gemini CLI, OpenCode, Cline, Claude Desktop). Modo stdio. 3 endpoints: swl_memory_search, swl_aprendizajes_recientes, swl_instintos_activos. SIN auth, SIN rate limiting, SIN HTTP transport, SIN tests integración. NO USAR EN PRODUCCIÓN. Trigger para hardening: uso real ≥2 runtimes diferentes consistentemente por ≥1 mes. El binario `swl-mcp-server` se instala automáticamente vía npm install -g (declarado en package.json bin). NO se propaga al runtime SWL — vive en el paquete npm como herramienta opt-in. Ver scripts/mcp-server/README.md para 11 limitaciones explícitas y diseño futuro.",
1140
+ "tipo": "scripts",
1141
+ "archivos": [
1142
+ "bin/swl-mcp-server.js",
1143
+ "scripts/mcp-server/handlers.js",
1144
+ "scripts/mcp-server/README.md"
1145
+ ],
1146
+ "targets": [
1147
+ "claude",
1148
+ "openclaude",
1149
+ "copilot",
1150
+ "opencode",
1151
+ "codex",
1152
+ "gemini"
1153
+ ]
1154
+ },
1123
1155
  "rotacion-audit": {
1124
1156
  "descripcion": "Rotación de logs de auditoría (.planning/audit.jsonl y audit-merkle.jsonl) a archivos .gz mensuales en .planning/archivo/audit/. Política por defecto: entradas con timestamp mayor a 30 días se archivan. Zero-deps (fs/zlib/path), CRLF-safe, preserva cadena Merkle. Ejecución automática vía hook Stop 'rotar-audit-auto.js' cuando algún audit*.jsonl supera 5 MB (cooldown 6h). Ejecución manual vía 'node scripts/rotar-audit-logs.js' con --dry-run/--dias=N opcionales. Desactivable con SWL_AUDIT_ROTATE_OFF=1.",
1125
1157
  "tipo": "scripts",
@@ -397,7 +397,8 @@
397
397
  "hooks-observabilidad",
398
398
  "hooks-multica",
399
399
  "graphify-swl",
400
- "mcp-swl"
400
+ "mcp-swl",
401
+ "mcp-server-swl"
401
402
  ]
402
403
  }
403
404
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@saulwade/swl-ses",
3
- "version": "1.1.4",
4
- "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot con 59 agentes, 151 habilidades, 42 comandos, 64 reglas y 39 hooks. Soporta 11 lenguajes y 5 runtimes: Claude Code, Copilot, OpenCode, Codex y Gemini CLI. 100% en espanol (Mexico). Incluye gateway bidireccional con relay Telegram a Claude Code.",
3
+ "version": "1.2.0",
4
+ "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot con 59 agentes, 151 habilidades, 42 comandos, 64 reglas y 40 hooks. Soporta 11 lenguajes y 5 runtimes: Claude Code, Copilot, OpenCode, Codex y Gemini CLI. 100% en espanol (Mexico). Incluye gateway bidireccional con relay Telegram a Claude Code.",
5
5
  "bin": {
6
6
  "swl-ses": "bin/swl-ses.js",
7
- "swl-telegram-bot": "bin/swl-telegram-bot.js"
7
+ "swl-telegram-bot": "bin/swl-telegram-bot.js",
8
+ "swl-mcp-server": "bin/swl-mcp-server.js"
8
9
  },
9
10
  "files": [
10
11
  "bin",