@saulwade/swl-ses 1.4.1 → 1.5.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 (136) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +561 -560
  3. package/agentes/nemesis-auditor-swl.md +161 -161
  4. package/bin/swl-mcp-server.js +49 -22
  5. package/bin/swl-ses.js +74 -0
  6. package/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/contribuir.md +233 -233
  8. package/comandos/swl/ejecutar-fase.md +33 -4
  9. package/comandos/swl/metricas.md +72 -0
  10. package/comandos/swl/nemesis.md +122 -122
  11. package/gateway/lib/event-channel.js +191 -191
  12. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  13. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  14. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  15. package/habilidades/discutir-fase/SKILL.md +50 -2
  16. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  17. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  18. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -0
  19. package/habilidades/eval-framework/SKILL.md +212 -212
  20. package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
  21. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  22. package/habilidades/harness-claude-code/SKILL.md +299 -299
  23. package/habilidades/infra-github-actions/SKILL.md +166 -166
  24. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  25. package/habilidades/manejo-errores/.evolved.json +8 -8
  26. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  27. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  28. package/habilidades/patrones-python/SKILL.md +229 -229
  29. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  30. package/habilidades/planear-fase/SKILL.md +319 -319
  31. package/habilidades/protocolo-revision-swl/SKILL.md +276 -0
  32. package/habilidades/release-semver/.evolved.json +8 -8
  33. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
  34. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  35. package/habilidades/testing-python/SKILL.md +340 -340
  36. package/habilidades/verificar-trabajo/SKILL.md +49 -5
  37. package/habilidades/web-fetcher-routing/SKILL.md +75 -75
  38. package/hooks/claudemd-bloat-detector.js +161 -161
  39. package/hooks/lib/agent-routing.js +107 -107
  40. package/hooks/lib/auto-consolidator.js +335 -335
  41. package/hooks/lib/error-classifier.js +308 -308
  42. package/hooks/lib/merkle-audit.js +96 -96
  43. package/hooks/lib/provenance-tracker.js +191 -191
  44. package/hooks/lib/rate-limit-tracker.js +253 -253
  45. package/hooks/lib/resource-quota.js +122 -122
  46. package/hooks/lib/retry-jitter.js +165 -165
  47. package/hooks/lib/security-net.js +201 -201
  48. package/hooks/lib/skill-auditor.js +588 -588
  49. package/hooks/lib/sync-status.js +228 -228
  50. package/hooks/lib/taint-tracker.js +107 -107
  51. package/hooks/lib/text-similarity.js +241 -241
  52. package/hooks/lib/toon-compressor.js +245 -245
  53. package/hooks/registro-turnos.js +209 -209
  54. package/hooks/sugerir-regenerar-inventario.js +170 -170
  55. package/hooks/validar-formato-post-subagente.js +140 -140
  56. package/hooks/validar-memoria-hook.js +218 -218
  57. package/instintos/prompt-appendices.yaml +57 -57
  58. package/manifiestos/agent-output-schemas.json +57 -57
  59. package/manifiestos/modulos.json +1321 -1262
  60. package/manifiestos/perfiles.json +2 -1
  61. package/manifiestos/skills-lock.json +1114 -1114
  62. package/package.json +3 -3
  63. package/plantillas/auditor-veto-template.md +105 -105
  64. package/plantillas/github-workflows/README.md +47 -47
  65. package/plantillas/github-workflows/release-please.yml +44 -44
  66. package/plantillas/github-workflows/swl-ci.yml +107 -107
  67. package/plantillas/github-workflows/swl-security.yml +51 -51
  68. package/plugin.json +351 -343
  69. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  70. package/reglas/arreglar-al-detectar.md +147 -147
  71. package/reglas/fragmentos-compartidos.md +152 -152
  72. package/reglas/harness-claude-code.md +213 -213
  73. package/reglas/usar-context7.md +226 -226
  74. package/schemas/diary-entry.schema.json +80 -80
  75. package/scripts/audit-tools/audit-history.js +330 -330
  76. package/scripts/audit-tools/bundle-tracker.js +290 -290
  77. package/scripts/audit-tools/canary-monitor.js +352 -352
  78. package/scripts/audit-tools/code-profiler.js +605 -605
  79. package/scripts/audit-tools/dep-doctor.js +320 -320
  80. package/scripts/audit-tools/env-validator.js +206 -206
  81. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  82. package/scripts/audit-tools/lib/output.js +23 -23
  83. package/scripts/audit-tools/migration-checker.js +392 -392
  84. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  85. package/scripts/benchmark-memoria.js +167 -167
  86. package/scripts/configurar-branch-protection.js +418 -418
  87. package/scripts/derivar-feature-list.js +489 -0
  88. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  89. package/scripts/doctor.js +31 -4
  90. package/scripts/field-report.js +199 -199
  91. package/scripts/generar-checklists-consolidados.js +273 -273
  92. package/scripts/generar-inventario.js +420 -420
  93. package/scripts/generar-matriz-lenguajes.js +271 -271
  94. package/scripts/instalador.js +56 -5
  95. package/scripts/lib/artefactos-python.js +43 -43
  96. package/scripts/lib/benchmark-metrics.js +160 -160
  97. package/scripts/lib/budget-enforcer.js +252 -252
  98. package/scripts/lib/configurar-ci.js +380 -380
  99. package/scripts/lib/contadores-inventario.js +217 -217
  100. package/scripts/lib/detectar-runtime.js +75 -9
  101. package/scripts/lib/detectar-stack-detallado.js +307 -307
  102. package/scripts/lib/diary-entry.js +234 -234
  103. package/scripts/lib/estado.js +13 -1
  104. package/scripts/lib/eval-metrics-store.js +218 -218
  105. package/scripts/lib/eval-quality.js +171 -171
  106. package/scripts/lib/eval-schemas.js +144 -144
  107. package/scripts/lib/eval-self-correct.js +106 -106
  108. package/scripts/lib/eval-validator.js +185 -185
  109. package/scripts/lib/expandir-targets.js +71 -0
  110. package/scripts/lib/jaccard-similarity.js +98 -98
  111. package/scripts/lib/longmemeval-runner.js +125 -125
  112. package/scripts/lib/manifiestos.js +42 -1
  113. package/scripts/lib/npm-version.js +261 -261
  114. package/scripts/lib/paquetes-conocidos.js +50 -50
  115. package/scripts/lib/parsear-opciones.js +3 -0
  116. package/scripts/lib/prompt-builder.js +264 -264
  117. package/scripts/lib/rrf-fusion.js +175 -175
  118. package/scripts/lib/scoring-instintos.js +277 -277
  119. package/scripts/lib/semantic-search.js +252 -252
  120. package/scripts/lib/toml-merge.js +204 -0
  121. package/scripts/lib/transformadores/base.js +43 -9
  122. package/scripts/lib/transformadores/codex.js +375 -115
  123. package/scripts/lib/transformadores/cursor.js +359 -0
  124. package/scripts/lib/transformadores/index.js +2 -0
  125. package/scripts/limpiar-artefactos-python.js +131 -131
  126. package/scripts/mcp-server/README.md +122 -80
  127. package/scripts/mcp-server/auth.js +105 -0
  128. package/scripts/mcp-server/cache.js +106 -0
  129. package/scripts/mcp-server/handlers.js +386 -206
  130. package/scripts/mcp-server/telemetry.js +78 -0
  131. package/scripts/migrar-csv-a-array.js +168 -168
  132. package/scripts/migrar-fase-dominio.js +201 -201
  133. package/scripts/publicar.js +511 -511
  134. package/scripts/run-eval.js +141 -141
  135. package/scripts/validar-manifest.js +231 -195
  136. package/scripts/validar-userland-vacio.js +110 -110
@@ -1,206 +1,386 @@
1
- 'use strict';
2
-
3
- /**
4
- * handlers.js — Handlers para los 3 endpoints MCP stub de swl-ses.
5
- *
6
- * **EXPERIMENTAL** no producción. Sin auth, sin rate limiting, sin
7
- * tests robustos. Ver `scripts/mcp-server/README.md` para limitaciones.
8
- *
9
- * Los handlers leen el estado file-based de swl-ses (APRENDIZAJES.md,
10
- * .planning/sessions/, instintos/proyecto.yaml) y devuelven datos
11
- * estructurados al cliente MCP. NO escriben — solo lectura.
12
- *
13
- * @module scripts/mcp-server/handlers
14
- */
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
-
19
- const memorySearch = require('../../hooks/lib/memory-search');
20
- const scoringInstintos = require('../lib/scoring-instintos');
21
-
22
- // ── handler: swl_memory_search ────────────────────────────────────────────────
23
-
24
- /**
25
- * Búsqueda hybrid sobre memoria SWL (aprendizajes + sesiones + instintos).
26
- *
27
- * @param {object} args - { query: string, limit?: number, tipo?: string }
28
- * @returns {object} { results: Array, count: number }
29
- */
30
- function swlMemorySearch(baseDir, args) {
31
- if (!args || typeof args.query !== 'string' || !args.query.trim()) {
32
- return { error: 'query (string) requerido', results: [] };
33
- }
34
-
35
- const filtros = {};
36
- if (typeof args.limit === 'number' && args.limit > 0) filtros.limit = Math.min(args.limit, 50);
37
- if (typeof args.tipo === 'string') filtros.tipo = args.tipo;
38
-
39
- const results = memorySearch.search(baseDir, args.query, filtros);
40
- return {
41
- results: results.map(r => ({
42
- id: r.id,
43
- tipo: r.tipo,
44
- titulo: r.titulo,
45
- fecha: r.fecha,
46
- relevancia: r.relevancia,
47
- combinedScore: r.combinedScore,
48
- })),
49
- count: results.length,
50
- };
51
- }
52
-
53
- // ── handler: swl_aprendizajes_recientes ───────────────────────────────────────
54
-
55
- /**
56
- * Devuelve los N aprendizajes más recientes de APRENDIZAJES.md.
57
- *
58
- * @param {object} args - { limit?: number (default 10) }
59
- * @returns {object} { results, count }
60
- */
61
- function swlAprendizajesRecientes(baseDir, args = {}) {
62
- const limit = (typeof args.limit === 'number' && args.limit > 0)
63
- ? Math.min(args.limit, 50)
64
- : 10;
65
-
66
- const ruta = path.join(baseDir, '.planning', 'APRENDIZAJES.md');
67
- if (!fs.existsSync(ruta)) {
68
- return { error: 'APRENDIZAJES.md no encontrado', results: [] };
69
- }
70
-
71
- let contenido;
72
- try {
73
- contenido = fs.readFileSync(ruta, 'utf8');
74
- } catch (err) {
75
- return { error: 'Error de lectura: ' + err.message, results: [] };
76
- }
77
-
78
- const bloques = contenido.split(/^## /m).filter(b => b.trim().length > 0);
79
- // Los más recientes están al FINAL del archivo (append-only por convención)
80
- const recientes = bloques.slice(-limit).reverse();
81
-
82
- return {
83
- results: recientes.map((b, i) => {
84
- const primeraLinea = b.split('\n')[0].trim();
85
- const cuerpoTrim = b.split('\n').slice(1).join('\n').trim().slice(0, 500);
86
- return {
87
- index: bloques.length - i,
88
- titulo: primeraLinea,
89
- contenido: cuerpoTrim,
90
- };
91
- }),
92
- count: recientes.length,
93
- total: bloques.length,
94
- };
95
- }
96
-
97
- // ── handler: swl_instintos_activos ────────────────────────────────────────────
98
-
99
- /**
100
- * Devuelve instintos con effective_confidence ≥ umbral.
101
- *
102
- * @param {object} args - { minConfidence?: number (default 0.5), limit?: number }
103
- * @returns {object} { results, count }
104
- */
105
- function swlInstintosActivos(baseDir, args = {}) {
106
- const minConfidence = (typeof args.minConfidence === 'number')
107
- ? args.minConfidence : 0.5;
108
- const limit = (typeof args.limit === 'number' && args.limit > 0)
109
- ? Math.min(args.limit, 100) : 20;
110
-
111
- const ruta = path.join(baseDir, 'instintos', 'proyecto.yaml');
112
- if (!fs.existsSync(ruta)) {
113
- return { error: 'instintos/proyecto.yaml no encontrado', results: [] };
114
- }
115
-
116
- let contenido;
117
- try {
118
- contenido = fs.readFileSync(ruta, 'utf8');
119
- } catch (err) {
120
- return { error: 'Error de lectura: ' + err.message, results: [] };
121
- }
122
-
123
- // Parser simple sin dep YAML (mismo patrón que memory-search.js)
124
- const instinto_re = /- id:\s*(\S+)[\s\S]*?pattern:\s*"([^"]+)"[\s\S]*?confidence:\s*([\d.]+)[\s\S]*?status:\s*(\w+)/g;
125
- const results = [];
126
- let match;
127
- const ahora = new Date();
128
-
129
- while ((match = instinto_re.exec(contenido)) !== null) {
130
- const [, id, pattern, confidenceStr, status] = match;
131
- const confidence = parseFloat(confidenceStr);
132
- if (status !== 'active') continue;
133
-
134
- // Construir objeto mínimo para scoring
135
- const instinto = {
136
- id,
137
- pattern,
138
- confidence,
139
- status,
140
- // Sin más metadata, effective_confidence ≈ confidence
141
- };
142
- const effective = scoringInstintos.effectiveConfidence(instinto, ahora);
143
- if (effective < minConfidence) continue;
144
-
145
- results.push({
146
- id,
147
- pattern,
148
- confidence,
149
- effective_confidence: Math.round(effective * 1000) / 1000,
150
- status,
151
- });
152
- }
153
-
154
- results.sort((a, b) => b.effective_confidence - a.effective_confidence);
155
- return {
156
- results: results.slice(0, limit),
157
- count: Math.min(results.length, limit),
158
- total: results.length,
159
- };
160
- }
161
-
162
- // ── exports ───────────────────────────────────────────────────────────────────
163
-
164
- const HANDLERS = {
165
- swl_memory_search: {
166
- description: 'Búsqueda hybrid sobre memoria SWL (aprendizajes + sesiones + instintos) con RRF fusion.',
167
- inputSchema: {
168
- type: 'object',
169
- properties: {
170
- query: { type: 'string', description: 'Texto libre de búsqueda' },
171
- limit: { type: 'number', description: 'Máximo de resultados (default 20, max 50)' },
172
- tipo: { type: 'string', enum: ['aprendizaje', 'sesion', 'instinto'], description: 'Filtrar por tipo' },
173
- },
174
- required: ['query'],
175
- },
176
- handler: swlMemorySearch,
177
- },
178
- swl_aprendizajes_recientes: {
179
- description: 'Últimos N aprendizajes de .planning/APRENDIZAJES.md (más recientes primero).',
180
- inputSchema: {
181
- type: 'object',
182
- properties: {
183
- limit: { type: 'number', description: 'Cuántos retornar (default 10, max 50)' },
184
- },
185
- },
186
- handler: swlAprendizajesRecientes,
187
- },
188
- swl_instintos_activos: {
189
- description: 'Instintos con effective_confidence ≥ umbral. Default 0.5.',
190
- inputSchema: {
191
- type: 'object',
192
- properties: {
193
- minConfidence: { type: 'number', description: 'Umbral mínimo (default 0.5)' },
194
- limit: { type: 'number', description: 'Máximo (default 20, max 100)' },
195
- },
196
- },
197
- handler: swlInstintosActivos,
198
- },
199
- };
200
-
201
- module.exports = {
202
- HANDLERS,
203
- swlMemorySearch,
204
- swlAprendizajesRecientes,
205
- swlInstintosActivos,
206
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * handlers.js — Handlers para los 3 endpoints MCP del swl-mcp-server.
5
+ *
6
+ * v1.0.0 (ADR-0019 Sub-fase 3): integra caching mtime-based desde
7
+ * `scripts/mcp-server/cache.js`. Sigue siendo solo lectura.
8
+ *
9
+ * Los handlers leen el estado file-based de swl-ses (APRENDIZAJES.md,
10
+ * .planning/sessions/, instintos/proyecto.yaml) y devuelven datos
11
+ * estructurados al cliente MCP. NO escriben — solo lectura.
12
+ *
13
+ * @module scripts/mcp-server/handlers
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const memorySearch = require('../../hooks/lib/memory-search');
20
+ const scoringInstintos = require('../lib/scoring-instintos');
21
+ const { crearCache } = require('./cache');
22
+
23
+ // Cache singleton — compartido entre handlers para reducir IO en lecturas repetidas.
24
+ // mtime-based, invalidación automática cuando el archivo cambia. ADR-0019 Sub-fase 3.
25
+ const cache = crearCache();
26
+
27
+ // ── handler: swl_memory_search ────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Búsqueda hybrid sobre memoria SWL (aprendizajes + sesiones + instintos).
31
+ *
32
+ * @param {object} args - { query: string, limit?: number, tipo?: string }
33
+ * @returns {object} { results: Array, count: number }
34
+ */
35
+ function swlMemorySearch(baseDir, args) {
36
+ if (!args || typeof args.query !== 'string' || !args.query.trim()) {
37
+ return { error: 'query (string) requerido', results: [] };
38
+ }
39
+
40
+ const filtros = {};
41
+ if (typeof args.limit === 'number' && args.limit > 0) filtros.limit = Math.min(args.limit, 50);
42
+ if (typeof args.tipo === 'string') filtros.tipo = args.tipo;
43
+
44
+ const results = memorySearch.search(baseDir, args.query, filtros);
45
+ return {
46
+ results: results.map(r => ({
47
+ id: r.id,
48
+ tipo: r.tipo,
49
+ titulo: r.titulo,
50
+ fecha: r.fecha,
51
+ relevancia: r.relevancia,
52
+ combinedScore: r.combinedScore,
53
+ })),
54
+ count: results.length,
55
+ };
56
+ }
57
+
58
+ // ── handler: swl_aprendizajes_recientes ───────────────────────────────────────
59
+
60
+ /**
61
+ * Devuelve los N aprendizajes más recientes de APRENDIZAJES.md.
62
+ *
63
+ * @param {object} args - { limit?: number (default 10) }
64
+ * @returns {object} { results, count }
65
+ */
66
+ function swlAprendizajesRecientes(baseDir, args = {}) {
67
+ const limit = (typeof args.limit === 'number' && args.limit > 0)
68
+ ? Math.min(args.limit, 50)
69
+ : 10;
70
+
71
+ const ruta = path.join(baseDir, '.planning', 'APRENDIZAJES.md');
72
+ let cached;
73
+ try {
74
+ cached = cache.get(ruta, (contenido) => {
75
+ const bloques = contenido.split(/^## /m).filter(b => b.trim().length > 0);
76
+ return { bloques };
77
+ });
78
+ } catch (err) {
79
+ return { error: 'Error de lectura: ' + err.message, results: [] };
80
+ }
81
+ if (!cached) return { error: 'APRENDIZAJES.md no encontrado', results: [] };
82
+
83
+ const { bloques } = cached.data;
84
+ // Los más recientes están al FINAL del archivo (append-only por convención)
85
+ const recientes = bloques.slice(-limit).reverse();
86
+
87
+ return {
88
+ results: recientes.map((b, i) => {
89
+ const primeraLinea = b.split('\n')[0].trim();
90
+ const cuerpoTrim = b.split('\n').slice(1).join('\n').trim().slice(0, 500);
91
+ return {
92
+ index: bloques.length - i,
93
+ titulo: primeraLinea,
94
+ contenido: cuerpoTrim,
95
+ };
96
+ }),
97
+ count: recientes.length,
98
+ total: bloques.length,
99
+ };
100
+ }
101
+
102
+ // ── handler: swl_instintos_activos ────────────────────────────────────────────
103
+
104
+ /**
105
+ * Devuelve instintos con effective_confidence ≥ umbral.
106
+ *
107
+ * @param {object} args - { minConfidence?: number (default 0.5), limit?: number }
108
+ * @returns {object} { results, count }
109
+ */
110
+ function swlInstintosActivos(baseDir, args = {}) {
111
+ const minConfidence = (typeof args.minConfidence === 'number')
112
+ ? args.minConfidence : 0.5;
113
+ const limit = (typeof args.limit === 'number' && args.limit > 0)
114
+ ? Math.min(args.limit, 100) : 20;
115
+
116
+ const ruta = path.join(baseDir, 'instintos', 'proyecto.yaml');
117
+ if (!fs.existsSync(ruta)) {
118
+ return { error: 'instintos/proyecto.yaml no encontrado', results: [] };
119
+ }
120
+
121
+ let contenido;
122
+ try {
123
+ contenido = fs.readFileSync(ruta, 'utf8');
124
+ } catch (err) {
125
+ return { error: 'Error de lectura: ' + err.message, results: [] };
126
+ }
127
+
128
+ // Parser simple sin dep YAML (mismo patrón que memory-search.js)
129
+ const instinto_re = /- id:\s*(\S+)[\s\S]*?pattern:\s*"([^"]+)"[\s\S]*?confidence:\s*([\d.]+)[\s\S]*?status:\s*(\w+)/g;
130
+ const results = [];
131
+ let match;
132
+ const ahora = new Date();
133
+
134
+ while ((match = instinto_re.exec(contenido)) !== null) {
135
+ const [, id, pattern, confidenceStr, status] = match;
136
+ const confidence = parseFloat(confidenceStr);
137
+ if (status !== 'active') continue;
138
+
139
+ // Construir objeto mínimo para scoring
140
+ const instinto = {
141
+ id,
142
+ pattern,
143
+ confidence,
144
+ status,
145
+ // Sin más metadata, effective_confidence ≈ confidence
146
+ };
147
+ const effective = scoringInstintos.effectiveConfidence(instinto, ahora);
148
+ if (effective < minConfidence) continue;
149
+
150
+ results.push({
151
+ id,
152
+ pattern,
153
+ confidence,
154
+ effective_confidence: Math.round(effective * 1000) / 1000,
155
+ status,
156
+ });
157
+ }
158
+
159
+ results.sort((a, b) => b.effective_confidence - a.effective_confidence);
160
+ return {
161
+ results: results.slice(0, limit),
162
+ count: Math.min(results.length, limit),
163
+ total: results.length,
164
+ };
165
+ }
166
+
167
+ // ── handler: swl_list_skills ──────────────────────────────────────────────────
168
+
169
+ /**
170
+ * Lista los skills disponibles (nombre + descripción del frontmatter).
171
+ *
172
+ * Sub-fase 9 (v1.5.0). Útil para clientes MCP que no cargan skills filesystem
173
+ * nativamente y necesitan descubrir qué skills existen antes de invocarlos
174
+ * con swl_invoke_skill.
175
+ *
176
+ * Resolución del directorio de skills:
177
+ * 1. Si baseDir tiene `habilidades/`, usar ese (repo SWL como project root).
178
+ * 2. Si baseDir tiene `.claude/skills/`, usar ese (proyecto consumidor con SWL instalado).
179
+ * 3. Si baseDir tiene `.cursor/skills/`, usar ese.
180
+ * 4. Si nada de eso, devolver error.
181
+ *
182
+ * @param {string} baseDir
183
+ * @param {object} args - { dominio?: string, limit?: number }
184
+ */
185
+ function swlListSkills(baseDir, args = {}) {
186
+ const dirSkills = resolverDirSkills(baseDir);
187
+ if (!dirSkills) {
188
+ return { error: 'No se encontró directorio de skills (habilidades/, .claude/skills/, .cursor/skills/)', results: [] };
189
+ }
190
+
191
+ const limit = (typeof args.limit === 'number' && args.limit > 0)
192
+ ? Math.min(args.limit, 500) : 200;
193
+ const dominio = typeof args.dominio === 'string' ? args.dominio.toLowerCase() : null;
194
+
195
+ let nombres;
196
+ try {
197
+ nombres = fs.readdirSync(dirSkills, { withFileTypes: true })
198
+ .filter(d => d.isDirectory() && !d.name.startsWith('.'))
199
+ .map(d => d.name);
200
+ } catch (err) {
201
+ return { error: 'Error leyendo directorio: ' + err.message, results: [] };
202
+ }
203
+
204
+ const results = [];
205
+ for (const nombre of nombres) {
206
+ if (dominio && !nombre.toLowerCase().includes(dominio)) continue;
207
+ const skillMd = path.join(dirSkills, nombre, 'SKILL.md');
208
+ if (!fs.existsSync(skillMd)) continue;
209
+ try {
210
+ const contenido = fs.readFileSync(skillMd, 'utf-8');
211
+ const descMatch = contenido.match(/description:\s*>?\s*\n?\s*(.+?)(?=\n\w+:|\n---)/s);
212
+ const descripcion = descMatch ? descMatch[1].replace(/\s+/g, ' ').trim() : '';
213
+ results.push({
214
+ nombre,
215
+ descripcion: descripcion.length > 300 ? descripcion.slice(0, 297) + '...' : descripcion,
216
+ });
217
+ } catch { /* skill ilegible — saltar */ }
218
+ if (results.length >= limit) break;
219
+ }
220
+
221
+ return { results, count: results.length, total: nombres.length, dir: dirSkills };
222
+ }
223
+
224
+ // ── handler: swl_invoke_skill ─────────────────────────────────────────────────
225
+
226
+ /**
227
+ * Devuelve el contenido completo de un SKILL.md por nombre.
228
+ *
229
+ * Sub-fase 9 (v1.5.0). Útil para clientes MCP donde el cliente NO carga skills
230
+ * filesystem nativamente (Codex CLI en `--local`, Gemini CLI, otros) pero quiere
231
+ * acceder a conocimiento operacional SWL. El cliente recibe el cuerpo completo
232
+ * del SKILL.md y puede usarlo como contexto en su próxima llamada.
233
+ *
234
+ * Limitaciones aceptadas:
235
+ * - NO ejecuta scripts del skill — solo devuelve el SKILL.md como texto.
236
+ * - Si el skill referencia `recursos/X.md`, el cliente debe pedirlo aparte
237
+ * (no implementamos retrieval recursivo en v1.5.0).
238
+ * - El cuerpo se trunca a 100 KB para no saturar el contexto del cliente.
239
+ *
240
+ * @param {string} baseDir
241
+ * @param {object} args - { nombre: string }
242
+ */
243
+ function swlInvokeSkill(baseDir, args) {
244
+ if (!args || typeof args.nombre !== 'string' || !args.nombre.trim()) {
245
+ return { error: 'argumento "nombre" (string) requerido' };
246
+ }
247
+ // Sanitización: solo nombres en kebab-case (prevenir path traversal).
248
+ const nombre = args.nombre.trim();
249
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(nombre)) {
250
+ return { error: 'Nombre de skill inválido. Solo [a-zA-Z0-9-], 1-63 caracteres.' };
251
+ }
252
+
253
+ const dirSkills = resolverDirSkills(baseDir);
254
+ if (!dirSkills) {
255
+ return { error: 'No se encontró directorio de skills (habilidades/, .claude/skills/, .cursor/skills/)' };
256
+ }
257
+
258
+ const skillMd = path.join(dirSkills, nombre, 'SKILL.md');
259
+ if (!fs.existsSync(skillMd)) {
260
+ return { error: `Skill "${nombre}" no encontrado en ${dirSkills}` };
261
+ }
262
+
263
+ let contenido;
264
+ try {
265
+ contenido = fs.readFileSync(skillMd, 'utf-8');
266
+ } catch (err) {
267
+ return { error: 'Error leyendo SKILL.md: ' + err.message };
268
+ }
269
+
270
+ const MAX = 100 * 1024;
271
+ let truncado = false;
272
+ if (Buffer.byteLength(contenido, 'utf-8') > MAX) {
273
+ contenido = contenido.slice(0, MAX) + '\n\n> NOTA: contenido truncado a 100 KB.';
274
+ truncado = true;
275
+ }
276
+
277
+ // Listar recursos disponibles (sin leerlos) para que el cliente sepa qué pedir.
278
+ const recursos = [];
279
+ const dirRecursos = path.join(dirSkills, nombre, 'recursos');
280
+ if (fs.existsSync(dirRecursos)) {
281
+ try {
282
+ const archivos = fs.readdirSync(dirRecursos)
283
+ .filter(f => /\.(md|json|yaml|yml|txt)$/i.test(f));
284
+ recursos.push(...archivos.map(f => `recursos/${f}`));
285
+ } catch { /* sin recursos */ }
286
+ }
287
+
288
+ return { nombre, contenido, truncado, recursos, fuente: skillMd };
289
+ }
290
+
291
+ /**
292
+ * Resuelve el directorio de skills en el orden de precedencia:
293
+ * 1. baseDir/habilidades/ (repo SWL como project root)
294
+ * 2. baseDir/.claude/skills/ (proyecto consumidor con SWL instalado en Claude)
295
+ * 3. baseDir/.cursor/skills/ (proyecto con SWL instalado en Cursor)
296
+ *
297
+ * Retorna null si ninguno existe.
298
+ */
299
+ function resolverDirSkills(baseDir) {
300
+ const candidatos = [
301
+ path.join(baseDir, 'habilidades'),
302
+ path.join(baseDir, '.claude', 'skills'),
303
+ path.join(baseDir, '.cursor', 'skills'),
304
+ ];
305
+ for (const dir of candidatos) {
306
+ if (fs.existsSync(dir)) return dir;
307
+ }
308
+ return null;
309
+ }
310
+
311
+ // ── exports ───────────────────────────────────────────────────────────────────
312
+
313
+ const HANDLERS = {
314
+ swl_memory_search: {
315
+ description: 'Búsqueda hybrid sobre memoria SWL (aprendizajes + sesiones + instintos) con RRF fusion.',
316
+ schemaVersion: '1.0.0',
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ query: { type: 'string', description: 'Texto libre de búsqueda' },
321
+ limit: { type: 'number', description: 'Máximo de resultados (default 20, max 50)' },
322
+ tipo: { type: 'string', enum: ['aprendizaje', 'sesion', 'instinto'], description: 'Filtrar por tipo' },
323
+ },
324
+ required: ['query'],
325
+ },
326
+ handler: swlMemorySearch,
327
+ },
328
+ swl_aprendizajes_recientes: {
329
+ description: 'Últimos N aprendizajes de .planning/APRENDIZAJES.md (más recientes primero).',
330
+ schemaVersion: '1.0.0',
331
+ inputSchema: {
332
+ type: 'object',
333
+ properties: {
334
+ limit: { type: 'number', description: 'Cuántos retornar (default 10, max 50)' },
335
+ },
336
+ },
337
+ handler: swlAprendizajesRecientes,
338
+ },
339
+ swl_instintos_activos: {
340
+ description: 'Instintos con effective_confidence ≥ umbral. Default 0.5.',
341
+ schemaVersion: '1.0.0',
342
+ inputSchema: {
343
+ type: 'object',
344
+ properties: {
345
+ minConfidence: { type: 'number', description: 'Umbral mínimo (default 0.5)' },
346
+ limit: { type: 'number', description: 'Máximo (default 20, max 100)' },
347
+ },
348
+ },
349
+ handler: swlInstintosActivos,
350
+ },
351
+ swl_list_skills: {
352
+ description: 'Lista skills SWL disponibles (nombre + descripción). Útil para descubrir conocimiento operacional antes de invocar swl_invoke_skill.',
353
+ schemaVersion: '1.0.0',
354
+ inputSchema: {
355
+ type: 'object',
356
+ properties: {
357
+ dominio: { type: 'string', description: 'Filtro substring por nombre (opcional)' },
358
+ limit: { type: 'number', description: 'Máximo (default 200, max 500)' },
359
+ },
360
+ },
361
+ handler: swlListSkills,
362
+ },
363
+ swl_invoke_skill: {
364
+ description: 'Devuelve el SKILL.md completo de un skill SWL por nombre. Para clientes MCP que no cargan skills filesystem nativamente — el cliente recibe el cuerpo y lo usa como contexto.',
365
+ schemaVersion: '1.0.0',
366
+ inputSchema: {
367
+ type: 'object',
368
+ properties: {
369
+ nombre: { type: 'string', description: 'Nombre del skill (kebab-case, ej. fastapi-experto)' },
370
+ },
371
+ required: ['nombre'],
372
+ },
373
+ handler: swlInvokeSkill,
374
+ },
375
+ };
376
+
377
+ module.exports = {
378
+ HANDLERS,
379
+ swlMemorySearch,
380
+ swlAprendizajesRecientes,
381
+ swlInstintosActivos,
382
+ swlListSkills,
383
+ swlInvokeSkill,
384
+ // Expuesto para tests — permite invalidar el cache singleton entre runs.
385
+ _cache: cache,
386
+ };