347 lines
14 KiB
JavaScript
347 lines
14 KiB
JavaScript
/**
|
|
* 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
|