import fs from "fs" import yaml from "js-yaml" import { ScopeValidator, type ScopeValidationResult } from "./ScopeValidator" import { ApprovalManager, type ApprovalRequest } from "./ApprovalManager" export interface Intent { id: string name: string status: string owned_scope: string[] constraints: string[] acceptance_criteria: string[] } export interface OutOfScopeError { type: "OUT_OF_SCOPE" message: string files: string[] scope: string[] requires_approval: boolean } /** * Intent Hook Engine: Orchestrates intent context and scope enforcement * * Responsibilities: * 1. Load and manage active intents from .orchestration/active_intents.yaml * 2. Gate-keep access to restricted tools (write_file, apply_diff, execute_command) * 3. Validate proposed changes against intent scope (owned_scope) * 4. Require human approval for out-of-scope changes * 5. Provide intent context injection for agents * * Flow: * 1. Agent calls select_active_intent(intent_id) → loads context * 2. Agent proposes change via write_file/apply_diff * 3. Pre-hook validates scope of proposed files * 4. If out-of-scope: require request_human_approval before executing * 5. If approved with override: log decision and proceed * 6. Post-hook logs all changes to agent_trace.jsonl */ export class IntentHookEngine { private intents: Record = {} private currentSessionIntent: Intent | null = null private orchestrationDir = ".orchestration" private intentsPath = ".orchestration/active_intents.yaml" private tracePath = ".orchestration/agent_trace.jsonl" private scopeValidator = ScopeValidator private approvalManager = new ApprovalManager() constructor() { this.intents = this.loadIntents() } /** * Load intents from active_intents.yaml */ private loadIntents(): Record { try { if (!fs.existsSync(this.intentsPath)) return {} const file = fs.readFileSync(this.intentsPath, "utf8") const data = yaml.load(file) as any const intents: Record = {} if (Array.isArray(data?.active_intents)) { for (const item of data.active_intents) { if (item?.id) intents[item.id] = item as Intent } } return intents } catch (err) { console.warn("IntentHookEngine: failed to load intents:", err) return {} } } /** * Gatekeeper: check whether a tool is allowed given current session */ gatekeeper(tool: string): { allowed: boolean; message?: string } { const restrictedTools = ["write_file", "apply_diff", "execute_command", "write_to_file"] if (restrictedTools.includes(tool)) { if (!this.currentSessionIntent) { return { allowed: false, message: "You must cite a valid active Intent ID via select_active_intent before performing structural changes.", } } } return { allowed: true } } /** * Validate that proposed changes are within the current intent's scope * Used as a pre-hook for write_file, apply_diff, etc. * * @param filePaths - Array of file paths that will be modified * @returns validation result with scope check */ validateScope(filePaths: string[]): ScopeValidationResult { if (!this.currentSessionIntent) { return { isWithinScope: false, reason: "No active intent - cannot validate scope", attemptedPath: filePaths[0], } } return this.scopeValidator.arePathsInScope(filePaths, this.currentSessionIntent.owned_scope) } /** * Check if a single file is within scope */ isFileInScope(filePath: string): boolean { if (!this.currentSessionIntent) return false const result = this.scopeValidator.isPathInScope(filePath, this.currentSessionIntent.owned_scope) return result.isWithinScope } /** * Require human approval for out-of-scope changes * Blocks execution until approval decision is received */ async requestApprovalForOutOfScope( changeSummary: string, diff: string, filesAffected: string[], outOfScopeFiles: string[], ): Promise<{ approved: boolean; requiresOverride: boolean }> { const fullSummary = `${changeSummary}\n\nWARNING: The following files are outside the current intent's scope:\n${outOfScopeFiles.map((f) => ` - ${f}`).join("\n")}\n\nHuman approval required to override scope enforcement.` const request = ApprovalManager.createRequest( fullSummary, diff, filesAffected, this.currentSessionIntent?.id, ) const decision = await this.approvalManager.submitForApproval(request) return { approved: decision.approved, requiresOverride: decision.requires_override ?? false, } } /** * Record approval decision * Called by approval service after human review */ recordApprovalDecision( requestId: string, approved: boolean, approver: string, notes?: string, requiresOverride?: boolean, ): void { this.approvalManager.recordDecision(requestId, approved, approver, notes, requiresOverride) } /** * Get pending approval requests */ getPendingApprovals(): Record { const pending = this.approvalManager.getPendingRequests() const result: Record = {} for (const req of pending) { result[req.request_id] = req } return result } /** * Pre-Hook: validate intent selection and return context */ preHook(tool: string, payload: any): string | { allowed: boolean; message: string } { if (tool === "select_active_intent") { const intentId = payload?.intent_id const intents = this.loadIntents() const intent = intents?.[intentId] if (!intent) { throw new Error( `Invalid Intent ID: "${intentId}". You must cite a valid active Intent ID from .orchestration/active_intents.yaml`, ) } this.currentSessionIntent = intent const intentContextBlock = ` ${intent.id} ${intent.name} ${intent.status} ${intent.constraints.map((c) => ` - ${c}`).join("\n")} ${intent.owned_scope.map((s) => ` - ${s}`).join("\n")} ${intent.acceptance_criteria.map((ac) => ` - ${ac}`).join("\n")} ` return intentContextBlock } return { allowed: true } } /** * Get current active session intent */ getCurrentSessionIntent(): Intent | null { return this.currentSessionIntent } /** * Clear the current session intent */ clearSessionIntent(): void { this.currentSessionIntent = null } /** * Log trace entry with intent context */ logTrace(filePath: string, content: string): void { try { const hash = require("crypto").createHash("sha256").update(content, "utf8").digest("hex") const entry = { intent_id: this.currentSessionIntent?.id ?? null, path: filePath, sha256: hash, ts: new Date().toISOString(), } if (!fs.existsSync(this.orchestrationDir)) { fs.mkdirSync(this.orchestrationDir, { recursive: true }) } fs.appendFileSync(this.tracePath, JSON.stringify(entry) + "\n") } catch (err) { console.warn("IntentHookEngine: failed to log trace:", err) } } /** * Get all approvals for a specific intent */ getIntentApprovals(intentId: string): any[] { return this.approvalManager.getApprovalsByIntent(intentId) } /** * Check approval status by request ID */ isApprovalPending(requestId: string): boolean { const request = this.approvalManager.getPendingRequest(requestId) return !!request } } export const intentHookEngine = new IntentHookEngine()