import { existsSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; import { evaluateAiGate } from '../gate/evaluateAiGate'; import { GitService } from '../git/GitService'; import { runPlatformGate } from '../git/runPlatformGate'; import type { GateScope } from '../git/runPlatformGateFacts'; import { runEnterpriseAiGateCheck } from '../mcp/aiGateCheck'; import { writeMcpAiGateReceipt } from '../mcp/aiGateReceipt'; import { evaluateSddPolicy } from '../sdd'; import { writePreWriteLease } from './preWriteLease'; const PRE_WRITE_AUTOMATION_RETRY_BACKOFF_MS = 200; const PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES = new Set([ 'EVIDENCE_MISSING', 'EVIDENCE_INVALID', 'EVIDENCE_CHAIN_INVALID', 'EVIDENCE_STALE', 'EVIDENCE_GATE_BLOCKED', 'EVIDENCE_REPO_ROOT_MISMATCH', 'EVIDENCE_BRANCH_MISMATCH', 'EVIDENCE_RULES_COVERAGE_MISSING', 'EVIDENCE_RULES_COVERAGE_STAGE_MISMATCH', 'EVIDENCE_RULES_COVERAGE_INCOMPLETE', 'EVIDENCE_UNSUPPORTED_AUTO_RULES', 'EVIDENCE_TIMESTAMP_INVALID', 'EVIDENCE_TIMESTAMP_FUTURE', ]); const PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES = new Set([ 'MCP_ENTERPRISE_RECEIPT_MISSING', 'MCP_ENTERPRISE_RECEIPT_INVALID', 'MCP_ENTERPRISE_RECEIPT_STALE', 'MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH', 'MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH', 'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_INVALID', 'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_FUTURE', ]); export type PreWriteAutomationAction = { action: 'refresh_evidence' | 'refresh_mcp_receipt' | 'retry_backoff' | 'write_prewrite_lease'; status: 'OK' | 'FAILED'; details: string; }; export type PreWriteAutomationTrace = { attempted: boolean; actions: PreWriteAutomationAction[]; }; type PreWriteAutomationDependencies = { evaluateAiGate: typeof evaluateAiGate; runEnterpriseAiGateCheck: typeof runEnterpriseAiGateCheck; writeMcpAiGateReceipt: typeof writeMcpAiGateReceipt; writePreWriteLease: typeof writePreWriteLease; sleep: (ms: number) => Promise; retryBackoffMs: number; }; const defaultDependencies: PreWriteAutomationDependencies = { evaluateAiGate, runEnterpriseAiGateCheck, writeMcpAiGateReceipt, writePreWriteLease, sleep: (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }), retryBackoffMs: PRE_WRITE_AUTOMATION_RETRY_BACKOFF_MS, }; const hasAutoFixableEvidenceViolation = (aiGate: ReturnType): boolean => aiGate.violations.some((violation) => PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES.has(violation.code)); const hasEvidenceGateBlockedViolation = (aiGate: ReturnType): boolean => aiGate.violations.some((violation) => violation.code === 'EVIDENCE_GATE_BLOCKED'); const hasAutoFixableMcpReceiptViolation = (aiGate: ReturnType): boolean => aiGate.violations.some((violation) => PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code)); const collectAutoFixableViolationCodes = (aiGate: ReturnType): string[] => aiGate.violations .filter( (violation) => PRE_WRITE_AUTOFIXABLE_EVIDENCE_CODES.has(violation.code) || PRE_WRITE_AUTOFIXABLE_MCP_RECEIPT_CODES.has(violation.code) ) .map((violation) => violation.code) .sort((left, right) => left.localeCompare(right)); const PRE_WRITE_FUNCTIONAL_EXTENSIONS = [ '.swift', '.ts', '.tsx', '.js', '.jsx', '.kt', '.kts', ] as const; const PRE_WRITE_RUNTIME_ARTIFACT_PATHS = new Set([ '.ai_evidence.json', '.AI_EVIDENCE.json', '.pumuki/artifacts/mcp-ai-gate-receipt.json', '.pumuki/prewrite-lease.json', ]); const isFunctionalPath = (filePath: string): boolean => { const normalized = filePath.trim().toLowerCase(); return PRE_WRITE_FUNCTIONAL_EXTENSIONS.some((extension) => normalized.endsWith(extension)); }; const isRuntimeArtifactPath = (filePath: string): boolean => { const normalized = filePath.trim().replace(/\\/g, '/'); return PRE_WRITE_RUNTIME_ARTIFACT_PATHS.has(normalized); }; const collectStagedPaths = (repoRoot: string): ReadonlyArray | null => { try { return execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }) .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0); } catch { return null; } }; const resolvePreWriteRefreshScope = (aiGate: ReturnType): GateScope => { const staged = aiGate.repo_state.git.staged ?? 0; if (staged <= 0) { return { kind: 'workingTree' }; } const stagedPaths = collectStagedPaths(aiGate.repo_state.repo_root); if (stagedPaths === null || stagedPaths.some(isFunctionalPath)) { return { kind: 'staged' }; } if (stagedPaths.length === 0) { return { kind: 'workingTree' }; } if (stagedPaths.length > 0 && stagedPaths.every(isRuntimeArtifactPath)) { return { kind: 'workingTree' }; } return { kind: 'staged' }; }; export const buildPreWriteAutomationTrace = async (params: { repoRoot: string; sdd: ReturnType; aiGate: ReturnType; runPlatformGate: typeof runPlatformGate; }, dependencies: Partial = {}): Promise<{ aiGate: ReturnType; trace: PreWriteAutomationTrace; }> => { const activeDependencies: PreWriteAutomationDependencies = { ...defaultDependencies, ...dependencies, }; const trace: PreWriteAutomationTrace = { attempted: false, actions: [], }; if (params.sdd.stage !== 'PRE_WRITE') { return { aiGate: params.aiGate, trace, }; } let aiGate = params.aiGate; if ( process.env.PUMUKI_PRE_WRITE_REFRESH_GATE !== '0' || hasAutoFixableEvidenceViolation(aiGate) ) { trace.attempted = true; try { const gateExitCode = await params.runPlatformGate({ policy: { stage: 'PRE_WRITE', blockOnOrAbove: 'INFO', warnOnOrAbove: 'INFO', }, scope: resolvePreWriteRefreshScope(aiGate), auditMode: 'gate', dependencies: { printGateFindings: () => {}, }, }); trace.actions.push({ action: 'refresh_evidence', status: 'OK', details: `stage=PRE_WRITE runPlatformGate exit_code=${gateExitCode}`, }); aiGate = activeDependencies.runEnterpriseAiGateCheck({ repoRoot: params.repoRoot, stage: 'PRE_WRITE', requireMcpReceipt: true, }).result; } catch (error) { trace.actions.push({ action: 'refresh_evidence', status: 'FAILED', details: error instanceof Error ? error.message : 'Unknown PRE_COMMIT evidence refresh error', }); } } if (!aiGate.allowed && hasEvidenceGateBlockedViolation(aiGate)) { trace.attempted = true; try { const gateExitCode = await params.runPlatformGate({ policy: { stage: 'PRE_WRITE', blockOnOrAbove: 'ERROR', warnOnOrAbove: 'WARN', }, scope: resolvePreWriteRefreshScope(aiGate), auditMode: 'gate', dependencies: { printGateFindings: () => {}, }, }); trace.actions.push({ action: 'refresh_evidence', status: 'OK', details: `stage=PRE_WRITE runPlatformGate exit_code=${gateExitCode}`, }); aiGate = activeDependencies.runEnterpriseAiGateCheck({ repoRoot: params.repoRoot, stage: 'PRE_WRITE', requireMcpReceipt: true, }).result; } catch (error) { trace.actions.push({ action: 'refresh_evidence', status: 'FAILED', details: error instanceof Error ? error.message : 'Unknown PRE_PUSH evidence refresh error', }); } } if (hasAutoFixableMcpReceiptViolation(aiGate)) { trace.attempted = true; try { const aiGateWithoutReceiptRequirement = activeDependencies.evaluateAiGate({ repoRoot: params.repoRoot, stage: 'PRE_WRITE', requireMcpReceipt: false, }); const receiptWrite = activeDependencies.writeMcpAiGateReceipt({ repoRoot: params.repoRoot, stage: 'PRE_WRITE', status: aiGateWithoutReceiptRequirement.status, allowed: aiGateWithoutReceiptRequirement.allowed, }); trace.actions.push({ action: 'refresh_mcp_receipt', status: 'OK', details: `receipt=${receiptWrite.path}`, }); aiGate = activeDependencies.runEnterpriseAiGateCheck({ repoRoot: params.repoRoot, stage: 'PRE_WRITE', requireMcpReceipt: true, }).result; } catch (error) { trace.actions.push({ action: 'refresh_mcp_receipt', status: 'FAILED', details: error instanceof Error ? error.message : 'Unknown MCP receipt refresh error', }); } } const retryCodes = collectAutoFixableViolationCodes(aiGate); if (!aiGate.allowed && retryCodes.length > 0) { trace.attempted = true; try { await activeDependencies.sleep(activeDependencies.retryBackoffMs); aiGate = activeDependencies.runEnterpriseAiGateCheck({ repoRoot: params.repoRoot, stage: 'PRE_WRITE', requireMcpReceipt: true, }).result; trace.actions.push({ action: 'retry_backoff', status: 'OK', details: `delay_ms=${activeDependencies.retryBackoffMs} codes=${retryCodes.join(',')}`, }); } catch (error) { trace.actions.push({ action: 'retry_backoff', status: 'FAILED', details: error instanceof Error ? error.message : 'Unknown PRE_WRITE retry error', }); } } if (params.sdd.decision.allowed && aiGate.allowed && existsSync(params.repoRoot)) { trace.attempted = true; try { const git = new GitService(); const leaseResult = activeDependencies.writePreWriteLease({ repoRoot: params.repoRoot, git, allowExistingCodeChanges: true, }); trace.actions.push({ action: 'write_prewrite_lease', status: leaseResult.valid ? 'OK' : 'FAILED', details: `${leaseResult.code} path=${leaseResult.path}`, }); } catch (error) { trace.actions.push({ action: 'write_prewrite_lease', status: 'FAILED', details: error instanceof Error ? error.message : 'Unknown PRE_WRITE lease write error', }); } } return { aiGate, trace, }; };