/** * Guidance Hook Integration Layer * * Wires the EnforcementGates and ShardRetriever into the Claude Flow V3 * hook lifecycle. Each guidance concern is registered as a hook that * participates in the standard HookRegistry event flow. * * Hook mappings: * PreCommand -> EnforcementGates.evaluateCommand() (destructive ops + secrets) * PreToolUse -> EnforcementGates.evaluateToolUse() (tool allowlist + secrets) * PreEdit -> EnforcementGates.evaluateEdit() (diff size + secrets) * PreTask -> ShardRetriever.retrieve() (inject relevant shards) * PostTask -> RunLedger.finalizeEvent() (record run completion) * * @module @claude-flow/guidance/hooks */ import { HookEvent, HookPriority, } from '@claude-flow/hooks'; // ============================================================================ // Gate-Result -> Hook-Result Mapping // ============================================================================ /** * Convert an array of GateResults into a single HookResult. * * Severity ordering: block > require-confirmation > warn > allow. * The most restrictive decision drives the hook outcome. */ function gateResultsToHookResult(gateResults) { if (gateResults.length === 0) { return { success: true }; } const severityOrder = { block: 3, 'require-confirmation': 2, warn: 1, allow: 0, }; // Sort by severity descending to find the most restrictive const sorted = [...gateResults].sort((a, b) => severityOrder[b.decision] - severityOrder[a.decision]); const worst = sorted[0]; // Collect all warnings and reasons const allWarnings = []; const allReasons = []; for (const result of gateResults) { allReasons.push(`[${result.gateName}] ${result.reason}`); if (result.remediation) { allWarnings.push(`[${result.gateName}] ${result.remediation}`); } } switch (worst.decision) { case 'block': return { success: false, abort: true, error: allReasons.join(' | '), message: allReasons.join('\n'), warnings: allWarnings.length > 0 ? allWarnings : undefined, data: { gateDecision: 'block', gateResults: gateResults.map(r => ({ gate: r.gateName, decision: r.decision, reason: r.reason, })), }, }; case 'require-confirmation': return { success: false, abort: true, message: allReasons.join('\n'), warnings: allWarnings.length > 0 ? allWarnings : undefined, data: { gateDecision: 'require-confirmation', gateResults: gateResults.map(r => ({ gate: r.gateName, decision: r.decision, reason: r.reason, })), }, }; case 'warn': return { success: true, message: allReasons.join('\n'), warnings: allWarnings.length > 0 ? allWarnings : allReasons, data: { gateDecision: 'warn', gateResults: gateResults.map(r => ({ gate: r.gateName, decision: r.decision, reason: r.reason, })), }, }; default: return { success: true }; } } // ============================================================================ // Guidance Hook Provider // ============================================================================ /** * Provides guidance enforcement hooks for the V3 hook system. * * Registers hooks on a HookRegistry that wire each lifecycle event * (PreCommand, PreToolUse, PreEdit, PreTask, PostTask) to the * appropriate guidance subsystem (gates, retriever, ledger). */ export class GuidanceHookProvider { gates; retriever; ledger; /** IDs of hooks registered by this provider, for cleanup */ hookIds = []; /** Active run events keyed by task ID, for PostTask finalization */ activeRuns = new Map(); constructor(gates, retriever, ledger) { this.gates = gates; this.retriever = retriever; this.ledger = ledger; } /** * Register all guidance hooks on the given registry. * * Returns the array of generated hook IDs for tracking. */ registerAll(registry) { this.hookIds = []; // 1. PreCommand -> gate enforcement (Critical priority) this.hookIds.push(registry.register(HookEvent.PreCommand, (ctx) => this.handlePreCommand(ctx), HookPriority.Critical, { name: 'guidance-gate-pre-command', description: 'Evaluates commands for destructive ops and secrets', })); // 2. PreToolUse -> gate enforcement (Critical priority) this.hookIds.push(registry.register(HookEvent.PreToolUse, (ctx) => this.handlePreToolUse(ctx), HookPriority.Critical, { name: 'guidance-gate-pre-tool-use', description: 'Evaluates tool usage against allowlist and checks for secrets', })); // 3. PreEdit -> gate enforcement (High priority) this.hookIds.push(registry.register(HookEvent.PreEdit, (ctx) => this.handlePreEdit(ctx), HookPriority.High, { name: 'guidance-gate-pre-edit', description: 'Evaluates file edits for diff size and secrets', })); // 4. PreTask -> shard retrieval (Normal priority) this.hookIds.push(registry.register(HookEvent.PreTask, (ctx) => this.handlePreTask(ctx), HookPriority.Normal, { name: 'guidance-retriever-pre-task', description: 'Retrieves relevant guidance shards at task start', })); // 5. PostTask -> ledger finalization (Normal priority) this.hookIds.push(registry.register(HookEvent.PostTask, (ctx) => this.handlePostTask(ctx), HookPriority.Normal, { name: 'guidance-ledger-post-task', description: 'Finalizes the run event in the ledger on task completion', })); return [...this.hookIds]; } /** * Unregister all hooks previously registered by this provider. */ unregisterAll(registry) { for (const id of this.hookIds) { registry.unregister(id); } this.hookIds = []; } /** * Get the IDs of all registered hooks. */ getHookIds() { return [...this.hookIds]; } /** * Get the active run event for a given task ID (if any). */ getActiveRun(taskId) { return this.activeRuns.get(taskId); } // ========================================================================== // Hook Handlers // ========================================================================== /** * PreCommand handler: evaluate command through destructive ops and secrets gates. */ handlePreCommand(ctx) { const command = ctx.command?.raw; if (!command) { return { success: true }; } const gateResults = this.gates.evaluateCommand(command); return gateResultsToHookResult(gateResults); } /** * PreToolUse handler: evaluate tool usage against allowlist and check params for secrets. */ handlePreToolUse(ctx) { const toolName = ctx.tool?.name; if (!toolName) { return { success: true }; } const params = ctx.tool?.parameters ?? {}; const gateResults = this.gates.evaluateToolUse(toolName, params); return gateResultsToHookResult(gateResults); } /** * PreEdit handler: evaluate file edit for diff size and secrets. * * Extracts the file path from context. The content to scan comes from * metadata.content or is synthesized from available context. The diff * line count defaults to metadata.diffLines or 0. */ handlePreEdit(ctx) { const filePath = ctx.file?.path; if (!filePath) { return { success: true }; } const content = ctx.metadata?.content ?? ''; const diffLines = ctx.metadata?.diffLines ?? 0; const gateResults = this.gates.evaluateEdit(filePath, content, diffLines); return gateResultsToHookResult(gateResults); } /** * PreTask handler: classify intent and retrieve relevant guidance shards. * * Creates a new RunEvent in the active runs map for PostTask finalization. * Returns the retrieved policy text and shards as hook data. */ async handlePreTask(ctx) { const taskId = ctx.task?.id; const taskDescription = ctx.task?.description; if (!taskId || !taskDescription) { return { success: true }; } try { // Classify intent const { intent, confidence } = this.retriever.classifyIntent(taskDescription); // Retrieve relevant shards let retrievalResult = null; try { retrievalResult = await this.retriever.retrieve({ taskDescription, intent, }); } catch { // Retriever may not have a loaded bundle -- degrade gracefully } // Create a run event for ledger tracking const guidanceHash = retrievalResult?.constitution?.hash ?? 'unknown'; const runEvent = this.ledger.createEvent(taskId, intent, guidanceHash); if (retrievalResult) { runEvent.retrievedRuleIds = retrievalResult.shards.map(s => s.shard.rule.id); } this.activeRuns.set(taskId, runEvent); return { success: true, message: retrievalResult ? `Retrieved ${retrievalResult.shards.length} guidance shard(s) for intent "${intent}" (confidence: ${(confidence * 100).toFixed(0)}%)` : `Classified intent as "${intent}" (confidence: ${(confidence * 100).toFixed(0)}%). No policy bundle loaded.`, data: { intent, confidence, policyText: retrievalResult?.policyText ?? null, shardCount: retrievalResult?.shards.length ?? 0, contradictionsResolved: retrievalResult?.contradictionsResolved ?? 0, retrievalLatencyMs: retrievalResult?.latencyMs ?? 0, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), message: 'Failed to retrieve guidance shards for task', }; } } /** * PostTask handler: finalize the run event in the ledger. * * Looks up the active run by task ID, populates completion metadata, * and calls ledger.finalizeEvent(). */ handlePostTask(ctx) { const taskId = ctx.task?.id; if (!taskId) { return { success: true }; } const runEvent = this.activeRuns.get(taskId); if (!runEvent) { return { success: true, message: `No active run event found for task "${taskId}". Skipping finalization.`, }; } try { // Populate additional metadata from context if available if (ctx.task?.status) { runEvent.outcomeAccepted = ctx.task.status === 'completed'; } if (ctx.metadata?.toolsUsed && Array.isArray(ctx.metadata.toolsUsed)) { runEvent.toolsUsed = ctx.metadata.toolsUsed; } if (ctx.metadata?.filesTouched && Array.isArray(ctx.metadata.filesTouched)) { runEvent.filesTouched = ctx.metadata.filesTouched; } // Finalize the event in the ledger this.ledger.finalizeEvent(runEvent); this.activeRuns.delete(taskId); return { success: true, message: `Run event finalized for task "${taskId}" (duration: ${runEvent.durationMs}ms)`, data: { eventId: runEvent.eventId, taskId: runEvent.taskId, intent: runEvent.intent, durationMs: runEvent.durationMs, violationCount: runEvent.violations.length, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), message: `Failed to finalize run event for task "${taskId}"`, }; } } } // ============================================================================ // Factory // ============================================================================ /** * Create a GuidanceHookProvider and optionally register it on a registry. * * @param gates - The enforcement gates instance * @param retriever - The shard retriever instance * @param ledger - The run ledger instance * @param registry - Optional registry to auto-register on * @returns The provider and (if registry was given) the hook IDs */ export function createGuidanceHooks(gates, retriever, ledger, registry) { const provider = new GuidanceHookProvider(gates, retriever, ledger); const hookIds = registry ? provider.registerAll(registry) : []; return { provider, hookIds }; } export { gateResultsToHookResult }; //# sourceMappingURL=hooks.js.map