@saulwade/swl-ses 2.3.1 → 2.4.1

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 (112) hide show
  1. package/CLAUDE.md +241 -240
  2. package/README.md +597 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/accesibilidad-wcag-swl.md +690 -690
  6. package/agentes/arquitecto-swl.md +267 -267
  7. package/agentes/auto-evolucion-swl.md +908 -908
  8. package/agentes/backend-api-swl.md +472 -472
  9. package/agentes/backend-csharp-swl.md +420 -420
  10. package/agentes/backend-go-swl.md +390 -390
  11. package/agentes/backend-java-swl.md +281 -281
  12. package/agentes/backend-node-swl.md +479 -479
  13. package/agentes/backend-python-swl.md +605 -605
  14. package/agentes/backend-rust-swl.md +364 -364
  15. package/agentes/backend-workers-swl.md +482 -482
  16. package/agentes/cloud-infra-swl.md +509 -509
  17. package/agentes/consolidador-swl.md +541 -541
  18. package/agentes/datos-swl.md +605 -605
  19. package/agentes/depurador-swl.md +352 -352
  20. package/agentes/devops-ci-swl.md +400 -400
  21. package/agentes/disenador-ui-swl.md +569 -569
  22. package/agentes/documentador-swl.md +345 -345
  23. package/agentes/frontend-angular-swl.md +621 -621
  24. package/agentes/frontend-css-swl.md +716 -716
  25. package/agentes/frontend-react-swl.md +692 -692
  26. package/agentes/frontend-swl.md +496 -496
  27. package/agentes/frontend-tailwind-swl.md +826 -826
  28. package/agentes/gh-fix-ci-swl.md +275 -275
  29. package/agentes/implementador-swl.md +328 -328
  30. package/agentes/investigador-swl.md +432 -432
  31. package/agentes/investigador-ux-swl.md +505 -505
  32. package/agentes/llm-apps-swl.md +278 -278
  33. package/agentes/migrador-swl.md +442 -442
  34. package/agentes/mobile-android-swl.md +511 -511
  35. package/agentes/mobile-cross-swl.md +541 -541
  36. package/agentes/mobile-ios-swl.md +502 -502
  37. package/agentes/mobile-testing-swl.md +302 -302
  38. package/agentes/nemesis-auditor-swl.md +285 -285
  39. package/agentes/notificador-swl.md +918 -918
  40. package/agentes/observabilidad-swl.md +438 -438
  41. package/agentes/orquestador-swl.md +1053 -1026
  42. package/agentes/pagos-swl.md +310 -310
  43. package/agentes/perfilador-usuario-swl.md +321 -321
  44. package/agentes/planificador-swl.md +399 -399
  45. package/agentes/producto-prd-swl.md +589 -589
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +590 -590
  48. package/agentes/rendimiento-swl.md +713 -713
  49. package/agentes/resolutor-build-swl.md +245 -245
  50. package/agentes/revisor-angular-swl.md +278 -278
  51. package/agentes/revisor-codigo-swl.md +538 -505
  52. package/agentes/revisor-csharp-swl.md +264 -264
  53. package/agentes/revisor-go-swl.md +259 -259
  54. package/agentes/revisor-java-swl.md +257 -257
  55. package/agentes/revisor-kotlin-swl.md +273 -273
  56. package/agentes/revisor-nextjs-swl.md +281 -281
  57. package/agentes/revisor-php-swl.md +271 -271
  58. package/agentes/revisor-react-swl.md +278 -278
  59. package/agentes/revisor-rust-swl.md +346 -346
  60. package/agentes/revisor-seguridad-swl.md +399 -399
  61. package/agentes/revisor-swift-swl.md +268 -268
  62. package/agentes/revisor-typescript-swl.md +346 -346
  63. package/agentes/sre-swl.md +291 -291
  64. package/agentes/tdd-qa-swl.md +393 -393
  65. package/comandos/swl/contexto.md +0 -2
  66. package/comandos/swl/deuda-codigo.md +97 -0
  67. package/habilidades/angular-moderno/SKILL.md +5 -1
  68. package/habilidades/contenedores-docker/SKILL.md +3 -1
  69. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  70. package/habilidades/harness-claude-code/SKILL.md +4 -4
  71. package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +6 -5
  72. package/habilidades/legacy-code-rescue/SKILL.md +2 -1
  73. package/habilidades/meta-skills-estandar/SKILL.md +1 -1
  74. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
  75. package/habilidades/postgresql-experto/SKILL.md +3 -1
  76. package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
  77. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
  78. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
  79. package/hooks/audit-trail.js +4 -4
  80. package/hooks/calidad-pre-commit.js +15 -11
  81. package/hooks/captura-acciones-post.js +3 -2
  82. package/hooks/captura-acciones-session.js +3 -2
  83. package/hooks/contexto-iteracion.js +2 -1
  84. package/hooks/contexto-subagente.js +68 -0
  85. package/hooks/guardrail-modelo.js +13 -3
  86. package/hooks/inyeccion-contexto.js +12 -12
  87. package/hooks/registro-turnos.js +5 -4
  88. package/hooks/spec-gate.js +2 -1
  89. package/hooks/sugerir-contribuir.js +2 -1
  90. package/hooks/sugerir-regenerar-inventario.js +3 -2
  91. package/hooks/tdd-gate.js +2 -1
  92. package/llms.txt +3 -3
  93. package/manifiestos/canonical-hashes.json +667 -10
  94. package/manifiestos/hook-profiles.json +0 -2
  95. package/manifiestos/hooks-config.json +469 -477
  96. package/manifiestos/invariantes-criticos.json +30 -0
  97. package/manifiestos/modulos.json +1390 -1390
  98. package/manifiestos/skills-lock.json +24 -24
  99. package/package.json +93 -93
  100. package/plugin.json +369 -371
  101. package/schemas/agent-frontmatter.schema.json +3 -1
  102. package/scripts/instalador.js +4 -1
  103. package/scripts/lib/expandir-targets.js +71 -71
  104. package/scripts/lib/preservar-usuario.js +39 -5
  105. package/scripts/lib/toml-merge.js +204 -204
  106. package/scripts/mcp-server/auth.js +105 -105
  107. package/scripts/mcp-server/cache.js +106 -106
  108. package/scripts/validar.js +1 -1
  109. package/scripts/verificar-release.js +51 -0
  110. package/hooks/lib/context-compressor.js +0 -657
  111. package/hooks/linea-estado.js +0 -328
  112. package/hooks/monitor-contexto.js +0 -373
@@ -1,328 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Hook: linea-estado.js
6
- * Tipo: PostToolUse
7
- *
8
- * Muestra una línea de estado compacta con:
9
- * - Modelo activo
10
- * - Tarea actual (derivada del tool_name y parámetros)
11
- * - Directorio de trabajo abreviado
12
- * - Barra de progreso de contexto estimado
13
- *
14
- * Además escribe un archivo bridge en /tmp/swl-ctx-{session}.json
15
- * para que otros hooks (monitor-contexto, etc.) lean el estado.
16
- *
17
- * Integración con Claude Code hooks:
18
- * El hook no bloquea (exit 0 siempre).
19
- * Los mensajes informativos se escriben en stderr para que
20
- * Claude Code los muestre en la barra de estado o los ignore;
21
- * sin interferir con el flujo JSON en stdout.
22
- */
23
-
24
- const fs = require('fs');
25
- const path = require('path');
26
- const os = require('os');
27
-
28
- // ---------------------------------------------------------------------------
29
- // Constantes de configuración
30
- // ---------------------------------------------------------------------------
31
-
32
- /** Ancho de la barra de progreso del contexto (en caracteres de bloque). */
33
- const BARRA_ANCHO = 10;
34
-
35
- /**
36
- * Techo por defecto cuando no se detecta el modelo.
37
- * Mantiene la heurística histórica para compatibilidad.
38
- */
39
- const TOKENS_MAXIMOS_DEFAULT = 200_000;
40
-
41
- /**
42
- * Techos de contexto por modelo (en tokens).
43
- *
44
- * IMPORTANTE: las claves son prefijos del `model_id` reportado por Claude Code
45
- * en el evento del hook. La detección hace `modelId.includes(prefijo)` en orden;
46
- * los prefijos más específicos van primero para que `claude-opus-4-8` no
47
- * matchee accidentalmente con la entrada genérica `claude-opus`.
48
- *
49
- * Si el `model_id` contiene el sufijo `[1m]` (ej: `claude-opus-4-8[1m]`), el
50
- * techo se fuerza a 1M independientemente del modelo base — Anthropic usa
51
- * ese sufijo para indicar la variante de 1M context.
52
- */
53
- const TECHOS_POR_MODELO = {
54
- // Opus 4.8 — variante de 1M context (la actual por defecto)
55
- 'claude-opus-4-8': 1_000_000,
56
- // Opus 4.7 — variante de 1M context (histórica)
57
- 'claude-opus-4-7': 1_000_000,
58
- // Resto de modelos modernos: 200k context
59
- 'claude-opus-4-6': 200_000,
60
- 'claude-opus-4-5': 200_000,
61
- // Sonnet 5 — 1M context nativo en la API (doc oficial)
62
- 'claude-sonnet-5': 1_000_000,
63
- 'claude-sonnet-4-6': 200_000,
64
- 'claude-sonnet-4-5': 200_000,
65
- 'claude-haiku-4-5': 200_000,
66
- // Catch-all genéricos (orden: deben ir DESPUÉS de los específicos)
67
- 'claude-opus': 200_000,
68
- 'claude-sonnet': 200_000,
69
- 'claude-haiku': 200_000,
70
- };
71
-
72
- /**
73
- * Modelos conocidos y su etiqueta corta para la barra.
74
- * Clave: prefijo del model_id. Valor: etiqueta legible.
75
- */
76
- const ETIQUETAS_MODELO = {
77
- 'claude-opus': 'opus',
78
- 'claude-sonnet': 'sonnet',
79
- 'claude-haiku': 'haiku',
80
- };
81
-
82
- /**
83
- * Determina el techo de tokens del contexto según el modelo activo.
84
- *
85
- * Reglas (en orden):
86
- * 1. Si el `modelId` contiene `[1m]` → 1_000_000 (variante explícita 1M).
87
- * 2. Si el `modelId` matchea un prefijo de TECHOS_POR_MODELO → ese techo.
88
- * 3. Default → TOKENS_MAXIMOS_DEFAULT (200k).
89
- *
90
- * @param {string} modelId - Identificador completo del modelo, posiblemente
91
- * con sufijo de variante (ej: `claude-opus-4-8[1m]`).
92
- * @returns {number} Techo de tokens del contexto.
93
- */
94
- function techoTokens(modelId) {
95
- if (!modelId) return TOKENS_MAXIMOS_DEFAULT;
96
- const id = String(modelId);
97
- if (id.includes('[1m]') || id.includes('-1m')) return 1_000_000;
98
- for (const [prefijo, techo] of Object.entries(TECHOS_POR_MODELO)) {
99
- if (id.includes(prefijo)) return techo;
100
- }
101
- return TOKENS_MAXIMOS_DEFAULT;
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Utilidades
106
- // ---------------------------------------------------------------------------
107
-
108
- /**
109
- * Construye la etiqueta corta del modelo activo.
110
- * @param {string} modelId - Identificador completo del modelo.
111
- * @returns {string}
112
- */
113
- function etiquetaModelo(modelId) {
114
- if (!modelId) return 'modelo?';
115
- for (const [prefijo, etiqueta] of Object.entries(ETIQUETAS_MODELO)) {
116
- if (modelId.includes(prefijo)) return etiqueta;
117
- }
118
- // Fallback: primeros 12 caracteres del modelo
119
- return String(modelId).slice(0, 12);
120
- }
121
-
122
- /**
123
- * Determina la tarea actual a partir del nombre de la herramienta
124
- * y sus parámetros de entrada.
125
- * @param {string} toolName - Nombre de la tool que acaba de ejecutarse.
126
- * @param {object} toolInput - Parámetros de entrada de la tool.
127
- * @returns {string}
128
- */
129
- function tareaActual(toolName, toolInput) {
130
- if (!toolName) return 'idle';
131
-
132
- const nombre = String(toolName).toLowerCase();
133
-
134
- switch (nombre) {
135
- case 'write':
136
- case 'edit': {
137
- const filePath = toolInput?.file_path || toolInput?.path || '';
138
- const base = path.basename(String(filePath));
139
- return `editando ${base || 'archivo'}`;
140
- }
141
- case 'read': {
142
- const filePath = toolInput?.file_path || toolInput?.path || '';
143
- const base = path.basename(String(filePath));
144
- return `leyendo ${base || 'archivo'}`;
145
- }
146
- case 'bash': {
147
- // Mostrar primeros 28 chars del comando, sin saltos de línea
148
- const cmd = String(toolInput?.command || '').replace(/\s+/g, ' ').slice(0, 28);
149
- return `bash: ${cmd || '...'}`;
150
- }
151
- case 'grep': {
152
- const patron = String(toolInput?.pattern || '').slice(0, 20);
153
- return `grep: ${patron || '...'}`;
154
- }
155
- case 'glob': {
156
- const patron = String(toolInput?.pattern || '').slice(0, 20);
157
- return `glob: ${patron || '...'}`;
158
- }
159
- case 'task':
160
- case 'agent':
161
- return `agente: ${String(toolInput?.description || '').slice(0, 25) || '...'}`;
162
- default:
163
- return nombre.slice(0, 30);
164
- }
165
- }
166
-
167
- /**
168
- * Abrevia una ruta de directorio para mostrar en la barra de estado.
169
- * Muestra sólo los últimos N segmentos.
170
- * @param {string} dirPath - Ruta completa.
171
- * @param {number} maxSegmentos - Número máximo de segmentos a mostrar.
172
- * @returns {string}
173
- */
174
- function abreviarDirectorio(dirPath, maxSegmentos = 2) {
175
- if (!dirPath) return '.';
176
- // Normalizar separadores en Windows
177
- const normalizado = String(dirPath).replace(/\\/g, '/');
178
- const partes = normalizado.split('/').filter(Boolean);
179
- if (partes.length <= maxSegmentos) return normalizado;
180
- return '…/' + partes.slice(-maxSegmentos).join('/');
181
- }
182
-
183
- /**
184
- * Construye la barra de progreso visual.
185
- * @param {number} porcentaje - Valor entre 0 y 100.
186
- * @returns {string} Ej: "████████░░ 80%"
187
- */
188
- function barraProgreso(porcentaje) {
189
- const pct = Math.max(0, Math.min(100, porcentaje));
190
- const llenos = Math.round((pct / 100) * BARRA_ANCHO);
191
- const vacios = BARRA_ANCHO - llenos;
192
- const barra = '█'.repeat(llenos) + '░'.repeat(vacios);
193
- return `${barra} ${pct}%`;
194
- }
195
-
196
- /**
197
- * Estima el porcentaje de contexto utilizado.
198
- * Claude Code no expone directamente los tokens consumidos en el hook,
199
- * por lo que usamos una heurística basada en el número de invocaciones
200
- * de herramientas acumuladas en la sesión cuando el campo `usage` no está.
201
- *
202
- * La función lee el archivo bridge previo para mantener el contador,
203
- * y lo incrementa en cada invocación.
204
- *
205
- * @param {string} bridgePath - Ruta del archivo bridge.
206
- * @param {number} tokensReportados - Tokens reportados por Claude (si existen).
207
- * @param {number} techo - Techo de tokens del modelo activo.
208
- * @returns {{ porcentaje: number, invocaciones: number, techo: number }}
209
- */
210
- function estimarContexto(bridgePath, tokensReportados, techo) {
211
- const techoEfectivo = (typeof techo === 'number' && techo > 0)
212
- ? techo
213
- : TOKENS_MAXIMOS_DEFAULT;
214
- let invocaciones = 1;
215
-
216
- // Leer estado previo del bridge
217
- try {
218
- const previo = JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
219
- invocaciones = (previo.invocaciones || 0) + 1;
220
- } catch (_) {
221
- // Archivo no existe aún — primera invocación
222
- }
223
-
224
- // Si Claude reporta tokens usados (campo no-estándar, puede no existir)
225
- if (typeof tokensReportados === 'number' && tokensReportados > 0) {
226
- const pct = Math.min(100, Math.round((tokensReportados / techoEfectivo) * 100));
227
- return { porcentaje: pct, invocaciones, techo: techoEfectivo };
228
- }
229
-
230
- // Heurística: cada invocación consume ~500 tokens promedio (conversación + tool)
231
- const tokenesEstimados = invocaciones * 500;
232
- const pct = Math.min(99, Math.round((tokenesEstimados / techoEfectivo) * 100));
233
- return { porcentaje: pct, invocaciones, techo: techoEfectivo };
234
- }
235
-
236
- // ---------------------------------------------------------------------------
237
- // Entrypoint principal
238
- // ---------------------------------------------------------------------------
239
-
240
- // Solo conectar stdin cuando se ejecuta como script (hook),
241
- // no cuando se carga vía require() desde tests u otros módulos.
242
- if (require.main === module) iniciarHook();
243
-
244
- function iniciarHook() {
245
- let inputRaw = '';
246
-
247
- process.stdin.on('data', chunk => {
248
- inputRaw += chunk;
249
- });
250
-
251
- process.stdin.on('end', () => {
252
- try {
253
- const data = JSON.parse(inputRaw);
254
-
255
- // Extraer campos del evento de hook de Claude Code
256
- // Estructura documentada de PostToolUse:
257
- // data.session_id, data.model, data.tool_name, data.tool_input,
258
- // data.tool_result, data.usage (si existe)
259
- const sessionId = String(data.session_id || 'default');
260
- const modelId = String(data.model || '');
261
- const toolName = String(data.tool_name || '');
262
- const toolInput = data.tool_input || {};
263
- const tokensUsados = data.usage?.input_tokens ?? data.usage?.total_tokens ?? 0;
264
-
265
- // Ruta del archivo bridge compartido entre hooks
266
- const bridgePath = path.join(os.tmpdir(), `swl-ctx-${sessionId}.json`);
267
-
268
- // Resolver techo según modelo (Opus 4.8 con sufijo [1m] = 1M; resto 200k)
269
- const techo = techoTokens(modelId);
270
-
271
- // Calcular métricas
272
- const { porcentaje, invocaciones } = estimarContexto(bridgePath, tokensUsados, techo);
273
- const modeloLabel = etiquetaModelo(modelId);
274
- const tarea = tareaActual(toolName, toolInput);
275
- const directorio = abreviarDirectorio(process.cwd());
276
- const barra = barraProgreso(porcentaje);
277
-
278
- // Construir payload del bridge para otros hooks
279
- const bridgePayload = {
280
- sessionId,
281
- modelId,
282
- modeloLabel,
283
- tarea,
284
- directorio,
285
- porcentaje,
286
- invocaciones,
287
- tokensUsados,
288
- techoTokens: techo,
289
- timestamp: new Date().toISOString(),
290
- };
291
-
292
- // Escribir bridge (ignorar error si /tmp no existe en este entorno)
293
- try {
294
- fs.writeFileSync(bridgePath, JSON.stringify(bridgePayload, null, 2), 'utf8');
295
- } catch (errBridge) {
296
- // No bloquear el flujo si no se puede escribir el bridge
297
- // No se pudo escribir bridge — ignorar
298
- }
299
-
300
- // Construir la línea de estado como JSON válido para Claude Code
301
- const lineaEstado = `${modeloLabel} | ${tarea} | ${directorio} | ctx: ${barra}`;
302
-
303
- // PostToolUse hooks: no escribir nada a stdout para permitir
304
- // que Claude Code procese la respuesta sin interferencia.
305
-
306
- } catch (err) {
307
- // Si el hook falla por cualquier razón, nunca bloquear a Claude
308
- // No escribir a stderr — Claude Code lo reporta como hook error
309
- }
310
- });
311
- }
312
-
313
- if (require.main !== module) {
314
- module.exports = {
315
- techoTokens,
316
- estimarContexto,
317
- etiquetaModelo,
318
- tareaActual,
319
- abreviarDirectorio,
320
- barraProgreso,
321
- _internals: {
322
- TECHOS_POR_MODELO,
323
- TOKENS_MAXIMOS_DEFAULT,
324
- ETIQUETAS_MODELO,
325
- BARRA_ANCHO,
326
- },
327
- };
328
- }
@@ -1,373 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Hook: monitor-contexto.js
6
- * Tipo: PostToolUse
7
- *
8
- * Lee el archivo bridge generado por linea-estado.js y emite advertencias
9
- * cuando el contexto estimado supera umbrales críticos.
10
- *
11
- * Umbrales por defecto (calibrados con evidencia empírica de
12
- * github.com/anthropics/claude-code issue #34685 — degradación detectable
13
- * ~20%, auto-compactación de Claude Code dispara ~40%, sweet spot 40-50%):
14
- *
15
- * ≥ 40%: WARNING — degradación detectable, evitar trabajo complejo
16
- * ≥ 55%: CRITICAL — detenerse, guardar estado, compactar
17
- * ≥ 35% en punto natural: sugerencia de compactación proactiva
18
- *
19
- * Override por env var (útil para experimentos o sesiones cortas):
20
- * SWL_CTX_WARNING (default 40)
21
- * SWL_CTX_CRITICAL (default 55)
22
- * SWL_CTX_PROACTIVA (default 35)
23
- *
24
- * Histórico: hasta v1.1.0 los umbrales eran 65/75/50. Se bajaron en v1.1.1
25
- * tras analizar el issue #34685 que documenta degradación al 20-40%.
26
- *
27
- * Debounce: solo emite aviso cada N_DEBOUNCE invocaciones de herramienta.
28
- * La escalación de severidad (pasar de WARNING a CRITICAL) bypasea el debounce.
29
- *
30
- * El hook nunca bloquea (siempre exit 0); sólo emite mensajes en stdout
31
- * como texto enriquecido que Claude Code puede mostrar al usuario.
32
- */
33
-
34
- const fs = require('fs');
35
- const path = require('path');
36
- const os = require('os');
37
-
38
- const { phase1_PruneToolResults, shouldCompress } = require('./lib/context-compressor');
39
-
40
- // ---------------------------------------------------------------------------
41
- // Constantes de configuración
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * Lee un umbral numérico desde env var con default. Inválido o fuera de rango
46
- * (0-99) → usar default.
47
- */
48
- function umbralEnv(nombre, def) {
49
- const valor = parseInt(process.env[nombre] || '', 10);
50
- if (Number.isFinite(valor) && valor >= 0 && valor < 100) return valor;
51
- return def;
52
- }
53
-
54
- /**
55
- * Resuelve los tres umbrales aplicando defaults y validando el invariante de
56
- * ordenamiento: PROACTIVA < WARNING < CRITICAL.
57
- *
58
- * Si los env vars violan el orden (p.ej. WARNING=80 y CRITICAL=50, que haría
59
- * CRITICAL inalcanzable lógicamente) o PROACTIVA >= WARNING (haciendo la
60
- * sugerencia proactiva inalcanzable), se descarta la configuración inválida
61
- * y se usan los defaults para los tres. La fallback es silenciosa porque el
62
- * hook nunca debe romper el flujo principal.
63
- */
64
- function resolverUmbrales() {
65
- const defaults = { proactiva: 35, warning: 40, critical: 55 };
66
-
67
- const proactiva = umbralEnv('SWL_CTX_PROACTIVA', defaults.proactiva);
68
- const warning = umbralEnv('SWL_CTX_WARNING', defaults.warning);
69
- const critical = umbralEnv('SWL_CTX_CRITICAL', defaults.critical);
70
-
71
- if (proactiva < warning && warning < critical) {
72
- return { proactiva, warning, critical };
73
- }
74
- return defaults;
75
- }
76
-
77
- const UMBRALES = resolverUmbrales();
78
-
79
- /** Porcentaje de USO a partir del cual se emite WARNING. */
80
- const UMBRAL_WARNING = UMBRALES.warning;
81
-
82
- /** Porcentaje de USO a partir del cual se emite CRITICAL. */
83
- const UMBRAL_CRITICAL = UMBRALES.critical;
84
-
85
- /**
86
- * Número de invocaciones de herramienta entre avisos consecutivos del mismo nivel.
87
- * La escalación de severidad siempre bypasea este valor.
88
- */
89
- const N_DEBOUNCE = 5;
90
-
91
- /** Cada N invocaciones, sugerir compactacion si estamos en punto natural. */
92
- const N_COMPACTACION_PROACTIVA = 40;
93
-
94
- /** Umbral de uso para activar compactacion proactiva (antes del warning). */
95
- const UMBRAL_COMPACTACION_PROACTIVA = UMBRALES.proactiva;
96
-
97
- /**
98
- * Ruta del archivo de estado interno de este monitor.
99
- * Almacena cuándo se emitió el último aviso y con qué severidad.
100
- */
101
- function rutaEstadoMonitor(sessionId) {
102
- return path.join(os.tmpdir(), `swl-monitor-${sessionId}.json`);
103
- }
104
-
105
- // ---------------------------------------------------------------------------
106
- // Tipos de severidad
107
- // ---------------------------------------------------------------------------
108
- const SEVERIDAD = {
109
- NINGUNA: 0,
110
- WARNING: 1,
111
- CRITICAL: 2,
112
- };
113
-
114
- // ---------------------------------------------------------------------------
115
- // Utilidades
116
- // ---------------------------------------------------------------------------
117
-
118
- /**
119
- * Lee el archivo bridge de linea-estado.js.
120
- * @param {string} sessionId
121
- * @returns {object|null} Payload del bridge o null si no existe/falla.
122
- */
123
- function leerBridge(sessionId) {
124
- const bridgePath = path.join(os.tmpdir(), `swl-ctx-${sessionId}.json`);
125
- try {
126
- return JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
127
- } catch (_) {
128
- return null;
129
- }
130
- }
131
-
132
- /**
133
- * Lee el estado interno previo del monitor.
134
- * @param {string} estadoPath
135
- * @returns {{ ultimaInvocacion: number, ultimaSeveridad: number }}
136
- */
137
- function leerEstadoMonitor(estadoPath) {
138
- try {
139
- const datos = JSON.parse(fs.readFileSync(estadoPath, 'utf8'));
140
- return {
141
- ultimaInvocacion: datos.ultimaInvocacion ?? 0,
142
- ultimaSeveridad: datos.ultimaSeveridad ?? SEVERIDAD.NINGUNA,
143
- compresionEmitida: datos.compresionEmitida ?? false,
144
- };
145
- } catch (_) {
146
- return { ultimaInvocacion: 0, ultimaSeveridad: SEVERIDAD.NINGUNA, compresionEmitida: false };
147
- }
148
- }
149
-
150
- /**
151
- * Persiste el estado del monitor.
152
- * @param {string} estadoPath
153
- * @param {object} estado
154
- */
155
- function guardarEstadoMonitor(estadoPath, estado) {
156
- try {
157
- fs.writeFileSync(estadoPath, JSON.stringify(estado, null, 2), 'utf8');
158
- } catch (_) {
159
- // Ignorar errores de escritura — no bloquear el flujo
160
- }
161
- }
162
-
163
- /**
164
- * Determina la severidad actual en función del porcentaje de uso.
165
- * @param {number} porcentajeUso - Porcentaje de contexto consumido (0-100).
166
- * @returns {number} Valor de SEVERIDAD.*
167
- */
168
- function calcularSeveridad(porcentajeUso) {
169
- if (porcentajeUso >= UMBRAL_CRITICAL) return SEVERIDAD.CRITICAL;
170
- if (porcentajeUso >= UMBRAL_WARNING) return SEVERIDAD.WARNING;
171
- return SEVERIDAD.NINGUNA;
172
- }
173
-
174
- /**
175
- * Detecta si la ultima operacion es un punto natural para compactar.
176
- * Puntos naturales:
177
- * - Despues de un git commit exitoso
178
- * - Despues de correr tests exitosamente
179
- * - Cuando tool_name es "Agent" (fin de subagente)
180
- * @param {object} data - Payload del hook
181
- * @returns {boolean}
182
- */
183
- function esPuntoNatural(data) {
184
- const toolName = String(data.tool_name || '');
185
- const toolInput = data.tool_input || {};
186
- const comando = String(toolInput.command || '');
187
-
188
- // Despues de un commit exitoso
189
- if (toolName === 'Bash' && /\bgit\s+commit\b/.test(comando)) return true;
190
-
191
- // Despues de correr tests
192
- if (toolName === 'Bash' && /\b(pytest|jest|vitest|go test|cargo test|dotnet test|phpunit|npm test)\b/.test(comando)) return true;
193
-
194
- // Fin de un subagente
195
- if (toolName === 'Agent') return true;
196
-
197
- return false;
198
- }
199
-
200
- /**
201
- * Genera el mensaje de sugerencia de compactacion proactiva.
202
- * @param {number} porcentajeUso
203
- * @param {number} invocaciones
204
- * @returns {string}
205
- */
206
- function generarMensajeCompactacion(porcentajeUso, invocaciones) {
207
- return [
208
- '',
209
- '--- SUGERENCIA DE COMPACTACION ---',
210
- ` Contexto: ${porcentajeUso}% usado | ${invocaciones} tool calls en sesion`,
211
- ' Punto natural de pausa detectado.',
212
- ' Considera ejecutar /compact o /swl:compactar para liberar contexto',
213
- ' antes de continuar con tareas complejas.',
214
- '',
215
- ].join('\n');
216
- }
217
-
218
- /**
219
- * Genera el mensaje de advertencia según severidad.
220
- * El mensaje se emite en stdout para que Claude Code lo procese
221
- * como feedback informativo dentro de la conversación.
222
- *
223
- * @param {number} severidad
224
- * @param {number} porcentajeUso
225
- * @param {number} porcentajeRestante
226
- * @returns {string}
227
- */
228
- function generarMensaje(severidad, porcentajeUso, porcentajeRestante) {
229
- const barra = '█'.repeat(Math.round(porcentajeUso / 10)) +
230
- '░'.repeat(10 - Math.round(porcentajeUso / 10));
231
-
232
- if (severidad === SEVERIDAD.CRITICAL) {
233
- return [
234
- '',
235
- '╔══════════════════════════════════════════════════════╗',
236
- '║ CONTEXTO CRÍTICO — ACCIÓN REQUERIDA ║',
237
- '╚══════════════════════════════════════════════════════╝',
238
- ` Contexto usado: ${barra} ${porcentajeUso}% (${porcentajeRestante}% restante)`,
239
- '',
240
- ' Acciones recomendadas:',
241
- ' 1. Guarda el estado actual con un commit atómico.',
242
- ' 2. Ejecuta /compact o solicita compactación del contexto.',
243
- ' 3. Divide la tarea en partes más pequeñas.',
244
- ' 4. No inicies trabajo complejo hasta compactar.',
245
- '',
246
- ].join('\n');
247
- }
248
-
249
- if (severidad === SEVERIDAD.WARNING) {
250
- return [
251
- '',
252
- '⚠ ADVERTENCIA DE CONTEXTO',
253
- ` Contexto usado: ${barra} ${porcentajeUso}% (${porcentajeRestante}% restante)`,
254
- ' Evitar tareas complejas. Considera compactar pronto.',
255
- '',
256
- ].join('\n');
257
- }
258
-
259
- return '';
260
- }
261
-
262
- // ---------------------------------------------------------------------------
263
- // Entrypoint principal
264
- // ---------------------------------------------------------------------------
265
-
266
- let inputRaw = '';
267
-
268
- process.stdin.on('data', chunk => {
269
- inputRaw += chunk;
270
- });
271
-
272
- process.stdin.on('end', () => {
273
- try {
274
- const data = JSON.parse(inputRaw);
275
-
276
- const sessionId = String(data.session_id || 'default');
277
- const estadoPath = rutaEstadoMonitor(sessionId);
278
-
279
- // Leer el bridge de linea-estado
280
- const bridge = leerBridge(sessionId);
281
- if (!bridge) {
282
- // Sin bridge no hay datos — salir silenciosamente
283
- return;
284
- }
285
-
286
- const porcentajeUso = bridge.porcentaje ?? 0;
287
- const invocacionesActual = bridge.invocaciones ?? 0;
288
- const porcentajeRestante = 100 - porcentajeUso;
289
- const severidadActual = calcularSeveridad(porcentajeUso);
290
-
291
- // Leer estado previo del monitor (necesario tanto para WARNING/CRITICAL como para proactiva)
292
- const estadoPrevio = leerEstadoMonitor(estadoPath);
293
- const { ultimaInvocacion, ultimaSeveridad } = estadoPrevio;
294
-
295
- // Resetear flag de compresión si el contexto bajó (post-compactación real)
296
- if (severidadActual < SEVERIDAD.CRITICAL && estadoPrevio.compresionEmitida) {
297
- guardarEstadoMonitor(estadoPath, {
298
- ...estadoPrevio,
299
- compresionEmitida: false,
300
- timestamp: new Date().toISOString(),
301
- });
302
- }
303
-
304
- if (severidadActual !== SEVERIDAD.NINGUNA) {
305
- // Determinar si debemos emitir aviso:
306
- // a) Escalación de severidad → siempre emitir (bypasea debounce)
307
- // b) Mismo nivel → solo si han pasado N_DEBOUNCE invocaciones
308
- const esEscalacion = severidadActual > ultimaSeveridad;
309
- const invocacionesDesdeUltimoAviso = invocacionesActual - ultimaInvocacion;
310
- const debounceExpirado = invocacionesDesdeUltimoAviso >= N_DEBOUNCE;
311
-
312
- const debeEmitir = esEscalacion || debounceExpirado;
313
-
314
- if (debeEmitir) {
315
- const mensaje = generarMensaje(severidadActual, porcentajeUso, porcentajeRestante);
316
- if (mensaje) {
317
- // Emitir advertencia como systemMessage (stderr causa hook error)
318
- process.stdout.write(JSON.stringify({ systemMessage: mensaje }));
319
- }
320
-
321
- // Actualizar estado del monitor
322
- guardarEstadoMonitor(estadoPath, {
323
- ultimaInvocacion: invocacionesActual,
324
- ultimaSeveridad: severidadActual,
325
- compresionEmitida: estadoPrevio.compresionEmitida || false,
326
- timestamp: new Date().toISOString(),
327
- });
328
- }
329
- }
330
-
331
- // --- Compresión automática Fase 1 (sin LLM) al llegar a CRITICAL ---
332
- // Solo emitir UNA VEZ por sesión para evitar spam de banners
333
- if (severidadActual === SEVERIDAD.CRITICAL && shouldCompress(porcentajeUso)) {
334
- const yaEmitioBanner = estadoPrevio.compresionEmitida === true;
335
- if (!yaEmitioBanner) {
336
- try {
337
- const mensajeCompresion = [
338
- '',
339
- '--- COMPRESION AUTOMATICA (Fases 1+1b) ---',
340
- ` Contexto: ${porcentajeUso}% usado`,
341
- ' Fase 1: tool results antiguos podados por posición.',
342
- ' Fase 1b: resultados de Read/Bash/Grep/Glob > 15 turnos comprimidos por TTL.',
343
- ' Para compresión avanzada (Fases 2-4), ejecuta /swl:compactar',
344
- ' o pide: "comprime el contexto preservando las decisiones clave".',
345
- '',
346
- ].join('\n');
347
- process.stdout.write(JSON.stringify({ systemMessage: mensajeCompresion }));
348
- // Marcar como emitido para no repetir
349
- guardarEstadoMonitor(estadoPath, {
350
- ...estadoPrevio,
351
- ultimaInvocacion: invocacionesActual,
352
- ultimaSeveridad: severidadActual,
353
- compresionEmitida: true,
354
- timestamp: new Date().toISOString(),
355
- });
356
- } catch (_) {}
357
- }
358
- }
359
-
360
- // --- Compactacion proactiva: sugerir en punto natural antes de alcanzar WARNING ---
361
- if (severidadActual === SEVERIDAD.NINGUNA &&
362
- porcentajeUso >= UMBRAL_COMPACTACION_PROACTIVA &&
363
- invocacionesActual % N_COMPACTACION_PROACTIVA === 0 &&
364
- esPuntoNatural(data)) {
365
- const mensajeCompactacion = generarMensajeCompactacion(porcentajeUso, invocacionesActual);
366
- process.stdout.write(JSON.stringify({ systemMessage: mensajeCompactacion }));
367
- }
368
-
369
- } catch (err) {
370
- // El hook nunca bloquea por errores internos
371
- // No escribir a stderr — Claude Code lo reporta como hook error
372
- }
373
- });