@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
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: validacion-ci-sistema
3
3
  description: Validación de integridad del sistema SWL inspirado en ECC CI stack. Cubre validación de frontmatter de agentes, validación de skills (SKILL.md presente y no vacío), validación de hooks (sintaxis Node.js, exit codes), validación de comandos, validación de reglas, catálogo automático con métricas y script ejecutable de validación.
4
- version: "1.0.0"
4
+ version: "1.1.0"
5
5
  herramientasPermitidas: [Read, Bash]
6
6
  exclusiones:
7
7
  - "No cargar para validar el código de aplicación del usuario (tests, linting de Python/TS, cobertura) — este skill valida la integridad del sistema SWL (frontmatter de agentes, SKILL.md presentes, exit codes de hooks); para validación de código cargar `reglas/pruebas.md` o invocar `tdd-qa-swl`."
8
8
  - "No cargar para diagnosticar por qué un agente específico falló en una tarea — este skill detecta problemas estructurales del sistema SWL, no bugs en la lógica de un agente; para diagnóstico de agente usar `evaluacion-agentes`."
9
- - "No cargar si `/swl:salud` acaba de ejecutarse sin errores — la validación CI es un subconjunto de lo que salud corre; no tiene sentido duplicarla en la misma sesión."
9
+ - "No cargar si `/swl:status salud` acaba de ejecutarse sin errores — la validación CI es un subconjunto de lo que salud corre; no tiene sentido duplicarla en la misma sesión."
10
10
  - "No cargar para agregar un componente nuevo al sistema — primero crear el componente (agente, skill, hook) y luego ejecutar validación CI para verificarlo; no como guía de creación."
11
11
  evolvable: true # default para skill estandar
12
12
  ---
@@ -20,6 +20,22 @@ El sistema SWL crece con el tiempo. Sin validación, acumula:
20
20
  - Hooks que fallan con códigos de salida incorrectos
21
21
  - Comandos con formato inválido
22
22
 
23
+ ## Precondición — solo aplica al repo host de SWL
24
+
25
+ Este skill valida los **componentes** del sistema SWL (`agentes/`, `habilidades/`,
26
+ `comandos/swl/`, `reglas/`, `hooks/`). Solo tiene sentido en el repo que los
27
+ **hospeda** (swl-ses o un fork). En un proyecto **consumidor** (usa las
28
+ convenciones SWL vía `~/.claude/` pero no aloja los componentes), no hay nada que
29
+ validar y un score de componentes sería engañoso. El gate determinista vive en
30
+ `/swl:status salud` (Paso 0):
31
+
32
+ ```bash
33
+ node scripts/lib/detectar-host-swl.js --json
34
+ ```
35
+
36
+ Si `esHost: false`, NO ejecutar este skill: reportar que `salud` no aplica y
37
+ redirigir a los subcomandos de `/swl:status` que sí aplican al proyecto consumidor.
38
+
23
39
  ---
24
40
 
25
41
  ## Reglas de Validación por Componente
@@ -124,7 +140,7 @@ Para código fuente completo de todos los validadores (agentes, skills, hooks, c
124
140
  ## Cuándo NO cargar
125
141
 
126
142
  - Se valida código de aplicación del usuario (tests Python/TS, cobertura, linting) — este skill valida la integridad del sistema SWL; para código de aplicación cargar `reglas/pruebas.md` o invocar `tdd-qa-swl`.
127
- - `/swl:salud` acaba de ejecutarse sin errores en esta sesión — la validación CI es un subconjunto de lo que salud ejecuta; duplicarla no aporta información nueva.
143
+ - `/swl:status salud` acaba de ejecutarse sin errores en esta sesión — la validación CI es un subconjunto de lo que salud ejecuta; duplicarla no aporta información nueva.
128
144
  - El objetivo es diagnosticar por qué un agente específico falló en su tarea de dominio — este skill detecta problemas estructurales (frontmatter faltante, exit code incorrecto), no bugs en la lógica de un agente; para eso usar `evaluacion-agentes`.
129
145
  - Se está creando un componente nuevo y se necesita orientación sobre la estructura — este skill verifica componentes existentes, no guía la creación; usar `estructura-proyecto-claude` para crear con la estructura correcta desde el inicio.
130
146
 
@@ -132,5 +148,5 @@ Para código fuente completo de todos los validadores (agentes, skills, hooks, c
132
148
 
133
149
  - **Hook de observabilidad que usa `process.exit(1)` detectado como error crítico**: el validador marca el hook como inválido pero el hook fue escrito intencionalmente para bloquear en ciertos escenarios. Causa: el validador distingue entre hooks de observación (deben usar `exit(0)` siempre) y hooks de bloqueo (pueden usar `exit(1)` para rechazar acciones); si el hook no está categorizado correctamente, el validador aplica la regla incorrecta. Solución: verificar si el hook es bloqueante o de observación antes de corregir — un guardrail de seguridad que usa `exit(1)` es correcto; un hook de telemetría que usa `exit(1)` es un bug.
134
150
  - **Agente con `name` diferente al nombre de archivo pasa validación en local pero falla en CI**: el validador local compara el campo `name` del frontmatter con el nombre del directorio, pero CI usa la ruta del archivo. Causa: un agente renombrado actualiza el directorio pero no el campo `name` en el frontmatter. Solución: el checklist dice "name debe coincidir con nombre del archivo (kebab-case)" — al renombrar un agente o skill, actualizar el campo `name` del frontmatter en el mismo commit.
135
- - **`npm run validate` pasa sin advertencias pero `/swl:salud` reporta inconsistencias**: el script de validación CI cubre un subconjunto de las verificaciones de salud. Causa: las validaciones de grafo de dependencias, métricas de evolución y consistencia de versiones solo corren en `/swl:salud`, no en el validador CI básico. Solución: antes de un release o merge importante, ejecutar `/swl:salud` completo, no solo la validación CI — la validación CI es el mínimo para commits diarios.
151
+ - **`npm run validate` pasa sin advertencias pero `/swl:status salud` reporta inconsistencias**: el script de validación CI cubre un subconjunto de las verificaciones de salud. Causa: las validaciones de grafo de dependencias, métricas de evolución y consistencia de versiones solo corren en `/swl:status salud`, no en el validador CI básico. Solución: antes de un release o merge importante, ejecutar `/swl:status salud` completo, no solo la validación CI — la validación CI es el mínimo para commits diarios.
136
152
  - **Skills con `description: >` YAML multiline registrados como sin frontmatter**: el validador usa una regex basada solo en LF (`/^---\n[\s\S]*?\n---/`) que no parsea correctamente archivos con terminaciones CRLF. Causa: archivos creados en Windows con CRLF en el frontmatter. Solución: si el validador reporta `sin-frontmatter` en un skill que claramente tiene frontmatter, verificar las terminaciones de línea con `file skill.md` — convertir CRLF→LF con `git config core.autocrlf input` o `sed -i 's/\r//'`.
@@ -44,7 +44,7 @@
44
44
 
45
45
  const fs = require('fs');
46
46
  const path = require('path');
47
- const { execSync } = require('child_process');
47
+ const { execSync, execFileSync } = require('child_process');
48
48
 
49
49
  const { normalizeForDetection } = require('./lib/normalize-input');
50
50
 
@@ -151,7 +151,9 @@ function obtenerArchivosStagedGit() {
151
151
  */
152
152
  function obtenerContenidoStaged(rutaRelativa) {
153
153
  try {
154
- const contenido = execSync(`git show :${rutaRelativa}`, {
154
+ // execFileSync (sin shell): el nombre de archivo se pasa como argumento
155
+ // directo, no se interpreta por el shell (defensa S-recom verificación F09).
156
+ const contenido = execFileSync('git', ['show', `:${rutaRelativa}`], {
155
157
  encoding: 'buffer',
156
158
  stdio: ['pipe', 'pipe', 'pipe'],
157
159
  });
@@ -172,7 +174,7 @@ function obtenerContenidoStaged(rutaRelativa) {
172
174
  */
173
175
  function obtenerTamanoStaged(rutaRelativa) {
174
176
  try {
175
- const salida = execSync(`git cat-file -s :${rutaRelativa}`, {
177
+ const salida = execFileSync('git', ['cat-file', '-s', `:${rutaRelativa}`], {
176
178
  encoding: 'utf8',
177
179
  stdio: ['pipe', 'pipe', 'pipe'],
178
180
  });
@@ -824,12 +826,194 @@ function verificarRiskScore() {
824
826
  }
825
827
  }
826
828
 
829
+ // ---------------------------------------------------------------------------
830
+ // Gate G3 — tests-presence (warn-only). Fase 09, Slice 2.
831
+ // ---------------------------------------------------------------------------
832
+
833
+ /**
834
+ * ¿La ruta es un archivo de test? Consolida los patrones de test antes
835
+ * dispersos en este hook (verificarConsoleLog, verificarPrintPython) más
836
+ * convenciones multi-lenguaje. git entrega rutas relativas con separador `/`.
837
+ *
838
+ * @param {string} ruta - ruta relativa al repo
839
+ * @returns {boolean}
840
+ */
841
+ function esArchivoTest(ruta) {
842
+ return (
843
+ /\.(test|spec)\.[jt]sx?$/.test(ruta) || // foo.test.ts, foo.spec.jsx
844
+ /(^|[/\\])__tests__[/\\]/.test(ruta) || // __tests__/
845
+ /(^|[/\\])tests?[/\\]/.test(ruta) || // test/ o tests/
846
+ /(^|[/\\])spec[/\\]/.test(ruta) || // spec/
847
+ /(^|[/\\])test_[^/\\]*\.py$/.test(ruta) || // test_foo.py
848
+ /test.*\.py$/.test(ruta) || // pytest naming laxo
849
+ /_test\.(py|go)$/.test(ruta) || // foo_test.go, foo_test.py
850
+ /Tests?\.(java|kt|cs)$/.test(ruta) || // FooTest.java, FooTests.cs
851
+ /_spec\.rb$/.test(ruta) || // foo_spec.rb
852
+ /Test\.php$/.test(ruta) // FooTest.php
853
+ );
854
+ }
855
+
856
+ /**
857
+ * ¿La ruta es un archivo generado/derivado/lock que NO debe exigir tests?
858
+ *
859
+ * @param {string} ruta - ruta relativa al repo
860
+ * @returns {boolean}
861
+ */
862
+ function esArchivoGenerado(ruta) {
863
+ return (
864
+ /(^|[/\\])(INVENTARIO|SALUD)\.md$/.test(ruta) ||
865
+ /(^|[/\\])llms\.txt$/.test(ruta) ||
866
+ /\.lock$/.test(ruta) ||
867
+ /(^|[/\\])(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|Cargo\.lock|go\.sum)$/.test(ruta) ||
868
+ /(^|[/\\])(dist|build|coverage|node_modules|__pycache__|\.next|target)[/\\]/.test(ruta) ||
869
+ /\.min\.(js|css)$/.test(ruta) ||
870
+ /(^|[/\\])\.planning[/\\]/.test(ruta)
871
+ );
872
+ }
873
+
874
+ /**
875
+ * ¿La ruta es código fuente (lenguaje soportado), excluyendo test/generado/
876
+ * docs/config? Lo que dispara el gate cuando se toca sin tests.
877
+ *
878
+ * @param {string} ruta - ruta relativa al repo
879
+ * @returns {boolean}
880
+ */
881
+ function esArchivoFuente(ruta) {
882
+ if (esArchivoTest(ruta) || esArchivoGenerado(ruta)) return false;
883
+ // Excluir docs/config/datos por extensión.
884
+ if (/\.(md|json|ya?ml|toml|ini|cfg|conf|txt|svg|png|jpe?g|gif|ico|csv|html?|xml|env)$/i.test(ruta)) {
885
+ return false;
886
+ }
887
+ return /\.(jsx?|tsx?|py|go|rs|java|kt|cs|rb|php|swift|c|cc|cpp|h|hpp|vue|svelte)$/i.test(ruta);
888
+ }
889
+
890
+ /**
891
+ * Extrae el mensaje del flag -m de un comando `git commit`. Soporta comillas
892
+ * dobles, simples o sin comillas (primera palabra). Devuelve '' si no hay -m.
893
+ *
894
+ * @param {string} comando
895
+ * @returns {string}
896
+ */
897
+ function extraerMensajeCommit(comando) {
898
+ // Soporta: -m "x" | -m 'x' | -m x | -m"x" | -m=x | --message="x" | --message x.
899
+ // Multi -m: captura el primero (el subject, donde vive el prefijo Conventional).
900
+ // -F/--file: no hay mensaje en la línea → retorna '' → NO se exenta → advierte
901
+ // (conservador y correcto para un gate warn-only). Verificación Fase 09, S-3.
902
+ const m = comando.match(/(?:-m|--message)\s*=?\s*(?:"([^"]*)"|'([^']*)'|(\S+))/);
903
+ if (!m) return '';
904
+ return m[1] || m[2] || m[3] || '';
905
+ }
906
+
907
+ /**
908
+ * ¿El prefijo Conventional Commit exime del gate de tests? (docs/chore/style)
909
+ *
910
+ * @param {string} mensaje
911
+ * @returns {boolean}
912
+ */
913
+ function esPrefijoExentoDeTests(mensaje) {
914
+ // Tolera prefijo(s) de trazabilidad de `ejecutar-fase` antes del tipo CC:
915
+ // `[T-NN]`, `[REQ-NN]`, `[F<fase>·T-NN]` (uno o más, consecutivos). Desde la
916
+ // Fase 12 los IDs se namespacean por fase (DT-IDS-NAMESPACE). El prefijo no
917
+ // cambia la semántica: un `[F12·T-03] docs(...)` sigue siendo exento de tests.
918
+ return /^\s*(?:\[(?:F\d+·)?(?:T|REQ)-\d+(?:-\d+)?\]\s*)*(docs|chore|style)(\([^)]*\))?!?:/i.test(mensaje);
919
+ }
920
+
921
+ /**
922
+ * Gate G3: detecta un diff staged que toca código fuente SIN tocar tests.
923
+ *
924
+ * @param {string[]} archivosStaged - rutas relativas staged
925
+ * @param {string} comandoCommit - el comando bash `git commit ...`
926
+ * @returns {{ fuentes: string[] } | null} datos del warning, o null si no aplica
927
+ */
928
+ function verificarTestPresence(archivosStaged, comandoCommit) {
929
+ const mensaje = extraerMensajeCommit(comandoCommit);
930
+ if (esPrefijoExentoDeTests(mensaje)) return null; // docs:/chore:/style: exentos
931
+
932
+ const fuentes = archivosStaged.filter(esArchivoFuente);
933
+ if (fuentes.length === 0) return null; // no toca código fuente
934
+
935
+ const tieneTests = archivosStaged.some(esArchivoTest);
936
+ if (tieneTests) return null; // hay al menos un test
937
+
938
+ return { fuentes };
939
+ }
940
+
941
+ /**
942
+ * Ejecutor real de cobertura (default de coverageDiferencial en el hook).
943
+ * Corre el runner detectado con timeout y parsea su reporte JSON al mapa
944
+ * { archivoRelativo: pct }. Best-effort: lanza si el runner falla o el reporte
945
+ * no es parseable, y coverageDiferencial captura el throw devolviendo null.
946
+ * @param {string[]} fuentes
947
+ * @param {{ runner: string, comando: string[], reporte: string }} runner
948
+ * @returns {Object<string, number>}
949
+ */
950
+ function ejecutarCoverage(fuentes, runner) {
951
+ const { execFileSync } = require('child_process');
952
+ const path = require('path');
953
+ const cwd = process.cwd();
954
+ // Correr el runner con coverage (timeout duro — un commit no debe colgarse).
955
+ execFileSync(runner.comando[0], runner.comando.slice(1), {
956
+ cwd,
957
+ stdio: 'ignore',
958
+ timeout: Number(process.env.SWL_COVERAGE_DIFF_TIMEOUT_MS) || 120000,
959
+ });
960
+ const reporteAbs = path.join(cwd, runner.reporte);
961
+ const raw = fs.readFileSync(reporteAbs, 'utf8');
962
+
963
+ // pytest-cov coverage.json: { files: { "src/a.py": { summary: { percent_covered } } } }
964
+ if (runner.runner === 'pytest') {
965
+ const data = JSON.parse(raw);
966
+ const out = {};
967
+ for (const [f, info] of Object.entries(data.files || {})) {
968
+ const pct = info?.summary?.percent_covered;
969
+ if (typeof pct === 'number') out[f.replace(/\\/g, '/')] = pct;
970
+ }
971
+ return out;
972
+ }
973
+ // istanbul coverage-final.json (jest/vitest): { "/abs/path": { s: {...}, statementMap } }
974
+ if (runner.runner === 'vitest' || runner.runner === 'jest') {
975
+ const data = JSON.parse(raw);
976
+ const out = {};
977
+ for (const [abs, info] of Object.entries(data)) {
978
+ const stmts = info?.s ? Object.values(info.s) : [];
979
+ if (!stmts.length) continue;
980
+ const cubiertos = stmts.filter((n) => n > 0).length;
981
+ const rel = path.relative(cwd, abs).replace(/\\/g, '/');
982
+ out[rel] = (cubiertos / stmts.length) * 100;
983
+ }
984
+ return out;
985
+ }
986
+ // go coverprofile: líneas "file.go:from.col,to.col stmts count"
987
+ if (runner.runner === 'go') {
988
+ const porArchivo = {};
989
+ for (const linea of raw.split('\n')) {
990
+ const m = linea.match(/^(.+\.go):\d+\.\d+,\d+\.\d+\s+(\d+)\s+(\d+)$/);
991
+ if (!m) continue;
992
+ const [, f, stmts, count] = m;
993
+ porArchivo[f] = porArchivo[f] || { total: 0, cub: 0 };
994
+ porArchivo[f].total += Number(stmts);
995
+ if (Number(count) > 0) porArchivo[f].cub += Number(stmts);
996
+ }
997
+ const out = {};
998
+ for (const [f, v] of Object.entries(porArchivo)) {
999
+ out[f.replace(/\\/g, '/')] = v.total ? (v.cub / v.total) * 100 : 0;
1000
+ }
1001
+ return out;
1002
+ }
1003
+ return {};
1004
+ }
1005
+
827
1006
  // ---------------------------------------------------------------------------
828
1007
  // Entrypoint principal
829
1008
  // ---------------------------------------------------------------------------
830
1009
 
831
1010
  let inputRaw = '';
832
1011
 
1012
+ // Solo enganchar stdin cuando el hook se ejecuta directamente (no al hacer
1013
+ // `require()` desde un test). Permite testear las funciones puras sin disparar
1014
+ // el flujo de stdin.
1015
+ if (require.main === module) {
1016
+
833
1017
  process.stdin.on('data', chunk => {
834
1018
  inputRaw += chunk;
835
1019
  });
@@ -861,6 +1045,76 @@ process.stdin.on('end', () => {
861
1045
  return;
862
1046
  }
863
1047
 
1048
+ // --- Gate G3: tests-presence (warn-only) ---
1049
+ // Detecta diffs que tocan código fuente sin tocar tests y emite un nudge.
1050
+ // NO bloquea (no exit 2): esta iteración es warn-only para calibrar falsos
1051
+ // positivos (renames, refactors puros). Exenciones: docs:/chore:/style: y
1052
+ // archivos generados/lock.
1053
+ //
1054
+ // PROMOCIÓN A BLOCKING: tras ~2 semanas de calibración sin falsos positivos
1055
+ // recurrentes, documentar la promoción en un ADR (patrón ADR-0027) y
1056
+ // convertir este bloque en `process.exit(2)` con la razón en stdout/stderr.
1057
+ // Hasta entonces: solo nudge.
1058
+ try {
1059
+ const g3 = verificarTestPresence(archivos, comando);
1060
+ if (g3) {
1061
+ const tracker = require('./lib/nudge-tracker');
1062
+ const muestra = g3.fuentes.slice(0, 3).join(', ');
1063
+ const sufijo = g3.fuentes.length > 3 ? ` (+${g3.fuentes.length - 3} más)` : '';
1064
+ tracker.emit({
1065
+ kind: 'tests-presence',
1066
+ target: g3.fuentes[0],
1067
+ source: 'hooks/calidad-pre-commit.js',
1068
+ message:
1069
+ `Gate G3 (warn): el commit toca ${g3.fuentes.length} archivo(s) de ` +
1070
+ `código fuente sin tocar ningún test: ${muestra}${sufijo}. ` +
1071
+ `Considera agregar o actualizar tests. Exenta el commit con prefijo ` +
1072
+ `docs:/chore:/style: si no aplica.`,
1073
+ data: { fuentes: g3.fuentes, gate: 'G3' },
1074
+ // 'optimize' = mejora de cobertura (no corrige un bug observado).
1075
+ // El criterio del plan mencionaba "warn", pero el enum válido del
1076
+ // tracker es repair|optimize|innovate; 'warn' se descartaría.
1077
+ mutation_category: 'optimize',
1078
+ risk_level: 'low',
1079
+ });
1080
+ }
1081
+ } catch (_) {
1082
+ // El gate G3 nunca rompe el commit (warn-only, best-effort).
1083
+ }
1084
+
1085
+ // --- Gate G2-cov: coverage diferencial (opt-in SWL_COVERAGE_DIFF=1) ---
1086
+ // Default OFF → cero latencia. Solo cuando el usuario lo activa: detecta el
1087
+ // runner, mide cobertura de los módulos tocados y emite nudge warn-only si
1088
+ // alguno cae bajo el umbral. Nunca bloquea (D-08).
1089
+ if (process.env.SWL_COVERAGE_DIFF === '1') {
1090
+ try {
1091
+ const fuentes = archivos.filter(esArchivoFuente);
1092
+ const runner = fuentes.length ? detectarRunnerCoverage(process.cwd()) : null;
1093
+ if (runner) {
1094
+ const umbral = Number(process.env.SWL_COVERAGE_DIFF_UMBRAL) || 80;
1095
+ const res = coverageDiferencial(fuentes, runner, { ejecutor: ejecutarCoverage, umbral });
1096
+ if (res && res.bajos.length) {
1097
+ const tracker = require('./lib/nudge-tracker');
1098
+ const muestra = res.bajos.slice(0, 3).join(', ');
1099
+ tracker.emit({
1100
+ kind: 'coverage-diff',
1101
+ target: res.bajos[0],
1102
+ source: 'hooks/calidad-pre-commit.js',
1103
+ message:
1104
+ `Coverage diferencial (warn): ${res.bajos.length} módulo(s) tocado(s) ` +
1105
+ `bajo ${umbral}% de cobertura: ${muestra}. Runner: ${runner.runner}. ` +
1106
+ `Agrega tests antes de mergear. Desactivar: SWL_COVERAGE_DIFF=0.`,
1107
+ data: { bajos: res.bajos, runner: runner.runner, umbral, gate: 'G2-cov' },
1108
+ mutation_category: 'optimize',
1109
+ risk_level: 'low',
1110
+ });
1111
+ }
1112
+ }
1113
+ } catch (_) {
1114
+ // Coverage diferencial es best-effort opt-in: nunca rompe el commit.
1115
+ }
1116
+ }
1117
+
864
1118
  // Verificar cada archivo
865
1119
  const todasLasViolaciones = [];
866
1120
 
@@ -927,3 +1181,90 @@ process.stdin.on('end', () => {
927
1181
  // El hook nunca bloquea por errores internos propios
928
1182
  }
929
1183
  });
1184
+
1185
+ } // fin if (require.main === module)
1186
+
1187
+ // ─── Coverage diferencial opt-in (gate G2-cov, Fase 11) ──────────────────────
1188
+ //
1189
+ // Opt-in estricto: solo corre con SWL_COVERAGE_DIFF=1 (default off → cero
1190
+ // latencia). Mide la cobertura de los módulos tocados en el diff staged y emite
1191
+ // nudge warn-only `coverage-diff` si alguno cae bajo el umbral (default 80%).
1192
+ // Decisión D-08 del 11-CONTEXTO.md.
1193
+
1194
+ /**
1195
+ * Detecta el runner de cobertura del proyecto por marcadores de archivo.
1196
+ * Detección barata (sin ejecutar nada) y segura ante ausencia.
1197
+ * @param {string} cwd
1198
+ * @returns {{ runner: string, comando: string[], reporte: string }|null}
1199
+ */
1200
+ function detectarRunnerCoverage(cwd) {
1201
+ const existe = (rel) => {
1202
+ try { return fs.existsSync(require('path').join(cwd, rel)); } catch { return false; }
1203
+ };
1204
+ const path = require('path');
1205
+
1206
+ // Node: leer package.json para distinguir vitest/jest.
1207
+ let pkg = null;
1208
+ try {
1209
+ pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
1210
+ } catch { /* sin package.json */ }
1211
+ const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
1212
+
1213
+ if (deps.vitest || existe('vitest.config.ts') || existe('vitest.config.js')) {
1214
+ return { runner: 'vitest', comando: ['npx', 'vitest', 'run', '--coverage'], reporte: 'coverage/coverage-final.json' };
1215
+ }
1216
+ if (deps.jest || existe('jest.config.js') || existe('jest.config.ts')) {
1217
+ return { runner: 'jest', comando: ['npx', 'jest', '--coverage', '--coverageReporters=json'], reporte: 'coverage/coverage-final.json' };
1218
+ }
1219
+ // Python: pytest-cov.
1220
+ if (existe('pyproject.toml') || existe('pytest.ini') || existe('setup.cfg') || existe('tox.ini')) {
1221
+ return { runner: 'pytest', comando: ['pytest', '--cov', '--cov-report=json'], reporte: 'coverage.json' };
1222
+ }
1223
+ // Go: cobertura nativa.
1224
+ if (existe('go.mod')) {
1225
+ return { runner: 'go', comando: ['go', 'test', '-coverprofile=coverage.out', './...'], reporte: 'coverage.out' };
1226
+ }
1227
+ return null;
1228
+ }
1229
+
1230
+ /**
1231
+ * Mide la cobertura de los módulos tocados y devuelve los que están bajo umbral.
1232
+ * El cómputo real de cobertura se delega a `opts.ejecutor` (inyectable para
1233
+ * tests; por defecto no implementado para evitar correr runners pesados en el
1234
+ * hook sin un parser robusto por formato). Best-effort: si el ejecutor falla,
1235
+ * devuelve null y el hook no emite nada.
1236
+ *
1237
+ * @param {string[]} fuentes Archivos de código fuente del diff staged.
1238
+ * @param {object} runner Descriptor de detectarRunnerCoverage.
1239
+ * @param {object} [opts]
1240
+ * @param {function} [opts.ejecutor] (fuentes, runner) => { [archivo]: pct }.
1241
+ * @param {number} [opts.umbral=80]
1242
+ * @returns {{ bajos: string[], cobertura: object }|null}
1243
+ */
1244
+ function coverageDiferencial(fuentes, runner, opts = {}) {
1245
+ const { ejecutor, umbral = 80 } = opts;
1246
+ if (typeof ejecutor !== 'function') return null;
1247
+ let cobertura;
1248
+ try {
1249
+ cobertura = ejecutor(fuentes, runner);
1250
+ } catch (_) {
1251
+ return null; // runner no instalado / reporte ilegible — best-effort.
1252
+ }
1253
+ if (!cobertura || typeof cobertura !== 'object') return null;
1254
+ const bajos = fuentes.filter(
1255
+ (f) => typeof cobertura[f] === 'number' && cobertura[f] < umbral,
1256
+ );
1257
+ return { bajos, cobertura };
1258
+ }
1259
+
1260
+ // Exportar funciones puras del gate G3 + coverage diferencial para testing.
1261
+ module.exports = {
1262
+ esArchivoTest,
1263
+ esArchivoGenerado,
1264
+ esArchivoFuente,
1265
+ extraerMensajeCommit,
1266
+ esPrefijoExentoDeTests,
1267
+ verificarTestPresence,
1268
+ detectarRunnerCoverage,
1269
+ coverageDiferencial,
1270
+ };
@@ -22,7 +22,22 @@ const fs = require('fs');
22
22
  const path = require('path');
23
23
  const os = require('os');
24
24
 
25
- const { compararSemver, versionRemotaParalela } = require('../scripts/lib/npm-version');
25
+ // La lib npm-version se distribuye junto con el hook (modulos.json hooks-core).
26
+ // Require tolerante: repo madre / destino copy (../scripts/lib), destino
27
+ // aplanado (./lib), o null. Antes de v2.2 este require era directo y la lib NO
28
+ // se distribuía: en TODA instalación destino el hook moría con MODULE_NOT_FOUND
29
+ // que el wrapper de settings.json traga en silencio — el aviso de nuevas
30
+ // versiones nunca llegó a otros equipos (reportado 2026-06-12). Sin la lib, el
31
+ // hook emite un diagnóstico throttled en vez de callar (anti-fallback-silencioso).
32
+ let compararSemver = null;
33
+ let versionRemotaParalela = null;
34
+ try {
35
+ ({ compararSemver, versionRemotaParalela } = require('../scripts/lib/npm-version'));
36
+ } catch (_) {
37
+ try {
38
+ ({ compararSemver, versionRemotaParalela } = require('./lib/npm-version'));
39
+ } catch (_2) { /* null — diagnosticado en el entrypoint */ }
40
+ }
26
41
 
27
42
  // ---------------------------------------------------------------------------
28
43
  // Constantes
@@ -176,6 +191,29 @@ process.stdin.on('data', chunk => { inputRaw += chunk; });
176
191
 
177
192
  process.stdin.on('end', async () => {
178
193
  try {
194
+ // Lib no disponible (instalación incompleta o versión previa al fix de
195
+ // distribución): avisar una vez por ventana de throttle en vez de morir
196
+ // en silencio. `remota` con sentinel truthy → ventana de 24h, no de 1h.
197
+ if (!versionRemotaParalela || !compararSemver) {
198
+ if (debeVerificar()) {
199
+ try {
200
+ fs.writeFileSync(flagPath(), JSON.stringify({
201
+ timestamp: Date.now(),
202
+ local: null,
203
+ remota: 'lib-no-disponible',
204
+ hayNueva: false,
205
+ notificado: 2,
206
+ }), 'utf8');
207
+ } catch { /* silencioso */ }
208
+ process.stderr.write(
209
+ '[swl-ses/check-update] lib npm-version no disponible — el aviso de ' +
210
+ 'nuevas versiones está inactivo. Reinstala con: ' +
211
+ 'npx -y @saulwade/swl-ses@latest update\n'
212
+ );
213
+ }
214
+ return;
215
+ }
216
+
179
217
  // Throttle: solo verificar cada 24h
180
218
  if (!debeVerificar()) {
181
219
  // Si el último check detectó actualización, repetir notificación (1 vez)
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: ciclo-evolucion-subagente.js
6
+ * Tipo: SubagentStop (async: true — fire-and-forget)
7
+ *
8
+ * Entry point del ciclo de evolución en el evento SubagentStop. Delega a la
9
+ * sub-etapa de auto-evolución (antes `auto-evolucion.js`), que analiza el
10
+ * payload del subagente, registra en el log y emite nudges de evolución
11
+ * (fallos/volumen/loop/drift). Fase 11, D-12.
12
+ *
13
+ * Nunca bloquea (siempre exit 0). Zero-deps fuera de hooks/lib/.
14
+ */
15
+
16
+ const cicloEvolucion = require('./lib/ciclo-evolucion');
17
+
18
+ let inputRaw = '';
19
+ process.stdin.on('data', chunk => { inputRaw += chunk; });
20
+ process.stdin.on('end', () => {
21
+ try {
22
+ cicloEvolucion.ejecutarSubagentStop(inputRaw);
23
+ } catch {
24
+ // El hook nunca bloquea por error interno.
25
+ }
26
+ });
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Hook: ciclo-evolucion.js
6
+ * Tipo: Stop (async: true — fire-and-forget)
7
+ *
8
+ * Entry point del ciclo de evolución en el evento Stop. Ejecuta en UN solo
9
+ * proceso las sub-etapas que antes eran dos hooks separados
10
+ * (`metricas-evolucion.js` + `actualizar-perfil-usuario.js`), reduciendo de
11
+ * dos arranques de Node por sesión a uno (Fase 11, D-12).
12
+ *
13
+ * Nunca bloquea (siempre exit 0). Zero-deps fuera de hooks/lib/.
14
+ */
15
+
16
+ const cicloEvolucion = require('./lib/ciclo-evolucion');
17
+
18
+ let inputRaw = '';
19
+ process.stdin.on('data', chunk => { inputRaw += chunk; });
20
+ process.stdin.on('end', () => {
21
+ try {
22
+ cicloEvolucion.ejecutarStop(inputRaw);
23
+ } catch {
24
+ // El hook nunca bloquea por error interno.
25
+ }
26
+ });
@@ -663,6 +663,19 @@ const PATRONES_ARCHIVO_SWL_EXCLUIDO = [
663
663
  /[\\/]package\.json$/,
664
664
  /[\\/]plugin\.json$/,
665
665
  /[\\/]package-lock\.json$/,
666
+ // Scripts temporales del agente (convención .tmp-*.js|.md en la raíz del
667
+ // repo, borrados en el mismo turno). Sus comentarios de cabecera describen
668
+ // la operación con keywords narrativas ("sustitución", "idempotente",
669
+ // "reescribe") que el hook confunde con descubrimientos. Detectado
670
+ // 2026-06-11: el comentario de .tmp-fusion-status.js se promovió como
671
+ // entrada "descubrimiento" a APRENDIZAJES.md.
672
+ /(?:^|[\\/])\.tmp-[^\\/]+$/,
673
+ // Reglas globales instaladas del usuario (~/.claude/rules/): editar esas
674
+ // copias es mantenimiento de configuración, no descubrimiento. El patrón
675
+ // /reglas[\\/]/ de arriba no las cubre porque el directorio se llama
676
+ // "rules" (inglés). Detectado 2026-06-11: fragmentos de una edición a
677
+ // verificar-citas-normativas.md (global) se promovieron como "patrón".
678
+ /(?:^|[\\/])\.claude[\\/]rules[\\/]/,
666
679
  ];
667
680
 
668
681
  /**