claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
421 lines (379 loc) • 9.69 kB
text/typescript
/**
* V3 Hook Executor
*
* Executes hooks in priority order with timeout handling,
* error recovery, and result aggregation.
*/
import type {
HookEvent,
HookContext,
HookResult,
HookExecutionOptions,
HookExecutionResult,
HookEntry,
} from '../types.js';
import { HookRegistry, defaultRegistry } from '../registry/index.js';
/**
* Default execution options
*/
const DEFAULT_OPTIONS: Required<HookExecutionOptions> = {
continueOnError: false,
timeout: 5000,
emitEvents: true,
};
/**
* Hook Executor - executes hooks for events
*/
export class HookExecutor {
private registry: HookRegistry;
private eventEmitter?: {
emit: (event: string, data: unknown) => void;
};
constructor(registry?: HookRegistry) {
this.registry = registry ?? defaultRegistry;
}
/**
* Set event emitter for hook execution events
*/
setEventEmitter(emitter: { emit: (event: string, data: unknown) => void }): void {
this.eventEmitter = emitter;
}
/**
* Execute all hooks for an event
*/
async execute<T = unknown>(
event: HookEvent,
context: Partial<HookContext<T>>,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const opts = { ...DEFAULT_OPTIONS, ...options };
const startTime = Date.now();
// Build full context
const fullContext: HookContext<T> = {
event,
timestamp: new Date(),
...context,
} as HookContext<T>;
// Get hooks for event
const hooks = this.registry.getForEvent(event, true);
if (hooks.length === 0) {
return {
success: true,
hooksExecuted: 0,
hooksFailed: 0,
executionTime: Date.now() - startTime,
results: [],
finalContext: fullContext,
};
}
// Execute hooks in priority order
const results: HookExecutionResult['results'] = [];
const warnings: string[] = [];
const messages: string[] = [];
let aborted = false;
let hooksFailed = 0;
for (const hook of hooks) {
if (aborted) break;
const hookStart = Date.now();
let result: HookResult;
try {
result = await this.executeWithTimeout(
hook,
fullContext,
opts.timeout
);
} catch (error) {
result = {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
const hookDuration = Date.now() - hookStart;
results.push({
hookId: hook.id,
hookName: hook.name,
success: result.success,
duration: hookDuration,
error: result.error,
});
// Collect warnings and messages
if (result.warnings) {
warnings.push(...result.warnings);
}
if (result.message) {
messages.push(result.message);
}
// Update context with hook data
if (result.data) {
Object.assign(fullContext, { metadata: { ...fullContext.metadata, ...result.data } });
}
// Record stats
this.registry.recordExecution(result.success, hookDuration);
// Handle failure
if (!result.success) {
hooksFailed++;
if (opts.emitEvents && this.eventEmitter) {
this.eventEmitter.emit('hook:failed', {
hookId: hook.id,
hookName: hook.name,
event,
error: result.error,
});
}
if (!opts.continueOnError) {
aborted = true;
break;
}
}
// Handle abort request
if (result.abort) {
aborted = true;
break;
}
// Emit success event
if (opts.emitEvents && this.eventEmitter && result.success) {
this.eventEmitter.emit('hook:executed', {
hookId: hook.id,
hookName: hook.name,
event,
duration: hookDuration,
});
}
}
const executionTime = Date.now() - startTime;
// Emit completion event
if (opts.emitEvents && this.eventEmitter) {
this.eventEmitter.emit('hooks:completed', {
event,
hooksExecuted: results.length,
hooksFailed,
executionTime,
aborted,
});
}
return {
success: hooksFailed === 0 && !aborted,
aborted,
hooksExecuted: results.length,
hooksFailed,
executionTime,
results,
finalContext: fullContext,
warnings: warnings.length > 0 ? warnings : undefined,
messages: messages.length > 0 ? messages : undefined,
};
}
/**
* Execute a single hook with timeout
*/
private async executeWithTimeout(
hook: HookEntry,
context: HookContext,
timeout: number
): Promise<HookResult> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Hook ${hook.id} timed out after ${timeout}ms`));
}, timeout);
Promise.resolve(hook.handler(context))
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
/**
* Execute hooks for pre-tool-use event
*/
async preToolUse(
toolName: string,
parameters: Record<string, unknown>,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.PreToolUse,
{
tool: { name: toolName, parameters },
},
options
);
}
/**
* Execute hooks for post-tool-use event
*/
async postToolUse(
toolName: string,
parameters: Record<string, unknown>,
duration: number,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.PostToolUse,
{
tool: { name: toolName, parameters },
duration,
},
options
);
}
/**
* Execute hooks for pre-edit event
*/
async preEdit(
filePath: string,
operation: 'create' | 'modify' | 'delete',
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.PreEdit,
{
file: { path: filePath, operation },
},
options
);
}
/**
* Execute hooks for post-edit event
*/
async postEdit(
filePath: string,
operation: 'create' | 'modify' | 'delete',
duration: number,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.PostEdit,
{
file: { path: filePath, operation },
duration,
},
options
);
}
/**
* Execute hooks for pre-command event
*/
async preCommand(
command: string,
workingDirectory?: string,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.PreCommand,
{
command: { raw: command, workingDirectory },
},
options
);
}
/**
* Execute hooks for post-command event
*/
async postCommand(
command: string,
exitCode: number,
output?: string,
error?: string,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.PostCommand,
{
command: { raw: command, exitCode, output, error },
},
options
);
}
/**
* Execute hooks for session-start event
*/
async sessionStart(
sessionId: string,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.SessionStart,
{
session: { id: sessionId, startedAt: new Date() },
},
options
);
}
/**
* Execute hooks for session-end event
*/
async sessionEnd(
sessionId: string,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.SessionEnd,
{
session: { id: sessionId, startedAt: new Date() },
},
options
);
}
/**
* Execute hooks for agent-spawn event
*/
async agentSpawn(
agentId: string,
agentType: string,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.AgentSpawn,
{
agent: { id: agentId, type: agentType },
},
options
);
}
/**
* Execute hooks for agent-terminate event
*/
async agentTerminate(
agentId: string,
agentType: string,
status: string,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
const { HookEvent } = await import('../types.js');
return this.execute(
HookEvent.AgentTerminate,
{
agent: { id: agentId, type: agentType, status },
},
options
);
}
}
/**
* Default global executor instance
*/
export const defaultExecutor = new HookExecutor();
/**
* Convenience function to execute hooks on the default executor
*/
export async function executeHooks<T = unknown>(
event: HookEvent,
context: Partial<HookContext<T>>,
options?: HookExecutionOptions
): Promise<HookExecutionResult> {
return defaultExecutor.execute(event, context, options);
}
export { HookExecutor as default };