@saulwade/swl-ses 1.9.0 → 2.1.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 (142) hide show
  1. package/CLAUDE.md +196 -196
  2. package/README.md +579 -579
  3. package/agentes/_propose-step.md +90 -0
  4. package/agentes/accesibilidad-wcag-swl.md +3 -3
  5. package/agentes/auto-evolucion-swl.md +908 -908
  6. package/agentes/disenador-ui-swl.md +6 -5
  7. package/agentes/frontend-angular-swl.md +2 -2
  8. package/agentes/frontend-css-swl.md +2 -2
  9. package/agentes/frontend-react-swl.md +4 -4
  10. package/agentes/frontend-swl.md +6 -6
  11. package/agentes/implementador-swl.md +2 -0
  12. package/agentes/investigador-ux-swl.md +5 -5
  13. package/agentes/orquestador-swl.md +9 -7
  14. package/agentes/perfilador-usuario-swl.md +321 -308
  15. package/agentes/producto-prd-swl.md +1 -1
  16. package/agentes/red-team-swl.md +218 -218
  17. package/agentes/tdd-qa-swl.md +17 -1
  18. package/bin/swl-ses.js +1 -1
  19. package/comandos/swl/actualizar.md +1 -1
  20. package/comandos/swl/aprender.md +2 -2
  21. package/comandos/swl/aprobar-plan.md +153 -0
  22. package/comandos/swl/ayuda.md +3 -3
  23. package/comandos/swl/briefing.md +122 -0
  24. package/comandos/swl/compactar.md +29 -2
  25. package/comandos/swl/discutir-fase.md +23 -2
  26. package/comandos/swl/ejecutar-fase.md +59 -6
  27. package/comandos/swl/evolucionar.md +1 -1
  28. package/comandos/swl/inbox.md +1 -1
  29. package/comandos/swl/instalar.md +1 -1
  30. package/comandos/swl/nemesis.md +1 -1
  31. package/comandos/swl/planear-fase.md +19 -1
  32. package/comandos/swl/plugins.md +1 -1
  33. package/comandos/swl/release.md +47 -1
  34. package/comandos/swl/status.md +348 -0
  35. package/comandos/swl/verificar.md +27 -1
  36. package/habilidades/ai-runtime-security/SKILL.md +1 -1
  37. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  38. package/habilidades/benchmark-memoria/SKILL.md +1 -1
  39. package/habilidades/calidad-contract-testing/SKILL.md +165 -0
  40. package/habilidades/changelog-generator/SKILL.md +9 -2
  41. package/habilidades/changelog-generator/scripts/parse-commits.js +13 -1
  42. package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
  43. package/habilidades/drift-detection/SKILL.md +179 -179
  44. package/habilidades/ejecutar-fase/SKILL.md +541 -468
  45. package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
  46. package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
  47. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
  48. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
  49. package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
  50. package/habilidades/harness-claude-code/SKILL.md +10 -7
  51. package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
  52. package/habilidades/instalar-sistema/SKILL.md +3 -3
  53. package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
  54. package/habilidades/perfil-usuario/SKILL.md +200 -200
  55. package/habilidades/planear-fase/SKILL.md +26 -4
  56. package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
  57. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  58. package/habilidades/proceso-debate-adversarial/SKILL.md +2 -2
  59. package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
  60. package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
  61. package/habilidades/swl-claudemd/SKILL.md +50 -210
  62. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
  63. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
  64. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
  65. package/habilidades/swl-dashboard/SKILL.md +9 -9
  66. package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
  67. package/habilidades/tdd-workflow/SKILL.md +715 -673
  68. package/habilidades/validacion-ci-sistema/SKILL.md +20 -4
  69. package/hooks/calidad-pre-commit.js +344 -3
  70. package/hooks/check-update.js +39 -1
  71. package/hooks/ciclo-evolucion-subagente.js +26 -0
  72. package/hooks/ciclo-evolucion.js +26 -0
  73. package/hooks/extraccion-aprendizajes.js +13 -0
  74. package/hooks/lib/autonomia.js +208 -0
  75. package/hooks/lib/briefing.js +474 -0
  76. package/hooks/lib/ciclo-evolucion.js +47 -0
  77. package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
  78. package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
  79. package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
  80. package/hooks/lib/evolution-tracker.js +24 -3
  81. package/hooks/lib/propose-step.js +357 -0
  82. package/hooks/session-briefing.js +98 -0
  83. package/hooks/spec-gate.js +211 -0
  84. package/hooks/tdd-gate.js +241 -0
  85. package/hooks/telemetria-skill-routing.js +100 -0
  86. package/hooks/validar-intent-spec.js +30 -10
  87. package/instintos/autonomia.yaml +27 -0
  88. package/llms.txt +6 -6
  89. package/manifiestos/hooks-config.json +44 -17
  90. package/manifiestos/modulos.json +40 -15
  91. package/manifiestos/skills-lock.json +64 -57
  92. package/package.json +93 -93
  93. package/plugin.json +371 -375
  94. package/reglas/accesibilidad.md +10 -0
  95. package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
  96. package/reglas/api-diseno.md +9 -0
  97. package/reglas/auditorias-documentales-estructurales.md +7 -0
  98. package/reglas/cloud-infra.md +8 -0
  99. package/reglas/consultar-vault-primero.md +195 -0
  100. package/reglas/debatir-antes-de-aceptar.md +158 -0
  101. package/reglas/fragmentos-compartidos.md +5 -0
  102. package/reglas/git-coauthor.md +100 -0
  103. package/reglas/gobernanza.md +4 -4
  104. package/reglas/hooks.md +6 -0
  105. package/reglas/intent-engineering.md +4 -0
  106. package/reglas/markitdown.md +8 -0
  107. package/reglas/memoria-consolidada.md +1 -1
  108. package/reglas/monitor-ci.md +309 -0
  109. package/reglas/patrones.md +6 -0
  110. package/reglas/registro-componentes-nuevos.md +39 -2
  111. package/reglas/seguridad-agentes.md +1 -1
  112. package/reglas/sesiones-paralelas.md +180 -0
  113. package/reglas/skills-estandar.md +6 -0
  114. package/reglas/testing.md +7 -0
  115. package/reglas/tests-cleanup.md +4 -0
  116. package/reglas/usar-code-review-graph.md +155 -0
  117. package/reglas/usar-sistema-swl.md +1 -1
  118. package/reglas/verificar-citas-normativas.md +548 -0
  119. package/scripts/instalador.js +52 -6
  120. package/scripts/lib/ci-reader.js +193 -0
  121. package/scripts/lib/detectar-host-swl.js +175 -0
  122. package/scripts/lib/evidencia-release.js +322 -0
  123. package/scripts/lib/gate-hooks-requires.js +249 -0
  124. package/scripts/lib/gate-licencias.js +212 -0
  125. package/scripts/lib/git-metricas.js +257 -0
  126. package/scripts/lib/gitignore-manifest.js +29 -1
  127. package/scripts/lib/metricas-dora.js +204 -0
  128. package/scripts/lib/plan-lock.js +275 -0
  129. package/scripts/migrar-fase-dominio.js +0 -1
  130. package/scripts/tui/ejecutores.js +1 -1
  131. package/scripts/validar-manifest.js +92 -1
  132. package/scripts/verificar-evolucion.js +54 -4
  133. package/scripts/verificar-release.js +102 -0
  134. package/scripts/verificar-trazabilidad.js +298 -0
  135. package/agentes/ux-disenador-swl.md +0 -503
  136. package/comandos/swl/dashboard.md +0 -146
  137. package/comandos/swl/evolucion-estado.md +0 -191
  138. package/comandos/swl/metricas.md +0 -376
  139. package/comandos/swl/salud.md +0 -481
  140. package/reglas/arquitectura.evolved.json +0 -7
  141. package/reglas/seguridad.evolved.json +0 -7
  142. package/reglas/verificar-citas-temporales.md +0 -139
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * plan-lock.js — Firma y verificación de integridad de PLAN.md (Gate G1).
5
+ *
6
+ * Materializa el gate G1 del enforcement SDD (Fase 09, ADR de la Revisión
7
+ * Evolutiva 03): un PLAN aprobado vía `/swl:aprobar-plan` queda firmado con
8
+ * un SHA256 escrito en `.planning/locks/<basename>.lock`. `ejecutar-fase`
9
+ * recomputa ese hash al arrancar; un plan mutado tras la aprobación detiene
10
+ * la ejecución. Cierra el hueco "el plan puede mutar tras aprobación" que
11
+ * dejaba SDD en ~60%.
12
+ *
13
+ * Cláusula de gracia D-05 (compatibilidad legacy): un PLAN con
14
+ * `estado: aprobado` en su frontmatter pero SIN `.lock` correspondiente
15
+ * (los planes creados antes de la Fase 09) se considera `modo: "legacy"` y
16
+ * se ejecuta con advertencia, sin bloqueo. Solo los planes firmados vía
17
+ * `/swl:aprobar-plan` (post-Fase 09) exigen coincidencia de hash.
18
+ *
19
+ * MODELO DE AMENAZA (gate G1): el lock detecta MUTACIÓN ACCIDENTAL o no
20
+ * aprobada del PLAN tras su firma, y planes alterados por un agente que
21
+ * confabula. NO es un control criptográfico contra un adversario con acceso
22
+ * de escritura al repo — quien pueda commitear puede recalcular el SHA256 y
23
+ * re-firmar. La evidencia real de aprobación es `aprobadoPor:` + el commit del
24
+ * lock por `/swl:aprobar-plan` en el audit trail de git. El SHA256 solo detecta
25
+ * drift entre firma y verificación. (Verificación Fase 09, hallazgo S-6.)
26
+ *
27
+ * API:
28
+ * firmarPlan(planPath, opciones?) -> { ok, sha256, lockPath, archivo }
29
+ * verificarPlan(planPath) -> { ok, modo, motivo, ... }
30
+ * resolverLockPath(planPath) -> string (ruta del .lock esperado)
31
+ *
32
+ * Zero-dependencies. Compatible Windows (hash sobre buffer crudo). Node 18+.
33
+ *
34
+ * Origen: Fase 09 — enforcement SDD/TDD vNext H1, Slice 1.
35
+ */
36
+
37
+ const fs = require('fs');
38
+ const path = require('path');
39
+ const crypto = require('crypto');
40
+
41
+ // Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo idéntico
42
+ // al de scripts/generar-skills-lock.js para no acoplar a un layout fijo.
43
+ let atomicWriteSync;
44
+ try {
45
+ ({ atomicWriteSync } = require(path.join(__dirname, '..', '..', 'hooks', 'lib', 'atomic-write.js')));
46
+ } catch {
47
+ atomicWriteSync = (p, c, e) => {
48
+ const dir = path.dirname(p);
49
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
50
+ fs.writeFileSync(p, c, e);
51
+ };
52
+ }
53
+
54
+ const LOCK_VERSION = 1;
55
+
56
+ // Basename válido de un PLAN de fase: `PLAN.md` o `0N-PLAN.md`. Evita que
57
+ // `firmarPlan` produzca locks de archivos arbitrarios (falsearía el audit
58
+ // trail de aprobación). Verificación Fase 09, hallazgo S-2.
59
+ const PLAN_BASENAME_RE = /^(\d{2}-)?PLAN\.md$/;
60
+
61
+ /**
62
+ * SHA256 de un buffer. Idéntico a scripts/generar-skills-lock.js:42-44.
63
+ * Se hashea el buffer crudo (no string utf8) para que el hash sea byte-exacto
64
+ * y detecte incluso cambios de fin de línea introducidos por un editor.
65
+ *
66
+ * @param {Buffer|string} data
67
+ * @returns {string} hex digest
68
+ */
69
+ function sha256(data) {
70
+ return crypto.createHash('sha256').update(data).digest('hex');
71
+ }
72
+
73
+ /**
74
+ * Extrae el campo `estado:` del frontmatter YAML del PLAN.
75
+ * Reutiliza el patrón CRLF-safe de scripts/generar-skills-lock.js:46-55.
76
+ *
77
+ * @param {string} contenido
78
+ * @returns {string|null} valor de `estado` o null si no hay frontmatter/campo
79
+ */
80
+ function leerEstado(contenido) {
81
+ const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
82
+ if (!m) return null;
83
+ for (const linea of m[1].split(/\r?\n/)) {
84
+ const kv = linea.match(/^estado:\s*(.*)$/);
85
+ if (kv) return kv[1].trim();
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Resuelve el directorio `.planning/locks/` ascendiendo desde el PLAN hasta
92
+ * encontrar el directorio `.planning` ancestro. Robusto ante planes en
93
+ * `.planning/fases/0N-PLAN.md` o `.planning/PLAN.md`.
94
+ *
95
+ * Hardening S-1 (verificación Fase 09):
96
+ * - Resuelve symlinks con `fs.realpathSync` antes de ascender, para que un
97
+ * symlink plantado (`temp/.planning` → `/etc/.planning`) no permita escapar
98
+ * del árbol real del proyecto.
99
+ * - SIN fallback ciego: si no hay `.planning` ancestro, retorna `null` (antes
100
+ * escribía `locks/` al lado del PLAN, en ubicación arbitraria). El caller
101
+ * trata `null` como error explícito.
102
+ *
103
+ * @param {string} planPath
104
+ * @returns {string|null} ruta absoluta del directorio de locks, o null si no
105
+ * hay `.planning` ancestro.
106
+ */
107
+ function resolverLocksDir(planPath) {
108
+ let dir;
109
+ try {
110
+ dir = path.dirname(fs.realpathSync(path.resolve(planPath)));
111
+ } catch {
112
+ dir = path.dirname(path.resolve(planPath));
113
+ }
114
+ while (dir !== path.dirname(dir)) {
115
+ if (path.basename(dir) === '.planning') {
116
+ return path.join(dir, 'locks');
117
+ }
118
+ dir = path.dirname(dir);
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Ruta del `.lock` esperado para un PLAN dado.
125
+ *
126
+ * @param {string} planPath
127
+ * @returns {string|null} ruta absoluta del archivo `.lock`, o null si el PLAN
128
+ * no tiene un `.planning` ancestro.
129
+ */
130
+ function resolverLockPath(planPath) {
131
+ const dir = resolverLocksDir(planPath);
132
+ return dir ? path.join(dir, `${path.basename(planPath)}.lock`) : null;
133
+ }
134
+
135
+ /**
136
+ * Firma un PLAN: computa su SHA256 y escribe el `.lock` correspondiente.
137
+ * Idempotente — re-firmar sobreescribe el lock (caso re-aprobación).
138
+ *
139
+ * @param {string} planPath
140
+ * @param {object} [opciones]
141
+ * @param {string} [opciones.firmadoPor='swl:aprobar-plan']
142
+ * @param {string} [opciones.firmadoEn] ISO timestamp; default new Date()
143
+ * @returns {{ok: boolean, sha256?: string, lockPath?: string, archivo?: string, motivo?: string}}
144
+ */
145
+ function firmarPlan(planPath, opciones = {}) {
146
+ if (!fs.existsSync(planPath)) {
147
+ return { ok: false, motivo: `PLAN no existe: ${planPath}` };
148
+ }
149
+ const archivo = path.basename(planPath);
150
+ if (!PLAN_BASENAME_RE.test(archivo)) {
151
+ return {
152
+ ok: false,
153
+ motivo: `basename inválido para firma: "${archivo}". Solo se firman PLAN.md o 0N-PLAN.md de fases.`,
154
+ };
155
+ }
156
+ const lockPath = resolverLockPath(planPath);
157
+ if (!lockPath) {
158
+ return {
159
+ ok: false,
160
+ motivo: `el PLAN no tiene un directorio .planning ancestro: ${planPath}`,
161
+ };
162
+ }
163
+ const buffer = fs.readFileSync(planPath);
164
+ const hash = sha256(buffer);
165
+
166
+ const lock = {
167
+ version: LOCK_VERSION,
168
+ archivo,
169
+ sha256: hash,
170
+ firmadoEn: opciones.firmadoEn || new Date().toISOString(),
171
+ firmadoPor: opciones.firmadoPor || 'swl:aprobar-plan',
172
+ };
173
+
174
+ atomicWriteSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
175
+ return { ok: true, sha256: hash, lockPath, archivo };
176
+ }
177
+
178
+ /**
179
+ * Verifica la integridad de un PLAN contra su `.lock`.
180
+ *
181
+ * Modos de retorno:
182
+ * - `firmado` : existe lock y el hash coincide -> ok: true
183
+ * - `legacy` : NO existe lock pero estado:aprobado (gracia D-05) -> ok: true
184
+ * - `mutado` : existe lock y el hash NO coincide -> ok: false
185
+ * - `lock-corrupto`: el lock existe pero no es JSON válido -> ok: false
186
+ * - `sin-firmar` : NO existe lock y estado != aprobado -> ok: false
187
+ * - `sin-planning`: el PLAN no tiene `.planning` ancestro -> ok: false
188
+ * - `no-existe` : el PLAN no existe en disco -> ok: false
189
+ *
190
+ * @param {string} planPath
191
+ * @returns {{ok: boolean, modo: string, motivo?: string, hashEsperado?: string, hashActual?: string, lockPath?: string}}
192
+ */
193
+ function verificarPlan(planPath) {
194
+ if (!fs.existsSync(planPath)) {
195
+ return { ok: false, modo: 'no-existe', motivo: `PLAN no existe: ${planPath}` };
196
+ }
197
+ const lockPath = resolverLockPath(planPath);
198
+ if (!lockPath) {
199
+ return {
200
+ ok: false,
201
+ modo: 'sin-planning',
202
+ motivo: `el PLAN no tiene un directorio .planning ancestro: ${planPath}`,
203
+ };
204
+ }
205
+ const buffer = fs.readFileSync(planPath);
206
+ const hashActual = sha256(buffer);
207
+
208
+ if (!fs.existsSync(lockPath)) {
209
+ // Cláusula de gracia D-05: plan aprobado antes de que existiera el lock.
210
+ const estado = leerEstado(buffer.toString('utf8'));
211
+ if (estado === 'aprobado') {
212
+ return {
213
+ ok: true,
214
+ modo: 'legacy',
215
+ motivo: 'plan aprobado pre-Fase09 (sin lock); ejecutar con advertencia',
216
+ hashActual,
217
+ lockPath,
218
+ };
219
+ }
220
+ return {
221
+ ok: false,
222
+ modo: 'sin-firmar',
223
+ motivo: `plan sin aprobar y sin lock (estado: ${estado || 'ausente'})`,
224
+ hashActual,
225
+ lockPath,
226
+ };
227
+ }
228
+
229
+ let lock;
230
+ try {
231
+ lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
232
+ } catch (err) {
233
+ return {
234
+ ok: false,
235
+ modo: 'lock-corrupto',
236
+ motivo: `lock malformado en ${lockPath}: ${err.message}`,
237
+ hashActual,
238
+ lockPath,
239
+ };
240
+ }
241
+
242
+ const hashEsperado = lock && typeof lock.sha256 === 'string' ? lock.sha256 : null;
243
+ if (!hashEsperado) {
244
+ return {
245
+ ok: false,
246
+ modo: 'lock-corrupto',
247
+ motivo: `lock sin campo sha256 en ${lockPath}`,
248
+ hashActual,
249
+ lockPath,
250
+ };
251
+ }
252
+
253
+ if (hashEsperado === hashActual) {
254
+ return { ok: true, modo: 'firmado', hashEsperado, hashActual, lockPath };
255
+ }
256
+
257
+ return {
258
+ ok: false,
259
+ modo: 'mutado',
260
+ motivo: 'plan mutado tras aprobación (el hash no coincide con el lock)',
261
+ hashEsperado,
262
+ hashActual,
263
+ lockPath,
264
+ };
265
+ }
266
+
267
+ module.exports = {
268
+ firmarPlan,
269
+ verificarPlan,
270
+ resolverLockPath,
271
+ resolverLocksDir,
272
+ sha256,
273
+ leerEstado,
274
+ LOCK_VERSION,
275
+ };
@@ -80,7 +80,6 @@ const MAPEO = {
80
80
  'revisor-typescript-swl': ['verify', 'quality'],
81
81
  'sre-swl': ['release', 'infra'],
82
82
  'tdd-qa-swl': ['verify', 'quality'],
83
- 'ux-disenador-swl': ['plan', 'ux'],
84
83
  };
85
84
 
86
85
  function migrarAgente(nombre) {
@@ -47,7 +47,7 @@ function _calcularTotalesDelPlan(opciones) {
47
47
  // mediante resolverPerfil, que devuelve { perfil, modulos, archivos, warnings }.
48
48
  try {
49
49
  const { resolverPerfil } = require('../lib/manifiestos');
50
- const resolucion = resolverPerfil(opciones.profile || 'core', {});
50
+ const resolucion = resolverPerfil(opciones.profile || 'completo', {});
51
51
  if (!resolucion || !Array.isArray(resolucion.archivos)) return {};
52
52
  const totales = {};
53
53
  for (const archivo of resolucion.archivos) {
@@ -149,6 +149,88 @@ function verificarModulosReferenciadosPorPerfil(modulos, perfiles) {
149
149
  return false;
150
150
  }
151
151
 
152
+ function fragmentosDeclaradosEnAgente(contenido) {
153
+ // Extrae los nombres de fragmento (_x) del frontmatter `fragmentos:` de un
154
+ // agente. Soporta lista multilínea (`fragmentos:\n - _x`) e inline
155
+ // (`fragmentos: [_x, _y]`).
156
+ const fm = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---/);
157
+ if (!fm) return [];
158
+ const lineas = fm[1].split(/\r?\n/);
159
+ const out = [];
160
+ let dentro = false;
161
+ for (const l of lineas) {
162
+ const inline = l.match(/^fragmentos:\s*\[(.*)\]\s*$/);
163
+ if (inline) {
164
+ inline[1].split(',').forEach(s => { const t = s.trim(); if (t) out.push(t); });
165
+ dentro = false;
166
+ continue;
167
+ }
168
+ if (/^fragmentos:\s*$/.test(l)) { dentro = true; continue; }
169
+ if (dentro) {
170
+ const m = l.match(/^\s*-\s*(_[a-z][a-z0-9-]*)\s*$/);
171
+ if (m) { out.push(m[1]); continue; }
172
+ if (/^\S/.test(l)) dentro = false; // siguiente clave de primer nivel
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function verificarFragmentos(declarados) {
179
+ // DT-FRAGMENTOS-VALIDACION (Fase 13): dos invariantes que la regla
180
+ // fragmentos-compartidos.md declara y que antes no se enforzaban:
181
+ // 1. Todo fragmento agentes/_*.md debe estar referenciado por ≥2 agentes.
182
+ // 2. Ningún fragmento agentes/_*.md debe aparecer en modulos.json (no es agente).
183
+ const base = path.join(RAIZ, 'agentes');
184
+ if (!fs.existsSync(base)) {
185
+ log(' [OK] fragmentos: sin directorio agentes/ (nada que validar)');
186
+ return true;
187
+ }
188
+ const fragmentos = fs.readdirSync(base)
189
+ .filter(f => f.startsWith('_') && f.endsWith('.md'))
190
+ .map(f => f.replace(/\.md$/, '')); // '_propose-step'
191
+
192
+ // Contar referencias por fragmento en los agentes reales (no fragmentos).
193
+ const referencias = new Map(fragmentos.map(f => [f, 0]));
194
+ const agentes = fs.readdirSync(base)
195
+ .filter(f => f.endsWith('.md') && !f.startsWith('_'));
196
+ for (const ag of agentes) {
197
+ const refs = fragmentosDeclaradosEnAgente(fs.readFileSync(path.join(base, ag), 'utf-8'));
198
+ for (const r of refs) {
199
+ if (referencias.has(r)) referencias.set(r, referencias.get(r) + 1);
200
+ }
201
+ }
202
+
203
+ let ok = true;
204
+
205
+ // Check 1: ≥2 referencias.
206
+ const subreferenciados = fragmentos.filter(f => referencias.get(f) < 2);
207
+ if (subreferenciados.length === 0) {
208
+ log(` [OK] fragmentos: ${fragmentos.length}/${fragmentos.length} referenciados por ≥2 agentes`);
209
+ } else {
210
+ ok = false;
211
+ log(` [FALLA] fragmentos: ${subreferenciados.length} referenciado(s) por <2 agentes`);
212
+ for (const f of subreferenciados) {
213
+ log(` → agentes/${f}.md (referencias: ${referencias.get(f)})`);
214
+ errores.push({ categoria: 'fragmentos', archivo: `agentes/${f}.md`, tipo: 'fragmento_subreferenciado' });
215
+ }
216
+ }
217
+
218
+ // Check 2: no en modulos.json.
219
+ const enModulos = fragmentos.filter(f => declarados.has(`agentes/${f}.md`));
220
+ if (enModulos.length === 0) {
221
+ log(' [OK] fragmentos: ninguno declarado como agente en modulos.json');
222
+ } else {
223
+ ok = false;
224
+ log(` [FALLA] fragmentos: ${enModulos.length} declarado(s) en modulos.json (no son agentes)`);
225
+ for (const f of enModulos) {
226
+ log(` → agentes/${f}.md`);
227
+ errores.push({ categoria: 'fragmentos', archivo: `agentes/${f}.md`, tipo: 'fragmento_en_modulos' });
228
+ }
229
+ }
230
+
231
+ return ok;
232
+ }
233
+
152
234
  function verificarDeclarados1a1ExistenEnDisco(declarados) {
153
235
  // Caso inverso: declarados en manifest pero no presentes en disco
154
236
  const rotos = [];
@@ -205,6 +287,9 @@ function main() {
205
287
  const perfilOk = verificarModulosReferenciadosPorPerfil(modulos, perfiles);
206
288
  if (!perfilOk) todoOk = false;
207
289
 
290
+ const fragmentosOk = verificarFragmentos(declarados);
291
+ if (!fragmentosOk) todoOk = false;
292
+
208
293
  const inversoOk = verificarDeclarados1a1ExistenEnDisco(declarados);
209
294
  if (!inversoOk) todoOk = false;
210
295
 
@@ -219,6 +304,8 @@ function main() {
219
304
  log(' - "no_declarado_hooks_config": registrar el hook en manifiestos/hooks-config.json');
220
305
  log(' - "modulo_huerfano_sin_perfil": agregar el módulo al array `modulos` de al menos un perfil en manifiestos/perfiles.json');
221
306
  log(' - "declarado_no_existe": eliminar la ruta de modulos.json (archivo borrado) o crear el archivo');
307
+ log(' - "fragmento_subreferenciado": el fragmento agentes/_*.md debe ser usado por ≥2 agentes (declararlo en su frontmatter `fragmentos:`) o re-incrustarlo en el único agente que lo usa');
308
+ log(' - "fragmento_en_modulos": quitar el fragmento agentes/_*.md de modulos.json (no es un agente routable)');
222
309
  }
223
310
 
224
311
  if (salidaJson) {
@@ -228,4 +315,8 @@ function main() {
228
315
  process.exit(todoOk ? 0 : 1);
229
316
  }
230
317
 
231
- main();
318
+ if (require.main === module) {
319
+ main();
320
+ }
321
+
322
+ module.exports = { fragmentosDeclaradosEnAgente };
@@ -33,6 +33,26 @@ const fs = require('fs');
33
33
  const path = require('path');
34
34
  const { execSync } = require('child_process');
35
35
 
36
+ /**
37
+ * Detecta si `dir` es la raíz del paquete swl-ses (repo madre).
38
+ *
39
+ * Espeja la excepción de `markAsEvolved` (hooks/lib/evolution-tracker.js):
40
+ * en el repo madre los cambios del mantenedor se rastrean por git + bump de
41
+ * `version`, NO por metadatos `evolved-*` — el tracker se NIEGA a marcarlos.
42
+ * Antes de este fix, los CHECKs 2/3 exigían aquí lo que el tracker rechaza
43
+ * escribir → el gate del Paso 6 de /swl:aprender era insatisfacible en el
44
+ * repo madre (detectado 2026-06-12, aprendizaje M4). El set de nombres debe
45
+ * mantenerse alineado con `isPackageRoot` del tracker.
46
+ */
47
+ function esRaizDelPaquete(dir) {
48
+ try {
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir || process.cwd(), 'package.json'), 'utf8'));
50
+ return pkg.name === '@saulwade/swl-ses' || pkg.name === 'swl-ses';
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
36
56
  /** Extrae el bloque de frontmatter YAML del inicio del archivo. */
37
57
  function extraerFrontmatter(contenido) {
38
58
  const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(contenido);
@@ -132,8 +152,16 @@ function archivosModificados(since) {
132
152
  }
133
153
  }
134
154
 
135
- /** Verifica un archivo concreto. Devuelve `{ archivo, problemas[], info }`. */
136
- function verificarArchivo(filePath) {
155
+ /**
156
+ * Verifica un archivo concreto. Devuelve `{ archivo, problemas[], info }`.
157
+ * @param {object} [opts] - `opts.raizPaquete` fuerza/inyecta la detección de
158
+ * repo madre (default: `esRaizDelPaquete(process.cwd())`). En repo madre los
159
+ * CHECKs 2/3 (evolved) se omiten — solo aplican version presente + bump.
160
+ */
161
+ function verificarArchivo(filePath, opts = {}) {
162
+ const raizPaquete = opts.raizPaquete !== undefined
163
+ ? opts.raizPaquete
164
+ : esRaizDelPaquete(process.cwd());
137
165
  const resultado = { archivo: filePath, problemas: [], info: {} };
138
166
 
139
167
  if (!fs.existsSync(filePath)) {
@@ -186,7 +214,15 @@ function verificarArchivo(filePath) {
186
214
  // CHECK 2: evolved = true (en frontmatter o en .evolved.json)
187
215
  // v1.6.1 (Cabo A4): exento si deprecated:true — no tiene sentido exigir
188
216
  // metadatos de evolución para componentes que se van a eliminar.
189
- if (ev.evolved !== 'true' && !esDeprecated) {
217
+ // Exento también en el repo madre: markAsEvolved se NIEGA a marcar ahí
218
+ // (los cambios del mantenedor se rastrean por git + bump de version), así
219
+ // que exigirlo haría el gate insatisfacible. Ver esRaizDelPaquete().
220
+ if (raizPaquete) {
221
+ resultado.info.notas = resultado.info.notas || [];
222
+ resultado.info.notas.push(
223
+ 'raíz del paquete: checks evolved omitidos (cambios del mantenedor = git + bump)'
224
+ );
225
+ } else if (ev.evolved !== 'true' && !esDeprecated) {
190
226
  resultado.problemas.push(
191
227
  '`evolved` no encontrado (ni en frontmatter ni en .evolved.json del directorio)'
192
228
  );
@@ -284,6 +320,13 @@ function main() {
284
320
  archivos = args.filter(a => !a.startsWith('--'));
285
321
  }
286
322
 
323
+ if (esRaizDelPaquete(process.cwd())) {
324
+ process.stdout.write(
325
+ '[nota] Raíz del paquete swl-ses: los checks de `evolved` se omiten — ' +
326
+ 'los cambios del mantenedor se rastrean por git + bump de `version`.\n'
327
+ );
328
+ }
329
+
287
330
  let fallos = 0;
288
331
  for (const archivo of archivos) {
289
332
  const r = verificarArchivo(archivo);
@@ -302,4 +345,11 @@ function main() {
302
345
 
303
346
  if (require.main === module) main();
304
347
 
305
- module.exports = { verificarArchivo, extraerFrontmatter, leerCampo, obtenerVersionEnHEAD, main };
348
+ module.exports = {
349
+ verificarArchivo,
350
+ extraerFrontmatter,
351
+ leerCampo,
352
+ obtenerVersionEnHEAD,
353
+ esRaizDelPaquete,
354
+ main,
355
+ };
@@ -32,6 +32,10 @@
32
32
  const fs = require('fs');
33
33
  const path = require('path');
34
34
  const contadoresLib = require('./lib/contadores-inventario');
35
+ // Gate de licencias (Fase 14, ADR-0038) — WARN-ONLY: reporta copyleft pero
36
+ // NUNCA bloquea el release en v1 (D-14-02). Cargado como módulo, no subproceso.
37
+ const { evaluarLicencias } = require('./lib/gate-licencias');
38
+ const { evaluarHooksRequires } = require('./lib/gate-hooks-requires');
35
39
 
36
40
  const CWD = process.cwd();
37
41
 
@@ -272,6 +276,18 @@ function main() {
272
276
  fallasObligatorias++;
273
277
  }
274
278
 
279
+ // Gate de requires hooks→scripts: valida que toda lib de scripts/ requerida
280
+ // por un hook DISTRIBUIDO (incluidas dependencias transitivas) esté registrada
281
+ // en modulos.json y cubierta en todos los perfiles que instalan el hook.
282
+ // Bloqueante: sin esto el hook muere con MODULE_NOT_FOUND en el destino y el
283
+ // wrapper de settings.json lo traga en silencio. Origen: bug check-update —
284
+ // el aviso de nuevas versiones nunca llegó a instalaciones destino (2026-06-12,
285
+ // commit 12b2a31). Misma familia que el gate de bin-imports, eje instalador→destino.
286
+ const gateHooksRequires = ejecutarGateHooksRequires();
287
+ if (gateHooksRequires.disponible && gateHooksRequires.hallazgos.length > 0) {
288
+ fallasObligatorias++;
289
+ }
290
+
275
291
  // Gate de consistencia cross-manifest del campo description:
276
292
  // valida que package.json#description y plugin.json#description tengan
277
293
  // las mismas cifras (60 agentes / N habilidades / M comandos / K reglas / L hooks)
@@ -295,6 +311,12 @@ function main() {
295
311
  }
296
312
  }
297
313
 
314
+ // Gate de licencias (Fase 14, ADR-0038) — WARN-ONLY por diseño (D-14-02):
315
+ // clasifica el árbol prod (copyleft fuerte/débil, desconocida) pero NUNCA
316
+ // suma a fallasObligatorias. Solo informa; la promoción a blocking exigiría
317
+ // calibración + ADR posterior (mismo patrón que G0/G2/G3).
318
+ const gateLicencias = ejecutarGateLicencias();
319
+
298
320
  if (jsonOut) {
299
321
  process.stdout.write(JSON.stringify({
300
322
  version,
@@ -304,8 +326,10 @@ function main() {
304
326
  warnings_opcionales: warningsOpcionales,
305
327
  contadores_gate: gateContadores,
306
328
  bin_imports_gate: gateBinImports,
329
+ hooks_requires_gate: gateHooksRequires,
307
330
  description_gate: gateDescription,
308
331
  aiisms_gate: aiismsGate,
332
+ gate_licencias: gateLicencias,
309
333
  resultados: resultados.map(({ entrada, resultado }) => ({
310
334
  archivo: entrada.archivo,
311
335
  obligatorio: entrada.obligatorio,
@@ -378,6 +402,28 @@ function main() {
378
402
  process.stdout.write('\n');
379
403
  process.stdout.write('Gate de bin-imports: no disponible — ' + gateBinImports.error + '\n');
380
404
  }
405
+ if (gateHooksRequires.disponible) {
406
+ process.stdout.write('\n');
407
+ process.stdout.write('Gate de requires hooks→scripts (vs modulos.json + perfiles.json):\n');
408
+ process.stdout.write(
409
+ ' Hooks distribuidos analizados: ' + gateHooksRequires.hooksDistribuidos +
410
+ ' | dependencias hooks→scripts: ' + gateHooksRequires.dependencias.length + '\n'
411
+ );
412
+ if (gateHooksRequires.hallazgos.length === 0) {
413
+ process.stdout.write(' [OK] Toda lib de scripts/ requerida por hooks distribuidos está registrada y cubierta\n');
414
+ } else {
415
+ for (const h of gateHooksRequires.hallazgos) {
416
+ process.stdout.write(' [FALLA] [' + h.tipo + '] ' + h.detalle + '\n');
417
+ }
418
+ process.stdout.write(
419
+ ' Bloqueo: registra la lib (y sus transitivas) en el módulo del hook en modulos.json. ' +
420
+ 'Sin esto el hook muere en silencio en el destino (wrapper traga MODULE_NOT_FOUND).\n'
421
+ );
422
+ }
423
+ } else if (gateHooksRequires.error) {
424
+ process.stdout.write('\n');
425
+ process.stdout.write('Gate de hooks-requires: no disponible — ' + gateHooksRequires.error + '\n');
426
+ }
381
427
  if (gateDescription.disponible) {
382
428
  process.stdout.write('\n');
383
429
  process.stdout.write('Gate de description (package.json vs plugin.json vs INVENTARIO.md):\n');
@@ -420,6 +466,29 @@ function main() {
420
466
  );
421
467
  }
422
468
  }
469
+ if (gateLicencias.disponible) {
470
+ process.stdout.write('\n');
471
+ process.stdout.write('Gate de licencias (árbol prod — WARN-ONLY, no bloquea):\n');
472
+ const r = gateLicencias.resumen;
473
+ process.stdout.write(
474
+ ' permisiva: ' + r.permisiva + ' | débil: ' + r.debil +
475
+ ' | fuerte: ' + r.fuerte + ' | desconocida: ' + r.desconocida + '\n'
476
+ );
477
+ const alertas = gateLicencias.hallazgos.filter((h) => h.clasificacion !== 'permisiva');
478
+ if (alertas.length > 0) {
479
+ for (const h of alertas) {
480
+ process.stdout.write(
481
+ ' [WARN ' + h.clasificacion + '] ' + h.paquete + '@' + h.version + ' — ' + h.licencia + '\n'
482
+ );
483
+ }
484
+ process.stdout.write(' Aviso: licencias no permisivas detectadas (informativo, no bloquea el release).\n');
485
+ } else {
486
+ process.stdout.write(' [OK] Todas las dependencias de producción son permisivas.\n');
487
+ }
488
+ } else if (gateLicencias.error) {
489
+ process.stdout.write('\n');
490
+ process.stdout.write('Gate de licencias: no disponible — ' + gateLicencias.error + '\n');
491
+ }
423
492
  if (fallasObligatorias > 0) {
424
493
  process.stdout.write('\n');
425
494
  process.stdout.write('Corrige los archivos marcados como FALLA antes de hacer push o publicar.\n');
@@ -583,6 +652,18 @@ function ejecutarGateBinImports() {
583
652
  }
584
653
  }
585
654
 
655
+ /**
656
+ * Gate de requires hooks→scripts (lib: scripts/lib/gate-hooks-requires.js).
657
+ * Bloqueante. Ver comentario en el flujo principal.
658
+ */
659
+ function ejecutarGateHooksRequires() {
660
+ try {
661
+ return evaluarHooksRequires(CWD);
662
+ } catch (err) {
663
+ return { disponible: false, error: err.message };
664
+ }
665
+ }
666
+
586
667
  function ejecutarGateAiisms() {
587
668
  const { spawnSync } = require('child_process');
588
669
  const detector = path.join(CWD, 'habilidades', 'estilo-sin-ai-isms', 'scripts', 'detectar_aiisms.py');
@@ -795,6 +876,26 @@ function ejecutarGateDescription(contadoresReales) {
795
876
  };
796
877
  }
797
878
 
879
+ /**
880
+ * Gate de licencias (Fase 14, ADR-0038) — WARN-ONLY.
881
+ *
882
+ * Clasifica las licencias del árbol de producción (vía scripts/lib/gate-licencias)
883
+ * y devuelve el resumen para impresión. NO bloquea el release: el caller NUNCA
884
+ * suma este resultado a fallasObligatorias (D-14-02, calibración primero).
885
+ *
886
+ * @param {string} [baseDir=CWD] raíz del proyecto a evaluar (parametrizable
887
+ * para tests; en producción usa el CWD del proceso).
888
+ * @returns {{disponible: boolean, hallazgos?: Array, resumen?: object, error?: string}}
889
+ */
890
+ function ejecutarGateLicencias(baseDir = CWD) {
891
+ try {
892
+ const { hallazgos, resumen } = evaluarLicencias(baseDir);
893
+ return { disponible: true, hallazgos, resumen };
894
+ } catch (err) {
895
+ return { disponible: false, error: err.message };
896
+ }
897
+ }
898
+
798
899
  if (require.main === module) main();
799
900
 
800
901
  module.exports = {
@@ -803,4 +904,5 @@ module.exports = {
803
904
  versionObjetivo,
804
905
  extraerCifrasDescription,
805
906
  ejecutarGateDescription,
907
+ ejecutarGateLicencias,
806
908
  };