mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-28 06:22:58 +08:00
chore: sync repo dogfood helpers to 3.23.0 + gitignore auto-refresh runtime stamp
The repo dogfoods ruflo; its committed .claude/helpers lagged the installed package, so the version-stamped auto-refresh (ADR-174) re-copied them on every CLI-invoking hook, dirtying the tree each turn. Sync them to current and gitignore the runtime .helpers-version stamp + audit-trail manifest copy so the auto-refresh no-ops and the tree stays clean. Co-Authored-By: RuFlo <ruv@ruv.net>
This commit is contained in:
Regular → Executable
+59
-274
@@ -27,16 +27,29 @@ const CYAN = '\x1b[0;36m';
|
||||
const DIM = '\x1b[2m';
|
||||
const RESET = '\x1b[0m';
|
||||
|
||||
const YELLOW = '\x1b[0;33m';
|
||||
const log = (msg) => console.log(`${CYAN}[AutoMemory] ${msg}${RESET}`);
|
||||
const success = (msg) => console.log(`${GREEN}[AutoMemory] ✓ ${msg}${RESET}`);
|
||||
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
|
||||
|
||||
// #2545: fail LOUD instead of a silent dim skip. When @claude-flow/memory cannot
|
||||
// be resolved, self-learning imports are a no-op — the user must see this and be
|
||||
// told exactly how to fix it (on both stdout, so it shows in the Claude Code hook
|
||||
// transcript, and stderr, per the issue's requested channel).
|
||||
function warnMemoryUnavailable() {
|
||||
const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${PROJECT_ROOT} — self-learning imports are DISABLED.`;
|
||||
const line2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
|
||||
console.log(`${YELLOW}${line1}${RESET}`);
|
||||
console.log(`${YELLOW}${line2}${RESET}`);
|
||||
process.stderr.write(`${line1}\n${line2}\n`);
|
||||
}
|
||||
|
||||
const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
|
||||
|
||||
// ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
|
||||
// Track the backend currently in use so a SIGTERM/SIGINT mid-run can still
|
||||
// flush it (the JSON backend persists, a SQLite-backed one closes/flushes WAL)
|
||||
// instead of leaving a half-written store or a stale lock behind.
|
||||
// Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
|
||||
// (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
|
||||
// of leaving a half-written store or a stale lock behind.
|
||||
let activeBackend = null;
|
||||
let shuttingDown = false;
|
||||
function trackBackend(b) { activeBackend = b; return b; }
|
||||
@@ -151,77 +164,31 @@ class JsonFileBackend {
|
||||
// Resolve memory package path (local dev or npm installed)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Resolve a bundled package file (`<pkg>/<relativeFile>`) by trying, in order:
|
||||
* 1. PROJECT_ROOT/v3/<pkg>/<relativeFile> (monorepo dev layout)
|
||||
* 2. CWD/node_modules/<pkg>/<relativeFile> (user's project deps)
|
||||
* 3. node_modules walk-up from PROJECT_ROOT
|
||||
* 4. node_modules walk-up from CWD
|
||||
* 5. Global npm prefix — checks the ruflo / claude-flow / @claude-flow/cli
|
||||
* bundles, AND the package installed globally on its own
|
||||
*
|
||||
* Returns the resolved absolute path, or null. See issue #2285.
|
||||
*/
|
||||
async function resolveBundledFile(pkg, relativeFile) {
|
||||
const monorepo = join(PROJECT_ROOT, 'v3', pkg, relativeFile);
|
||||
if (existsSync(monorepo)) return monorepo;
|
||||
|
||||
const cwd = process.env.CLAUDE_FLOW_CWD || process.cwd();
|
||||
const inCwd = join(cwd, 'node_modules', pkg, relativeFile);
|
||||
if (existsSync(inCwd)) return inCwd;
|
||||
|
||||
const walkUp = (start) => {
|
||||
let dir = start;
|
||||
while (dir && dir !== dirname(dir)) {
|
||||
const candidate = join(dir, 'node_modules', pkg, relativeFile);
|
||||
if (existsSync(candidate)) return candidate;
|
||||
dir = dirname(dir);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fromProject = walkUp(PROJECT_ROOT);
|
||||
if (fromProject) return fromProject;
|
||||
|
||||
const fromCwd = walkUp(cwd);
|
||||
if (fromCwd) return fromCwd;
|
||||
|
||||
// Global npm prefix fallback — handles `npm i -g ruflo` and friends, where
|
||||
// the marketplace clone (PROJECT_ROOT) has no access to the bundled deps.
|
||||
async function loadMemoryPackage() {
|
||||
// Strategy 0 (#2545): sidecar recorded by `init` / `doctor --fix`. On the
|
||||
// documented `npx ruflo` path @claude-flow/memory (an optionalDependency of
|
||||
// the CLI) lands in the npx cache, which is NOT on the walk-up path from the
|
||||
// project — so init resolves it from the CLI's own context and records the
|
||||
// absolute path here. This is the only strategy that works on that install.
|
||||
try {
|
||||
const { execSync } = await import('child_process');
|
||||
const npmPrefix = execSync('npm prefix -g', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
if (npmPrefix) {
|
||||
// Both Unix (<prefix>/lib/node_modules) and Windows (<prefix>/node_modules)
|
||||
const roots = [join(npmPrefix, 'lib', 'node_modules'), join(npmPrefix, 'node_modules')];
|
||||
const wrappers = ['ruflo', 'claude-flow', join('@claude-flow', 'cli')];
|
||||
for (const root of roots) {
|
||||
for (const wrapper of wrappers) {
|
||||
const candidate = join(root, wrapper, 'node_modules', pkg, relativeFile);
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
const direct = join(root, pkg, relativeFile);
|
||||
if (existsSync(direct)) return direct;
|
||||
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
|
||||
if (existsSync(sidecar)) {
|
||||
const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
||||
if (rec?.distPath && existsSync(rec.distPath)) {
|
||||
return await import(`file://${rec.distPath}`);
|
||||
}
|
||||
}
|
||||
} catch { /* npm not on PATH or no global install */ }
|
||||
} catch { /* fall through */ }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function loadMemoryPackage() {
|
||||
// Strategy 1-5: locate dist/index.js across dev/installed/global layouts
|
||||
const resolved = await resolveBundledFile('@claude-flow/memory', 'dist/index.js');
|
||||
if (resolved) {
|
||||
// Strategy 1: Local dev (built dist)
|
||||
const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
|
||||
if (existsSync(localDist)) {
|
||||
try {
|
||||
return await import(`file://${resolved}`);
|
||||
return await import(`file://${localDist}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
|
||||
// Strategy 6: Use createRequire for CJS-style resolution (handles nested node_modules
|
||||
// Strategy 2: Use createRequire for CJS-style resolution (handles nested node_modules
|
||||
// when installed as a transitive dependency via npx ruflo / npx claude-flow)
|
||||
try {
|
||||
const { createRequire } = await import('module');
|
||||
@@ -229,11 +196,24 @@ async function loadMemoryPackage() {
|
||||
return require('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 7: ESM import (works when @claude-flow/memory is a direct dependency)
|
||||
// Strategy 3: ESM import (works when @claude-flow/memory is a direct dependency)
|
||||
try {
|
||||
return await import('@claude-flow/memory');
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
|
||||
let searchDir = PROJECT_ROOT;
|
||||
const { parse } = await import('path');
|
||||
while (searchDir !== parse(searchDir).root) {
|
||||
const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
|
||||
if (existsSync(candidate)) {
|
||||
try {
|
||||
return await import(`file://${candidate}`);
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
searchDir = dirname(searchDir);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -283,7 +263,7 @@ async function doImport() {
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
dim('Memory package not available — skipping auto memory import');
|
||||
warnMemoryUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,10 +272,7 @@ async function doImport() {
|
||||
await backend.initialize();
|
||||
|
||||
const bridgeConfig = {
|
||||
// Use Claude Code's invocation cwd (the user's project), not PROJECT_ROOT
|
||||
// — PROJECT_ROOT resolves to the plugin clone when this hook runs from
|
||||
// ~/.claude/plugins/marketplaces/ruflo/. See issue #2284.
|
||||
workingDir: process.env.CLAUDE_FLOW_CWD || process.cwd(),
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
@@ -327,36 +304,6 @@ async function doImport() {
|
||||
dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
|
||||
dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
|
||||
|
||||
// Bridge to AgentDB: store entries with ONNX vector embeddings for semantic search
|
||||
let vectorized = 0;
|
||||
try {
|
||||
const cliDistPath = await resolveBundledFile('@claude-flow/cli', 'dist/src/memory/memory-initializer.js');
|
||||
if (cliDistPath) {
|
||||
const memInit = await import(`file://${cliDistPath}`);
|
||||
await memInit.initializeMemoryDatabase({ force: false, verbose: false });
|
||||
|
||||
const entries = await backend.query({});
|
||||
for (const entry of entries) {
|
||||
if (!entry.content || entry.content.length < 10) continue;
|
||||
try {
|
||||
await memInit.storeEntry({
|
||||
key: entry.key || entry.id,
|
||||
value: entry.content,
|
||||
namespace: entry.namespace || 'auto-memory',
|
||||
generateEmbeddingFlag: true,
|
||||
});
|
||||
vectorized++;
|
||||
} catch { /* skip entries that fail to embed */ }
|
||||
}
|
||||
|
||||
if (vectorized > 0) {
|
||||
success(`Vectorized ${vectorized} entries into AgentDB (ONNX 384-dim)`);
|
||||
}
|
||||
}
|
||||
} catch (vecErr) {
|
||||
dim(`AgentDB vectorization skipped: ${vecErr.message?.slice(0, 60)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
dim(`Import failed (non-critical): ${err.message}`);
|
||||
}
|
||||
@@ -369,7 +316,7 @@ async function doSync() {
|
||||
|
||||
const memPkg = await loadMemoryPackage();
|
||||
if (!memPkg || !memPkg.AutoMemoryBridge) {
|
||||
dim('Memory package not available — skipping sync');
|
||||
warnMemoryUnavailable();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -385,8 +332,7 @@ async function doSync() {
|
||||
}
|
||||
|
||||
const bridgeConfig = {
|
||||
// See doImport — must reflect the user's project, not PROJECT_ROOT (#2284)
|
||||
workingDir: process.env.CLAUDE_FLOW_CWD || process.cwd(),
|
||||
workingDir: PROJECT_ROOT,
|
||||
syncMode: 'on-session-end',
|
||||
};
|
||||
|
||||
@@ -416,21 +362,6 @@ async function doSync() {
|
||||
// Curate MEMORY.md index with graph-aware ordering
|
||||
await bridge.curateIndex();
|
||||
success('Curated MEMORY.md index');
|
||||
|
||||
// Flush intelligence patterns to disk (SONA + ReasoningBank)
|
||||
try {
|
||||
const intPath = await resolveBundledFile('@claude-flow/cli', 'dist/src/memory/intelligence.js');
|
||||
if (intPath) {
|
||||
const intelligence = await import(`file://${intPath}`);
|
||||
if (intelligence.flushPatterns) {
|
||||
intelligence.flushPatterns();
|
||||
const stats = intelligence.getIntelligenceStats?.();
|
||||
if (stats && stats.patternsLearned > 0) {
|
||||
success(`Flushed ${stats.patternsLearned} learned patterns to disk`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* non-critical */ }
|
||||
} catch (err) {
|
||||
dim(`Sync failed (non-critical): ${err.message}`);
|
||||
}
|
||||
@@ -443,8 +374,12 @@ async function doStatus() {
|
||||
const memPkg = await loadMemoryPackage();
|
||||
const config = readConfig();
|
||||
|
||||
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
|
||||
const hasSidecar = existsSync(sidecar);
|
||||
|
||||
console.log('\n=== Auto Memory Bridge Status ===\n');
|
||||
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found'}`);
|
||||
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found — self-learning DISABLED (fix: npm i -D @claude-flow/memory)'}`);
|
||||
console.log(` Resolver: ${hasSidecar ? '✅ .claude-flow/memory-package.json' : '⏸ no sidecar (run: npx ruflo@latest doctor --fix)'}`);
|
||||
console.log(` Store: ${existsSync(STORE_PATH) ? '✅ ' + STORE_PATH : '⏸ Not initialized'}`);
|
||||
console.log(` LearningBridge: ${config.learningBridge.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
|
||||
@@ -457,158 +392,9 @@ async function doStatus() {
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// AgentDB vector status
|
||||
try {
|
||||
const dbPath = join(PROJECT_ROOT, '.claude-flow', 'memory', 'memory.db');
|
||||
const swarmDbPath = join(PROJECT_ROOT, '.swarm', 'memory.db');
|
||||
const hasDb = existsSync(dbPath) || existsSync(swarmDbPath);
|
||||
console.log(` AgentDB: ${hasDb ? '✅ sql.js database exists' : '⏸ Not initialized'}`);
|
||||
|
||||
const intPath = await resolveBundledFile('@claude-flow/cli', 'dist/src/memory/intelligence.js');
|
||||
if (intPath) {
|
||||
const intelligence = await import(`file://${intPath}`);
|
||||
const stats = intelligence.getIntelligenceStats?.();
|
||||
if (stats) {
|
||||
console.log(` SONA: ${stats.sonaEnabled ? '✅ Active' : '⏸ Not initialized'}`);
|
||||
console.log(` Patterns: ${stats.patternsLearned} learned`);
|
||||
console.log(` Trajectories: ${stats.trajectoriesRecorded} recorded`);
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Import ALL Claude Code memories from all projects into AgentDB
|
||||
// ============================================================================
|
||||
|
||||
async function doImportAll() {
|
||||
log('Importing ALL Claude Code memories into AgentDB...');
|
||||
|
||||
const { homedir } = await import('os');
|
||||
const { readdirSync } = await import('fs');
|
||||
const claudeProjectsDir = join(homedir(), '.claude', 'projects');
|
||||
|
||||
if (!existsSync(claudeProjectsDir)) {
|
||||
dim('No Claude Code projects found at ' + claudeProjectsDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all memory files across all projects
|
||||
const memoryFiles = [];
|
||||
try {
|
||||
const projects = readdirSync(claudeProjectsDir, { withFileTypes: true });
|
||||
for (const project of projects) {
|
||||
if (!project.isDirectory()) continue;
|
||||
const memDir = join(claudeProjectsDir, project.name, 'memory');
|
||||
if (!existsSync(memDir)) continue;
|
||||
const files = readdirSync(memDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
memoryFiles.push({
|
||||
path: join(memDir, file),
|
||||
project: project.name,
|
||||
file,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
dim('Error scanning projects: ' + err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (memoryFiles.length === 0) {
|
||||
dim('No memory files found');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Found ${memoryFiles.length} memory files across ${new Set(memoryFiles.map(f => f.project)).size} projects`);
|
||||
|
||||
// Load memory-initializer for vectorized storage
|
||||
let memInit = null;
|
||||
try {
|
||||
const cliDistPath = await resolveBundledFile('@claude-flow/cli', 'dist/src/memory/memory-initializer.js');
|
||||
if (cliDistPath) {
|
||||
memInit = await import(`file://${cliDistPath}`);
|
||||
await memInit.initializeMemoryDatabase({ force: false, verbose: false });
|
||||
}
|
||||
} catch (err) {
|
||||
dim('Memory initializer not available: ' + err.message?.slice(0, 60));
|
||||
}
|
||||
|
||||
if (!memInit) {
|
||||
dim('Cannot vectorize without memory-initializer. Run: cd v3/@claude-flow/cli && npm run build');
|
||||
return;
|
||||
}
|
||||
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const memFile of memoryFiles) {
|
||||
try {
|
||||
const content = readFileSync(memFile.path, 'utf-8');
|
||||
|
||||
// Parse YAML frontmatter + body
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||
let name = memFile.file.replace('.md', '');
|
||||
let description = '';
|
||||
let type = 'auto-memory';
|
||||
let body = content;
|
||||
|
||||
if (frontmatterMatch) {
|
||||
const yaml = frontmatterMatch[1];
|
||||
body = frontmatterMatch[2].trim();
|
||||
const nameMatch = yaml.match(/^name:\s*(.+)$/m);
|
||||
const descMatch = yaml.match(/^description:\s*(.+)$/m);
|
||||
const typeMatch = yaml.match(/^type:\s*(.+)$/m);
|
||||
if (nameMatch) name = nameMatch[1].trim();
|
||||
if (descMatch) description = descMatch[1].trim();
|
||||
if (typeMatch) type = typeMatch[1].trim();
|
||||
}
|
||||
|
||||
// Split body into sections (## headers) for granular entries
|
||||
const sections = body.split(/^(?=## )/m).filter(s => s.trim().length > 20);
|
||||
|
||||
if (sections.length === 0) {
|
||||
// Store whole file as one entry
|
||||
if (body.length > 10) {
|
||||
await memInit.storeEntry({
|
||||
key: `claude-memory:${memFile.project}:${name}`,
|
||||
value: body.slice(0, 4096),
|
||||
namespace: 'claude-memories',
|
||||
generateEmbeddingFlag: true,
|
||||
});
|
||||
imported++;
|
||||
}
|
||||
} else {
|
||||
// Store each section separately for better search granularity
|
||||
for (const section of sections) {
|
||||
const titleMatch = section.match(/^##\s+(.+)/);
|
||||
const sectionTitle = titleMatch ? titleMatch[1].trim() : name;
|
||||
const sectionBody = section.replace(/^##\s+.+\n/, '').trim();
|
||||
if (sectionBody.length < 10) continue;
|
||||
|
||||
await memInit.storeEntry({
|
||||
key: `claude-memory:${memFile.project}:${name}:${sectionTitle.slice(0, 50)}`,
|
||||
value: sectionBody.slice(0, 4096),
|
||||
namespace: 'claude-memories',
|
||||
generateEmbeddingFlag: true,
|
||||
});
|
||||
imported++;
|
||||
}
|
||||
}
|
||||
|
||||
dim(` ✓ ${memFile.project}/${memFile.file} (${sections.length || 1} entries)`);
|
||||
} catch (err) {
|
||||
dim(` ✗ ${memFile.project}/${memFile.file}: ${err.message?.slice(0, 60)}`);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
success(`Imported ${imported} entries from ${memoryFiles.length} files (${skipped} skipped)`);
|
||||
success('All Claude Code memories now searchable via AgentDB vector search');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
@@ -630,16 +416,15 @@ process.on('unhandledRejection', (reason) => {
|
||||
try {
|
||||
switch (command) {
|
||||
case 'import': await doImport(); break;
|
||||
case 'import-all': await doImportAll(); break;
|
||||
case 'sync': await doSync(); break;
|
||||
case 'status': await doStatus(); break;
|
||||
default:
|
||||
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// Hooks must never crash Claude Code - fail silently
|
||||
dim(`Error (non-critical): ${err.message}`);
|
||||
try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
|
||||
}
|
||||
// Ensure clean exit for Claude Code hooks (exit 0 = success, no stderr = no error)
|
||||
// Force clean exit — process.exitCode alone isn't enough if async errors override it
|
||||
process.exit(0);
|
||||
|
||||
Regular → Executable
+132
-112
@@ -2,6 +2,15 @@
|
||||
/**
|
||||
* Claude Flow Hook Handler (Cross-Platform)
|
||||
* Dispatches hook events to the appropriate helper modules.
|
||||
*
|
||||
* Usage: node hook-handler.cjs <command> [args...]
|
||||
*
|
||||
* Commands:
|
||||
* route - Route a task to optimal agent (reads PROMPT from env/stdin)
|
||||
* pre-bash - Validate command safety before execution
|
||||
* post-edit - Record edit outcome for learning
|
||||
* session-restore - Restore previous session state
|
||||
* session-end - End session and persist state
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
@@ -9,6 +18,9 @@ const fs = require('fs');
|
||||
|
||||
const helpersDir = __dirname;
|
||||
|
||||
// Safe require with stdout suppression - the helper modules have CLI
|
||||
// sections that run unconditionally on require(), so we mute console
|
||||
// during the require to prevent noisy output.
|
||||
function safeRequire(modulePath) {
|
||||
try {
|
||||
if (fs.existsSync(modulePath)) {
|
||||
@@ -30,51 +42,43 @@ function safeRequire(modulePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const router = safeRequire(path.join(helpersDir, 'router.cjs'));
|
||||
const session = safeRequire(path.join(helpersDir, 'session.cjs'));
|
||||
const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));
|
||||
const router = safeRequire(path.join(helpersDir, 'router.js'));
|
||||
const session = safeRequire(path.join(helpersDir, 'session.js'));
|
||||
const memory = safeRequire(path.join(helpersDir, 'memory.js'));
|
||||
const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));
|
||||
|
||||
// ── Intelligence timeout protection (fixes #1530, #1531) ───────────────────
|
||||
var INTELLIGENCE_TIMEOUT_MS = 3000;
|
||||
// Race the (possibly-async) work against a real timeout.
|
||||
const INTELLIGENCE_TIMEOUT_MS = 3000;
|
||||
// Race the (possibly-async) work against a real timeout. The previous version
|
||||
// called fn() and clearTimeout(timer) immediately, so an async fn returned a
|
||||
// pending promise that resolved THROUGH the race — the timeout protected
|
||||
// nothing. This settles on whichever finishes first, then clears the timer.
|
||||
//
|
||||
// The previous implementation called `fn()` and then `clearTimeout(timer)`
|
||||
// immediately — but if `fn()` returns a *pending* promise (async work), the
|
||||
// timer was cancelled before the work settled and the promise resolved with the
|
||||
// still-pending promise, so the timeout protected nothing. This version only
|
||||
// settles when the work resolves OR the timeout fires, whichever comes first,
|
||||
// and clears the timer afterwards.
|
||||
//
|
||||
// LIMITATION (be honest): a *synchronous* CPU-bound `fn` (e.g. the current
|
||||
// intelligence.init(), which does blocking fs reads) cannot be interrupted by
|
||||
// any in-process timer — the event loop is blocked, so the timeout callback
|
||||
// can't run until `fn` already returned. The real guard for that case lives in
|
||||
// intelligence.cjs (MAX_DATA_FILE_SIZE / MAX_GRAPH_NODES caps). This util only
|
||||
// bounds work that yields to the event loop (async I/O, dynamic import, etc.).
|
||||
// LIMITATION: a synchronous blocking fn (the current intelligence.init() does
|
||||
// blocking fs reads) cannot be interrupted by any in-process timer — the event
|
||||
// loop is blocked. The real guard for that case is the readJSON file-size
|
||||
// limit in intelligence.cjs. This util only bounds work that yields (async I/O).
|
||||
function runWithTimeout(fn, label) {
|
||||
var timer;
|
||||
var timeout = new Promise(function(resolve) {
|
||||
timer = setTimeout(function() {
|
||||
let timer;
|
||||
const timeout = new Promise((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
process.stderr.write("[WARN] " + label + " timed out after " + INTELLIGENCE_TIMEOUT_MS + "ms, skipping\n");
|
||||
resolve(null);
|
||||
}, INTELLIGENCE_TIMEOUT_MS);
|
||||
});
|
||||
// Promise.resolve().then(fn) turns a synchronous throw into a rejection so it
|
||||
// is handled here rather than escaping the Promise constructor.
|
||||
var work = Promise.resolve().then(fn).catch(function() { return null; });
|
||||
return Promise.race([work, timeout]).then(function(result) {
|
||||
const work = Promise.resolve().then(fn).catch(() => null);
|
||||
return Promise.race([work, timeout]).then((result) => {
|
||||
clearTimeout(timer);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Get the command from argv
|
||||
const [,, command, ...args] = process.argv;
|
||||
|
||||
// Read stdin — Claude Code sends hook data as JSON via stdin
|
||||
// Uses a timeout to prevent hanging when stdin is in an ambiguous state
|
||||
// (not TTY, not a proper pipe) which happens with Claude Code hook invocations.
|
||||
// Read stdin with timeout — Claude Code sends hook data as JSON via stdin.
|
||||
// Timeout prevents hanging when stdin is not properly closed (common on Windows).
|
||||
async function readStdin() {
|
||||
if (process.stdin.isTTY) return '';
|
||||
return new Promise((resolve) => {
|
||||
@@ -94,11 +98,11 @@ async function readStdin() {
|
||||
|
||||
async function main() {
|
||||
// Global safety timeout: hooks must NEVER hang (#1530, #1531)
|
||||
var safetyTimer = setTimeout(function() {
|
||||
const safetyTimer = setTimeout(() => {
|
||||
process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n");
|
||||
process.exit(0);
|
||||
}, 5000);
|
||||
safetyTimer.unref();
|
||||
safetyTimer.unref(); // don't keep process alive just for this timer
|
||||
|
||||
let stdinData = '';
|
||||
try { stdinData = await readStdin(); } catch (e) { /* ignore stdin errors */ }
|
||||
@@ -108,21 +112,40 @@ async function main() {
|
||||
try { hookInput = JSON.parse(stdinData); } catch (e) { /* ignore parse errors */ }
|
||||
}
|
||||
|
||||
// Merge stdin data into prompt resolution: prefer stdin fields, then env vars.
|
||||
// NEVER fall back to argv args — shell glob expansion of braces in bash output
|
||||
// creates junk files (#1342). Use env vars or stdin only.
|
||||
// Normalize snake_case/camelCase: Claude Code sends tool_input/tool_name (snake_case)
|
||||
var toolInput = hookInput.toolInput || hookInput.tool_input || {};
|
||||
var toolName = hookInput.toolName || hookInput.tool_name || '';
|
||||
const toolInput = hookInput.toolInput || hookInput.tool_input || {};
|
||||
const toolName = hookInput.toolName || hookInput.tool_name || '';
|
||||
|
||||
// `toolInput` is an object (e.g. {command:"ls"}) — falling back to it
|
||||
// directly bound `prompt` to the object and tripped `.toLowerCase()` /
|
||||
// `.substring()` on every Bash hook (#1944). Use the `.command` field.
|
||||
var prompt = hookInput.prompt || hookInput.command || toolInput.command
|
||||
|| process.env.PROMPT || process.env.TOOL_INPUT_command || '';
|
||||
// Merge stdin data into prompt resolution: prefer stdin fields, then env, then argv.
|
||||
// `toolInput` is an object (e.g. {command:"ls"}) — it's truthy but not a string,
|
||||
// so falling back to it directly bound `prompt` to the object and tripped
|
||||
// `.toLowerCase()` / `.substring()` on every Bash hook (#1944). Use the
|
||||
// `.command` field instead, which is the actual string the hook needs.
|
||||
const prompt = hookInput.prompt || hookInput.command || toolInput.command
|
||||
|| process.env.PROMPT || process.env.TOOL_INPUT_command || args.join(' ') || '';
|
||||
|
||||
// ADR-174: capture FAILURES so the learning substrate has negative examples.
|
||||
// Claude Code's PostToolUse payload carries the tool result; a failed
|
||||
// Write/Edit/Bash surfaces as tool_response.is_error / an error string /
|
||||
// a non-zero exit code. Conservative — only a positive error signal counts
|
||||
// as failure (mirrors isToolFailure() in helpers-generator.ts).
|
||||
const toolFailed = (function (hi) {
|
||||
if (!hi || typeof hi !== 'object') return false;
|
||||
const tr = hi.tool_response != null ? hi.tool_response : (hi.toolResponse != null ? hi.toolResponse : hi.result);
|
||||
if (tr == null) return false;
|
||||
if (typeof tr === 'string') return /\b(error|failed|failure|exception|not found|no such|permission denied|traceback)\b/i.test(tr);
|
||||
if (typeof tr === 'object') {
|
||||
if (tr.is_error === true || tr.isError === true || tr.success === false || tr.error != null) return true;
|
||||
const code = tr.exit_code != null ? tr.exit_code : (tr.exitCode != null ? tr.exitCode : tr.code);
|
||||
if (typeof code === 'number' && code !== 0) return true;
|
||||
if (Array.isArray(tr.content) && tr.is_error === true) return true;
|
||||
}
|
||||
return false;
|
||||
})(hookInput);
|
||||
|
||||
const handlers = {
|
||||
'route': () => {
|
||||
// Inject ranked intelligence context before routing
|
||||
if (intelligence && intelligence.getContext) {
|
||||
try {
|
||||
const ctx = intelligence.getContext(prompt);
|
||||
@@ -131,14 +154,16 @@ const handlers = {
|
||||
}
|
||||
if (router && router.routeTask) {
|
||||
const result = router.routeTask(prompt);
|
||||
var output = [];
|
||||
output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));
|
||||
output.push('');
|
||||
output.push('+------------------- Primary Recommendation -------------------+');
|
||||
output.push('| Agent: ' + result.agent.padEnd(53) + '|');
|
||||
output.push('| Confidence: ' + (result.confidence * 100).toFixed(1) + '%' + ' '.repeat(44) + '|');
|
||||
output.push('| Reason: ' + result.reason.substring(0, 53).padEnd(53) + '|');
|
||||
output.push('+--------------------------------------------------------------+');
|
||||
// Format output for Claude Code hook consumption — real data only
|
||||
const output = [
|
||||
`[INFO] Routing task: ${prompt.substring(0, 80) || '(no prompt)'}`,
|
||||
'',
|
||||
'+------------------- Primary Recommendation -------------------+',
|
||||
`| Agent: ${result.agent.padEnd(53)}|`,
|
||||
`| Confidence: ${(result.confidence * 100).toFixed(1)}%${' '.repeat(44)}|`,
|
||||
`| Reason: ${(result.reason || '').substring(0, 53).padEnd(53)}|`,
|
||||
'+--------------------------------------------------------------+',
|
||||
];
|
||||
console.log(output.join('\n'));
|
||||
} else {
|
||||
console.log('[INFO] Router not available, using default routing');
|
||||
@@ -146,14 +171,16 @@ const handlers = {
|
||||
},
|
||||
|
||||
'pre-bash': () => {
|
||||
// Basic command safety check — prefer stdin command data from Claude Code.
|
||||
// String() wrap is belt-and-suspenders for #2017: even if a future regression
|
||||
// re-binds `prompt` or `hookInput.command` to a non-string, `.toLowerCase()`
|
||||
// can no longer throw a TypeError that the global try/catch would swallow.
|
||||
var cmd = String(hookInput.command || toolInput.command || prompt || '').toLowerCase();
|
||||
var dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
|
||||
for (var i = 0; i < dangerous.length; i++) {
|
||||
if (cmd.includes(dangerous[i])) {
|
||||
console.error('[BLOCKED] Dangerous command detected: ' + dangerous[i]);
|
||||
// can no longer throw a TypeError that the global try/catch would swallow
|
||||
// (silently exiting 0 and letting the dangerous command through).
|
||||
const cmd = String(hookInput.command || toolInput.command || prompt || '').toLowerCase();
|
||||
const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
|
||||
for (const d of dangerous) {
|
||||
if (cmd.includes(d)) {
|
||||
console.error(`[BLOCKED] Dangerous command detected: ${d}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -161,46 +188,60 @@ const handlers = {
|
||||
},
|
||||
|
||||
'post-edit': () => {
|
||||
// Record edit for session metrics
|
||||
if (session && session.metric) {
|
||||
try { session.metric('edits'); } catch (e) { /* no active session */ }
|
||||
}
|
||||
// Record edit for intelligence consolidation — prefer stdin data from Claude Code
|
||||
if (intelligence && intelligence.recordEdit) {
|
||||
try {
|
||||
var file = hookInput.file_path || toolInput.file_path
|
||||
const file = hookInput.file_path || toolInput.file_path
|
||||
|| process.env.TOOL_INPUT_file_path || args[0] || '';
|
||||
intelligence.recordEdit(file);
|
||||
intelligence.recordEdit(file, !toolFailed);
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
console.log('[OK] Edit recorded');
|
||||
console.log(toolFailed ? '[LEARN] Edit FAILURE recorded' : '[OK] Edit recorded');
|
||||
},
|
||||
|
||||
'session-restore': async () => {
|
||||
if (session) {
|
||||
var existing = session.restore && session.restore();
|
||||
// Try restore first, fall back to start
|
||||
const existing = session.restore && session.restore();
|
||||
if (!existing) {
|
||||
session.start && session.start();
|
||||
}
|
||||
} else {
|
||||
console.log('[OK] Session restored: session-' + Date.now());
|
||||
// Minimal session restore output
|
||||
const sessionId = `session-${Date.now()}`;
|
||||
console.log(`[INFO] Restoring session: %SESSION_ID%`);
|
||||
console.log('');
|
||||
console.log(`[OK] Session restored from %SESSION_ID%`);
|
||||
console.log(`New session ID: ${sessionId}`);
|
||||
console.log('');
|
||||
console.log('Restored State');
|
||||
console.log('+----------------+-------+');
|
||||
console.log('| Item | Count |');
|
||||
console.log('+----------------+-------+');
|
||||
console.log('| Tasks | 0 |');
|
||||
console.log('| Agents | 0 |');
|
||||
console.log('| Memory Entries | 0 |');
|
||||
console.log('+----------------+-------+');
|
||||
}
|
||||
// Initialize intelligence (with timeout — #1530)
|
||||
// Initialize intelligence graph after session restore (with timeout — #1530)
|
||||
if (intelligence && intelligence.init) {
|
||||
var initResult = await runWithTimeout(function() { return intelligence.init(); }, 'intelligence.init()');
|
||||
const initResult = await runWithTimeout(() => intelligence.init(), 'intelligence.init()');
|
||||
if (initResult && initResult.nodes > 0) {
|
||||
console.log('[INTELLIGENCE] Loaded ' + initResult.nodes + ' patterns, ' + initResult.edges + ' edges');
|
||||
console.log(`[INTELLIGENCE] Loaded ${initResult.nodes} patterns, ${initResult.edges} edges`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'session-end': async () => {
|
||||
// Consolidate intelligence (with timeout — #1530)
|
||||
// Consolidate intelligence before ending session (with timeout — #1530)
|
||||
if (intelligence && intelligence.consolidate) {
|
||||
var consResult = await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()');
|
||||
const consResult = await runWithTimeout(() => intelligence.consolidate(), 'intelligence.consolidate()');
|
||||
if (consResult && consResult.entries > 0) {
|
||||
var msg = '[INTELLIGENCE] Consolidated: ' + consResult.entries + ' entries, ' + consResult.edges + ' edges';
|
||||
if (consResult.newEntries > 0) msg += ', ' + consResult.newEntries + ' new';
|
||||
msg += ', PageRank recomputed';
|
||||
console.log(msg);
|
||||
console.log(`[INTELLIGENCE] Consolidated: ${consResult.entries} entries, ${consResult.edges} edges${consResult.newEntries > 0 ? `, ${consResult.newEntries} new` : ''}, PageRank recomputed`);
|
||||
}
|
||||
}
|
||||
if (session && session.end) {
|
||||
@@ -214,44 +255,25 @@ const handlers = {
|
||||
if (session && session.metric) {
|
||||
try { session.metric('tasks'); } catch (e) { /* no active session */ }
|
||||
}
|
||||
// Route the task if router is available
|
||||
if (router && router.routeTask && prompt) {
|
||||
var result = router.routeTask(prompt);
|
||||
console.log('[INFO] Task routed to: ' + result.agent + ' (confidence: ' + result.confidence + ')');
|
||||
const result = router.routeTask(prompt);
|
||||
console.log(`[INFO] Task routed to: ${result.agent} (confidence: ${result.confidence})`);
|
||||
} else {
|
||||
console.log('[OK] Task started');
|
||||
}
|
||||
},
|
||||
|
||||
'post-task': () => {
|
||||
// ADR-174: feed the REAL outcome (feedback() boosts confidence on success,
|
||||
// decays it on failure) instead of a hardcoded true — no more all-positive
|
||||
// signal that the substrate can't learn from.
|
||||
if (intelligence && intelligence.feedback) {
|
||||
try {
|
||||
intelligence.feedback(true);
|
||||
intelligence.feedback(!toolFailed);
|
||||
} catch (e) { /* non-fatal */ }
|
||||
}
|
||||
console.log('[OK] Task completed');
|
||||
},
|
||||
|
||||
'compact-manual': () => {
|
||||
console.log('PreCompact Guidance:');
|
||||
console.log('IMPORTANT: Review CLAUDE.md in project root for:');
|
||||
console.log(' - Available agents and concurrent usage patterns');
|
||||
console.log(' - Swarm coordination strategies (hierarchical, mesh, adaptive)');
|
||||
console.log(' - Critical concurrent execution rules (1 MESSAGE = ALL OPERATIONS)');
|
||||
console.log('Ready for compact operation');
|
||||
},
|
||||
|
||||
'compact-auto': () => {
|
||||
console.log('Auto-Compact Guidance (Context Window Full):');
|
||||
console.log('CRITICAL: Before compacting, ensure you understand:');
|
||||
console.log(' - All agents available in .claude/agents/ directory');
|
||||
console.log(' - Concurrent execution patterns from CLAUDE.md');
|
||||
console.log(' - Swarm coordination strategies for complex tasks');
|
||||
console.log('Apply GOLDEN RULE: Always batch operations in single messages');
|
||||
console.log('Auto-compact proceeding with full agent context');
|
||||
},
|
||||
|
||||
'status': () => {
|
||||
console.log('[OK] Status check');
|
||||
console.log(toolFailed ? '[LEARN] Task FAILURE recorded' : '[OK] Task completed');
|
||||
},
|
||||
|
||||
'stats': () => {
|
||||
@@ -263,29 +285,27 @@ const handlers = {
|
||||
},
|
||||
};
|
||||
|
||||
if (command && handlers[command]) {
|
||||
// Execute the handler
|
||||
if (command && handlers[command]) {
|
||||
try {
|
||||
await Promise.resolve(handlers[command]());
|
||||
} catch (e) {
|
||||
console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
|
||||
// Hooks should never crash Claude Code - fail silently
|
||||
console.log(`[WARN] Hook ${command} encountered an error: ${e.message}`);
|
||||
}
|
||||
} else if (command) {
|
||||
console.log('[OK] Hook: ' + command);
|
||||
// Unknown command - pass through without error
|
||||
console.log(`[OK] Hook: ${command}`);
|
||||
} else {
|
||||
console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
|
||||
console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|stats>');
|
||||
}
|
||||
}
|
||||
|
||||
// Only dispatch hooks when run directly (node hook-handler.cjs ...). When
|
||||
// require()'d by a test, just expose the internals so they can be unit-tested
|
||||
// without triggering stdin reads / process.exit.
|
||||
if (require.main === module) {
|
||||
main().catch(function(e) {
|
||||
console.log('[WARN] Hook handler error: ' + e.message);
|
||||
}).finally(function() {
|
||||
// Ensure clean exit for Claude Code hooks
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { runWithTimeout: runWithTimeout, INTELLIGENCE_TIMEOUT_MS: INTELLIGENCE_TIMEOUT_MS };
|
||||
// Hooks must ALWAYS exit 0 — Claude Code treats non-zero as "hook error"
|
||||
// and skips all subsequent hooks for the event.
|
||||
process.exitCode = 0;
|
||||
main().catch((e) => {
|
||||
try { console.log(`[WARN] Hook handler error: ${e.message}`); } catch (_) {}
|
||||
}).finally(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
Regular → Executable
+994
-198
File diff suppressed because it is too large
Load Diff
@@ -171,3 +171,9 @@ hive-mind-prompt-*.txt
|
||||
|
||||
# Publish-artifact mirror — regenerated by prepublishOnly (package.json:90); never edit or commit
|
||||
v3/@claude-flow/cli/plugins/
|
||||
|
||||
# Runtime artifacts of the version-stamped helper auto-refresh (ADR-174) —
|
||||
# regenerated on demand, not source. The signed manifest source of truth lives
|
||||
# in v3/@claude-flow/cli/.claude/helpers/helpers.manifest.json.
|
||||
.claude/helpers/.helpers-version
|
||||
.claude/helpers/helpers.manifest.json
|
||||
|
||||
Reference in New Issue
Block a user