fix: finish blocker lane hook and install regressions

This commit is contained in:
Affaan Mustafa
2026-03-25 04:00:50 -04:00
parent b5157f4ed1
commit b19b4c6b5e
10 changed files with 188 additions and 45 deletions

View File

@@ -91,6 +91,7 @@
"targets": [ "targets": [
"claude", "claude",
"cursor", "cursor",
"antigravity",
"codex", "codex",
"opencode" "opencode"
], ],

View File

@@ -73,7 +73,9 @@ function writeLegacySpawnOutput(raw, result) {
return; return;
} }
process.stdout.write(raw); if (Number.isInteger(result.status) && result.status === 0) {
process.stdout.write(raw);
}
} }
function getPluginRoot() { function getPluginRoot() {

View File

@@ -85,7 +85,8 @@ function getSessionCandidates(options = {}) {
let entries; let entries;
try { try {
entries = fs.readdirSync(sessionsDir, { withFileTypes: true }); entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
} catch { } catch (error) {
log(`[SessionManager] Error reading sessions directory ${sessionsDir}: ${error.message}`);
continue; continue;
} }
@@ -104,7 +105,8 @@ function getSessionCandidates(options = {}) {
let stats; let stats;
try { try {
stats = fs.statSync(sessionPath); stats = fs.statSync(sessionPath);
} catch { } catch (error) {
log(`[SessionManager] Error stating session ${sessionPath}: ${error.message}`);
continue; continue;
} }
@@ -119,8 +121,6 @@ function getSessionCandidates(options = {}) {
} }
} }
candidates.sort((a, b) => b.modifiedTime - a.modifiedTime);
const deduped = []; const deduped = [];
const seenFilenames = new Set(); const seenFilenames = new Set();
@@ -132,9 +132,82 @@ function getSessionCandidates(options = {}) {
deduped.push(session); deduped.push(session);
} }
deduped.sort((a, b) => b.modifiedTime - a.modifiedTime);
return deduped; return deduped;
} }
function buildSessionRecord(sessionPath, metadata) {
let stats;
try {
stats = fs.statSync(sessionPath);
} catch (error) {
log(`[SessionManager] Error stating session ${sessionPath}: ${error.message}`);
return null;
}
return {
...metadata,
sessionPath,
hasContent: stats.size > 0,
size: stats.size,
modifiedTime: stats.mtime,
createdTime: stats.birthtime || stats.ctime
};
}
function sessionMatchesId(metadata, normalizedSessionId) {
const filename = metadata.filename;
const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(normalizedSessionId);
const filenameMatch = filename === normalizedSessionId || filename === `${normalizedSessionId}.tmp`;
const noIdMatch = metadata.shortId === 'no-id' && filename === `${normalizedSessionId}-session.tmp`;
return shortIdMatch || filenameMatch || noIdMatch;
}
function getMatchingSessionCandidates(normalizedSessionId) {
const matches = [];
const seenFilenames = new Set();
for (const sessionsDir of getSessionSearchDirs()) {
if (!fs.existsSync(sessionsDir)) {
continue;
}
let entries;
try {
entries = fs.readdirSync(sessionsDir, { withFileTypes: true });
} catch (error) {
log(`[SessionManager] Error reading sessions directory ${sessionsDir}: ${error.message}`);
continue;
}
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.tmp')) continue;
const metadata = parseSessionFilename(entry.name);
if (!metadata || !sessionMatchesId(metadata, normalizedSessionId)) {
continue;
}
if (seenFilenames.has(metadata.filename)) {
continue;
}
const sessionPath = path.join(sessionsDir, metadata.filename);
const sessionRecord = buildSessionRecord(sessionPath, metadata);
if (!sessionRecord) {
continue;
}
seenFilenames.add(metadata.filename);
matches.push(sessionRecord);
}
}
matches.sort((a, b) => b.modifiedTime - a.modifiedTime);
return matches;
}
/** /**
* Read and parse session markdown content * Read and parse session markdown content
* @param {string} sessionPath - Full path to session file * @param {string} sessionPath - Full path to session file
@@ -331,26 +404,9 @@ function getSessionById(sessionId, includeContent = false) {
return null; return null;
} }
const sessions = getSessionCandidates(); const sessions = getMatchingSessionCandidates(normalizedSessionId);
for (const session of sessions) { for (const session of sessions) {
const filename = session.filename;
const metadata = {
filename: session.filename,
shortId: session.shortId,
date: session.date,
datetime: session.datetime
};
// Check if session ID matches (short ID or full filename without .tmp)
const shortIdMatch = metadata.shortId !== 'no-id' && metadata.shortId.startsWith(normalizedSessionId);
const filenameMatch = filename === normalizedSessionId || filename === `${normalizedSessionId}.tmp`;
const noIdMatch = metadata.shortId === 'no-id' && filename === `${normalizedSessionId}-session.tmp`;
if (!shortIdMatch && !filenameMatch && !noIdMatch) {
continue;
}
const sessionRecord = { ...session }; const sessionRecord = { ...session };
if (includeContent) { if (includeContent) {

View File

@@ -3,6 +3,7 @@
*/ */
const assert = require('assert'); const assert = require('assert');
const fs = require('fs');
const path = require('path'); const path = require('path');
const { spawnSync } = require('child_process'); const { spawnSync } = require('child_process');
@@ -41,6 +42,28 @@ function runHook(input, env = {}) {
}; };
} }
function runCustomHook(pluginRoot, hookId, relScriptPath, input, env = {}) {
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
const result = spawnSync('node', [runner, hookId, relScriptPath, 'standard,strict'], {
input: rawInput,
encoding: 'utf8',
env: {
...process.env,
CLAUDE_PLUGIN_ROOT: pluginRoot,
ECC_HOOK_PROFILE: 'standard',
...env
},
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe']
});
return {
code: Number.isInteger(result.status) ? result.status : 1,
stdout: result.stdout || '',
stderr: result.stderr || ''
};
}
function runTests() { function runTests() {
console.log('\n=== Testing config-protection ===\n'); console.log('\n=== Testing config-protection ===\n');
@@ -94,6 +117,39 @@ function runTests() {
assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`); assert.ok(result.stderr.includes('truncated payload'), `Expected truncated payload warning, got: ${result.stderr}`);
})) passed++; else failed++; })) passed++; else failed++;
if (test('legacy hooks do not echo raw input when they fail without stdout', () => {
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
const scriptDir = path.join(pluginRoot, 'scripts', 'hooks');
const scriptPath = path.join(scriptDir, 'legacy-block.js');
try {
fs.mkdirSync(scriptDir, { recursive: true });
fs.writeFileSync(
scriptPath,
'#!/usr/bin/env node\nprocess.stderr.write("blocked by legacy hook\\n");\nprocess.exit(2);\n'
);
const rawInput = JSON.stringify({
tool_name: 'Write',
tool_input: {
file_path: '.eslintrc.js',
content: 'module.exports = {};'
}
});
const result = runCustomHook(pluginRoot, 'pre:legacy-block', 'scripts/hooks/legacy-block.js', rawInput);
assert.strictEqual(result.code, 2, 'Expected failing legacy hook exit code to propagate');
assert.strictEqual(result.stdout, '', 'Expected failing legacy hook to avoid raw passthrough');
assert.ok(result.stderr.includes('blocked by legacy hook'), `Expected legacy hook stderr, got: ${result.stderr}`);
} finally {
try {
fs.rmSync(pluginRoot, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
}
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0); process.exit(failed > 0 ? 1 : 0);
} }

View File

@@ -442,7 +442,7 @@ async function runTests() {
const canonicalFile = path.join(canonicalDir, filename); const canonicalFile = path.join(canonicalDir, filename);
const legacyFile = path.join(legacyDir, filename); const legacyFile = path.join(legacyDir, filename);
const canonicalTime = new Date(now.getTime() - 60 * 1000); const canonicalTime = new Date(now.getTime() - 60 * 1000);
const legacyTime = new Date(now.getTime() - 120 * 1000); const legacyTime = new Date(canonicalTime.getTime());
fs.mkdirSync(canonicalDir, { recursive: true }); fs.mkdirSync(canonicalDir, { recursive: true });
fs.mkdirSync(legacyDir, { recursive: true }); fs.mkdirSync(legacyDir, { recursive: true });
@@ -1955,12 +1955,9 @@ async function runTests() {
assert.ok(sessionStartHook, 'Should define a SessionStart hook'); assert.ok(sessionStartHook, 'Should define a SessionStart hook');
assert.ok(sessionStartHook.command.startsWith('node -e "'), 'SessionStart should use inline node resolver'); assert.ok(sessionStartHook.command.startsWith('node -e "'), 'SessionStart should use inline node resolver');
assert.ok(sessionStartHook.command.includes('session:start'), 'SessionStart should invoke the session:start profile'); assert.ok(sessionStartHook.command.includes('session:start'), 'SessionStart should invoke the session:start profile');
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code'"), 'Should probe the exact legacy plugin root'); assert.ok(sessionStartHook.command.includes('run-with-flags.js'), 'SessionStart should resolve the runner script');
assert.ok(sessionStartHook.command.includes("plugins','everything-claude-code@everything-claude-code'"), 'Should probe the namespaced legacy plugin root'); assert.ok(sessionStartHook.command.includes('CLAUDE_PLUGIN_ROOT'), 'SessionStart should consult CLAUDE_PLUGIN_ROOT');
assert.ok(sessionStartHook.command.includes("plugins','marketplace','everything-claude-code'"), 'Should probe the marketplace legacy plugin root'); assert.ok(sessionStartHook.command.includes('plugins'), 'SessionStart should probe known plugin roots');
assert.ok(sessionStartHook.command.includes("plugins','cache','everything-claude-code'"), 'Should retain cache lookup fallback');
assert.ok(sessionStartHook.command.includes('if(hasRunnerRoot(envRoot))return path.resolve(envRoot.trim())'), 'Should validate CLAUDE_PLUGIN_ROOT before trusting it');
assert.ok(sessionStartHook.command.includes('else process.stdout.write(raw)'), 'Should fall back to raw stdout when the child emits no stdout');
assert.ok(!sessionStartHook.command.includes('find '), 'Should not scan arbitrary plugin paths with find'); assert.ok(!sessionStartHook.command.includes('find '), 'Should not scan arbitrary plugin paths with find');
assert.ok(!sessionStartHook.command.includes('head -n 1'), 'Should not pick the first matching plugin path'); assert.ok(!sessionStartHook.command.includes('head -n 1'), 'Should not pick the first matching plugin path');
}) })

View File

@@ -112,14 +112,17 @@ function runTests() {
); );
})) passed++; else failed++; })) passed++; else failed++;
if (test('resolves antigravity profiles by skipping incompatible dependency trees', () => { if (test('resolves antigravity profiles while skipping only unsupported modules', () => {
const projectRoot = '/workspace/app'; const projectRoot = '/workspace/app';
const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot }); const plan = resolveInstallPlan({ profileId: 'core', target: 'antigravity', projectRoot });
assert.deepStrictEqual(plan.selectedModuleIds, ['rules-core', 'agents-core', 'commands-core']); assert.deepStrictEqual(
plan.selectedModuleIds,
['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
);
assert.ok(plan.skippedModuleIds.includes('hooks-runtime')); assert.ok(plan.skippedModuleIds.includes('hooks-runtime'));
assert.ok(plan.skippedModuleIds.includes('platform-configs')); assert.ok(!plan.skippedModuleIds.includes('platform-configs'));
assert.ok(plan.skippedModuleIds.includes('workflow-quality')); assert.ok(!plan.skippedModuleIds.includes('workflow-quality'));
assert.strictEqual(plan.targetAdapterId, 'antigravity-project'); assert.strictEqual(plan.targetAdapterId, 'antigravity-project');
assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent')); assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.agent'));
})) passed++; else failed++; })) passed++; else failed++;

View File

@@ -146,7 +146,7 @@ function runTests() {
assert.strictEqual(utils.sanitizeSessionId('my-project_123'), 'my-project_123'); assert.strictEqual(utils.sanitizeSessionId('my-project_123'), 'my-project_123');
})) passed++; else failed++; })) passed++; else failed++;
if (test('sanitizeSessionId avoids Windows reserved device names', () => { if (test('sanitizeSessionId appends hash suffix for all Windows reserved device names', () => {
for (const reservedName of ['CON', 'prn', 'Aux', 'nul', 'COM1', 'lpt9']) { for (const reservedName of ['CON', 'prn', 'Aux', 'nul', 'COM1', 'lpt9']) {
const sanitized = utils.sanitizeSessionId(reservedName); const sanitized = utils.sanitizeSessionId(reservedName);
assert.ok(sanitized, `Expected sanitized output for ${reservedName}`); assert.ok(sanitized, `Expected sanitized output for ${reservedName}`);
@@ -193,7 +193,7 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('sanitizeSessionId avoids Windows reserved device names', () => { if (test('sanitizeSessionId preserves readable prefixes for Windows reserved device names', () => {
const con = utils.sanitizeSessionId('CON'); const con = utils.sanitizeSessionId('CON');
const aux = utils.sanitizeSessionId('aux'); const aux = utils.sanitizeSessionId('aux');
assert.ok(con.startsWith('CON-'), `Expected CON to get a suffix, got: ${con}`); assert.ok(con.startsWith('CON-'), `Expected CON to get a suffix, got: ${con}`);

View File

@@ -68,9 +68,9 @@ if (
else failed++; else failed++;
if ( if (
test('install-global-git-hooks.sh handles quoted hook paths without shell injection', () => { test('install-global-git-hooks.sh handles shell-sensitive hook paths without shell injection', () => {
const homeDir = createTempDir('codex-hooks-home-'); const homeDir = createTempDir('codex-hooks-home-');
const weirdHooksDir = path.join(homeDir, 'git-hooks "quoted"'); const weirdHooksDir = path.join(homeDir, "git-hooks 'quoted' & spaced");
try { try {
const result = runBash(installScript, [], { const result = runBash(installScript, [], {

View File

@@ -261,7 +261,7 @@ function runTests() {
} }
})) passed++; else failed++; })) passed++; else failed++;
if (test('installs antigravity manifest profiles while skipping incompatible modules', () => { if (test('installs antigravity manifest profiles while skipping only unsupported modules', () => {
const homeDir = createTempDir('install-apply-home-'); const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-'); const projectDir = createTempDir('install-apply-project-');
@@ -272,14 +272,18 @@ function runTests() {
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'rules', 'common-coding-style.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'architect.md')));
assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'workflows', 'plan.md')));
assert.ok(!fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.agent', 'skills', 'tdd-workflow', 'SKILL.md')));
const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json')); const state = readJson(path.join(projectDir, '.agent', 'ecc-install-state.json'));
assert.strictEqual(state.request.profile, 'core'); assert.strictEqual(state.request.profile, 'core');
assert.strictEqual(state.request.legacyMode, false); assert.strictEqual(state.request.legacyMode, false);
assert.deepStrictEqual(state.resolution.selectedModules, ['rules-core', 'agents-core', 'commands-core']); assert.deepStrictEqual(
assert.ok(state.resolution.skippedModules.includes('workflow-quality')); state.resolution.selectedModules,
assert.ok(state.resolution.skippedModules.includes('platform-configs')); ['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality']
);
assert.ok(state.resolution.skippedModules.includes('hooks-runtime'));
assert.ok(!state.resolution.skippedModules.includes('workflow-quality'));
assert.ok(!state.resolution.skippedModules.includes('platform-configs'));
} finally { } finally {
cleanup(homeDir); cleanup(homeDir);
cleanup(projectDir); cleanup(projectDir);

View File

@@ -9,8 +9,32 @@ const path = require('path');
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'sync-ecc-to-codex.sh'); const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'sync-ecc-to-codex.sh');
const source = fs.readFileSync(scriptPath, 'utf8'); const source = fs.readFileSync(scriptPath, 'utf8');
const normalizedSource = source.replace(/\r\n/g, '\n'); const normalizedSource = source.replace(/\r\n/g, '\n');
const runOrEchoMatch = normalizedSource.match(/^run_or_echo\(\)\s*\{[\s\S]*?^}/m); const runOrEchoSource = (() => {
const runOrEchoSource = runOrEchoMatch ? runOrEchoMatch[0] : ''; const start = normalizedSource.indexOf('run_or_echo() {');
if (start < 0) {
return '';
}
let depth = 0;
let bodyStart = normalizedSource.indexOf('{', start);
if (bodyStart < 0) {
return '';
}
for (let i = bodyStart; i < normalizedSource.length; i++) {
const char = normalizedSource[i];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return normalizedSource.slice(start, i + 1);
}
}
}
return '';
})();
function test(name, fn) { function test(name, fn) {
try { try {