refacil-sdd-ai 5.2.2 → 5.3.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.
Files changed (76) hide show
  1. package/NOTICE.md +46 -0
  2. package/README.md +209 -42
  3. package/agents/auditor.md +46 -0
  4. package/agents/debugger.md +41 -1
  5. package/agents/implementer.md +76 -10
  6. package/agents/investigator.md +36 -0
  7. package/agents/proposer.md +46 -2
  8. package/agents/tester.md +45 -8
  9. package/agents/validator.md +67 -13
  10. package/bin/cli.js +428 -83
  11. package/bin/postinstall.js +20 -0
  12. package/lib/bus/broker.js +121 -3
  13. package/lib/bus/spawn.js +189 -121
  14. package/lib/check-review.js +102 -0
  15. package/lib/codegraph-telemetry.js +135 -0
  16. package/lib/codegraph.js +273 -0
  17. package/lib/commands/autopilot.js +120 -0
  18. package/lib/commands/bus.js +29 -36
  19. package/lib/commands/compact.js +185 -46
  20. package/lib/commands/read-spec.js +352 -0
  21. package/lib/commands/sdd.js +429 -44
  22. package/lib/compact-guidance.js +122 -77
  23. package/lib/config.js +136 -0
  24. package/lib/global-paths.js +56 -20
  25. package/lib/hooks.js +32 -4
  26. package/lib/ide-detection.js +1 -1
  27. package/lib/ignore-files.js +5 -1
  28. package/lib/installer.js +202 -19
  29. package/lib/kapso.js +241 -0
  30. package/lib/methodology-migration-pending.js +13 -0
  31. package/lib/open-browser.js +32 -0
  32. package/lib/opencode-migrate.js +148 -0
  33. package/lib/opencode-plugin/index.js +84 -104
  34. package/lib/opencode-plugin/rules.js +236 -0
  35. package/lib/project-root.js +154 -0
  36. package/lib/repo-ide-sync.js +5 -0
  37. package/lib/spec-reader/lang.js +72 -0
  38. package/lib/spec-reader/md-parser.js +299 -0
  39. package/lib/spec-reader/session.js +139 -0
  40. package/lib/spec-reader/ui/app.js +685 -0
  41. package/lib/spec-reader/ui/index.html +59 -0
  42. package/lib/spec-reader/ui/mixed-lang.js +200 -0
  43. package/lib/spec-reader/ui/model-cache.js +117 -0
  44. package/lib/spec-reader/ui/style.css +294 -0
  45. package/lib/spec-reader/ui/supertonic-helper.js +565 -0
  46. package/lib/spec-sync.js +258 -0
  47. package/lib/test-scope.js +713 -0
  48. package/lib/testing-policy-sync.js +14 -2
  49. package/package.json +6 -3
  50. package/skills/apply/SKILL.md +39 -64
  51. package/skills/archive/SKILL.md +74 -48
  52. package/skills/ask/SKILL.md +43 -8
  53. package/skills/autopilot/SKILL.md +476 -0
  54. package/skills/bug/SKILL.md +52 -53
  55. package/skills/explore/SKILL.md +48 -1
  56. package/skills/guide/SKILL.md +31 -13
  57. package/skills/inbox/SKILL.md +9 -0
  58. package/skills/join/SKILL.md +1 -1
  59. package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
  60. package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
  61. package/skills/prereqs/SKILL.md +1 -1
  62. package/skills/propose/SKILL.md +74 -19
  63. package/skills/read-spec/SKILL.md +76 -0
  64. package/skills/reply/SKILL.md +42 -9
  65. package/skills/review/SKILL.md +63 -25
  66. package/skills/review/checklist.md +2 -2
  67. package/skills/say/SKILL.md +40 -4
  68. package/skills/setup/SKILL.md +59 -5
  69. package/skills/setup/troubleshooting.md +11 -3
  70. package/skills/stats/SKILL.md +157 -0
  71. package/skills/test/SKILL.md +35 -10
  72. package/skills/up-code/SKILL.md +20 -13
  73. package/skills/update/SKILL.md +32 -1
  74. package/skills/verify/SKILL.md +78 -41
  75. package/templates/compact-guidance.md +10 -0
  76. package/templates/methodology-guide.md +5 -0
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { globalOpenCodeDir, legacyOpenCodeDirs } = require('./global-paths');
6
+
7
+ const REFACIL_PREFIX = 'refacil-';
8
+ const PLUGIN_FILES = ['refacil-hooks.js', 'refacil-check-review.js', 'rules.js'];
9
+
10
+ function listRefacilEntries(dir) {
11
+ if (!fs.existsSync(dir)) return [];
12
+ try {
13
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.name.startsWith(REFACIL_PREFIX));
14
+ } catch (_) {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function primaryHasRefacilSkills(primaryDir) {
20
+ const skillsDir = path.join(primaryDir, 'skills');
21
+ return listRefacilEntries(skillsDir).length > 0;
22
+ }
23
+
24
+ function primaryHasRefacilAgents(primaryDir) {
25
+ const agentsDir = path.join(primaryDir, 'agents');
26
+ return listRefacilEntries(agentsDir).length > 0;
27
+ }
28
+
29
+ function copyRefacilTree(srcDir, destDir) {
30
+ if (!fs.existsSync(srcDir)) return 0;
31
+ let copied = 0;
32
+ fs.mkdirSync(destDir, { recursive: true });
33
+ for (const entry of listRefacilEntries(srcDir)) {
34
+ const srcPath = path.join(srcDir, entry.name);
35
+ const destPath = path.join(destDir, entry.name);
36
+ if (fs.existsSync(destPath)) continue;
37
+ try {
38
+ if (entry.isDirectory()) {
39
+ fs.cpSync(srcPath, destPath, { recursive: true });
40
+ } else {
41
+ fs.copyFileSync(srcPath, destPath);
42
+ }
43
+ copied++;
44
+ } catch (_) {}
45
+ }
46
+ return copied;
47
+ }
48
+
49
+ function copyRefacilPlugins(srcPlugins, destPlugins) {
50
+ if (!fs.existsSync(srcPlugins)) return 0;
51
+ let copied = 0;
52
+ fs.mkdirSync(destPlugins, { recursive: true });
53
+ for (const name of PLUGIN_FILES) {
54
+ const src = path.join(srcPlugins, name);
55
+ const dest = path.join(destPlugins, name);
56
+ if (!fs.existsSync(src) || fs.existsSync(dest)) continue;
57
+ try {
58
+ fs.copyFileSync(src, dest);
59
+ copied++;
60
+ } catch (_) {}
61
+ }
62
+ return copied;
63
+ }
64
+
65
+ /**
66
+ * Non-destructive migration: copy refacil artifacts from legacy OpenCode dirs into the primary config dir.
67
+ * Emits stderr warnings when migration occurs. Safe to call on every install.
68
+ * @param {string} [homeDir] - injectable for testing
69
+ * @returns {{ migrated: boolean, fromDirs: string[] }}
70
+ */
71
+ function migrateOpenCodeLegacyArtifacts(homeDir) {
72
+ const primary = globalOpenCodeDir(homeDir);
73
+ const needsSkills = !primaryHasRefacilSkills(primary);
74
+ const needsAgents = !primaryHasRefacilAgents(primary);
75
+ const pluginsDir = path.join(primary, 'plugins');
76
+ const needsPlugins = !PLUGIN_FILES.every((f) => fs.existsSync(path.join(pluginsDir, f)));
77
+
78
+ if (!needsSkills && !needsAgents && !needsPlugins) {
79
+ for (const legacyDir of legacyOpenCodeDirs(homeDir)) {
80
+ if (legacyDir === primary || !fs.existsSync(legacyDir)) continue;
81
+ const legacySkills = listRefacilEntries(path.join(legacyDir, 'skills'));
82
+ const legacyAgents = listRefacilEntries(path.join(legacyDir, 'agents'));
83
+ const legacyPlugins = path.join(legacyDir, 'plugins');
84
+ const hasLegacyPlugins = PLUGIN_FILES.some((f) => fs.existsSync(path.join(legacyPlugins, f)));
85
+ if (legacySkills.length > 0 || legacyAgents.length > 0 || hasLegacyPlugins) {
86
+ process.stderr.write(
87
+ `[refacil-sdd-ai] OpenCode primary config already has refacil artifacts; skipped legacy migration from ${legacyDir} (no overwrite).\n`,
88
+ );
89
+ }
90
+ }
91
+ return { migrated: false, fromDirs: [] };
92
+ }
93
+
94
+ const fromDirs = [];
95
+ let totalCopied = 0;
96
+
97
+ for (const legacyDir of legacyOpenCodeDirs(homeDir)) {
98
+ if (legacyDir === primary || !fs.existsSync(legacyDir)) continue;
99
+
100
+ const legacySkillsDir = path.join(legacyDir, 'skills');
101
+ const legacyAgentsDir = path.join(legacyDir, 'agents');
102
+ const legacyPluginsDir = path.join(legacyDir, 'plugins');
103
+ const legacySkills = listRefacilEntries(legacySkillsDir);
104
+ const legacyAgents = listRefacilEntries(legacyAgentsDir);
105
+ const hasLegacyPlugins = PLUGIN_FILES.some((f) => fs.existsSync(path.join(legacyPluginsDir, f)));
106
+
107
+ if (!needsSkills && legacySkills.length > 0) {
108
+ process.stderr.write(
109
+ `[refacil-sdd-ai] OpenCode primary config already has refacil artifacts; skipped legacy migration from ${legacyDir} (no overwrite).\n`,
110
+ );
111
+ } else if (!needsAgents && legacyAgents.length > 0) {
112
+ process.stderr.write(
113
+ `[refacil-sdd-ai] OpenCode primary config already has refacil artifacts; skipped legacy migration from ${legacyDir} (no overwrite).\n`,
114
+ );
115
+ } else if (!needsPlugins && hasLegacyPlugins) {
116
+ process.stderr.write(
117
+ `[refacil-sdd-ai] OpenCode primary config already has refacil artifacts; skipped legacy migration from ${legacyDir} (no overwrite).\n`,
118
+ );
119
+ }
120
+
121
+ let copied = 0;
122
+ if (needsSkills) {
123
+ copied += copyRefacilTree(path.join(legacyDir, 'skills'), path.join(primary, 'skills'));
124
+ }
125
+ if (needsAgents) {
126
+ copied += copyRefacilTree(path.join(legacyDir, 'agents'), path.join(primary, 'agents'));
127
+ }
128
+ if (needsPlugins) {
129
+ copied += copyRefacilPlugins(path.join(legacyDir, 'plugins'), pluginsDir);
130
+ }
131
+
132
+ if (copied > 0) {
133
+ fromDirs.push(legacyDir);
134
+ totalCopied += copied;
135
+ }
136
+ }
137
+
138
+ if (totalCopied > 0) {
139
+ process.stderr.write(
140
+ `[refacil-sdd-ai] Migrated OpenCode refacil artifacts from legacy path(s) to ${primary}\n`,
141
+ );
142
+ return { migrated: true, fromDirs };
143
+ }
144
+
145
+ return { migrated: false, fromDirs: [] };
146
+ }
147
+
148
+ module.exports = { migrateOpenCodeLegacyArtifacts };
@@ -4,7 +4,7 @@
4
4
  * refacil-sdd-ai OpenCode plugin
5
5
  *
6
6
  * Provides 4 hook equivalents for OpenCode:
7
- * - session.created → check-update logic (sync compact-guidance + testing-policy, flag pending migrations)
7
+ * - session.created → runs `refacil-sdd-ai check-update` (same CLI as Claude/Cursor/Codex)
8
8
  * - tui.prompt.append → notify-update logic (prompt user to run /refacil:update if pending)
9
9
  * - tool.execute.before → check-review + compact-bash logic
10
10
  *
@@ -15,6 +15,39 @@
15
15
  const path = require('path');
16
16
  const fs = require('fs');
17
17
 
18
+ /** @type {import('../check-review').evaluateGitPushReview | null} */
19
+ let evaluateGitPushReview = null;
20
+
21
+ (function loadCheckReviewModule() {
22
+ const candidates = [
23
+ // Co-installed by installOpenCodePlugin (global ~/.config/.../plugins/)
24
+ path.join(__dirname, 'refacil-check-review.js'),
25
+ // Running from package source (lib/opencode-plugin/index.js)
26
+ path.resolve(__dirname, '..', 'check-review.js'),
27
+ // Project-local node_modules
28
+ path.resolve(__dirname, '..', '..', 'node_modules', 'refacil-sdd-ai', 'lib', 'check-review.js'),
29
+ ];
30
+
31
+ for (const candidate of candidates) {
32
+ try {
33
+ if (fs.existsSync(candidate)) {
34
+ evaluateGitPushReview = require(candidate).evaluateGitPushReview;
35
+ return;
36
+ }
37
+ } catch (_) {
38
+ // try next candidate
39
+ }
40
+ }
41
+
42
+ try {
43
+ evaluateGitPushReview = require('refacil-sdd-ai/lib/check-review').evaluateGitPushReview;
44
+ } catch (_) {
45
+ process.stderr.write(
46
+ '[refacil-sdd-ai] WARNING: Could not load check-review.js — git push review gate disabled.\n',
47
+ );
48
+ }
49
+ })();
50
+
18
51
  // ── Resolve compact rules ────────────────────────────────────────────────────
19
52
  // When installed, this file lives at .opencode/plugins/refacil-hooks.js.
20
53
  // The compact rules live at <package>/lib/compact/rules.js.
@@ -25,10 +58,13 @@ let findRule = null;
25
58
 
26
59
  (function loadCompactRules() {
27
60
  const candidates = [
61
+ // Co-installed beside refacil-hooks.js (global ~/.config/opencode/plugins/)
62
+ path.join(__dirname, 'rules.js'),
28
63
  // Installed as plugin in .opencode/plugins/ — package is in node_modules
29
64
  path.resolve(__dirname, '..', '..', 'node_modules', 'refacil-sdd-ai', 'lib', 'compact', 'rules.js'),
30
65
  // Running from source (lib/opencode-plugin/index.js)
31
66
  path.resolve(__dirname, '..', 'compact', 'rules.js'),
67
+ path.resolve(__dirname, 'rules.js'),
32
68
  ];
33
69
 
34
70
  for (const candidate of candidates) {
@@ -66,12 +102,6 @@ function readPendingUpdateFlag(projectRoot) {
66
102
  }
67
103
  }
68
104
 
69
- function writePendingUpdateFlag(projectRoot, from, to) {
70
- try {
71
- fs.writeFileSync(getPendingUpdateFlagPath(projectRoot), JSON.stringify({ from, to }));
72
- } catch (_) {}
73
- }
74
-
75
105
  function clearPendingUpdateFlag(projectRoot) {
76
106
  try {
77
107
  const flagPath = getPendingUpdateFlagPath(projectRoot);
@@ -79,17 +109,6 @@ function clearPendingUpdateFlag(projectRoot) {
79
109
  } catch (_) {}
80
110
  }
81
111
 
82
- function readRepoVersion(projectRoot) {
83
- const versionFiles = ['.opencode/.sdd-version', '.claude/.sdd-version', '.cursor/.sdd-version'];
84
- for (const rel of versionFiles) {
85
- try {
86
- const raw = fs.readFileSync(path.join(projectRoot, rel), 'utf8').trim();
87
- if (raw) return raw;
88
- } catch (_) {}
89
- }
90
- return null;
91
- }
92
-
93
112
  /** Same resolution strategy as `lib/session-repo-sync.js` (kept local so the copied plugin stays self-contained). */
94
113
  function resolveRefacilPackageRootForOpenCode(projectRoot) {
95
114
  const marker = path.join('templates', 'testing-policy.md');
@@ -127,66 +146,58 @@ function loadMethodologyMigrationPending(projectRoot) {
127
146
  }
128
147
  }
129
148
 
130
- // ── Hook handlers ────────────────────────────────────────────────────────────
131
-
132
149
  /**
133
- * session.created equivalent of check-update (SessionStart hook)
134
- * Syncs compact-guidance + testing-policy like `refacil-sdd-ai check-update`, then flags pending methodology migrations.
150
+ * Run the same entrypoint as Claude/Cursor/Codex SessionStart hooks.
151
+ * Prefers `node <package>/bin/cli.js check-update` when the package resolves from the repo;
152
+ * falls back to global `refacil-sdd-ai check-update`.
135
153
  */
136
- async function checkUpdateHandler(event) {
137
- const projectRoot = event.projectRoot || process.cwd();
138
-
154
+ function runCheckUpdateCli(projectRoot) {
155
+ const { execFileSync, execSync } = require('child_process');
139
156
  const pkgRoot = resolveRefacilPackageRootForOpenCode(projectRoot);
140
- if (pkgRoot) {
141
- try {
142
- const { syncRepoSessionMarkers } = require(path.join(pkgRoot, 'lib', 'session-repo-sync.js'));
143
- const out = syncRepoSessionMarkers(projectRoot, pkgRoot);
144
- if (out.compact && out.compact.status === 'error') {
145
- process.stderr.write(`[refacil-sdd-ai] compact-guidance: ${out.compact.message}\n`);
146
- }
147
- if (out.testing && out.testing.status === 'error') {
148
- process.stderr.write(`[refacil-sdd-ai] testing-policy: ${out.testing.message}\n`);
149
- } else if (
150
- out.testing &&
151
- ['created-file', 'appended', 'replaced', 'written-empty'].includes(out.testing.status)
152
- ) {
153
- process.stderr.write(`[refacil-sdd-ai] testing-policy: ${out.testing.status} (.agents/testing.md)\n`);
154
- }
155
- } catch (err) {
156
- process.stderr.write(`[refacil-sdd-ai] session repo sync: ${err.message}\n`);
157
- }
158
- }
157
+ const opts = {
158
+ cwd: projectRoot,
159
+ encoding: 'utf8',
160
+ timeout: 120000,
161
+ stdio: ['ignore', 'pipe', 'pipe'],
162
+ // Mirror workspace into the same env vars Cursor/Claude hooks set (child-only).
163
+ env: {
164
+ ...process.env,
165
+ CURSOR_PROJECT_DIR: projectRoot,
166
+ CLAUDE_PROJECT_DIR: projectRoot,
167
+ },
168
+ };
159
169
 
160
- // Check if there is a pending methodology migration
161
170
  try {
162
- const migFn = loadMethodologyMigrationPending(projectRoot);
163
- if (!migFn) return;
164
- const mig = migFn(projectRoot);
165
- const repoVersion = readRepoVersion(projectRoot);
166
-
167
- // Try to get the current package version via refacil-sdd-ai CLI
168
- let packageVersion = null;
169
- try {
170
- const { execSync } = require('child_process');
171
- packageVersion = execSync('refacil-sdd-ai --version', {
172
- encoding: 'utf8',
173
- timeout: 5000,
174
- stdio: ['pipe', 'pipe', 'pipe'],
175
- }).trim();
176
- } catch (_) {}
177
-
178
- const existingFlag = readPendingUpdateFlag(projectRoot);
179
-
180
- if (mig.pending) {
181
- writePendingUpdateFlag(projectRoot, repoVersion, packageVersion);
182
- } else if (existingFlag) {
183
- clearPendingUpdateFlag(projectRoot);
171
+ if (pkgRoot) {
172
+ const cliPath = path.join(pkgRoot, 'bin', 'cli.js');
173
+ const stdout = execFileSync(process.execPath, [cliPath, 'check-update'], opts);
174
+ if (stdout) process.stderr.write(String(stdout));
175
+ return;
184
176
  }
177
+ const stdout = execSync('refacil-sdd-ai check-update', { ...opts, shell: true });
178
+ if (stdout) process.stderr.write(String(stdout));
185
179
  } catch (err) {
186
- process.stderr.write(`[refacil-sdd-ai] check-update handler error: ${err.message}\n`);
180
+ if (err.stdout) process.stderr.write(String(err.stdout));
181
+ if (err.stderr) process.stderr.write(String(err.stderr));
182
+ if (err.status !== undefined && err.status !== 0) {
183
+ process.stderr.write(`[refacil-sdd-ai] check-update exited with code ${err.status}\n`);
184
+ } else if (err.message) {
185
+ process.stderr.write(`[refacil-sdd-ai] check-update: ${err.message}\n`);
186
+ }
187
187
  }
188
188
  }
189
189
 
190
+ // ── Hook handlers ────────────────────────────────────────────────────────────
191
+
192
+ /**
193
+ * session.created — equivalent of check-update (SessionStart hook)
194
+ * Delegates to `refacil-sdd-ai check-update` for full parity (npm/skills sync, compact-guidance, CodeGraph reindex).
195
+ */
196
+ async function checkUpdateHandler(event) {
197
+ const projectRoot = event.projectRoot || process.cwd();
198
+ runCheckUpdateCli(projectRoot);
199
+ }
200
+
190
201
  /**
191
202
  * tui.prompt.append — equivalent of notify-update (UserPromptSubmit hook)
192
203
  * Returns an instruction string if there is a pending update, otherwise returns nothing.
@@ -224,7 +235,7 @@ async function notifyUpdateHandler(event) {
224
235
 
225
236
  /**
226
237
  * tool.execute.before — handles Bash tool calls:
227
- * (a) check-review: blocks git push if any active change is missing .review-passed
238
+ * (a) check-review: blocks git push if an active change has started implementation without .review-passed
228
239
  * (b) compact-bash: rewrites matched commands to reduce token usage
229
240
  */
230
241
  async function toolExecuteBeforeHandler(event) {
@@ -236,41 +247,10 @@ async function toolExecuteBeforeHandler(event) {
236
247
 
237
248
  const projectRoot = event.projectRoot || process.cwd();
238
249
 
239
- // (a) check-review: block git push if missing .review-passed
240
- if (/git\s+push/.test(command)) {
241
- const sddChangesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
242
- if (fs.existsSync(sddChangesDir)) {
243
- let entries;
244
- try {
245
- entries = fs.readdirSync(sddChangesDir, { withFileTypes: true });
246
- } catch (_) {
247
- entries = [];
248
- }
249
-
250
- const activeChanges = entries.filter(
251
- (e) => e.isDirectory() && e.name !== 'archive',
252
- );
253
-
254
- if (activeChanges.length > 0) {
255
- const missing = activeChanges.filter(
256
- (e) => !fs.existsSync(path.join(sddChangesDir, e.name, '.review-passed')),
257
- );
258
-
259
- if (missing.length > 0) {
260
- const names = missing.map((e) => e.name).join(', ');
261
- const reason =
262
- missing.length === 1
263
- ? `[refacil-sdd-ai] Review pending for: ${names}. ` +
264
- 'Stop the push and run /refacil:review on that change before pushing code. ' +
265
- 'If the review passes, retry the git push.'
266
- : `[refacil-sdd-ai] Multiple changes without approved review: ${names}. ` +
267
- 'Stop the push and ask the user to explicitly select which change they want to push. ' +
268
- 'Then run /refacil:review <change-name> for that specific change and retry the push.';
269
-
270
- throw new Error(reason);
271
- }
272
- }
273
- }
250
+ // (a) check-review: same rules as refacil-sdd-ai check-review CLI (shared lib/check-review.js)
251
+ if (evaluateGitPushReview) {
252
+ const block = evaluateGitPushReview(command, projectRoot);
253
+ if (block) throw new Error(block.reason);
274
254
  }
275
255
 
276
256
  // (b) compact-bash: rewrite matched commands to reduce token usage
@@ -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 };