refacil-sdd-ai 2.4.0 → 2.6.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 # Tabla de rewrites + tokens estimados + USD ahorrado
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 # tabla de rewrites + 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 rewrite genera una linea JSON en `~/.refacil-sdd-ai/compact.log` con timestamp, ruleId y estimacion de tokens ahorrados. El log se usa para el comando `compact stats` que agrega por regla 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,59 @@ 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.totalRewrites === 0) {
539
+ console.log('\n No hay rewrites registrados todavia. El hook compact-bash aun no se ha disparado o esta deshabilitado.\n');
540
+ return;
541
+ }
542
+
543
+ const sorted = Object.entries(s.byRule).sort((a, b) => b[1].saved - a[1].saved);
544
+
545
+ console.log(`\n compact-bash stats (${s.totalRewrites} rewrites en total)\n`);
546
+ for (const [id, data] of sorted) {
547
+ const kTokens = (data.saved / 1000).toFixed(1);
548
+ console.log(
549
+ ` ${id.padEnd(18)} ${String(data.count).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens estimados`,
550
+ );
551
+ }
552
+ const totalK = (s.totalSaved / 1000).toFixed(1);
553
+ const costUsd = ((s.totalSaved / 1_000_000) * 3).toFixed(2);
554
+ console.log(` ${'-'.repeat(62)}`);
555
+ console.log(
556
+ ` ${'Total'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalK.padStart(7)}k tokens (~$${costUsd} USD, Sonnet input)`,
557
+ );
558
+ console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
559
+ if (compactTelemetry.isDisabled()) {
560
+ console.log(' Estado: DESHABILITADO (no se registran nuevos rewrites)');
561
+ }
562
+ console.log('');
563
+ }
564
+
456
565
  function help() {
457
566
  console.log(`
458
567
  refacil-sdd-ai — Metodologia SDD-AI con OpenSpec
@@ -462,6 +571,12 @@ function help() {
462
571
  update Re-copia skills (para actualizar a nueva version del paquete)
463
572
  check-update Verifica si hay una version mas reciente en npm y sincroniza compact-guidance en AGENTS.md
464
573
  check-review Verifica que el review se haya completado (usado por hook PreToolUse)
574
+ compact-bash Reescribe comandos Bash bare para reducir tokens (usado por hook PreToolUse)
575
+ compact Subcomandos del hook compact-bash:
576
+ compact stats - Estadisticas de rewrites y ahorro estimado
577
+ compact disable - Desactiva el rewrite temporalmente
578
+ compact enable - Re-activa el rewrite
579
+ compact clear-log - Borra el log historico
465
580
  clean Elimina skills y remueve hooks SDD-AI de .claude/settings.json
466
581
  help Muestra esta ayuda
467
582
 
@@ -473,7 +588,7 @@ function help() {
473
588
 
474
589
  Requisitos:
475
590
  - Node.js >= 20.19.0 (requerido por OpenSpec)
476
- - Claude Code o Cursor instalado
591
+ - Claude Code >= 2.1.89 (requerido por compact-bash para rewrite silencioso) o Cursor
477
592
  `);
478
593
  }
479
594
 
@@ -494,6 +609,12 @@ switch (command) {
494
609
  case 'check-review':
495
610
  checkReview();
496
611
  break;
612
+ case 'compact-bash':
613
+ compactBash.run();
614
+ break;
615
+ case 'compact':
616
+ handleCompactSubcommand(process.argv[3]);
617
+ break;
497
618
  case 'clean':
498
619
  clean();
499
620
  break;
@@ -0,0 +1,50 @@
1
+ const fs = require('fs');
2
+ const { findRule } = 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) return;
23
+
24
+ let newCommand;
25
+ try {
26
+ newCommand = rule.rewrite(origCommand);
27
+ } catch (_) {
28
+ return;
29
+ }
30
+
31
+ if (!newCommand || newCommand === origCommand) return;
32
+
33
+ telemetry.logRewrite(rule.id, rule.savedTokensEst);
34
+
35
+ const output = {
36
+ hookSpecificOutput: {
37
+ hookEventName: 'PreToolUse',
38
+ permissionDecision: 'allow',
39
+ permissionDecisionReason: `compact-bash: ${rule.reason}`,
40
+ updatedInput: {
41
+ ...input.tool_input,
42
+ command: newCommand,
43
+ },
44
+ },
45
+ };
46
+
47
+ process.stdout.write(JSON.stringify(output));
48
+ }
49
+
50
+ module.exports = { run };
@@ -0,0 +1,190 @@
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
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+log)/, '$1 --oneline -20'),
17
+ reason: 'git log → --oneline -20',
18
+ savedTokensEst: 850,
19
+ },
20
+ {
21
+ id: 'git-status',
22
+ match: (cmd) =>
23
+ /^\s*git\s+status(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
24
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+status)/, '$1 -s'),
25
+ reason: 'git status → -s',
26
+ savedTokensEst: 120,
27
+ },
28
+ {
29
+ id: 'git-diff',
30
+ match: (cmd) => /^\s*git\s+diff\s*$/.test(cmd),
31
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+diff)\s*$/, '$1 --stat'),
32
+ reason: 'git diff → --stat',
33
+ savedTokensEst: 400,
34
+ },
35
+ {
36
+ id: 'git-show',
37
+ match: (cmd) =>
38
+ /^\s*git\s+show(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
39
+ rewrite: (cmd) => cmd.replace(/^(\s*git\s+show)/, '$1 --stat'),
40
+ reason: 'git show → --stat',
41
+ savedTokensEst: 200,
42
+ },
43
+ {
44
+ id: 'docker-logs',
45
+ match: (cmd) => {
46
+ if (!/^\s*docker\s+logs(\s|$)/.test(cmd)) return false;
47
+ if (/\s--tail\b/.test(cmd)) return false;
48
+ if (/\s-n\s+\d/.test(cmd)) return false;
49
+ if (/\s--since\b/.test(cmd)) return false;
50
+ return true;
51
+ },
52
+ rewrite: (cmd) => cmd.replace(/^(\s*docker\s+logs)/, '$1 --tail 100'),
53
+ reason: 'docker logs → --tail 100',
54
+ savedTokensEst: 1500,
55
+ },
56
+ {
57
+ id: 'pkg-test',
58
+ match: (cmd) => /^\s*(npm|yarn|pnpm)\s+(test|t)\s*$/.test(cmd),
59
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -80`,
60
+ reason: 'test bare → tail -80',
61
+ savedTokensEst: 2400,
62
+ },
63
+ {
64
+ id: 'jest-bare',
65
+ match: (cmd) => /^\s*(npx\s+)?jest\s*$/.test(cmd),
66
+ rewrite: (cmd) =>
67
+ cmd.replace(/^(\s*(?:npx\s+)?jest)\s*$/, '$1 --silent --reporters=summary'),
68
+ reason: 'jest → silent summary',
69
+ savedTokensEst: 1800,
70
+ },
71
+ {
72
+ id: 'pytest-bare',
73
+ match: (cmd) => /^\s*pytest\s*$/.test(cmd),
74
+ rewrite: (cmd) => cmd.replace(/^(\s*pytest)\s*$/, '$1 -q'),
75
+ reason: 'pytest → -q',
76
+ savedTokensEst: 600,
77
+ },
78
+ // --- Fase 2A: linters / type checkers / build ---
79
+ {
80
+ id: 'eslint',
81
+ match: (cmd) => /^\s*eslint(\s+[^-]\S*)*\s*$/.test(cmd),
82
+ rewrite: (cmd) => {
83
+ const tokens = cmd.trim().split(/\s+/);
84
+ if (tokens.length === 1) {
85
+ return 'eslint . --format compact --quiet';
86
+ }
87
+ return `${cmd.trim()} --format compact`;
88
+ },
89
+ reason: 'eslint → --format compact',
90
+ savedTokensEst: 700,
91
+ },
92
+ {
93
+ id: 'biome-check',
94
+ match: (cmd) =>
95
+ /^\s*biome\s+check(\s|$)/.test(cmd) && !/--reporter\b/.test(cmd),
96
+ rewrite: (cmd) =>
97
+ cmd.replace(/^(\s*biome\s+check)/, '$1 --reporter=summary'),
98
+ reason: 'biome check → --reporter=summary',
99
+ savedTokensEst: 500,
100
+ },
101
+ {
102
+ id: 'tsc',
103
+ match: (cmd) => {
104
+ if (!/^\s*(npx\s+)?tsc(\s|$)/.test(cmd)) return false;
105
+ if (/(^|\s)--watch\b|(^|\s)-w\b/.test(cmd)) return false;
106
+ if (hasPipeOrRedirect(cmd)) return false;
107
+ return true;
108
+ },
109
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | head -80`,
110
+ reason: 'tsc → head -80',
111
+ savedTokensEst: 1200,
112
+ },
113
+ {
114
+ id: 'prettier-check',
115
+ match: (cmd) =>
116
+ /^\s*prettier\s+--check\b/.test(cmd) && !/--loglevel\b/.test(cmd),
117
+ rewrite: (cmd) =>
118
+ cmd.replace(/^(\s*prettier\s+--check)/, '$1 --loglevel warn'),
119
+ reason: 'prettier --check → --loglevel warn',
120
+ savedTokensEst: 300,
121
+ },
122
+ {
123
+ id: 'npm-audit',
124
+ match: (cmd) => /^\s*npm\s+audit\s*$/.test(cmd),
125
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -10`,
126
+ reason: 'npm audit → tail -10',
127
+ savedTokensEst: 900,
128
+ },
129
+ {
130
+ id: 'npm-ls',
131
+ match: (cmd) => /^\s*npm\s+ls\s*$/.test(cmd),
132
+ rewrite: (cmd) => cmd.replace(/^(\s*npm\s+ls)/, '$1 --depth=0'),
133
+ reason: 'npm ls → --depth=0',
134
+ savedTokensEst: 700,
135
+ },
136
+ {
137
+ id: 'cargo-bare',
138
+ match: (cmd) => /^\s*cargo\s+(build|test|check)\s*$/.test(cmd),
139
+ rewrite: (cmd) => `${cmd.trim()} --quiet`,
140
+ reason: 'cargo → --quiet',
141
+ savedTokensEst: 400,
142
+ },
143
+ {
144
+ id: 'go-test',
145
+ match: (cmd) => {
146
+ if (!/^\s*go\s+test\b/.test(cmd)) return false;
147
+ const rest = cmd.trim().substring('go test'.length);
148
+ if (/\s-\S/.test(rest)) return false;
149
+ if (hasPipeOrRedirect(cmd)) return false;
150
+ return true;
151
+ },
152
+ rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -80`,
153
+ reason: 'go test → tail -80',
154
+ savedTokensEst: 1500,
155
+ },
156
+ {
157
+ id: 'mvn-test',
158
+ match: (cmd) => /^\s*mvn\s+test\s*$/.test(cmd),
159
+ rewrite: (cmd) => `${cmd.trim()} -q`,
160
+ reason: 'mvn test → -q',
161
+ savedTokensEst: 1800,
162
+ },
163
+ {
164
+ id: 'gradle-test',
165
+ match: (cmd) => /^\s*(\.\/gradlew|gradle)\s+test\s*$/.test(cmd),
166
+ rewrite: (cmd) => `${cmd.trim()} -q`,
167
+ reason: 'gradle test → -q',
168
+ savedTokensEst: 1500,
169
+ },
170
+ {
171
+ id: 'ps-aux',
172
+ // Unix-only: en Windows `ps` mapea a PowerShell Get-Process y no entiende estos flags
173
+ match: (cmd) =>
174
+ process.platform !== 'win32' && /^\s*ps\s+aux\s*$/.test(cmd),
175
+ rewrite: () => 'ps -eo pid,pcpu,pmem,comm | head -30',
176
+ reason: 'ps aux → compact columns + head -30',
177
+ savedTokensEst: 800,
178
+ },
179
+ ];
180
+
181
+ function findRule(cmd) {
182
+ if (typeof cmd !== 'string' || !cmd.trim()) return null;
183
+ if (/\bCOMPACT=0\b/.test(cmd)) return null;
184
+ for (const rule of RULES) {
185
+ if (rule.match(cmd)) return rule;
186
+ }
187
+ return null;
188
+ }
189
+
190
+ module.exports = { RULES, findRule };
@@ -0,0 +1,93 @@
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 logRewrite(ruleId, savedTokensEst) {
20
+ try {
21
+ ensureDir();
22
+ const line =
23
+ JSON.stringify({
24
+ ts: new Date().toISOString(),
25
+ ruleId,
26
+ savedTokensEst: savedTokensEst || 0,
27
+ }) + '\n';
28
+ fs.appendFileSync(LOG_PATH, line);
29
+ } catch (_) {
30
+ // Telemetry must never break the hook
31
+ }
32
+ }
33
+
34
+ function readLog() {
35
+ try {
36
+ const content = fs.readFileSync(LOG_PATH, 'utf8');
37
+ return content
38
+ .split('\n')
39
+ .filter(Boolean)
40
+ .map((line) => {
41
+ try {
42
+ return JSON.parse(line);
43
+ } catch (_) {
44
+ return null;
45
+ }
46
+ })
47
+ .filter(Boolean);
48
+ } catch (_) {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ function stats() {
54
+ const entries = readLog();
55
+ const byRule = {};
56
+ let totalSaved = 0;
57
+ for (const e of entries) {
58
+ if (!byRule[e.ruleId]) byRule[e.ruleId] = { count: 0, saved: 0 };
59
+ byRule[e.ruleId].count++;
60
+ byRule[e.ruleId].saved += e.savedTokensEst || 0;
61
+ totalSaved += e.savedTokensEst || 0;
62
+ }
63
+ return { byRule, totalSaved, totalRewrites: entries.length };
64
+ }
65
+
66
+ function disable() {
67
+ ensureDir();
68
+ fs.writeFileSync(DISABLED_PATH, new Date().toISOString());
69
+ }
70
+
71
+ function enable() {
72
+ try {
73
+ fs.unlinkSync(DISABLED_PATH);
74
+ } catch (_) {}
75
+ }
76
+
77
+ function clearLog() {
78
+ try {
79
+ fs.unlinkSync(LOG_PATH);
80
+ } catch (_) {}
81
+ }
82
+
83
+ module.exports = {
84
+ HOME_DIR,
85
+ LOG_PATH,
86
+ DISABLED_PATH,
87
+ isDisabled,
88
+ logRewrite,
89
+ stats,
90
+ disable,
91
+ enable,
92
+ clearLog,
93
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refacil-sdd-ai",
3
- "version": "2.4.0",
3
+ "version": "2.6.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"