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.
- package/NOTICE.md +46 -0
- package/README.md +209 -42
- package/agents/auditor.md +46 -0
- package/agents/debugger.md +41 -1
- package/agents/implementer.md +76 -10
- package/agents/investigator.md +36 -0
- package/agents/proposer.md +46 -2
- package/agents/tester.md +45 -8
- package/agents/validator.md +67 -13
- package/bin/cli.js +428 -83
- package/bin/postinstall.js +20 -0
- package/lib/bus/broker.js +121 -3
- package/lib/bus/spawn.js +189 -121
- package/lib/check-review.js +102 -0
- package/lib/codegraph-telemetry.js +135 -0
- package/lib/codegraph.js +273 -0
- package/lib/commands/autopilot.js +120 -0
- package/lib/commands/bus.js +29 -36
- package/lib/commands/compact.js +185 -46
- package/lib/commands/read-spec.js +352 -0
- package/lib/commands/sdd.js +429 -44
- package/lib/compact-guidance.js +122 -77
- package/lib/config.js +136 -0
- package/lib/global-paths.js +56 -20
- package/lib/hooks.js +32 -4
- package/lib/ide-detection.js +1 -1
- package/lib/ignore-files.js +5 -1
- package/lib/installer.js +202 -19
- package/lib/kapso.js +241 -0
- package/lib/methodology-migration-pending.js +13 -0
- package/lib/open-browser.js +32 -0
- package/lib/opencode-migrate.js +148 -0
- package/lib/opencode-plugin/index.js +84 -104
- package/lib/opencode-plugin/rules.js +236 -0
- package/lib/project-root.js +154 -0
- package/lib/repo-ide-sync.js +5 -0
- package/lib/spec-reader/lang.js +72 -0
- package/lib/spec-reader/md-parser.js +299 -0
- package/lib/spec-reader/session.js +139 -0
- package/lib/spec-reader/ui/app.js +685 -0
- package/lib/spec-reader/ui/index.html +59 -0
- package/lib/spec-reader/ui/mixed-lang.js +200 -0
- package/lib/spec-reader/ui/model-cache.js +117 -0
- package/lib/spec-reader/ui/style.css +294 -0
- package/lib/spec-reader/ui/supertonic-helper.js +565 -0
- package/lib/spec-sync.js +258 -0
- package/lib/test-scope.js +713 -0
- package/lib/testing-policy-sync.js +14 -2
- package/package.json +6 -3
- package/skills/apply/SKILL.md +39 -64
- package/skills/archive/SKILL.md +74 -48
- package/skills/ask/SKILL.md +43 -8
- package/skills/autopilot/SKILL.md +476 -0
- package/skills/bug/SKILL.md +52 -53
- package/skills/explore/SKILL.md +48 -1
- package/skills/guide/SKILL.md +31 -13
- package/skills/inbox/SKILL.md +9 -0
- package/skills/join/SKILL.md +1 -1
- package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
- package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
- package/skills/prereqs/SKILL.md +1 -1
- package/skills/propose/SKILL.md +74 -19
- package/skills/read-spec/SKILL.md +76 -0
- package/skills/reply/SKILL.md +42 -9
- package/skills/review/SKILL.md +63 -25
- package/skills/review/checklist.md +2 -2
- package/skills/say/SKILL.md +40 -4
- package/skills/setup/SKILL.md +59 -5
- package/skills/setup/troubleshooting.md +11 -3
- package/skills/stats/SKILL.md +157 -0
- package/skills/test/SKILL.md +35 -10
- package/skills/up-code/SKILL.md +20 -13
- package/skills/update/SKILL.md +32 -1
- package/skills/verify/SKILL.md +78 -41
- package/templates/compact-guidance.md +10 -0
- 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
|
|
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
|
-
*
|
|
134
|
-
*
|
|
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
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
154
|
+
function runCheckUpdateCli(projectRoot) {
|
|
155
|
+
const { execFileSync, execSync } = require('child_process');
|
|
139
156
|
const pkgRoot = resolveRefacilPackageRootForOpenCode(projectRoot);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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(
|
|
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
|
|
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:
|
|
240
|
-
if (
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
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 };
|