@saulwade/swl-ses 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +196 -196
- package/README.md +579 -579
- package/agentes/_propose-step.md +90 -0
- package/agentes/implementador-swl.md +2 -0
- package/agentes/orquestador-swl.md +2 -0
- package/agentes/perfilador-usuario-swl.md +14 -1
- package/bin/swl-ses.js +64 -1
- package/comandos/swl/adoptar-proyecto.md +258 -255
- package/comandos/swl/aprender.md +828 -840
- package/comandos/swl/aprobar-plan.md +26 -37
- package/comandos/swl/autoresearch.md +12 -14
- package/comandos/swl/briefing.md +119 -0
- package/comandos/swl/checkpoint.md +10 -15
- package/comandos/swl/claudemd.md +239 -234
- package/comandos/swl/compactar.md +29 -2
- package/comandos/swl/configurar-ci.md +20 -19
- package/comandos/swl/cron.md +10 -12
- package/comandos/swl/discutir-fase.md +8 -5
- package/comandos/swl/ejecutar-fase.md +15 -2
- package/comandos/swl/evolucionar.md +6 -11
- package/comandos/swl/inbox.md +10 -10
- package/comandos/swl/modelo.md +7 -9
- package/comandos/swl/notificaciones.md +19 -116
- package/comandos/swl/nuevo-proyecto.md +205 -205
- package/comandos/swl/planear-fase.md +5 -3
- package/comandos/swl/release.md +46 -0
- package/comandos/swl/status.md +333 -279
- package/comandos/swl/verificar.md +817 -812
- package/habilidades/changelog-generator/scripts/parse-commits.js +6 -4
- package/habilidades/ejecutar-fase/SKILL.md +541 -518
- package/habilidades/planear-fase/SKILL.md +3 -2
- package/habilidades/swl-claudemd/SKILL.md +10 -6
- package/habilidades/tdd-workflow/SKILL.md +715 -713
- package/habilidades/validacion-ci-sistema/SKILL.md +17 -1
- package/hooks/calidad-pre-commit.js +5 -1
- package/hooks/check-update.js +39 -1
- package/hooks/lib/autonomia.js +208 -0
- package/hooks/lib/briefing.js +474 -0
- package/hooks/lib/propose-step.js +358 -0
- package/hooks/session-briefing.js +98 -0
- package/hooks/telemetria-skill-routing.js +100 -0
- package/instintos/autonomia.yaml +27 -0
- package/llms.txt +4 -4
- package/manifiestos/hooks-config.json +18 -0
- package/manifiestos/modulos.json +25 -3
- package/manifiestos/skills-lock.json +17 -17
- package/package.json +93 -93
- package/plugin.json +371 -371
- package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
- package/reglas/consultar-vault-primero.md +195 -0
- package/reglas/debatir-antes-de-aceptar.md +158 -0
- package/reglas/git-coauthor.md +100 -0
- package/reglas/monitor-ci.md +309 -0
- package/reglas/registro-componentes-nuevos.md +38 -10
- package/reglas/sesiones-paralelas.md +180 -0
- package/reglas/usar-code-review-graph.md +155 -0
- package/reglas/verificar-citas-normativas.md +548 -0
- package/scripts/auditar-claudemd.js +38 -0
- package/scripts/cli/aprobar-plan.js +73 -0
- package/scripts/cli/briefing.js +23 -0
- package/scripts/cli/ciclo-evolucion.js +26 -0
- package/scripts/cli/configurar-ci.js +40 -0
- package/scripts/cli/derivar-feature-list.js +25 -0
- package/scripts/cli/detectar-host.js +27 -0
- package/scripts/cli/diary-entry.js +69 -0
- package/scripts/cli/execution-state.js +18 -0
- package/scripts/cli/gateway-notify.js +41 -0
- package/scripts/cli/liberar-fase.js +42 -0
- package/scripts/cli/loop-telemetry.js +125 -0
- package/scripts/cli/mark-evolved.js +56 -0
- package/scripts/cli/metricas-dora.js +26 -0
- package/scripts/cli/near-duplicate.js +55 -0
- package/scripts/cli/notificaciones.js +123 -0
- package/scripts/cli/propose-step.js +29 -0
- package/scripts/cli/schedule-parse.js +19 -0
- package/scripts/cli/sugerir-modelo.js +20 -0
- package/scripts/cli/verificar-plan.js +36 -0
- package/scripts/cli/verificar-trazabilidad.js +35 -0
- package/scripts/derivar-feature-list.js +1 -0
- package/scripts/instalador.js +52 -6
- package/scripts/lib/auditar-invocaciones-comandos.js +104 -0
- 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/metricas-dora.js +204 -0
- package/scripts/lib/resolver-plan-fase.js +37 -0
- package/scripts/tui/ejecutores.js +1 -1
- package/scripts/validar-manifest.js +92 -1
- package/scripts/validar.js +13 -0
- package/scripts/verificar-evolucion.js +54 -4
- package/scripts/verificar-release.js +102 -0
- package/scripts/verificar-trazabilidad.js +12 -6
- package/reglas/arquitectura.evolved.json +0 -7
- package/reglas/seguridad.evolved.json +0 -7
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper CLI `swl-ses notificaciones <op>` — gestiona las notificaciones
|
|
5
|
+
* Telegram y su bot daemon. Reemplaza los inline
|
|
6
|
+
* `require('./scripts/lib/notificaciones-telegram')` relativos al proyecto
|
|
7
|
+
* (rotos downstream; ver docs/invocacion-cli-cross-scope.md). Encapsula también
|
|
8
|
+
* el formato de salida que antes vivía en el comando.
|
|
9
|
+
*
|
|
10
|
+
* Ops:
|
|
11
|
+
* init (requiere TTY) bot-start
|
|
12
|
+
* status bot-stop
|
|
13
|
+
* disable [--purge] bot-status
|
|
14
|
+
* repair bot-restart
|
|
15
|
+
* bot-enable-autostart
|
|
16
|
+
* bot-disable-autostart
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const nt = require('../lib/notificaciones-telegram');
|
|
20
|
+
|
|
21
|
+
function imprimirStatus() {
|
|
22
|
+
const s = nt.status();
|
|
23
|
+
const botS = nt.botStatus();
|
|
24
|
+
const autostartLabel =
|
|
25
|
+
process.platform === 'win32'
|
|
26
|
+
? 'Scheduled Task'
|
|
27
|
+
: process.platform === 'linux'
|
|
28
|
+
? 'systemd --user'
|
|
29
|
+
: 'LaunchAgent';
|
|
30
|
+
const mudos = s.proyectosSilenciados.length
|
|
31
|
+
? s.proyectosSilenciados.join(', ')
|
|
32
|
+
: 'ninguno';
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log('Notificaciones Telegram — estado actual');
|
|
35
|
+
console.log(' .env: ', s.envExiste ? 'existe' : 'no existe');
|
|
36
|
+
console.log(' token: ', s.tokenConfigurado ? 'configurado' : 'no configurado');
|
|
37
|
+
console.log(
|
|
38
|
+
' chat_id: ',
|
|
39
|
+
s.chatIdConfigurado ? 'configurado (' + s.chatIdParcial + ')' : 'no configurado'
|
|
40
|
+
);
|
|
41
|
+
console.log(' proyectos mudos: ', mudos);
|
|
42
|
+
console.log(' bot daemon: ', botS.activo ? 'activo (PID ' + botS.pid + ')' : 'detenido');
|
|
43
|
+
console.log(' autostart: usar /swl:notificaciones bot enable-autostart (' + autostartLabel + ')');
|
|
44
|
+
console.log('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function imprimirBotStatus() {
|
|
48
|
+
const r = nt.botStatus();
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log('Bot daemon — estado actual');
|
|
51
|
+
console.log(' activo:', r.activo ? 'sí' : 'no');
|
|
52
|
+
console.log(' pid: ', r.pid !== null ? r.pid : 'n/a');
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function reportarOk(prefijo, r, mensajeDefault) {
|
|
57
|
+
if (r.ok) {
|
|
58
|
+
console.log('[' + prefijo + ']', r.mensaje || mensajeDefault);
|
|
59
|
+
} else {
|
|
60
|
+
console.error('[' + prefijo + '] Error:', r.error);
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Tabla de dispatch de los ops del bot daemon: [prefijo, fn, mensajeDefault].
|
|
66
|
+
const BOT_OPS = {
|
|
67
|
+
'bot-start': ['bot', () => nt.botStart(), 'Bot iniciado.'],
|
|
68
|
+
'bot-stop': ['bot', () => nt.botStop(), 'Bot detenido.'],
|
|
69
|
+
'bot-restart': ['bot', () => nt.botRestart(), 'Bot reiniciado.'],
|
|
70
|
+
'bot-enable-autostart': ['autostart', () => nt.botEnableAutostart(), 'Autostart habilitado.'],
|
|
71
|
+
'bot-disable-autostart': ['autostart', () => nt.botDisableAutostart(), 'Autostart deshabilitado.'],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
async function notificaciones(opciones = {}) {
|
|
75
|
+
const op = (opciones._args && opciones._args[0]) || opciones.op || 'status';
|
|
76
|
+
|
|
77
|
+
if (BOT_OPS[op]) {
|
|
78
|
+
const [prefijo, fn, msg] = BOT_OPS[op];
|
|
79
|
+
reportarOk(prefijo, fn(), msg);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (op) {
|
|
84
|
+
case 'init': {
|
|
85
|
+
if (!process.stdin.isTTY) {
|
|
86
|
+
console.log('Este subcomando requiere entrada interactiva (TTY).');
|
|
87
|
+
console.log('Ejecuta desde tu terminal:');
|
|
88
|
+
console.log(' swl-ses notificaciones init');
|
|
89
|
+
console.log('O usa el instalador:');
|
|
90
|
+
console.log(' npx -y @saulwade/swl-ses@latest install');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const r = await nt.init({ esTty: true });
|
|
94
|
+
console.log('[resultado]', r.resultado, r.detalle || '');
|
|
95
|
+
if (r.resultado === 'error') process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case 'status':
|
|
99
|
+
imprimirStatus();
|
|
100
|
+
return;
|
|
101
|
+
case 'disable': {
|
|
102
|
+
const r = nt.disable({ confirmar: true, conservarEnv: !opciones.purge });
|
|
103
|
+
console.log('[resultado]', r.resultado, r.detalle || '');
|
|
104
|
+
if (r.resultado === 'error') process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case 'repair': {
|
|
108
|
+
const r = await nt.repair();
|
|
109
|
+
console.log('[repair]', r.resultado, r.detalle || '');
|
|
110
|
+
if (r.resultado === 'error') process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case 'bot-status':
|
|
114
|
+
imprimirBotStatus();
|
|
115
|
+
return;
|
|
116
|
+
default:
|
|
117
|
+
console.error('Op desconocida: ' + op);
|
|
118
|
+
console.error('Ops: init|status|disable|repair|bot-start|bot-stop|bot-status|bot-restart|bot-enable-autostart|bot-disable-autostart');
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = notificaciones;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper CLI para `swl-ses propose-step` — anexo propositivo del RESUMEN de
|
|
5
|
+
* fase (ADR-0037). Envuelve hooks/lib/propose-step.js (que lee process.argv).
|
|
6
|
+
*
|
|
7
|
+
* Reemplaza el `node hooks/lib/propose-step.js --rango=...` relativo al proyecto
|
|
8
|
+
* que /swl:ejecutar-fase invocaba — roto downstream (ver
|
|
9
|
+
* docs/invocacion-cli-cross-scope.md).
|
|
10
|
+
*
|
|
11
|
+
* Uso: swl-ses propose-step --rango=<base>..HEAD (opt-out SWL_PROPOSE=0)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { main } = require('../../hooks/lib/propose-step.js');
|
|
15
|
+
|
|
16
|
+
function proposeStep(opciones = {}) {
|
|
17
|
+
opciones = opciones || {};
|
|
18
|
+
const args = ['node', 'propose-step'];
|
|
19
|
+
if (opciones.rango) args.push(`--rango=${opciones.rango}`);
|
|
20
|
+
const argvOriginal = process.argv;
|
|
21
|
+
process.argv = args;
|
|
22
|
+
try {
|
|
23
|
+
return main(process.argv);
|
|
24
|
+
} finally {
|
|
25
|
+
process.argv = argvOriginal;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = proposeStep;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper CLI `swl-ses schedule-parse` — parsea una frase en inglés a expresión
|
|
5
|
+
* cron. Reemplaza el inline `require('./scripts/lib/schedule-parser')` relativo
|
|
6
|
+
* al proyecto (roto downstream; ver docs/invocacion-cli-cross-scope.md).
|
|
7
|
+
*
|
|
8
|
+
* Uso: swl-ses schedule-parse "every morning at 9am"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { parseNaturalSchedule } = require('../lib/schedule-parser');
|
|
12
|
+
|
|
13
|
+
function scheduleParse(opciones = {}) {
|
|
14
|
+
const frase =
|
|
15
|
+
(opciones._args && opciones._args.join(' ')) || opciones.texto || '';
|
|
16
|
+
console.log(JSON.stringify(parseNaturalSchedule(frase), null, 2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = scheduleParse;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper CLI `swl-ses sugerir-modelo` — evalúa la complejidad de una tarea y
|
|
5
|
+
* sugiere el modelo (opus/sonnet/haiku). Reemplaza el inline
|
|
6
|
+
* `require('./hooks/lib/model-router.js')` relativo al proyecto (roto downstream;
|
|
7
|
+
* ver docs/invocacion-cli-cross-scope.md).
|
|
8
|
+
*
|
|
9
|
+
* Uso: swl-ses sugerir-modelo "<descripción de la tarea>"
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { sugerirModelo } = require('../../hooks/lib/model-router.js');
|
|
13
|
+
|
|
14
|
+
function sugerirModeloCli(opciones = {}) {
|
|
15
|
+
const desc =
|
|
16
|
+
(opciones._args && opciones._args.join(' ')) || opciones.desc || '';
|
|
17
|
+
console.log(JSON.stringify(sugerirModelo(desc), null, 2));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = sugerirModeloCli;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Subcomando `swl-ses verificar-plan --fase=N` — verificación read-only del lock
|
|
5
|
+
* de un PLAN (gate G1). Lo usa /swl:ejecutar-fase antes de implementar y
|
|
6
|
+
* /swl:aprobar-plan Paso 2 para detectar planes ya firmados/mutados/legacy.
|
|
7
|
+
*
|
|
8
|
+
* Reemplaza el `node -e "require('./scripts/lib/plan-lock')"` inline que se
|
|
9
|
+
* rompía downstream (ver docs/invocacion-cli-cross-scope.md).
|
|
10
|
+
*
|
|
11
|
+
* Exit 0 si ok (firmado o legacy); exit 1 si mutado/sin-firmar/corrupto.
|
|
12
|
+
*
|
|
13
|
+
* Uso: swl-ses verificar-plan --fase=N [--plan=<ruta>]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { verificarPlan } = require('../lib/plan-lock');
|
|
17
|
+
const { resolverPlanPath } = require('../lib/resolver-plan-fase');
|
|
18
|
+
|
|
19
|
+
function verificarPlanCli(opciones = {}) {
|
|
20
|
+
opciones = opciones || {};
|
|
21
|
+
const planPath = resolverPlanPath(opciones);
|
|
22
|
+
if (!planPath) {
|
|
23
|
+
console.error('Uso: swl-ses verificar-plan --fase=N [--plan=<ruta>]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const r = verificarPlan(planPath);
|
|
27
|
+
console.log(JSON.stringify(r, null, 2));
|
|
28
|
+
process.exit(r.ok ? 0 : 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = verificarPlanCli;
|
|
32
|
+
|
|
33
|
+
if (require.main === module) {
|
|
34
|
+
const { parsearOpciones } = require('../lib/parsear-opciones');
|
|
35
|
+
verificarPlanCli(parsearOpciones(process.argv.slice(2)));
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper CLI para el subcomando `swl-ses verificar-trazabilidad`.
|
|
5
|
+
*
|
|
6
|
+
* Traduce las opciones parseadas del bin a las flags que espera el script
|
|
7
|
+
* standalone `scripts/verificar-trazabilidad.js` (que lee process.argv y hace
|
|
8
|
+
* su propio process.exit). Mismo patrón que scripts/cli/audit-claudemd.js.
|
|
9
|
+
*
|
|
10
|
+
* Reemplaza el `node scripts/verificar-trazabilidad.js` relativo al proyecto que
|
|
11
|
+
* /swl:aprobar-plan invocaba — roto downstream (ver
|
|
12
|
+
* docs/invocacion-cli-cross-scope.md).
|
|
13
|
+
*
|
|
14
|
+
* Uso: swl-ses verificar-trazabilidad --fase=N [--solo-plan] [--json]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { main } = require('../verificar-trazabilidad.js');
|
|
18
|
+
|
|
19
|
+
function verificarTrazabilidad(opciones = {}) {
|
|
20
|
+
opciones = opciones || {};
|
|
21
|
+
const args = [];
|
|
22
|
+
if (opciones.fase != null && opciones.fase !== true) args.push(`--fase=${opciones.fase}`);
|
|
23
|
+
if (opciones['solo-plan'] || opciones.solo_plan) args.push('--solo-plan');
|
|
24
|
+
if (opciones.json) args.push('--json');
|
|
25
|
+
|
|
26
|
+
const argvOriginal = process.argv;
|
|
27
|
+
process.argv = ['node', 'verificar-trazabilidad.js', ...args];
|
|
28
|
+
try {
|
|
29
|
+
main();
|
|
30
|
+
} finally {
|
|
31
|
+
process.argv = argvOriginal;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = verificarTrazabilidad;
|
package/scripts/instalador.js
CHANGED
|
@@ -101,17 +101,24 @@ async function install(opciones) {
|
|
|
101
101
|
// 1. opciones.profile — flag explícito del usuario (también acepta --perfil)
|
|
102
102
|
// 2. manifest.profile — declarado en .swl-ses del proyecto
|
|
103
103
|
// 3. install-state existente en el destino — preserva el perfil ya instalado
|
|
104
|
-
// 4. '
|
|
104
|
+
// 4. 'completo' — default final
|
|
105
105
|
//
|
|
106
106
|
// Antes del fix v1.3.5, `install --force` sin --profile defaulteaba directamente
|
|
107
|
-
//
|
|
107
|
+
// al default aunque ya existiera una instalación con perfil distinto. El usuario
|
|
108
108
|
// perdía silenciosamente componentes y el update subsiguiente reproducía el
|
|
109
109
|
// estado contaminado (los archivos del completo quedaban con contenido viejo).
|
|
110
110
|
//
|
|
111
|
+
// El default final es 'completo' (cambiado desde 'core'): una instalación sin
|
|
112
|
+
// --profile entrega el sistema entero (todos los agentes, skills y comandos).
|
|
113
|
+
// Razón: un `install --target=... --with-mcp --global --force` sin --perfil
|
|
114
|
+
// parecía una instalación completa pero entregaba el perfil mínimo, dejando al
|
|
115
|
+
// usuario sin la mayoría de comandos /swl:* (caso reportado 2026-06-12). Para
|
|
116
|
+
// instalar solo el núcleo, pasar --perfil core explícito.
|
|
117
|
+
//
|
|
111
118
|
// La detección del install-state aquí es deliberadamente best-effort: si no
|
|
112
|
-
// existe o no se puede leer, caemos al default '
|
|
119
|
+
// existe o no se puede leer, caemos al default 'completo'. Solo aplica para
|
|
113
120
|
// `install --force` (reinstalación); en una primera instalación no hay state
|
|
114
|
-
// todavía y
|
|
121
|
+
// todavía y el sistema completo es el default razonable.
|
|
115
122
|
let perfilDeState = null;
|
|
116
123
|
if (!opciones.profile && !(manifest && manifest.profile)) {
|
|
117
124
|
try {
|
|
@@ -131,7 +138,7 @@ async function install(opciones) {
|
|
|
131
138
|
const perfil = opciones.profile
|
|
132
139
|
|| (manifest && manifest.profile)
|
|
133
140
|
|| perfilDeState
|
|
134
|
-
|| '
|
|
141
|
+
|| 'completo';
|
|
135
142
|
|
|
136
143
|
console.log(`swl-ses install v${VERSION}`);
|
|
137
144
|
console.log(`${'='.repeat(40)}`);
|
|
@@ -247,7 +254,7 @@ async function install(opciones) {
|
|
|
247
254
|
if (allLangs && !tieneReglasLenguaje && !esGlobal) {
|
|
248
255
|
console.log(
|
|
249
256
|
'\n[stack] Aviso: --all-langs ignorado — el perfil actual (' +
|
|
250
|
-
(resolucion.perfil || '
|
|
257
|
+
(resolucion.perfil || 'completo') +
|
|
251
258
|
') no incluye reglas de lenguajes. ' +
|
|
252
259
|
'Usa --perfil completo o --perfil polyglot para activar las reglas por lenguaje.'
|
|
253
260
|
);
|
|
@@ -644,6 +651,45 @@ async function install(opciones) {
|
|
|
644
651
|
}
|
|
645
652
|
}
|
|
646
653
|
|
|
654
|
+
// Copiar fragmentos compartidos (agentes/_*.md) — NO van en modulos.json
|
|
655
|
+
// (no son agentes routables) pero los agentes que los importan vía
|
|
656
|
+
// `fragmentos:` los leen al cargarse. El loader los espera en el dir de
|
|
657
|
+
// agentes del destino. Se copian raw (sin transformar: no tienen frontmatter).
|
|
658
|
+
// Cierra DT-FRAGMENTOS-DISTRIBUCION (Fase 13).
|
|
659
|
+
if (
|
|
660
|
+
runtime.tiposSoportados &&
|
|
661
|
+
runtime.tiposSoportados.includes('agentes') &&
|
|
662
|
+
rutas.agentes &&
|
|
663
|
+
resolucion.archivos.some(a => a.tipo === 'agentes')
|
|
664
|
+
) {
|
|
665
|
+
const fragmentosOrigen = path.join(RAIZ_PKG, 'agentes');
|
|
666
|
+
try {
|
|
667
|
+
const fragmentos = fs.existsSync(fragmentosOrigen)
|
|
668
|
+
? fs.readdirSync(fragmentosOrigen).filter(f => f.startsWith('_') && f.endsWith('.md'))
|
|
669
|
+
: [];
|
|
670
|
+
if (fragmentos.length > 0 && !fs.existsSync(rutas.agentes)) {
|
|
671
|
+
fs.mkdirSync(rutas.agentes, { recursive: true });
|
|
672
|
+
}
|
|
673
|
+
let fragmentosCopiados = 0;
|
|
674
|
+
for (const frag of fragmentos) {
|
|
675
|
+
const destino = path.join(rutas.agentes, frag);
|
|
676
|
+
fs.copyFileSync(path.join(fragmentosOrigen, frag), destino);
|
|
677
|
+
registrarArchivo(estado, {
|
|
678
|
+
origen: `agentes/${frag}`,
|
|
679
|
+
destino,
|
|
680
|
+
tipo: 'fragmentos', // distinto de 'agentes' → no infla el conteo del doctor
|
|
681
|
+
});
|
|
682
|
+
fragmentosCopiados++;
|
|
683
|
+
}
|
|
684
|
+
if (fragmentosCopiados > 0) {
|
|
685
|
+
console.log(` + Fragmentos compartidos: ${fragmentosCopiados} (agentes/_*.md)`);
|
|
686
|
+
}
|
|
687
|
+
} catch (err) {
|
|
688
|
+
console.error(` ! Error copiando fragmentos: ${err.message}`);
|
|
689
|
+
errores++;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
647
693
|
// Instalar userland (prioridad sobre core)
|
|
648
694
|
for (const archivo of archivosUserland) {
|
|
649
695
|
try {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* auditar-invocaciones-comandos.js
|
|
5
|
+
*
|
|
6
|
+
* Gate anti-regresión de la clase de bug "invocación relativa al proyecto en
|
|
7
|
+
* comandos user-facing" (ver docs/invocacion-cli-cross-scope.md).
|
|
8
|
+
*
|
|
9
|
+
* Un comando que corre en el proyecto del usuario NUNCA debe invocar scripts SWL
|
|
10
|
+
* con ruta relativa al proyecto (`node scripts/lib/...`, `node hooks/...`,
|
|
11
|
+
* `require('./scripts/...')`): esas rutas no existen downstream. Debe usar el
|
|
12
|
+
* subcomando del CLI (`swl-ses <sub>` / `npx ... <sub>` / `node scripts/cli/<sub>.js`).
|
|
13
|
+
*
|
|
14
|
+
* Solo escanea bloques de código (``` ... ```) para no marcar menciones
|
|
15
|
+
* prohibitivas en prosa. Excluye `scripts/cli/` (forma repo-madre sancionada).
|
|
16
|
+
*
|
|
17
|
+
* Función pura + entrypoint CLI con exit 1 si hay violaciones.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
// Comandos que corren SOLO en el repo madre de swl-ses (el CWD ES el repo, así
|
|
24
|
+
// que `node scripts/...` relativo es correcto). NO son user-facing.
|
|
25
|
+
const SELF_DEV = new Set([
|
|
26
|
+
'release',
|
|
27
|
+
'contribuir',
|
|
28
|
+
'evaluar-skill',
|
|
29
|
+
'reflect-skills',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Patrones de invocación relativa al proyecto que se rompen downstream.
|
|
33
|
+
// Se evalúan SOLO dentro de bloques de código.
|
|
34
|
+
const PATRONES = [
|
|
35
|
+
{ re: /\bnode\s+(?:\.\/)?scripts\/(?!cli\/)/, desc: 'node scripts/ (usa swl-ses <sub>)' },
|
|
36
|
+
{ re: /\bnode\s+(?:\.\/)?hooks\//, desc: 'node hooks/ (usa swl-ses <sub>)' },
|
|
37
|
+
{ re: /require\(\s*['"]\.\.?\/scripts\//, desc: "require('./scripts/ inline" },
|
|
38
|
+
{ re: /require\(\s*['"]\.\.?\/hooks\//, desc: "require('./hooks/ inline" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} [comandosDir] default: comandos/swl relativo al repo de este script.
|
|
43
|
+
* @returns {{ok:boolean, violaciones:Array<{comando:string,linea:number,texto:string,patron:string}>, escaneados:number}}
|
|
44
|
+
*/
|
|
45
|
+
function auditarInvocacionesComandos(comandosDir) {
|
|
46
|
+
const dir = comandosDir || path.join(__dirname, '..', '..', 'comandos', 'swl');
|
|
47
|
+
const violaciones = [];
|
|
48
|
+
let escaneados = 0;
|
|
49
|
+
|
|
50
|
+
let archivos;
|
|
51
|
+
try {
|
|
52
|
+
archivos = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
|
|
53
|
+
} catch {
|
|
54
|
+
return { ok: true, violaciones, escaneados: 0 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const archivo of archivos) {
|
|
58
|
+
const comando = archivo.replace(/\.md$/, '');
|
|
59
|
+
if (SELF_DEV.has(comando)) continue;
|
|
60
|
+
escaneados++;
|
|
61
|
+
|
|
62
|
+
const contenido = fs.readFileSync(path.join(dir, archivo), 'utf8');
|
|
63
|
+
const lineas = contenido.split('\n');
|
|
64
|
+
let dentroCodigo = false;
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
67
|
+
const linea = lineas[i];
|
|
68
|
+
if (/^\s*```/.test(linea)) {
|
|
69
|
+
dentroCodigo = !dentroCodigo;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!dentroCodigo) continue;
|
|
73
|
+
for (const { re, desc } of PATRONES) {
|
|
74
|
+
if (re.test(linea)) {
|
|
75
|
+
violaciones.push({
|
|
76
|
+
comando,
|
|
77
|
+
linea: i + 1,
|
|
78
|
+
texto: linea.trim(),
|
|
79
|
+
patron: desc,
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: violaciones.length === 0, violaciones, escaneados };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { auditarInvocacionesComandos, SELF_DEV };
|
|
91
|
+
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
const r = auditarInvocacionesComandos();
|
|
94
|
+
if (r.ok) {
|
|
95
|
+
console.log(`[OK] invocaciones-comandos — ${r.escaneados} comandos user-facing sin rutas relativas al proyecto`);
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
console.error(`[FALLA] invocaciones-comandos — ${r.violaciones.length} invocación(es) relativa(s) al proyecto en comandos user-facing:`);
|
|
99
|
+
for (const v of r.violaciones) {
|
|
100
|
+
console.error(` ${v.comando}.md:${v.linea} [${v.patron}] ${v.texto}`);
|
|
101
|
+
}
|
|
102
|
+
console.error('\nUsa el patrón cross-scope (docs/invocacion-cli-cross-scope.md): swl-ses <sub> / npx ... <sub> / node scripts/cli/<sub>.js');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ci-reader.js
|
|
5
|
+
*
|
|
6
|
+
* Métricas DORA dependientes de CI (GitHub Actions vía `gh run list`):
|
|
7
|
+
* - change failure rate: runs fallidos / runs completados en la ventana.
|
|
8
|
+
* - MTTR (mean/median time to restore): mediana del tiempo de un run fallido
|
|
9
|
+
* hasta el siguiente run exitoso.
|
|
10
|
+
*
|
|
11
|
+
* `gh` es OPCIONAL (D-15-01): si no está instalado, no autenticado, o el repo no
|
|
12
|
+
* tiene remoto GitHub, las funciones devuelven `{disponible:false, razon}` SIN
|
|
13
|
+
* lanzar. El executor `gh` es inyectable (`opts.ejecutorGh`) para tests sin gh
|
|
14
|
+
* real. Funciones puras sobre el array de runs. Entrypoint CLI `--json`.
|
|
15
|
+
* Parte de la Fase 15 (ADR-0039).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { execFileSync } = require('node:child_process');
|
|
19
|
+
|
|
20
|
+
const MS_DIA = 24 * 60 * 60 * 1000;
|
|
21
|
+
const MS_HORA = 60 * 60 * 1000;
|
|
22
|
+
const LIMITE_DEFAULT = 200;
|
|
23
|
+
|
|
24
|
+
const CONCLUSIONES_FALLO = new Set(['failure', 'timed_out', 'startup_failure']);
|
|
25
|
+
const CONCLUSIONES_EXITO = new Set(['success']);
|
|
26
|
+
|
|
27
|
+
const GH_MAX_BUFFER = 8 * 1024 * 1024; // 8 MiB
|
|
28
|
+
const GH_TIMEOUT_MS = 8000; // gh puede tardar por red; degrada si excede (H-04)
|
|
29
|
+
|
|
30
|
+
/** Executor real de gh. Lanza si gh no está disponible/autenticado. */
|
|
31
|
+
function ejecutorGhReal(args) {
|
|
32
|
+
return execFileSync('gh', args, {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
maxBuffer: GH_MAX_BUFFER,
|
|
36
|
+
timeout: GH_TIMEOUT_MS,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Categoriza el fallo de gh sin filtrar e.message crudo a logs/persistencia (H-03). */
|
|
41
|
+
function _razonGh(e) {
|
|
42
|
+
if (e && e.code === 'ENOENT') return 'gh no instalado';
|
|
43
|
+
if (e && e.killed && e.signal) return 'gh excedió el timeout';
|
|
44
|
+
return 'gh no disponible o sin acceso al repo';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Obtiene los runs de CI. Degrada sin lanzar.
|
|
49
|
+
* @returns {{disponible:boolean, runs?:Array, razon?:string}}
|
|
50
|
+
*/
|
|
51
|
+
function obtenerRuns(opts = {}) {
|
|
52
|
+
const { ejecutorGh = ejecutorGhReal, limite = LIMITE_DEFAULT, rama } = opts;
|
|
53
|
+
let salida;
|
|
54
|
+
try {
|
|
55
|
+
salida = ejecutorGh([
|
|
56
|
+
'run', 'list',
|
|
57
|
+
'--json', 'databaseId,conclusion,createdAt,updatedAt,headBranch,workflowName',
|
|
58
|
+
'--limit', String(limite),
|
|
59
|
+
]);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { disponible: false, razon: _razonGh(e) };
|
|
62
|
+
}
|
|
63
|
+
let runs;
|
|
64
|
+
try {
|
|
65
|
+
runs = JSON.parse(salida);
|
|
66
|
+
} catch {
|
|
67
|
+
return { disponible: false, razon: 'salida de gh no es JSON válido' };
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(runs)) return { disponible: false, razon: 'salida de gh inesperada' };
|
|
70
|
+
if (rama) runs = runs.filter((r) => r && r.headBranch === rama);
|
|
71
|
+
return { disponible: true, runs };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _runsEnVentana(runs, ventanaDias, hoy) {
|
|
75
|
+
const desde = new Date(hoy.getTime() - ventanaDias * MS_DIA).getTime();
|
|
76
|
+
const hasta = hoy.getTime();
|
|
77
|
+
return runs.filter((r) => {
|
|
78
|
+
const t = Date.parse(r && r.createdAt);
|
|
79
|
+
return Number.isFinite(t) && t >= desde && t <= hasta;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _mediana(valoresOrdenados) {
|
|
84
|
+
const n = valoresOrdenados.length;
|
|
85
|
+
if (n === 0) return null;
|
|
86
|
+
const mid = Math.floor(n / 2);
|
|
87
|
+
return n % 2 ? valoresOrdenados[mid] : (valoresOrdenados[mid - 1] + valoresOrdenados[mid]) / 2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Change failure rate en la ventana.
|
|
92
|
+
* @returns {{disponible:boolean, cfr?:number, totalRuns?:number, fallidos?:number,
|
|
93
|
+
* sinDatos?:boolean, razon?:string}}
|
|
94
|
+
*/
|
|
95
|
+
function changeFailureRate(opts = {}) {
|
|
96
|
+
const { ventanaDias = 30, hoy = new Date() } = opts;
|
|
97
|
+
const base = opts.runs ? { disponible: true, runs: opts.runs } : obtenerRuns(opts);
|
|
98
|
+
if (!base.disponible) return { disponible: false, razon: base.razon };
|
|
99
|
+
|
|
100
|
+
const enVentana = _runsEnVentana(base.runs, ventanaDias, hoy);
|
|
101
|
+
const completados = enVentana.filter(
|
|
102
|
+
(r) => CONCLUSIONES_EXITO.has(r.conclusion) || CONCLUSIONES_FALLO.has(r.conclusion)
|
|
103
|
+
);
|
|
104
|
+
if (completados.length === 0) {
|
|
105
|
+
return { disponible: true, sinDatos: true, totalRuns: 0, fallidos: 0, cfr: 0 };
|
|
106
|
+
}
|
|
107
|
+
const fallidos = completados.filter((r) => CONCLUSIONES_FALLO.has(r.conclusion)).length;
|
|
108
|
+
return {
|
|
109
|
+
disponible: true,
|
|
110
|
+
sinDatos: false,
|
|
111
|
+
totalRuns: completados.length,
|
|
112
|
+
fallidos,
|
|
113
|
+
cfr: fallidos / completados.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* MTTR: mediana del tiempo de un run fallido hasta el siguiente run exitoso.
|
|
119
|
+
* @returns {{disponible:boolean, mttrHoras?:number, recuperaciones?:number,
|
|
120
|
+
* sinDatos?:boolean, razon?:string}}
|
|
121
|
+
*/
|
|
122
|
+
function meanTimeToRestore(opts = {}) {
|
|
123
|
+
const { ventanaDias = 30, hoy = new Date() } = opts;
|
|
124
|
+
const base = opts.runs ? { disponible: true, runs: opts.runs } : obtenerRuns(opts);
|
|
125
|
+
if (!base.disponible) return { disponible: false, razon: base.razon };
|
|
126
|
+
|
|
127
|
+
const enVentana = _runsEnVentana(base.runs, ventanaDias, hoy)
|
|
128
|
+
.filter((r) => CONCLUSIONES_EXITO.has(r.conclusion) || CONCLUSIONES_FALLO.has(r.conclusion))
|
|
129
|
+
.map((r) => ({
|
|
130
|
+
fallo: CONCLUSIONES_FALLO.has(r.conclusion),
|
|
131
|
+
detectado: Date.parse(r.updatedAt || r.createdAt),
|
|
132
|
+
}))
|
|
133
|
+
.filter((r) => Number.isFinite(r.detectado))
|
|
134
|
+
.sort((a, b) => a.detectado - b.detectado);
|
|
135
|
+
|
|
136
|
+
const recuperaciones = [];
|
|
137
|
+
for (let i = 0; i < enVentana.length; i++) {
|
|
138
|
+
if (!enVentana[i].fallo) continue;
|
|
139
|
+
const exito = enVentana.slice(i + 1).find((r) => !r.fallo);
|
|
140
|
+
if (exito) {
|
|
141
|
+
const h = (exito.detectado - enVentana[i].detectado) / MS_HORA;
|
|
142
|
+
if (h >= 0) recuperaciones.push(h);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
recuperaciones.sort((a, b) => a - b);
|
|
146
|
+
if (recuperaciones.length === 0) {
|
|
147
|
+
return { disponible: true, sinDatos: true, recuperaciones: 0, mttrHoras: null };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
disponible: true,
|
|
151
|
+
sinDatos: false,
|
|
152
|
+
recuperaciones: recuperaciones.length,
|
|
153
|
+
mttrHoras: _mediana(recuperaciones),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
obtenerRuns,
|
|
159
|
+
changeFailureRate,
|
|
160
|
+
meanTimeToRestore,
|
|
161
|
+
ejecutorGhReal,
|
|
162
|
+
CONCLUSIONES_FALLO,
|
|
163
|
+
CONCLUSIONES_EXITO,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Entrypoint CLI: node scripts/lib/ci-reader.js [--json] [--dias=N]
|
|
167
|
+
if (!require.main || require.main === module) {
|
|
168
|
+
const args = process.argv.slice(2);
|
|
169
|
+
const json = args.includes('--json');
|
|
170
|
+
const diasArg = args.find((a) => a.startsWith('--dias='));
|
|
171
|
+
const ventanaDias = Math.max(1, Math.min(365, (diasArg ? parseInt(diasArg.slice('--dias='.length), 10) : 30) || 30));
|
|
172
|
+
|
|
173
|
+
// Una sola llamada a gh; si no está disponible, no se re-intenta.
|
|
174
|
+
const base = obtenerRuns({ limite: LIMITE_DEFAULT });
|
|
175
|
+
let out;
|
|
176
|
+
if (!base.disponible) {
|
|
177
|
+
out = { disponible: false, razon: base.razon };
|
|
178
|
+
} else {
|
|
179
|
+
const cfr = changeFailureRate({ ventanaDias, runs: base.runs });
|
|
180
|
+
const mttr = meanTimeToRestore({ ventanaDias, runs: base.runs });
|
|
181
|
+
out = { ventana_dias: ventanaDias, changeFailureRate: cfr, mttr };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (json) {
|
|
185
|
+
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
|
|
186
|
+
} else if (!base.disponible) {
|
|
187
|
+
process.stdout.write(`CI no disponible: ${base.razon}\n`);
|
|
188
|
+
} else {
|
|
189
|
+
process.stdout.write(`Change failure rate: ${out.changeFailureRate.sinDatos ? 'sin datos' : (out.changeFailureRate.cfr * 100).toFixed(1) + '%'}\n`);
|
|
190
|
+
process.stdout.write(`MTTR: ${out.mttr.sinDatos ? 'sin datos' : out.mttr.mttrHoras.toFixed(1) + 'h'}\n`);
|
|
191
|
+
}
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|