refacil-sdd-ai 2.4.0 → 2.7.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/README.md CHANGED
@@ -7,7 +7,9 @@ Instala skills para **Claude Code** y **Cursor** que guian al desarrollador por
7
7
  ## Requisitos
8
8
 
9
9
  - **Node.js >= 20.19.0** (requerido por OpenSpec)
10
- - **Claude Code** o **Cursor** instalado
10
+ - **Claude Code >= 2.1.89** (requerido por el hook `compact-bash` para rewrite silencioso de comandos via `updatedInput`) o **Cursor**
11
+
12
+ `refacil-sdd-ai init` verifica la version de Claude Code y muestra advertencia si es inferior. Con version < 2.1.89 el resto de la metodologia funciona, pero el hook `compact-bash` no tendra efecto (Claude Code ignora `updatedInput` en versiones antiguas).
11
13
 
12
14
  ## Instalacion y Setup
13
15
 
@@ -53,6 +55,11 @@ refacil-sdd-ai init # Instalar skills, hooks y crear configs en el repo
53
55
  refacil-sdd-ai update # Actualizar skills y hooks a la ultima version
54
56
  refacil-sdd-ai check-update # Verifica si hay una version mas reciente en npm (usado por hook)
55
57
  refacil-sdd-ai check-review # Verifica que el review se haya completado (usado por hook)
58
+ refacil-sdd-ai compact-bash # Hook de rewrite de comandos Bash bare (usado por hook, no invocar manual)
59
+ refacil-sdd-ai compact stats # Estadisticas completas (hook + ya-compacto) + tokens estimados + USD
60
+ refacil-sdd-ai compact disable # Desactiva el hook compact-bash sin desinstalarlo
61
+ refacil-sdd-ai compact enable # Reactiva el hook compact-bash
62
+ refacil-sdd-ai compact clear-log # Limpia ~/.refacil-sdd-ai/compact.log
56
63
  refacil-sdd-ai clean # Eliminar skills y remover hooks SDD-AI del repo
57
64
  refacil-sdd-ai help # Ver ayuda
58
65
  ```
@@ -328,6 +335,7 @@ El paquete instala hooks en `.claude/settings.json` durante `init` y `update`:
328
335
  | Hook | Evento | Que hace |
329
336
  |------|--------|----------|
330
337
  | `check-update` | `SessionStart` | Verifica si hay nueva version del paquete en npm y la instala automaticamente. Tambien **sincroniza el bloque `compact-guidance`** en `AGENTS.md` (ver [Eficiencia de tokens](#eficiencia-de-tokens-bloque-auto-gestionado-en-agentsmd)). |
338
+ | `compact-bash` | `PreToolUse` (Bash) | Reescribe **silenciosamente** comandos Bash bare (git/tests/docker logs) a su forma compacta usando `updatedInput`. Sin turnos extra y sin que Claude vea el cambio. Requiere Claude Code >= 2.1.89. |
331
339
  | `check-review` | `PreToolUse` (Bash) | Intercepta `git push` y verifica que exista `.review-passed` en cada cambio activo de `openspec/changes/`. Si falta, **bloquea el push** y emite instrucciones para ejecutar `/refacil:review`. El hook no invoca skills por si mismo. |
332
340
 
333
341
  #### Flujo del hook de review
@@ -378,6 +386,84 @@ openspec/changes/fix-session-timeout-redis/
378
386
 
379
387
  El archivo `.review-passed` contiene: veredicto, fecha, resumen, cantidad de hallazgos y si hubo blockers.
380
388
 
389
+ ### Hook `compact-bash` — rewrite silencioso de comandos Bash
390
+
391
+ Segunda capa de reduccion de tokens, **sin costo conversacional**. Antes de que Claude Code ejecute un comando Bash, el hook `compact-bash` inspecciona el comando y, si matchea una regla, lo reescribe usando el campo `updatedInput` del hook. Claude **no ve el cambio**, no hay turno adicional, no hay bloqueo.
392
+
393
+ **Reglas activas (19 reglas)**:
394
+
395
+ Fase 1 — git, tests base, docker logs:
396
+
397
+ | Comando bare | Reescrito a | Ahorro tipico |
398
+ |---|---|---|
399
+ | `git log` | `git log --oneline -20` | ~85% |
400
+ | `git status` | `git status -s` | ~70% |
401
+ | `git diff` (sin args) | `git diff --stat` | ~80% |
402
+ | `git show` | `git show --stat` | ~70% |
403
+ | `docker logs <container>` | `docker logs --tail 100 <container>` | ~80%+ |
404
+ | `npm test` / `yarn test` / `pnpm test` | `… 2>&1 \| tail -80` | ~90% |
405
+ | `jest` | `jest --silent --reporters=summary` | ~85% |
406
+ | `pytest` | `pytest -q` | ~60% |
407
+
408
+ Fase 2A — linters, type checkers, build, sistema:
409
+
410
+ | Comando bare | Reescrito a | Ahorro tipico |
411
+ |---|---|---|
412
+ | `eslint` | `eslint . --format compact --quiet` | ~70% |
413
+ | `eslint <path>` | `eslint <path> --format compact` | ~60% |
414
+ | `biome check` | `biome check --reporter=summary` | ~65% |
415
+ | `tsc` / `npx tsc …` | `… 2>&1 \| head -80` | variable |
416
+ | `prettier --check <path>` | `prettier --check <path> --loglevel warn` | ~50% |
417
+ | `npm audit` | `npm audit 2>&1 \| tail -10` | ~80% |
418
+ | `npm ls` | `npm ls --depth=0` | ~90% |
419
+ | `cargo build` / `cargo test` / `cargo check` | `… --quiet` | ~50% |
420
+ | `go test …` (sin flags) | `… 2>&1 \| tail -80` | ~70% |
421
+ | `mvn test` | `mvn test -q` | ~60% |
422
+ | `./gradlew test` / `gradle test` | `… -q` | ~60% |
423
+ | `ps aux` | `ps -eo pid,pcpu,pmem,comm \| head -30` | ~80% |
424
+
425
+ **Detector de intencion**: si el comando ya tiene flags explicitos (`git log -p`, `git log --all`, `jest --watch`, `docker logs --tail 50 …`), el hook **no interviene**. Tu intencion manda.
426
+
427
+ **Escape mecanismo**: prefija `COMPACT=0` al comando para desactivar el rewrite puntualmente: `COMPACT=0 git log`.
428
+
429
+ **Pipes y redirecciones** en test bare: si ya hay `|` o `>`, el detector no las envuelve de nuevo.
430
+
431
+ **Subcomandos de control**:
432
+
433
+ ```bash
434
+ refacil-sdd-ai compact stats # estadisticas completas (hook + ya-compacto) + tokens estimados + USD
435
+ refacil-sdd-ai compact disable # desactiva el hook sin desinstalar
436
+ refacil-sdd-ai compact enable # reactiva el hook
437
+ refacil-sdd-ai compact clear-log # limpia ~/.refacil-sdd-ai/compact.log
438
+ ```
439
+
440
+ **Telemetria**: cada evento genera una linea JSON en `~/.refacil-sdd-ai/compact.log` con timestamp, ruleId y estimacion de tokens. Hay dos tipos de evento: `hook_rewrite` (el hook reescribe) y `already_compact` (el comando ya llega compacto, asumido por skill/agente). `compact stats` muestra ambos por defecto y calcula costo estimado (a razon de $3/MTok input de Sonnet, conservador). La telemetria es local; nada se envia a la nube.
441
+
442
+ **Flujo del hook**:
443
+
444
+ ```
445
+ Claude emite "git log"
446
+
447
+
448
+ Hook compact-bash recibe tool_input via stdin (JSON)
449
+
450
+
451
+ ¿Matchea alguna regla y no tiene intent flags?
452
+
453
+ ├─ SI ──► stdout JSON:
454
+ │ {
455
+ │ "hookSpecificOutput": {
456
+ │ "hookEventName": "PreToolUse",
457
+ │ "permissionDecision": "allow",
458
+ │ "updatedInput": { "command": "git log --oneline -20", ... },
459
+ │ "permissionDecisionReason": "compact-bash: git log → --oneline -20"
460
+ │ }
461
+ │ }
462
+ │ Claude Code ejecuta el comando reescrito. Claude no lo ve.
463
+
464
+ └─ NO ──► stdout vacio → Claude Code ejecuta el comando original.
465
+ ```
466
+
381
467
  ### Eficiencia de tokens (bloque auto-gestionado en AGENTS.md)
382
468
 
383
469
  La metodologia SDD-AI genera consumo elevado de contexto (artefactos, specs, prompts de skills). Para compensarlo, `refacil-sdd-ai` mantiene un bloque de guia de eficiencia de tokens dentro de `AGENTS.md` que instruye a la IA sobre como pedir salidas compactas (Read con offset/limit, `git log --oneline`, tests solo con failures, etc.).
@@ -475,6 +561,29 @@ npm uninstall -g refacil-sdd-ai
475
561
  - [Claude Code](https://claude.ai/code) — CLI de Anthropic
476
562
  - [Cursor](https://cursor.sh) — IDE con IA
477
563
 
564
+ ## Cambios recientes
565
+
566
+ **v2.6.0** — Fase 2 del hook `compact-bash`:
567
+ - 11 reglas nuevas: `eslint`, `biome check`, `tsc`, `prettier --check`, `npm audit`, `npm ls`, `cargo build/test/check`, `go test`, `mvn test`, `gradle test`, `ps aux` (total: 19 reglas)
568
+ - Telemetria local en `~/.refacil-sdd-ai/compact.log`
569
+ - Subcomandos `compact stats | disable | enable | clear-log`
570
+ - `ps-aux` solo se activa en Unix (Linux/Mac); en Windows la regla no interviene
571
+
572
+ **v2.5.0** — Fase 1 del hook `compact-bash`:
573
+ - Hook `PreToolUse` que reescribe silenciosamente comandos Bash bare via `updatedInput` (requiere Claude Code >= 2.1.89)
574
+ - Reglas iniciales: git log/status/diff/show, docker logs, npm/yarn/pnpm test, jest, pytest
575
+ - Escape mecanismo `COMPACT=0 <cmd>`
576
+
577
+ **v2.4.0** — Reduccion de tokens en la metodologia:
578
+ - Boilerplate "Antes de empezar" unificado en 9 skills
579
+ - Validacion de OpenSpec simplificada en `prereqs/SKILL.md`
580
+ - Templates consolidados (`claude-md.md` + `cursorrules.md` → `methodology-guide.md` unico)
581
+ - Compactacion de `review/SKILL.md`, `bug/SKILL.md`, `test/SKILL.md`
582
+
583
+ **v2.3.0** — Bloque `compact-guidance` auto-gestionado en `AGENTS.md`:
584
+ - Hook `SessionStart` sincroniza el bloque en cada sesion
585
+ - Fuente de verdad: `templates/compact-guidance.md`
586
+
478
587
  ## Licencia
479
588
 
480
589
  MIT
package/bin/cli.js CHANGED
@@ -6,6 +6,8 @@ const {
6
6
  syncCompactGuidance,
7
7
  removeCompactGuidance,
8
8
  } = require('../lib/compact-guidance');
9
+ const compactBash = require('../lib/compact/bash');
10
+ const compactTelemetry = require('../lib/compact/telemetry');
9
11
 
10
12
  const SKILLS = [
11
13
  'setup',
@@ -128,6 +130,29 @@ function removeSkills() {
128
130
  return removed;
129
131
  }
130
132
 
133
+ function checkClaudeCodeVersion() {
134
+ const { execSync } = require('child_process');
135
+ try {
136
+ const output = execSync('claude --version 2>&1', {
137
+ encoding: 'utf8',
138
+ timeout: 5000,
139
+ stdio: ['pipe', 'pipe', 'pipe'],
140
+ }).trim();
141
+ const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
142
+ if (!match) return { ok: null, version: null };
143
+ const maj = Number(match[1]);
144
+ const min = Number(match[2]);
145
+ const patch = Number(match[3]);
146
+ const ok =
147
+ maj > 2 ||
148
+ (maj === 2 && min > 1) ||
149
+ (maj === 2 && min === 1 && patch >= 89);
150
+ return { ok, version: `${maj}.${min}.${patch}` };
151
+ } catch (_) {
152
+ return { ok: null, version: null };
153
+ }
154
+ }
155
+
131
156
  function checkNodeVersion() {
132
157
  const version = process.version; // e.g. v20.19.5
133
158
  const major = parseInt(version.split('.')[0].replace('v', ''));
@@ -174,9 +199,28 @@ function installHook() {
174
199
  changed = true;
175
200
  }
176
201
 
177
- // PreToolUse: check-review
202
+ // PreToolUse
178
203
  if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
179
204
 
205
+ // compact-bash (must run BEFORE check-review so rewrite is visible to subsequent hooks)
206
+ const hasCompactHook = settings.hooks.PreToolUse.some(
207
+ (h) => h._sdd_compact === true,
208
+ );
209
+ if (!hasCompactHook) {
210
+ settings.hooks.PreToolUse.unshift({
211
+ _sdd_compact: true,
212
+ matcher: 'Bash',
213
+ hooks: [
214
+ {
215
+ type: 'command',
216
+ command: 'refacil-sdd-ai compact-bash',
217
+ },
218
+ ],
219
+ });
220
+ changed = true;
221
+ }
222
+
223
+ // check-review
180
224
  const hasReviewHook = settings.hooks.PreToolUse.some(
181
225
  (h) => h._sdd_review === true,
182
226
  );
@@ -232,7 +276,7 @@ function uninstallHook() {
232
276
  if (Array.isArray(settings.hooks.PreToolUse)) {
233
277
  const original = settings.hooks.PreToolUse.length;
234
278
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
235
- (h) => h._sdd_review !== true,
279
+ (h) => h._sdd_review !== true && h._sdd_compact !== true,
236
280
  );
237
281
  if (settings.hooks.PreToolUse.length !== original) changed = true;
238
282
  if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
@@ -362,6 +406,18 @@ function init() {
362
406
  console.log(` Node.js ${process.version} OK`);
363
407
  }
364
408
 
409
+ // Check Claude Code version (for compact-bash hook)
410
+ const claudeCheck = checkClaudeCodeVersion();
411
+ if (claudeCheck.ok === true) {
412
+ console.log(` Claude Code ${claudeCheck.version} OK`);
413
+ } else if (claudeCheck.ok === false) {
414
+ console.log(`\n ADVERTENCIA: Claude Code ${claudeCheck.version} detectado.`);
415
+ console.log(' El hook compact-bash requiere Claude Code >= 2.1.89 para rewrite silencioso.');
416
+ console.log(' Con version inferior se instala igual pero el rewrite no tendra efecto.');
417
+ console.log(' Actualiza con: npm install -g @anthropic-ai/claude-code\n');
418
+ }
419
+ // ok === null: claude no esta en PATH, silencioso
420
+
365
421
  // Install skills
366
422
  const count = installSkills();
367
423
  console.log(` ${count} skills instaladas en .claude/skills/ y .cursor/skills/`);
@@ -453,6 +509,95 @@ function clean() {
453
509
  console.log(' Para eliminar OpenSpec: rm -rf openspec/ .claude/commands/opsx .cursor/commands/opsx\n');
454
510
  }
455
511
 
512
+ // --- Compact subcommands (stats / enable / disable / clear-log) ---
513
+
514
+ function handleCompactSubcommand(sub) {
515
+ switch (sub) {
516
+ case 'stats':
517
+ showCompactStats();
518
+ break;
519
+ case 'disable':
520
+ compactTelemetry.disable();
521
+ console.log(' compact-bash deshabilitado. Reactiva con: refacil-sdd-ai compact enable');
522
+ break;
523
+ case 'enable':
524
+ compactTelemetry.enable();
525
+ console.log(' compact-bash habilitado.');
526
+ break;
527
+ case 'clear-log':
528
+ compactTelemetry.clearLog();
529
+ console.log(' compact.log limpiado.');
530
+ break;
531
+ default:
532
+ console.log('Uso: refacil-sdd-ai compact <stats|disable|enable|clear-log>');
533
+ }
534
+ }
535
+
536
+ function showCompactStats() {
537
+ const s = compactTelemetry.stats();
538
+ if (s.totalEvents === 0) {
539
+ console.log('\n No hay eventos registrados todavia. Ejecuta comandos Bash para generar telemetria de compactacion.\n');
540
+ return;
541
+ }
542
+
543
+ const sortedRewrites = Object.entries(s.byRule)
544
+ .filter(([, data]) => data.rewriteCount > 0)
545
+ .sort((a, b) => b[1].rewriteSaved - a[1].rewriteSaved);
546
+ const sortedAlreadyCompact = Object.entries(s.byRule)
547
+ .filter(([, data]) => data.alreadyCompactCount > 0)
548
+ .sort((a, b) => b[1].alreadyCompactPotential - a[1].alreadyCompactPotential);
549
+
550
+ console.log(`\n compact-bash stats\n`);
551
+ console.log(` Rewrites por hook: ${s.totalRewrites}`);
552
+ console.log(` Comandos ya compactos detectados (skill/agente): ${s.totalAlreadyCompact}\n`);
553
+
554
+ if (sortedRewrites.length > 0) {
555
+ console.log(' Ahorro aplicado por hook (rewrite):');
556
+ for (const [id, data] of sortedRewrites) {
557
+ const kTokens = (data.rewriteSaved / 1000).toFixed(1);
558
+ console.log(
559
+ ` ${id.padEnd(18)} ${String(data.rewriteCount).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens`,
560
+ );
561
+ }
562
+ const totalHookK = (s.totalSaved / 1000).toFixed(1);
563
+ const hookUsd = ((s.totalSaved / 1_000_000) * 3).toFixed(2);
564
+ console.log(` ${'-'.repeat(62)}`);
565
+ console.log(
566
+ ` ${'Total hook'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalHookK.padStart(7)}k tokens (~$${hookUsd} USD)`,
567
+ );
568
+ console.log('');
569
+ }
570
+
571
+ if (sortedAlreadyCompact.length > 0) {
572
+ console.log(' Ahorro potencial ya capturado por skill/agente (sin rewrite):');
573
+ for (const [id, data] of sortedAlreadyCompact) {
574
+ const kTokens = (data.alreadyCompactPotential / 1000).toFixed(1);
575
+ console.log(
576
+ ` ${id.padEnd(18)} ${String(data.alreadyCompactCount).padStart(6)} eventos ~${kTokens.padStart(7)}k tokens potenciales`,
577
+ );
578
+ }
579
+ const totalAgentK = (s.totalAlreadyCompactPotential / 1000).toFixed(1);
580
+ const agentUsd = ((s.totalAlreadyCompactPotential / 1_000_000) * 3).toFixed(2);
581
+ console.log(` ${'-'.repeat(62)}`);
582
+ console.log(
583
+ ` ${'Total skill'.padEnd(18)} ${String(s.totalAlreadyCompact).padStart(6)} eventos ~${totalAgentK.padStart(7)}k tokens (~$${agentUsd} USD)`,
584
+ );
585
+ console.log('');
586
+ }
587
+
588
+ const totalK = (s.totalObservedPotential / 1000).toFixed(1);
589
+ const totalUsd = ((s.totalObservedPotential / 1_000_000) * 3).toFixed(2);
590
+ console.log(` ${'-'.repeat(62)}`);
591
+ console.log(
592
+ ` ${'Total observado'.padEnd(18)} ${String(s.totalEvents).padStart(6)} eventos ~${totalK.padStart(7)}k tokens (~$${totalUsd} USD, Sonnet input)`,
593
+ );
594
+ console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
595
+ if (compactTelemetry.isDisabled()) {
596
+ console.log(' Estado: DESHABILITADO (no se registran nuevos eventos)');
597
+ }
598
+ console.log('');
599
+ }
600
+
456
601
  function help() {
457
602
  console.log(`
458
603
  refacil-sdd-ai — Metodologia SDD-AI con OpenSpec
@@ -462,6 +607,12 @@ function help() {
462
607
  update Re-copia skills (para actualizar a nueva version del paquete)
463
608
  check-update Verifica si hay una version mas reciente en npm y sincroniza compact-guidance en AGENTS.md
464
609
  check-review Verifica que el review se haya completado (usado por hook PreToolUse)
610
+ compact-bash Reescribe comandos Bash bare para reducir tokens (usado por hook PreToolUse)
611
+ compact Subcomandos del hook compact-bash:
612
+ compact stats - Estadisticas completas (hook + ya-compacto) y ahorro estimado
613
+ compact disable - Desactiva el rewrite temporalmente
614
+ compact enable - Re-activa el rewrite
615
+ compact clear-log - Borra el log historico
465
616
  clean Elimina skills y remueve hooks SDD-AI de .claude/settings.json
466
617
  help Muestra esta ayuda
467
618
 
@@ -473,7 +624,7 @@ function help() {
473
624
 
474
625
  Requisitos:
475
626
  - Node.js >= 20.19.0 (requerido por OpenSpec)
476
- - Claude Code o Cursor instalado
627
+ - Claude Code >= 2.1.89 (requerido por compact-bash para rewrite silencioso) o Cursor
477
628
  `);
478
629
  }
479
630
 
@@ -494,6 +645,12 @@ switch (command) {
494
645
  case 'check-review':
495
646
  checkReview();
496
647
  break;
648
+ case 'compact-bash':
649
+ compactBash.run();
650
+ break;
651
+ case 'compact':
652
+ handleCompactSubcommand(process.argv[3]);
653
+ break;
497
654
  case 'clean':
498
655
  clean();
499
656
  break;
@@ -0,0 +1,56 @@
1
+ const fs = require('fs');
2
+ const { findRule, findAlreadyCompactRule } = require('./rules');
3
+ const telemetry = require('./telemetry');
4
+
5
+ function run() {
6
+ let input;
7
+ try {
8
+ const stdin = fs.readFileSync(0, 'utf8');
9
+ if (!stdin.trim()) return;
10
+ input = JSON.parse(stdin);
11
+ } catch (_) {
12
+ return;
13
+ }
14
+
15
+ if (input.tool_name !== 'Bash') return;
16
+ if (telemetry.isDisabled()) return;
17
+
18
+ const origCommand = input.tool_input && input.tool_input.command;
19
+ if (typeof origCommand !== 'string') return;
20
+
21
+ const rule = findRule(origCommand);
22
+ if (!rule) {
23
+ const compactRule = findAlreadyCompactRule(origCommand);
24
+ if (compactRule) {
25
+ telemetry.logAlreadyCompact(compactRule.id, compactRule.savedTokensEst);
26
+ }
27
+ return;
28
+ }
29
+
30
+ let newCommand;
31
+ try {
32
+ newCommand = rule.rewrite(origCommand);
33
+ } catch (_) {
34
+ return;
35
+ }
36
+
37
+ if (!newCommand || newCommand === origCommand) return;
38
+
39
+ telemetry.logRewrite(rule.id, rule.savedTokensEst);
40
+
41
+ const output = {
42
+ hookSpecificOutput: {
43
+ hookEventName: 'PreToolUse',
44
+ permissionDecision: 'allow',
45
+ permissionDecisionReason: `compact-bash: ${rule.reason}`,
46
+ updatedInput: {
47
+ ...input.tool_input,
48
+ command: newCommand,
49
+ },
50
+ },
51
+ };
52
+
53
+ process.stdout.write(JSON.stringify(output));
54
+ }
55
+
56
+ module.exports = { run };
@@ -0,0 +1,236 @@
1
+ function hasFlagAfterBase(cmd, baseTokens) {
2
+ const tokens = cmd.trim().split(/\s+/);
3
+ return tokens.slice(baseTokens).some((t) => t.startsWith('-'));
4
+ }
5
+
6
+ function hasPipeOrRedirect(cmd) {
7
+ return /[|><]/.test(cmd);
8
+ }
9
+
10
+ const RULES = [
11
+ // --- Fase 1: git / tests / docker logs ---
12
+ {
13
+ id: 'git-log',
14
+ match: (cmd) =>
15
+ /^\s*git\s+log(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
16
+ compactMatch: (cmd) =>
17
+ /^\s*git\s+log(\s|$)/.test(cmd) &&
18
+ (/--oneline\b/.test(cmd) || /(^|\s)-\d+\b/.test(cmd)),
19
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+log)/, '$1 --oneline -20'),
20
+ reason: 'git log → --oneline -20',
21
+ savedTokensEst: 850,
22
+ },
23
+ {
24
+ id: 'git-status',
25
+ match: (cmd) =>
26
+ /^\s*git\s+status(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
27
+ compactMatch: (cmd) =>
28
+ /^\s*git\s+status(\s|$)/.test(cmd) &&
29
+ (/\s-s(\s|$)/.test(cmd) || /--short\b/.test(cmd)),
30
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+status)/, '$1 -s'),
31
+ reason: 'git status → -s',
32
+ savedTokensEst: 120,
33
+ },
34
+ {
35
+ id: 'git-diff',
36
+ match: (cmd) => /^\s*git\s+diff\s*$/.test(cmd),
37
+ compactMatch: (cmd) => /^\s*git\s+diff(\s|$)/.test(cmd) && /--stat\b/.test(cmd),
38
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+diff)\s*$/, '$1 --stat'),
39
+ reason: 'git diff → --stat',
40
+ savedTokensEst: 400,
41
+ },
42
+ {
43
+ id: 'git-show',
44
+ match: (cmd) =>
45
+ /^\s*git\s+show(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
46
+ compactMatch: (cmd) => /^\s*git\s+show(\s|$)/.test(cmd) && /--stat\b/.test(cmd),
47
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+show)/, '$1 --stat'),
48
+ reason: 'git show → --stat',
49
+ savedTokensEst: 200,
50
+ },
51
+ {
52
+ id: 'docker-logs',
53
+ match: (cmd) => {
54
+ if (!/^\s*docker\s+logs(\s|$)/.test(cmd)) return false;
55
+ if (/\s--tail\b/.test(cmd)) return false;
56
+ if (/\s-n\s+\d/.test(cmd)) return false;
57
+ if (/\s--since\b/.test(cmd)) return false;
58
+ return true;
59
+ },
60
+ compactMatch: (cmd) =>
61
+ /^\s*docker\s+logs(\s|$)/.test(cmd) &&
62
+ (/\s--tail\b/.test(cmd) || /\s-n\s+\d/.test(cmd) || /\s--since\b/.test(cmd)),
63
+ rewrite: (cmd) => cmd.replace(/^(\s*docker\s+logs)/, '$1 --tail 100'),
64
+ reason: 'docker logs → --tail 100',
65
+ savedTokensEst: 1500,
66
+ },
67
+ {
68
+ id: 'pkg-test',
69
+ match: (cmd) => /^\s*(npm|yarn|pnpm)\s+(test|t)\s*$/.test(cmd),
70
+ compactMatch: (cmd) =>
71
+ /^\s*(npm|yarn|pnpm)\s+(test|t)\b/.test(cmd) &&
72
+ (hasPipeOrRedirect(cmd) || /--silent\b/.test(cmd) || /\b-q\b/.test(cmd)),
73
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -80`,
74
+ reason: 'test bare → tail -80',
75
+ savedTokensEst: 2400,
76
+ },
77
+ {
78
+ id: 'jest-bare',
79
+ match: (cmd) => /^\s*(npx\s+)?jest\s*$/.test(cmd),
80
+ compactMatch: (cmd) =>
81
+ /^\s*(npx\s+)?jest(\s|$)/.test(cmd) &&
82
+ (/--silent\b/.test(cmd) || /--reporters=summary\b/.test(cmd)),
83
+ rewrite: (cmd) =>
84
+ cmd.replace(/^(\s*(?:npx\s+)?jest)\s*$/, '$1 --silent --reporters=summary'),
85
+ reason: 'jest → silent summary',
86
+ savedTokensEst: 1800,
87
+ },
88
+ {
89
+ id: 'pytest-bare',
90
+ match: (cmd) => /^\s*pytest\s*$/.test(cmd),
91
+ compactMatch: (cmd) => /^\s*pytest(\s|$)/.test(cmd) && /(^|\s)-q(\s|$)/.test(cmd),
92
+ rewrite: (cmd) => cmd.replace(/^(\s*pytest)\s*$/, '$1 -q'),
93
+ reason: 'pytest → -q',
94
+ savedTokensEst: 600,
95
+ },
96
+ // --- Fase 2A: linters / type checkers / build ---
97
+ {
98
+ id: 'eslint',
99
+ match: (cmd) => /^\s*eslint(\s+[^-]\S*)*\s*$/.test(cmd),
100
+ compactMatch: (cmd) =>
101
+ /^\s*eslint(\s|$)/.test(cmd) &&
102
+ (/--format\s+compact\b/.test(cmd) || /--quiet\b/.test(cmd)),
103
+ rewrite: (cmd) => {
104
+ const tokens = cmd.trim().split(/\s+/);
105
+ if (tokens.length === 1) {
106
+ return 'eslint . --format compact --quiet';
107
+ }
108
+ return `${cmd.trim()} --format compact`;
109
+ },
110
+ reason: 'eslint → --format compact',
111
+ savedTokensEst: 700,
112
+ },
113
+ {
114
+ id: 'biome-check',
115
+ match: (cmd) =>
116
+ /^\s*biome\s+check(\s|$)/.test(cmd) && !/--reporter\b/.test(cmd),
117
+ compactMatch: (cmd) =>
118
+ /^\s*biome\s+check(\s|$)/.test(cmd) && /--reporter=summary\b/.test(cmd),
119
+ rewrite: (cmd) =>
120
+ cmd.replace(/^(\s*biome\s+check)/, '$1 --reporter=summary'),
121
+ reason: 'biome check → --reporter=summary',
122
+ savedTokensEst: 500,
123
+ },
124
+ {
125
+ id: 'tsc',
126
+ match: (cmd) => {
127
+ if (!/^\s*(npx\s+)?tsc(\s|$)/.test(cmd)) return false;
128
+ if (/(^|\s)--watch\b|(^|\s)-w\b/.test(cmd)) return false;
129
+ if (hasPipeOrRedirect(cmd)) return false;
130
+ return true;
131
+ },
132
+ compactMatch: (cmd) =>
133
+ /^\s*(npx\s+)?tsc(\s|$)/.test(cmd) && hasPipeOrRedirect(cmd),
134
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | head -80`,
135
+ reason: 'tsc → head -80',
136
+ savedTokensEst: 1200,
137
+ },
138
+ {
139
+ id: 'prettier-check',
140
+ match: (cmd) =>
141
+ /^\s*prettier\s+--check\b/.test(cmd) && !/--loglevel\b/.test(cmd),
142
+ compactMatch: (cmd) =>
143
+ /^\s*prettier\s+--check\b/.test(cmd) && /--loglevel\b/.test(cmd),
144
+ rewrite: (cmd) =>
145
+ cmd.replace(/^(\s*prettier\s+--check)/, '$1 --loglevel warn'),
146
+ reason: 'prettier --check → --loglevel warn',
147
+ savedTokensEst: 300,
148
+ },
149
+ {
150
+ id: 'npm-audit',
151
+ match: (cmd) => /^\s*npm\s+audit\s*$/.test(cmd),
152
+ compactMatch: (cmd) => /^\s*npm\s+audit(\s|$)/.test(cmd) && hasPipeOrRedirect(cmd),
153
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -10`,
154
+ reason: 'npm audit → tail -10',
155
+ savedTokensEst: 900,
156
+ },
157
+ {
158
+ id: 'npm-ls',
159
+ match: (cmd) => /^\s*npm\s+ls\s*$/.test(cmd),
160
+ compactMatch: (cmd) => /^\s*npm\s+ls(\s|$)/.test(cmd) && /--depth=0\b/.test(cmd),
161
+ rewrite: (cmd) => cmd.replace(/^(\s*npm\s+ls)/, '$1 --depth=0'),
162
+ reason: 'npm ls → --depth=0',
163
+ savedTokensEst: 700,
164
+ },
165
+ {
166
+ id: 'cargo-bare',
167
+ match: (cmd) => /^\s*cargo\s+(build|test|check)\s*$/.test(cmd),
168
+ compactMatch: (cmd) =>
169
+ /^\s*cargo\s+(build|test|check)(\s|$)/.test(cmd) && /--quiet\b/.test(cmd),
170
+ rewrite: (cmd) => `${cmd.trim()} --quiet`,
171
+ reason: 'cargo → --quiet',
172
+ savedTokensEst: 400,
173
+ },
174
+ {
175
+ id: 'go-test',
176
+ match: (cmd) => {
177
+ if (!/^\s*go\s+test\b/.test(cmd)) return false;
178
+ const rest = cmd.trim().substring('go test'.length);
179
+ if (/\s-\S/.test(rest)) return false;
180
+ if (hasPipeOrRedirect(cmd)) return false;
181
+ return true;
182
+ },
183
+ compactMatch: (cmd) => /^\s*go\s+test(\s|$)/.test(cmd) && hasPipeOrRedirect(cmd),
184
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -80`,
185
+ reason: 'go test → tail -80',
186
+ savedTokensEst: 1500,
187
+ },
188
+ {
189
+ id: 'mvn-test',
190
+ match: (cmd) => /^\s*mvn\s+test\s*$/.test(cmd),
191
+ compactMatch: (cmd) => /^\s*mvn\s+test(\s|$)/.test(cmd) && /(^|\s)-q(\s|$)/.test(cmd),
192
+ rewrite: (cmd) => `${cmd.trim()} -q`,
193
+ reason: 'mvn test → -q',
194
+ savedTokensEst: 1800,
195
+ },
196
+ {
197
+ id: 'gradle-test',
198
+ match: (cmd) => /^\s*(\.\/gradlew|gradle)\s+test\s*$/.test(cmd),
199
+ compactMatch: (cmd) =>
200
+ /^\s*(\.\/gradlew|gradle)\s+test(\s|$)/.test(cmd) &&
201
+ /(^|\s)-q(\s|$)/.test(cmd),
202
+ rewrite: (cmd) => `${cmd.trim()} -q`,
203
+ reason: 'gradle test → -q',
204
+ savedTokensEst: 1500,
205
+ },
206
+ {
207
+ id: 'ps-aux',
208
+ // Unix-only: en Windows `ps` mapea a PowerShell Get-Process y no entiende estos flags
209
+ match: (cmd) =>
210
+ process.platform !== 'win32' && /^\s*ps\s+aux\s*$/.test(cmd),
211
+ rewrite: () => 'ps -eo pid,pcpu,pmem,comm | head -30',
212
+ reason: 'ps aux → compact columns + head -30',
213
+ savedTokensEst: 800,
214
+ },
215
+ ];
216
+
217
+ function findRule(cmd) {
218
+ if (typeof cmd !== 'string' || !cmd.trim()) return null;
219
+ if (/\bCOMPACT=0\b/.test(cmd)) return null;
220
+ for (const rule of RULES) {
221
+ if (rule.match(cmd)) return rule;
222
+ }
223
+ return null;
224
+ }
225
+
226
+ function findAlreadyCompactRule(cmd) {
227
+ if (typeof cmd !== 'string' || !cmd.trim()) return null;
228
+ for (const rule of RULES) {
229
+ if (typeof rule.compactMatch === 'function' && rule.compactMatch(cmd)) {
230
+ return rule;
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+
236
+ module.exports = { RULES, findRule, findAlreadyCompactRule };
@@ -0,0 +1,137 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const HOME_DIR = path.join(os.homedir(), '.refacil-sdd-ai');
6
+ const LOG_PATH = path.join(HOME_DIR, 'compact.log');
7
+ const DISABLED_PATH = path.join(HOME_DIR, 'disabled');
8
+
9
+ function ensureDir() {
10
+ try {
11
+ fs.mkdirSync(HOME_DIR, { recursive: true });
12
+ } catch (_) {}
13
+ }
14
+
15
+ function isDisabled() {
16
+ return fs.existsSync(DISABLED_PATH);
17
+ }
18
+
19
+ function logEvent(eventType, ruleId, savedTokensEst, meta = {}) {
20
+ try {
21
+ ensureDir();
22
+ const line =
23
+ JSON.stringify({
24
+ ts: new Date().toISOString(),
25
+ eventType: eventType || 'hook_rewrite',
26
+ ruleId,
27
+ savedTokensEst: savedTokensEst || 0,
28
+ ...meta,
29
+ }) + '\n';
30
+ fs.appendFileSync(LOG_PATH, line);
31
+ } catch (_) {
32
+ // Telemetry must never break the hook
33
+ }
34
+ }
35
+
36
+ function logRewrite(ruleId, savedTokensEst) {
37
+ logEvent('hook_rewrite', ruleId, savedTokensEst);
38
+ }
39
+
40
+ function logAlreadyCompact(ruleId, savedTokensEst) {
41
+ // Assumption: command arrived compact due to skills/agent discipline.
42
+ logEvent('already_compact', ruleId, savedTokensEst, {
43
+ source: 'skill_assumed',
44
+ });
45
+ }
46
+
47
+ function readLog() {
48
+ try {
49
+ const content = fs.readFileSync(LOG_PATH, 'utf8');
50
+ return content
51
+ .split('\n')
52
+ .filter(Boolean)
53
+ .map((line) => {
54
+ try {
55
+ return JSON.parse(line);
56
+ } catch (_) {
57
+ return null;
58
+ }
59
+ })
60
+ .filter(Boolean);
61
+ } catch (_) {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ function stats() {
67
+ const entries = readLog();
68
+ const byRule = {};
69
+ let totalSaved = 0;
70
+ let totalAlreadyCompactPotential = 0;
71
+ let totalAlreadyCompact = 0;
72
+ let totalRewrites = 0;
73
+
74
+ for (const e of entries) {
75
+ const eventType = e.eventType || 'hook_rewrite';
76
+ if (!byRule[e.ruleId]) {
77
+ byRule[e.ruleId] = {
78
+ rewriteCount: 0,
79
+ rewriteSaved: 0,
80
+ alreadyCompactCount: 0,
81
+ alreadyCompactPotential: 0,
82
+ };
83
+ }
84
+
85
+ if (eventType === 'already_compact') {
86
+ byRule[e.ruleId].alreadyCompactCount++;
87
+ byRule[e.ruleId].alreadyCompactPotential += e.savedTokensEst || 0;
88
+ totalAlreadyCompact++;
89
+ totalAlreadyCompactPotential += e.savedTokensEst || 0;
90
+ } else {
91
+ byRule[e.ruleId].rewriteCount++;
92
+ byRule[e.ruleId].rewriteSaved += e.savedTokensEst || 0;
93
+ totalRewrites++;
94
+ totalSaved += e.savedTokensEst || 0;
95
+ }
96
+ }
97
+
98
+ return {
99
+ byRule,
100
+ totalSaved,
101
+ totalRewrites,
102
+ totalAlreadyCompact,
103
+ totalAlreadyCompactPotential,
104
+ totalObservedPotential: totalSaved + totalAlreadyCompactPotential,
105
+ totalEvents: entries.length,
106
+ };
107
+ }
108
+
109
+ function disable() {
110
+ ensureDir();
111
+ fs.writeFileSync(DISABLED_PATH, new Date().toISOString());
112
+ }
113
+
114
+ function enable() {
115
+ try {
116
+ fs.unlinkSync(DISABLED_PATH);
117
+ } catch (_) {}
118
+ }
119
+
120
+ function clearLog() {
121
+ try {
122
+ fs.unlinkSync(LOG_PATH);
123
+ } catch (_) {}
124
+ }
125
+
126
+ module.exports = {
127
+ HOME_DIR,
128
+ LOG_PATH,
129
+ DISABLED_PATH,
130
+ isDisabled,
131
+ logRewrite,
132
+ logAlreadyCompact,
133
+ stats,
134
+ disable,
135
+ enable,
136
+ clearLog,
137
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refacil-sdd-ai",
3
- "version": "2.4.0",
3
+ "version": "2.7.0",
4
4
  "description": "SDD-AI: Specification-Driven Development with AI — metodologia de desarrollo con IA usando OpenSpec, Claude Code y Cursor",
5
5
  "bin": {
6
6
  "refacil-sdd-ai": "./bin/cli.js"