@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.
- package/CLAUDE.md +196 -196
- package/README.md +579 -579
- package/agentes/_propose-step.md +90 -0
- package/agentes/accesibilidad-wcag-swl.md +3 -3
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/disenador-ui-swl.md +6 -5
- package/agentes/frontend-angular-swl.md +2 -2
- package/agentes/frontend-css-swl.md +2 -2
- package/agentes/frontend-react-swl.md +4 -4
- package/agentes/frontend-swl.md +6 -6
- package/agentes/implementador-swl.md +2 -0
- package/agentes/investigador-ux-swl.md +5 -5
- package/agentes/orquestador-swl.md +9 -7
- package/agentes/perfilador-usuario-swl.md +321 -308
- package/agentes/producto-prd-swl.md +1 -1
- package/agentes/red-team-swl.md +218 -218
- package/agentes/tdd-qa-swl.md +17 -1
- package/bin/swl-ses.js +1 -1
- package/comandos/swl/actualizar.md +1 -1
- package/comandos/swl/aprender.md +2 -2
- package/comandos/swl/aprobar-plan.md +153 -0
- package/comandos/swl/ayuda.md +3 -3
- package/comandos/swl/briefing.md +122 -0
- package/comandos/swl/compactar.md +29 -2
- package/comandos/swl/discutir-fase.md +23 -2
- package/comandos/swl/ejecutar-fase.md +59 -6
- package/comandos/swl/evolucionar.md +1 -1
- package/comandos/swl/inbox.md +1 -1
- package/comandos/swl/instalar.md +1 -1
- package/comandos/swl/nemesis.md +1 -1
- package/comandos/swl/planear-fase.md +19 -1
- package/comandos/swl/plugins.md +1 -1
- package/comandos/swl/release.md +47 -1
- package/comandos/swl/status.md +348 -0
- package/comandos/swl/verificar.md +27 -1
- package/habilidades/ai-runtime-security/SKILL.md +1 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/benchmark-memoria/SKILL.md +1 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -0
- package/habilidades/changelog-generator/SKILL.md +9 -2
- package/habilidades/changelog-generator/scripts/parse-commits.js +13 -1
- package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-fase/SKILL.md +541 -468
- package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
- package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
- package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
- package/habilidades/harness-claude-code/SKILL.md +10 -7
- package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
- package/habilidades/instalar-sistema/SKILL.md +3 -3
- package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/planear-fase/SKILL.md +26 -4
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
- package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
- package/habilidades/proceso-debate-adversarial/SKILL.md +2 -2
- package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
- package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
- package/habilidades/swl-claudemd/SKILL.md +50 -210
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
- package/habilidades/swl-dashboard/SKILL.md +9 -9
- package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
- package/habilidades/tdd-workflow/SKILL.md +715 -673
- package/habilidades/validacion-ci-sistema/SKILL.md +20 -4
- package/hooks/calidad-pre-commit.js +344 -3
- package/hooks/check-update.js +39 -1
- package/hooks/ciclo-evolucion-subagente.js +26 -0
- package/hooks/ciclo-evolucion.js +26 -0
- package/hooks/extraccion-aprendizajes.js +13 -0
- package/hooks/lib/autonomia.js +208 -0
- package/hooks/lib/briefing.js +474 -0
- package/hooks/lib/ciclo-evolucion.js +47 -0
- package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
- package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
- package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
- package/hooks/lib/evolution-tracker.js +24 -3
- package/hooks/lib/propose-step.js +357 -0
- package/hooks/session-briefing.js +98 -0
- package/hooks/spec-gate.js +211 -0
- package/hooks/tdd-gate.js +241 -0
- package/hooks/telemetria-skill-routing.js +100 -0
- package/hooks/validar-intent-spec.js +30 -10
- package/instintos/autonomia.yaml +27 -0
- package/llms.txt +6 -6
- package/manifiestos/hooks-config.json +44 -17
- package/manifiestos/modulos.json +40 -15
- package/manifiestos/skills-lock.json +64 -57
- package/package.json +93 -93
- package/plugin.json +371 -375
- package/reglas/accesibilidad.md +10 -0
- package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
- package/reglas/api-diseno.md +9 -0
- package/reglas/auditorias-documentales-estructurales.md +7 -0
- package/reglas/cloud-infra.md +8 -0
- package/reglas/consultar-vault-primero.md +195 -0
- package/reglas/debatir-antes-de-aceptar.md +158 -0
- package/reglas/fragmentos-compartidos.md +5 -0
- package/reglas/git-coauthor.md +100 -0
- package/reglas/gobernanza.md +4 -4
- package/reglas/hooks.md +6 -0
- package/reglas/intent-engineering.md +4 -0
- package/reglas/markitdown.md +8 -0
- package/reglas/memoria-consolidada.md +1 -1
- package/reglas/monitor-ci.md +309 -0
- package/reglas/patrones.md +6 -0
- package/reglas/registro-componentes-nuevos.md +39 -2
- package/reglas/seguridad-agentes.md +1 -1
- package/reglas/sesiones-paralelas.md +180 -0
- package/reglas/skills-estandar.md +6 -0
- package/reglas/testing.md +7 -0
- package/reglas/tests-cleanup.md +4 -0
- package/reglas/usar-code-review-graph.md +155 -0
- package/reglas/usar-sistema-swl.md +1 -1
- package/reglas/verificar-citas-normativas.md +548 -0
- package/scripts/instalador.js +52 -6
- package/scripts/lib/ci-reader.js +193 -0
- package/scripts/lib/detectar-host-swl.js +175 -0
- package/scripts/lib/evidencia-release.js +322 -0
- package/scripts/lib/gate-hooks-requires.js +249 -0
- package/scripts/lib/gate-licencias.js +212 -0
- package/scripts/lib/git-metricas.js +257 -0
- package/scripts/lib/gitignore-manifest.js +29 -1
- package/scripts/lib/metricas-dora.js +204 -0
- package/scripts/lib/plan-lock.js +275 -0
- package/scripts/migrar-fase-dominio.js +0 -1
- package/scripts/tui/ejecutores.js +1 -1
- package/scripts/validar-manifest.js +92 -1
- package/scripts/verificar-evolucion.js +54 -4
- package/scripts/verificar-release.js +102 -0
- package/scripts/verificar-trazabilidad.js +298 -0
- package/agentes/ux-disenador-swl.md +0 -503
- package/comandos/swl/dashboard.md +0 -146
- package/comandos/swl/evolucion-estado.md +0 -191
- package/comandos/swl/metricas.md +0 -376
- package/comandos/swl/salud.md +0 -481
- package/reglas/arquitectura.evolved.json +0 -7
- package/reglas/seguridad.evolved.json +0 -7
- package/reglas/verificar-citas-temporales.md +0 -139
|
@@ -64,6 +64,7 @@ const EXCLUDED_FILENAME_PATTERNS = [
|
|
|
64
64
|
/\.rej$/, // patch reject
|
|
65
65
|
/\.merge_file_/, // merge tools (kdiff3, etc.)
|
|
66
66
|
/~$/, // editores tipo Emacs/Vim
|
|
67
|
+
/\.evolved-diff\.(md|txt)$/, // diffs de merge (no son componentes; .md legacy)
|
|
67
68
|
];
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -407,7 +408,13 @@ const DIFF_NOISY_THRESHOLD = 50;
|
|
|
407
408
|
*
|
|
408
409
|
* Estrategia: toma el archivo nuevo como base y re-aplica los campos de
|
|
409
410
|
* evolución (frontmatter evolved-*). Las mutaciones de contenido se preservan
|
|
410
|
-
* generando un archivo
|
|
411
|
+
* generando un archivo `.evolved-diff.txt` que Claude puede re-aplicar.
|
|
412
|
+
*
|
|
413
|
+
* Extensión `.txt` (no `.md`) deliberada: el diff vive junto al componente
|
|
414
|
+
* evolucionado (incluyendo `commands/`), pero el harness de Claude Code indexa
|
|
415
|
+
* todo `.md` dentro de `commands/` como slash-command — un `aprender.evolved-diff.md`
|
|
416
|
+
* aparecería como `/swl:aprender.evolved-diff`. Con `.txt` el harness no lo indexa
|
|
417
|
+
* y `scanEvolved` (que solo recorre `.md`) tampoco lo confunde con un componente.
|
|
411
418
|
*
|
|
412
419
|
* Comparación: solo el body (post-frontmatter) se compara línea-a-línea.
|
|
413
420
|
* El frontmatter SIEMPRE diverge (el destino tiene campos `evolved-*` que el
|
|
@@ -415,7 +422,8 @@ const DIFF_NOISY_THRESHOLD = 50;
|
|
|
415
422
|
* contarlo como mutación genera ruido por desplazamiento.
|
|
416
423
|
*
|
|
417
424
|
* Limpieza: cuando un merge posterior elimina la divergencia (diffs vacíos),
|
|
418
|
-
* borra el `.evolved-diff.
|
|
425
|
+
* borra el `.evolved-diff.txt` huérfano de sesiones previas si existe (y el
|
|
426
|
+
* `.evolved-diff.md` legacy de versiones anteriores a esta corrección).
|
|
419
427
|
*
|
|
420
428
|
* Cap defensivo: si tras alinear correctamente el body aún hay más de
|
|
421
429
|
* `DIFF_NOISY_THRESHOLD` líneas distintas, genera un resumen estadístico
|
|
@@ -470,7 +478,17 @@ function mergeEvolved(destino, origen, versionNueva) {
|
|
|
470
478
|
}
|
|
471
479
|
}
|
|
472
480
|
|
|
473
|
-
const diffPath = destino.replace(/\.md$/, '.evolved-diff.
|
|
481
|
+
const diffPath = destino.replace(/\.md$/, '.evolved-diff.txt');
|
|
482
|
+
// Legacy: versiones previas escribían el diff como `.evolved-diff.md`, que
|
|
483
|
+
// el harness indexaba como slash-command. Se limpia siempre que se toca el
|
|
484
|
+
// componente, exista o no divergencia nueva.
|
|
485
|
+
const diffPathLegacy = destino.replace(/\.md$/, '.evolved-diff.md');
|
|
486
|
+
const limpiarLegacy = () => {
|
|
487
|
+
if (fs.existsSync(diffPathLegacy)) {
|
|
488
|
+
try { fs.unlinkSync(diffPathLegacy); return true; } catch { /* best-effort */ }
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
};
|
|
474
492
|
|
|
475
493
|
if (diffs.length === 0) {
|
|
476
494
|
// Sin diferencias reales — limpiar diff huérfano si existe (de sesión
|
|
@@ -486,6 +504,7 @@ function mergeEvolved(destino, origen, versionNueva) {
|
|
|
486
504
|
// el merge sigue siendo válido.
|
|
487
505
|
}
|
|
488
506
|
}
|
|
507
|
+
if (limpiarLegacy()) cleanedDiff = true;
|
|
489
508
|
|
|
490
509
|
// force: true — `mergeEvolved` solo se invoca en contexto de update
|
|
491
510
|
// intencional. El skip de isPackageRoot() aplica a la primera marca
|
|
@@ -557,6 +576,8 @@ function mergeEvolved(destino, origen, versionNueva) {
|
|
|
557
576
|
].join('\n');
|
|
558
577
|
|
|
559
578
|
atomicWriteSync(diffPath, diffContent, 'utf8');
|
|
579
|
+
// Si existía el `.evolved-diff.md` legacy, eliminarlo: el `.txt` lo reemplaza.
|
|
580
|
+
limpiarLegacy();
|
|
560
581
|
|
|
561
582
|
return { merged: true, diffPath, diffsCount: diffs.length, truncated };
|
|
562
583
|
} catch (err) {
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hooks/lib/propose-step.js — Fase 13 (ADR-0037): propose-step de adyacencias.
|
|
5
|
+
*
|
|
6
|
+
* Separa PROPONER de ACTUAR: al cerrar una tarea/fase, evalúa el diff contra una
|
|
7
|
+
* checklist mecanizable de adyacencias de riesgo y emite un anexo PROPOSITIVO.
|
|
8
|
+
* Nunca bloquea, nunca ejecuta. El anexo es texto; el usuario decide si actúa.
|
|
9
|
+
*
|
|
10
|
+
* Taxonomía v1 (D-13-01): 2 señales de alta precisión.
|
|
11
|
+
* - auth-pii-pagos: el cambio toca autenticación, PII o pagos.
|
|
12
|
+
* - migracion-schema: el cambio introduce o modifica el esquema de datos.
|
|
13
|
+
*
|
|
14
|
+
* Telemetría de aceptación en archivo separado .planning/user-profile/
|
|
15
|
+
* propose-telemetria.json, reusando la lógica pura de hooks/lib/briefing.js
|
|
16
|
+
* (D-13-07). Opt-out con SWL_PROPOSE=0.
|
|
17
|
+
*
|
|
18
|
+
* Zero-deps (Node stdlib). Require dual ./lib/X → ./X para funcionar tanto en el
|
|
19
|
+
* repo madre como en el destino aplanado por el instalador (patrón D-17 de F12).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { execFileSync } = require('child_process');
|
|
25
|
+
|
|
26
|
+
// ─── require dual (repo madre: ./lib/X ; destino aplanado: ./X) ──────────────
|
|
27
|
+
|
|
28
|
+
function requireDual(nombre) {
|
|
29
|
+
try {
|
|
30
|
+
return require(`./${nombre}`); // repo madre (mismo dir) o destino aplanado
|
|
31
|
+
} catch (e1) {
|
|
32
|
+
if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
|
|
33
|
+
return require(`./lib/${nombre}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let atomicWriteJSON;
|
|
38
|
+
try {
|
|
39
|
+
({ atomicWriteJSON } = requireDual('atomic-write'));
|
|
40
|
+
} catch (_) {
|
|
41
|
+
// Fallback no-atómico: funciona, pierde la garantía de write atómico.
|
|
42
|
+
atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let briefing;
|
|
46
|
+
try {
|
|
47
|
+
briefing = requireDual('briefing');
|
|
48
|
+
} catch (_) {
|
|
49
|
+
briefing = null; // sin telemetría compartida; la detección sigue funcionando
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── detectores de señales ───────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// Patrones de auth/PII/pagos. Acotados a alta precisión: word boundaries donde
|
|
55
|
+
// el término es ambiguo (rfc, card, cvv, pago) para no disparar con prosa.
|
|
56
|
+
const RE_AUTH_PII_DIFF = new RegExp(
|
|
57
|
+
[
|
|
58
|
+
'password', 'passwd', 'contraseña',
|
|
59
|
+
'\\btoken\\b', 'secret', 'api[_-]?key', '\\bjwt\\b', 'oauth',
|
|
60
|
+
'authorization', '\\bbearer\\b', '\\bcredential', '\\bsession\\b',
|
|
61
|
+
'stripe', 'payment', '\\bpago\\b', '\\bpagos\\b', 'tarjeta',
|
|
62
|
+
'\\bcvv\\b', '\\bcard\\b', '\\bcurp\\b', '\\brfc\\b',
|
|
63
|
+
].join('|'),
|
|
64
|
+
'i',
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const RE_AUTH_PII_PATH = new RegExp(
|
|
68
|
+
'(^|/)(auth|login|logout|oauth|jwt|session|credential|credentials|password|payment|pagos?|stripe|checkout)([/._-]|$)',
|
|
69
|
+
'i',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Patrones de migración / esquema de datos.
|
|
73
|
+
const RE_SCHEMA_PATH = new RegExp(
|
|
74
|
+
'(^|/)(migrations?|alembic|models?|schema|prisma)([/._-]|$)|\\.sql$|schema\\.prisma$',
|
|
75
|
+
'i',
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const RE_SCHEMA_DIFF = new RegExp(
|
|
79
|
+
[
|
|
80
|
+
'\\bALTER\\s+TABLE\\b', '\\bCREATE\\s+TABLE\\b', '\\bDROP\\s+TABLE\\b',
|
|
81
|
+
'\\bADD\\s+COLUMN\\b', '\\bDROP\\s+COLUMN\\b', '\\bRENAME\\s+(TABLE|COLUMN)\\b',
|
|
82
|
+
'op\\.(create_table|add_column|drop_column|alter_column)',
|
|
83
|
+
'createTable|addColumn|dropColumn', // ORMs JS (knex, sequelize)
|
|
84
|
+
].join('|'),
|
|
85
|
+
'i',
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
function _primerPathQueMatchea(paths, re) {
|
|
89
|
+
if (!Array.isArray(paths)) return null;
|
|
90
|
+
for (const p of paths) {
|
|
91
|
+
if (typeof p === 'string' && re.test(p)) return p;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Cota de tamaño del diff antes de aplicar regex: acota ReDoS y memoria. El
|
|
97
|
+
// detector solo necesita el primer match, así que 2 MiB son más que suficientes.
|
|
98
|
+
const MAX_DIFF_SCAN = 2 * 1024 * 1024;
|
|
99
|
+
|
|
100
|
+
// Devuelve SOLO si el diff matchea (boolean), nunca el fragmento matcheado: la
|
|
101
|
+
// evidencia del anexo NO debe contener slices del diff (podrían arrastrar el
|
|
102
|
+
// secreto adyacente al keyword). Hardening por construcción.
|
|
103
|
+
function _diffMatchea(diff, re) {
|
|
104
|
+
if (typeof diff !== 'string') return false;
|
|
105
|
+
return re.test(diff.length > MAX_DIFF_SCAN ? diff.slice(0, MAX_DIFF_SCAN) : diff);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Detecta si el cambio toca autenticación, PII o pagos.
|
|
110
|
+
* @param {string[]} paths - rutas de archivos del diff.
|
|
111
|
+
* @param {string} diff - contenido del diff.
|
|
112
|
+
* @returns {null|{categoria,titulo,evidencia,accion}}
|
|
113
|
+
*/
|
|
114
|
+
function detectarAuthPiiPagos(paths, diff) {
|
|
115
|
+
const porPath = _primerPathQueMatchea(paths, RE_AUTH_PII_PATH);
|
|
116
|
+
const porDiff = porPath ? false : _diffMatchea(diff, RE_AUTH_PII_DIFF);
|
|
117
|
+
if (!porPath && !porDiff) return null;
|
|
118
|
+
return {
|
|
119
|
+
categoria: 'auth-pii-pagos',
|
|
120
|
+
titulo: 'El cambio toca autenticación, PII o pagos',
|
|
121
|
+
evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
|
|
122
|
+
accion:
|
|
123
|
+
'Confirma revisión de seguridad (revisor-seguridad-swl) y tests de ' +
|
|
124
|
+
'autorización/validación antes de cerrar; no expongas secretos en logs.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detecta si el cambio introduce o modifica el esquema de datos.
|
|
130
|
+
* @param {string[]} paths
|
|
131
|
+
* @param {string} diff
|
|
132
|
+
* @returns {null|{categoria,titulo,evidencia,accion}}
|
|
133
|
+
*/
|
|
134
|
+
function detectarMigracionSchema(paths, diff) {
|
|
135
|
+
const porPath = _primerPathQueMatchea(paths, RE_SCHEMA_PATH);
|
|
136
|
+
const porDiff = porPath ? false : _diffMatchea(diff, RE_SCHEMA_DIFF);
|
|
137
|
+
if (!porPath && !porDiff) return null;
|
|
138
|
+
return {
|
|
139
|
+
categoria: 'migracion-schema',
|
|
140
|
+
titulo: 'El cambio introduce o modifica el esquema de datos',
|
|
141
|
+
evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
|
|
142
|
+
accion:
|
|
143
|
+
'Confirma plan de rollback / expand-contract y reversibilidad de la ' +
|
|
144
|
+
'migración (migrador-swl); verifica que no rompe datos existentes.',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const DETECTORES = [detectarAuthPiiPagos, detectarMigracionSchema];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Evalúa todas las señales sobre un diff. Función pura: solo datos, sin side
|
|
152
|
+
* effects (REQ-13-04).
|
|
153
|
+
* @param {string[]} paths
|
|
154
|
+
* @param {string} diff
|
|
155
|
+
* @returns {{señales: Array<{categoria,titulo,evidencia,accion}>}}
|
|
156
|
+
*/
|
|
157
|
+
function evaluarSenales(paths, diff) {
|
|
158
|
+
const señales = [];
|
|
159
|
+
for (const detectar of DETECTORES) {
|
|
160
|
+
const s = detectar(paths, diff);
|
|
161
|
+
if (s) señales.push(s);
|
|
162
|
+
}
|
|
163
|
+
return { señales };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── anexo propositivo ───────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Formatea el anexo propositivo. Devuelve null si no hay señales activas (silencio
|
|
170
|
+
* total, D-13-03) o si todas las categorías están silenciadas.
|
|
171
|
+
* @param {Array} señales
|
|
172
|
+
* @param {Set<string>} silenciadas - categorías que la telemetría silenció.
|
|
173
|
+
* @returns {string|null}
|
|
174
|
+
*/
|
|
175
|
+
function formatearAnexo(señales, silenciadas) {
|
|
176
|
+
const sil = silenciadas instanceof Set ? silenciadas : new Set();
|
|
177
|
+
const activas = Array.isArray(señales) ? señales.filter((s) => s && !sil.has(s.categoria)) : [];
|
|
178
|
+
if (activas.length === 0) return null;
|
|
179
|
+
const lineas = [
|
|
180
|
+
'## Anexo propositivo — adyacencias de riesgo',
|
|
181
|
+
'',
|
|
182
|
+
'Sugerencias, **no acciones**: nada se ejecuta ni se bloquea automáticamente. ' +
|
|
183
|
+
'Revisa si aplican.',
|
|
184
|
+
'',
|
|
185
|
+
];
|
|
186
|
+
for (const s of activas) {
|
|
187
|
+
lineas.push(`- [${s.categoria}] ${s.titulo} (${s.evidencia}) → ${s.accion}`);
|
|
188
|
+
}
|
|
189
|
+
return lineas.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── telemetría de aceptación (archivo separado, REQ-13-08/09) ───────────────
|
|
193
|
+
//
|
|
194
|
+
// Modelo a nivel de CATEGORÍA (no por hash). Las señales del propose tienen
|
|
195
|
+
// título fijo por categoría → el conteo por-hash de briefing.actualizarTelemetria
|
|
196
|
+
// nunca alcanzaría MIN_MUESTRAS_SILENCIO. Se reusan los UMBRALES de briefing.js
|
|
197
|
+
// (D-13-07) y categoriasSilenciadas, pero el conteo es por exposición:
|
|
198
|
+
// - registrarPropose: mostrado += 1 por categoría mostrada (automático).
|
|
199
|
+
// - registrarFeedback: actuado/ignorado += 1 (canal de aceptación explícito).
|
|
200
|
+
// - silenciada se recomputa con los mismos umbrales que el briefing.
|
|
201
|
+
|
|
202
|
+
const TELE_PATH = ['.planning', 'user-profile', 'propose-telemetria.json'];
|
|
203
|
+
|
|
204
|
+
// Umbrales: reusar los de briefing.js; fallback a los mismos valores si la lib
|
|
205
|
+
// no está disponible en el destino.
|
|
206
|
+
const UMBRAL = {
|
|
207
|
+
RATIO_SILENCIO: (briefing && briefing.RATIO_SILENCIO) || 0.8,
|
|
208
|
+
MIN_MUESTRAS_SILENCIO: (briefing && briefing.MIN_MUESTRAS_SILENCIO) || 5,
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
function telemetriaPath(baseDir) {
|
|
212
|
+
return path.join(baseDir || process.cwd(), ...TELE_PATH);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function _catVacia() {
|
|
216
|
+
return { mostrado: 0, actuado: 0, ignorado: 0, silenciada: false, ultima_ts: null };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Lee la telemetría de propose; fallback a estructura vacía. */
|
|
220
|
+
function leerTelemetriaPropose(baseDir) {
|
|
221
|
+
try {
|
|
222
|
+
const raw = fs.readFileSync(telemetriaPath(baseDir), 'utf8');
|
|
223
|
+
const obj = JSON.parse(raw);
|
|
224
|
+
return { categorias: obj.categorias && typeof obj.categorias === 'object' ? obj.categorias : {} };
|
|
225
|
+
} catch (_) {
|
|
226
|
+
return { categorias: {} };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Recalcula silenciada con los umbrales del briefing. Muta la categoría dada. */
|
|
231
|
+
function _recomputarSilenciada(c) {
|
|
232
|
+
c.silenciada =
|
|
233
|
+
c.mostrado >= UMBRAL.MIN_MUESTRAS_SILENCIO &&
|
|
234
|
+
c.ignorado / c.mostrado >= UMBRAL.RATIO_SILENCIO;
|
|
235
|
+
return c;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function _persistir(baseDir, tele) {
|
|
239
|
+
try {
|
|
240
|
+
const dir = path.dirname(telemetriaPath(baseDir));
|
|
241
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
242
|
+
atomicWriteJSON(telemetriaPath(baseDir), tele);
|
|
243
|
+
} catch (_) {
|
|
244
|
+
// persistir es best-effort; no romper el cierre de la tarea.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Registra la exposición de un anexo: mostrado += 1 por cada categoría presente.
|
|
250
|
+
* @param {string} baseDir
|
|
251
|
+
* @param {Array} señales
|
|
252
|
+
* @param {string} hoyISO - timestamp inyectable para tests deterministas.
|
|
253
|
+
* @returns {{categorias}}
|
|
254
|
+
*/
|
|
255
|
+
function registrarPropose(baseDir, señales, hoyISO) {
|
|
256
|
+
const tele = leerTelemetriaPropose(baseDir);
|
|
257
|
+
const hoy = hoyISO || new Date().toISOString();
|
|
258
|
+
for (const s of Array.isArray(señales) ? señales : []) {
|
|
259
|
+
if (!s || !s.categoria) continue;
|
|
260
|
+
const c = tele.categorias[s.categoria] || _catVacia();
|
|
261
|
+
c.mostrado += 1;
|
|
262
|
+
c.ultima_ts = hoy;
|
|
263
|
+
_recomputarSilenciada(c);
|
|
264
|
+
tele.categorias[s.categoria] = c;
|
|
265
|
+
}
|
|
266
|
+
_persistir(baseDir, tele);
|
|
267
|
+
return tele;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Registra feedback de aceptación de una categoría (canal explícito).
|
|
272
|
+
* @param {string} baseDir
|
|
273
|
+
* @param {string} categoria
|
|
274
|
+
* @param {'actuado'|'ignorado'} tipo
|
|
275
|
+
* @param {string} hoyISO
|
|
276
|
+
* @returns {{categorias}}
|
|
277
|
+
*/
|
|
278
|
+
function registrarFeedback(baseDir, categoria, tipo, hoyISO) {
|
|
279
|
+
if (tipo !== 'actuado' && tipo !== 'ignorado') {
|
|
280
|
+
throw new Error(`registrarFeedback: tipo inválido "${tipo}" (esperado actuado|ignorado)`);
|
|
281
|
+
}
|
|
282
|
+
const tele = leerTelemetriaPropose(baseDir);
|
|
283
|
+
const c = tele.categorias[categoria] || _catVacia();
|
|
284
|
+
c[tipo] += 1;
|
|
285
|
+
c.ultima_ts = hoyISO || new Date().toISOString();
|
|
286
|
+
_recomputarSilenciada(c);
|
|
287
|
+
tele.categorias[categoria] = c;
|
|
288
|
+
_persistir(baseDir, tele);
|
|
289
|
+
return tele;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Set de categorías silenciadas según la telemetría de propose. */
|
|
293
|
+
function categoriasSilenciadasPropose(baseDir) {
|
|
294
|
+
const tele = leerTelemetriaPropose(baseDir);
|
|
295
|
+
if (briefing && briefing.categoriasSilenciadas) {
|
|
296
|
+
return briefing.categoriasSilenciadas(tele);
|
|
297
|
+
}
|
|
298
|
+
const set = new Set();
|
|
299
|
+
for (const [cat, c] of Object.entries(tele.categorias || {})) {
|
|
300
|
+
if (c && c.silenciada) set.add(cat);
|
|
301
|
+
}
|
|
302
|
+
return set;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
const MAX_DIFF_BUFFER = 32 * 1024 * 1024; // 32 MiB — tope de salida de git diff
|
|
308
|
+
// Rango git válido: refs/SHAs y rangos `a..b` / `a...b`. Rechaza flags (`--output`,
|
|
309
|
+
// `-G`) que git interpretaría aunque execFileSync evita el shell.
|
|
310
|
+
const RE_RANGO_VALIDO = /^[A-Za-z0-9_./@~^-]+(\.\.\.?[A-Za-z0-9_./@~^-]+)?$/;
|
|
311
|
+
|
|
312
|
+
function _gitDiff(rango) {
|
|
313
|
+
// execFileSync con array de args: no pasa por shell. Además se valida el rango
|
|
314
|
+
// y se usa el separador `--` para forzar que git lo trate como ref, no flag.
|
|
315
|
+
const r = rango || 'HEAD~1..HEAD';
|
|
316
|
+
if (!RE_RANGO_VALIDO.test(r)) return { paths: [], diff: '' };
|
|
317
|
+
try {
|
|
318
|
+
const paths = execFileSync('git', ['diff', '--name-only', r, '--'], { encoding: 'utf8' })
|
|
319
|
+
.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
320
|
+
const diff = execFileSync('git', ['diff', r, '--'], { encoding: 'utf8', maxBuffer: MAX_DIFF_BUFFER });
|
|
321
|
+
return { paths, diff };
|
|
322
|
+
} catch (_) {
|
|
323
|
+
return { paths: [], diff: '' };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function main(argv) {
|
|
328
|
+
// Opt-out: SWL_PROPOSE=0 → silencio inmediato.
|
|
329
|
+
if (process.env.SWL_PROPOSE === '0') return 0;
|
|
330
|
+
const args = argv.slice(2);
|
|
331
|
+
let rango = 'HEAD~1..HEAD';
|
|
332
|
+
for (const a of args) {
|
|
333
|
+
if (a.startsWith('--rango=')) rango = a.slice('--rango='.length);
|
|
334
|
+
}
|
|
335
|
+
const baseDir = process.cwd();
|
|
336
|
+
const { paths, diff } = _gitDiff(rango);
|
|
337
|
+
const { señales } = evaluarSenales(paths, diff);
|
|
338
|
+
registrarPropose(baseDir, señales);
|
|
339
|
+
const anexo = formatearAnexo(señales, categoriasSilenciadasPropose(baseDir));
|
|
340
|
+
if (anexo) process.stdout.write(anexo + '\n');
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (require.main === module) {
|
|
345
|
+
process.exit(main(process.argv));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
detectarAuthPiiPagos,
|
|
350
|
+
detectarMigracionSchema,
|
|
351
|
+
evaluarSenales,
|
|
352
|
+
formatearAnexo,
|
|
353
|
+
leerTelemetriaPropose,
|
|
354
|
+
registrarPropose,
|
|
355
|
+
registrarFeedback,
|
|
356
|
+
categoriasSilenciadasPropose,
|
|
357
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook: session-briefing.js
|
|
6
|
+
* Tipo: SessionStart
|
|
7
|
+
*
|
|
8
|
+
* Briefing proactivo de inicio de sesión (Fase 12, ADR-0036). Al abrir sesión
|
|
9
|
+
* en un proyecto con `.planning/`, presenta un digest no solicitado de señales
|
|
10
|
+
* accionables que el usuario no sabía que tenía que preguntar: ADRs Propuestos
|
|
11
|
+
* con reevaluación vencida, deuda con trigger por fecha cumplido, nudges sin
|
|
12
|
+
* accionar, gates en calibración con ventana cumplida, y trabajo de retoma
|
|
13
|
+
* pendiente. Silencio total cuando no hay señales nuevas.
|
|
14
|
+
*
|
|
15
|
+
* Solo lecturas de filesystem ya computado: cero LLM, cero red, presupuesto
|
|
16
|
+
* <200ms (REQ-12-02). Las señales caras viven en `/swl:briefing`. SIEMPRE sale 0.
|
|
17
|
+
*
|
|
18
|
+
* Zero-deps. Opt-out: SWL_BRIEFING=0. Zero-config: sin `.planning/` → silencio.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// Require con fallback dual: el instalador aplana `hooks/lib/X.js → <hooks>/X.js`
|
|
25
|
+
// en el destino, pero en el repo madre vive en `lib/`. Cubre ambos layouts (D-17).
|
|
26
|
+
function requireDual(nombre) {
|
|
27
|
+
try {
|
|
28
|
+
return require(`./lib/${nombre}`);
|
|
29
|
+
} catch (e1) {
|
|
30
|
+
if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
|
|
31
|
+
return require(`./${nombre}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let briefing;
|
|
36
|
+
let atomicWriteJSON;
|
|
37
|
+
try {
|
|
38
|
+
briefing = requireDual('briefing');
|
|
39
|
+
} catch (_) {
|
|
40
|
+
briefing = null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
({ atomicWriteJSON } = requireDual('atomic-write'));
|
|
44
|
+
} catch (_) {
|
|
45
|
+
atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let inputRaw = '';
|
|
49
|
+
process.stdin.on('data', (c) => { inputRaw += c; });
|
|
50
|
+
|
|
51
|
+
process.stdin.on('end', () => {
|
|
52
|
+
try {
|
|
53
|
+
if (process.env.SWL_BRIEFING === '0') return; // opt-out
|
|
54
|
+
if (!briefing) return; // lib ausente: degradar a silencio
|
|
55
|
+
|
|
56
|
+
const cwd = process.cwd();
|
|
57
|
+
if (!fs.existsSync(path.join(cwd, '.planning'))) return; // zero-config
|
|
58
|
+
|
|
59
|
+
const hoy = new Date();
|
|
60
|
+
const items = briefing.recolectarTodo(cwd, hoy);
|
|
61
|
+
const estadoPrevio = briefing.leerEstadoBriefing(cwd);
|
|
62
|
+
const dia = hoy.toISOString().slice(0, 10);
|
|
63
|
+
|
|
64
|
+
// Telemetría de aceptación (D-18, REQ-12-05): se actualiza SIEMPRE que el
|
|
65
|
+
// hook corre, comparando lo visto antes con lo presente ahora — independiente
|
|
66
|
+
// del dedupe de display. Alimenta a perfilador-usuario-swl para callar
|
|
67
|
+
// categorías que el usuario ignora consistentemente.
|
|
68
|
+
let silenciadas = new Set();
|
|
69
|
+
try {
|
|
70
|
+
const telePrev = briefing.leerTelemetria(cwd);
|
|
71
|
+
const teleNueva = briefing.actualizarTelemetria(telePrev, items, hoy.toISOString());
|
|
72
|
+
silenciadas = briefing.categoriasSilenciadas(teleNueva);
|
|
73
|
+
const dir = path.join(cwd, '.planning', 'user-profile');
|
|
74
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
atomicWriteJSON(path.join(dir, 'briefing-telemetria.json'), teleNueva);
|
|
76
|
+
} catch (_) { /* telemetría best-effort */ }
|
|
77
|
+
|
|
78
|
+
const digest = briefing.armarDigest(items, estadoPrevio, dia, {
|
|
79
|
+
categoriasSilenciadas: silenciadas,
|
|
80
|
+
});
|
|
81
|
+
if (!digest) return; // silencio total: sin señales nuevas
|
|
82
|
+
|
|
83
|
+
// Persistir el estado de dedupe (best-effort).
|
|
84
|
+
try {
|
|
85
|
+
const dir = path.join(cwd, '.planning', 'user-profile');
|
|
86
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
atomicWriteJSON(path.join(dir, 'briefing-estado.json'), digest.estado);
|
|
88
|
+
} catch (_) { /* persistir es best-effort; no romper el digest */ }
|
|
89
|
+
|
|
90
|
+
const output = {
|
|
91
|
+
hookSpecificOutput: {
|
|
92
|
+
hookEventName: 'SessionStart',
|
|
93
|
+
additionalContext: digest.texto,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
process.stdout.write(JSON.stringify(output));
|
|
97
|
+
} catch (_) { /* silencioso: el hook nunca bloquea la sesión */ }
|
|
98
|
+
});
|