mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
* refactor(hooks): consolidate PostToolUse hooks into sync/async dispatchers Replace 10 individual PostToolUse entries in hooks.json with two consolidated dispatcher entries (post:dispatcher:sync / post:dispatcher:async). The dispatcher's internal registry preserves every hook ID, matcher, and profile, so ECC_DISABLED_HOOKS and ECC_HOOK_PROFILE gating behave exactly as before. Performance (Edit event, actual hooks.json commands spawned in parallel like the harness does, median of 7 runs): - Blocking hook latency: 81ms -> 49ms (~40% faster; 7 blocking processes -> 1 sync dispatcher) - Node processes per tool call: 10 -> 2 (7 blocking + 3 async -> 1 sync + 1 async) - observe-runner now runs in-process (~370ms) inside the async dispatcher, which stays backgrounded (async: true, timeout 45s), so it adds no user-facing latency. Also: - dashboard-web lists dispatcher-managed child hooks so the hook inventory stays complete - post-edit-console-warn refactored to export run() for in-process dispatch while keeping standalone stdin behavior - dispatcher stdin reading is multi-byte safe (StringDecoder) and child hook exit codes propagate to the dispatcher exit code * test(hooks): replace emoji literal with unicode escape for CI unicode safety check * fix(hooks): adopt explicit cli() entrypoint and merge multi-hook stdout Address Greptile review on #2494: - Replace the non-standard 'require.main === undefined' guard with an explicit exported cli(). The hooks.json bootstraps now call require(s).cli(), so merely requiring the module (dashboard-web, test runners, Jest, worker threads) can never trigger dispatch, attach stdin listeners, or set process.exitCode. - Replace last-writer-wins stdout with mergeHookStdout(): when several hooks emit additionalContext envelopes they merge into a single PostToolUse envelope; non-mergeable raw stdout keeps the last hook's output and emits a stderr warning naming the dropped hook IDs, so nothing is lost silently. Also includes local formatter reformatting of the dispatcher and its test file (no behavioral changes beyond the above). * fix(hooks): keep post:bash:dispatcher phase reachable in minimal profile The Greptile P1 premise was partially incorrect: sub-hooks without explicit profiles default to standard,strict via parseProfiles() (scripts/lib/hook-flags.js), so audit/cost logs never ran under the minimal profile on main either — there is no user-visible regression. However, main did spawn the bash dispatcher phase unconditionally and let each sub-hook gate itself. Restore that semantic by opening the outer registry gate to minimal,standard,strict so a future sub-hook that opts into minimal is not silently blocked at the phase level. Adds the previously missing minimal-profile async dry-run test. * test(hooks): assert failing hook exit code propagates to real process status Spawns the actual dispatcher subprocess with an injected failing hook and asserts the OS-level exit status, stderr diagnostic, and suppressed pass-through — closing the E2E gap CodeRabbit flagged on #2494. * chore: retrigger CI (flaky windows powershell bootstrap test)
66 lines
1.8 KiB
JavaScript
66 lines
1.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PostToolUse Hook: Warn about console.log statements after edits
|
|
*
|
|
* Cross-platform (Windows, macOS, Linux)
|
|
*
|
|
* Runs after Edit tool use. If the edited JS/TS file contains console.log
|
|
* statements, warns with line numbers to help remove debug statements
|
|
* before committing.
|
|
*/
|
|
|
|
const { readFile } = require('../lib/utils');
|
|
|
|
const MAX_STDIN = 1024 * 1024; // 1MB limit
|
|
function run(data) {
|
|
const warnings = [];
|
|
try {
|
|
const input = JSON.parse(data);
|
|
const filePath = input.tool_input?.file_path;
|
|
|
|
if (filePath && /\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
|
const content = readFile(filePath);
|
|
if (content) {
|
|
const matches = content
|
|
.split('\n')
|
|
.map((line, index) => ({ line, index }))
|
|
.filter(item => /console\.log/.test(item.line))
|
|
.map(item => `${item.index + 1}: ${item.line.trim()}`);
|
|
|
|
if (matches.length > 0) {
|
|
warnings.push(`[Hook] WARNING: console.log found in ${filePath}`);
|
|
warnings.push(...matches.slice(0, 5));
|
|
warnings.push('[Hook] Remove console.log before committing');
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Invalid input — pass through
|
|
}
|
|
|
|
return {
|
|
stdout: data,
|
|
stderr: warnings.join('\n'),
|
|
exitCode: 0,
|
|
};
|
|
}
|
|
|
|
if (require.main === module) {
|
|
let data = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => {
|
|
if (data.length < MAX_STDIN) {
|
|
const remaining = MAX_STDIN - data.length;
|
|
data += chunk.substring(0, remaining);
|
|
}
|
|
});
|
|
process.stdin.on('end', () => {
|
|
const result = run(data);
|
|
if (result.stderr) process.stderr.write(`${result.stderr}\n`);
|
|
process.stdout.write(result.stdout);
|
|
process.exitCode = result.exitCode;
|
|
});
|
|
}
|
|
|
|
module.exports = { run };
|