aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
451 lines (404 loc) • 16.8 kB
JavaScript
/**
* @file Agent Persistence HITL Gate Integration
* @description Exports gate definitions and helper functions for agent persistence framework
* @implements @.aiwg/requirements/use-cases/UC-AP-004-enforce-recovery-protocol.md
* @architecture @.aiwg/architecture/agent-persistence-sad.md
* @gates @agentic/code/addons/agent-persistence/gates/
* @created 2026-02-03
* @issue #262
*/
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Gate definitions for agent persistence framework
*/
export const GATES = {
RECOVERY_ESCALATION: 'GATE-AP-RECOVERY',
FALSE_POSITIVE_OVERRIDE: 'GATE-AP-FALSE-POSITIVE',
DESTRUCTIVE_ACTION: 'GATE-AP-DESTRUCTIVE'
};
/**
* Load gate configuration from YAML file
* @param {string} gateId - Gate identifier
* @returns {Promise<object>} Gate configuration
*/
export async function loadGateConfig(gateId) {
const gateFiles = {
[GATES.RECOVERY_ESCALATION]: 'recovery-escalation-gate.yaml',
[GATES.FALSE_POSITIVE_OVERRIDE]: 'false-positive-override-gate.yaml',
[GATES.DESTRUCTIVE_ACTION]: 'destructive-action-gate.yaml'
};
const filename = gateFiles[gateId];
if (!filename) {
throw new Error(`Unknown gate ID: ${gateId}`);
}
const filepath = join(__dirname, filename);
const content = await readFile(filepath, 'utf8');
return yaml.load(content);
}
/**
* Evaluate gate trigger conditions
* @param {string} gateId - Gate identifier
* @param {object} context - Evaluation context
* @returns {boolean} True if gate should be triggered
*/
export function shouldTriggerGate(gateId, context) {
switch (gateId) {
case GATES.RECOVERY_ESCALATION:
return evaluateRecoveryEscalation(context);
case GATES.FALSE_POSITIVE_OVERRIDE:
return evaluateFalsePositiveOverride(context);
case GATES.DESTRUCTIVE_ACTION:
return evaluateDestructiveAction(context);
default:
return false;
}
}
/**
* Evaluate recovery escalation gate trigger
* @param {object} context
* @returns {boolean}
*/
function evaluateRecoveryEscalation(context) {
const {
recovery_attempts = 0,
confidence = 1.0,
severity = 'LOW',
infinite_loop_detected = false,
non_deterministic_failure = false
} = context;
// Trigger if max attempts reached
if (recovery_attempts >= 3) {
return true;
}
// Trigger if confidence too low
if (confidence < 0.5) {
return true;
}
// Trigger if CRITICAL severity
if (severity === 'CRITICAL') {
return true;
}
// Trigger if infinite loop detected
if (infinite_loop_detected) {
return true;
}
// Trigger if non-deterministic failure
if (non_deterministic_failure) {
return true;
}
return false;
}
/**
* Evaluate false positive override gate trigger
* @param {object} context
* @returns {boolean}
*/
function evaluateFalsePositiveOverride(context) {
const {
false_positive_reported = false,
detection_confidence = 1.0
} = context;
// Trigger if user reports false positive
if (false_positive_reported) {
return true;
}
// Trigger if detection confidence is low (may be false positive)
if (detection_confidence < 0.7) {
return true;
}
return false;
}
/**
* Evaluate destructive action gate trigger
* @param {object} context
* @returns {boolean}
*/
function evaluateDestructiveAction(context) {
const {
action_type,
tests_removed = 0,
coverage_delta = 0,
features_removed = 0,
assertions_weakened = 0,
skip_patterns_added = 0,
replacement_tests = 0,
coverage_maintained = false,
documented_in_requirements = false,
approved_by_pm = false
} = context;
// Check auto-approve conditions first
if (action_type === 'test_deletion') {
if (replacement_tests >= tests_removed && coverage_maintained) {
return false; // Auto-approve: legitimate test refactoring
}
}
if (action_type === 'feature_removal') {
if (documented_in_requirements && approved_by_pm) {
return false; // Auto-approve: intentional scope reduction
}
}
// Trigger if tests removed
if (tests_removed > 0) {
return true;
}
// Trigger if coverage regression > 2%
if (coverage_delta < -2.0) {
return true;
}
// Trigger if features removed
if (features_removed > 0) {
return true;
}
// Trigger if assertions weakened significantly
if (assertions_weakened > 2) {
return true;
}
// Trigger if skip patterns added
if (skip_patterns_added > 0) {
return true;
}
return false;
}
/**
* Format gate display per @.claude/rules/human-gate-display.md
* @param {string} gateId - Gate identifier
* @param {object} context - Display context
* @returns {string} Formatted gate display
*/
export function formatGateDisplay(gateId, context) {
switch (gateId) {
case GATES.RECOVERY_ESCALATION:
return formatRecoveryEscalationDisplay(context);
case GATES.FALSE_POSITIVE_OVERRIDE:
return formatFalsePositiveDisplay(context);
case GATES.DESTRUCTIVE_ACTION:
return formatDestructiveActionDisplay(context);
default:
return 'Unknown gate';
}
}
/**
* Format recovery escalation gate display
* @param {object} context
* @returns {string}
*/
function formatRecoveryEscalationDisplay(context) {
const {
task_description = 'Unknown task',
pattern_type = 'Unknown pattern',
severity = 'UNKNOWN',
recovery_attempts = 0,
agent_name = 'Unknown agent',
affected_files = [],
original_error = 'Unknown error',
detection_timestamp = new Date().toISOString(),
recovery_attempts_summary = 'No attempts recorded',
test_status = 'Unknown',
coverage_status = 'Unknown',
agent_confidence = 0.0,
session_id = 'unknown'
} = context;
return `
╭─────────────────────────────────────────────────────────────╮
│ HUMAN INTERVENTION REQUIRED │
│ Recovery Escalation Gate: GATE-AP-RECOVERY │
├─────────────────────────────────────────────────────────────┤
│ Context: │
│ • Task: ${task_description} │
│ • Pattern: ${pattern_type} (${severity}) │
│ • Recovery Attempts: ${recovery_attempts} / 3 │
│ • Agent: ${agent_name} │
│ │
│ Detection Details: │
│ • File(s): ${affected_files.join(', ')} │
│ • Original Error: ${original_error} │
│ • Detection Time: ${detection_timestamp} │
│ │
│ Recovery History: │
│ ${recovery_attempts_summary} │
│ │
│ Current State: │
│ • Tests: ${test_status} │
│ • Coverage: ${coverage_status} │
│ • Confidence: ${(agent_confidence * 100).toFixed(1)}% │
├─────────────────────────────────────────────────────────────┤
│ Options: │
│ [a] Approve - Override and allow agent to continue │
│ [r] Reject - Require manual fix from human │
│ [t] Retry - Reset counter, give agent another chance │
│ [v] View - Show detailed recovery attempt logs │
│ [d] Delegate - Assign to different agent │
│ [q] Abort - Stop task entirely │
╰─────────────────────────────────────────────────────────────╯
View recovery details at: .aiwg/persistence/recoveries/${session_id}-recovery.yaml
`.trim();
}
/**
* Format false positive override gate display
* @param {object} context
* @returns {string}
*/
function formatFalsePositiveDisplay(context) {
const {
pattern_type = 'Unknown pattern',
severity = 'UNKNOWN',
file_path = 'Unknown file',
detection_id = 'unknown',
change_summary = 'No summary available',
user_justification = 'No justification provided',
matching_rule = 'Unknown rule',
detection_confidence = 0.0,
detection_context = 'No context',
test_delta = 0,
coverage_delta = 0,
risk_assessment = 'Unknown'
} = context;
return `
╭─────────────────────────────────────────────────────────────╮
│ FALSE POSITIVE REVIEW │
│ Gate: GATE-AP-FALSE-POSITIVE │
├─────────────────────────────────────────────────────────────┤
│ Detection Details: │
│ • Pattern: ${pattern_type} │
│ • Severity: ${severity} │
│ • File: ${file_path} │
│ • Detection ID: ${detection_id} │
│ │
│ Change Summary: │
│ ${change_summary} │
│ │
│ User's Justification: │
│ ${user_justification} │
│ │
│ Pattern Matching Details: │
│ • Rule: ${matching_rule} │
│ • Confidence: ${(detection_confidence * 100).toFixed(1)}%│
│ • Context: ${detection_context} │
│ │
│ Impact if Allowed: │
│ • Test Count: ${test_delta >= 0 ? '+' : ''}${test_delta}│
│ • Coverage: ${coverage_delta >= 0 ? '+' : ''}${coverage_delta}%│
│ • Risk Level: ${risk_assessment} │
├─────────────────────────────────────────────────────────────┤
│ Options: │
│ [l] Legitimate - Allow change, mark as false positive │
│ [v] Violation - Confirm detection, block change │
│ [r] Needs Review - Request more context from user │
│ [d] Diff - Show detailed diff of changes │
│ [h] History - Show pattern history for this file │
│ [w] Whitelist - Allow pattern for this file/context │
╰─────────────────────────────────────────────────────────────╯
`.trim();
}
/**
* Format destructive action gate display
* @param {object} context
* @returns {string}
*/
function formatDestructiveActionDisplay(context) {
const {
task_description = 'Unknown task',
agent_name = 'Unknown agent',
action_type = 'Unknown action',
severity = 'UNKNOWN',
action_description = 'No description',
file_count = 0,
tests_removed = 0,
coverage_delta = 0,
features_list = 'None',
agent_justification = 'No justification provided',
risk_level = 'Unknown',
reversible = 'Unknown',
prod_impact = 'Unknown',
affected_files = []
} = context;
return `
╭─────────────────────────────────────────────────────────────╮
│ DESTRUCTIVE ACTION APPROVAL REQUIRED │
│ Gate: GATE-AP-DESTRUCTIVE │
├─────────────────────────────────────────────────────────────┤
│ Context: │
│ • Task: ${task_description} │
│ • Agent: ${agent_name} │
│ • Action Type: ${action_type} │
│ • Severity: ${severity} │
│ │
│ Requested Action: │
│ ${action_description} │
│ │
│ Impact Analysis: │
│ • Files Affected: ${file_count} │
│ • Tests Removed: ${tests_removed} │
│ • Coverage Impact: ${coverage_delta >= 0 ? '+' : ''}${coverage_delta}%│
│ • Features Affected: ${features_list} │
│ │
│ Agent's Justification: │
│ ${agent_justification} │
│ │
│ Risk Assessment: │
│ • Risk Level: ${risk_level} │
│ • Reversibility: ${reversible} │
│ • Production Impact: ${prod_impact} │
├─────────────────────────────────────────────────────────────┤
│ Options: │
│ [a] Approve - This action is intentional │
│ [r] Reject - Find alternative approach │
│ [v] View - Show detailed changes │
│ [d] Diff - Compare before/after │
│ [s] Suggest - Propose alternative solution │
│ [q] Abort - Stop task entirely │
╰─────────────────────────────────────────────────────────────╯
Affected files: ${affected_files.join(', ')}
`.trim();
}
/**
* Log gate decision to audit trail
* @param {string} gateId - Gate identifier
* @param {string} decision - Decision made (approve/reject/etc)
* @param {object} context - Decision context
* @returns {object} Audit record
*/
export function logGateDecision(gateId, decision, context) {
const auditRecord = {
gate_id: gateId,
gate_name: getGateName(gateId),
decision,
timestamp: new Date().toISOString(),
user: context.user || 'unknown',
context: {
...context,
// Remove sensitive data
user: undefined
}
};
// In production, this would write to .aiwg/gates/decisions.log
// For now, return the record for logging by caller
return auditRecord;
}
/**
* Get human-readable gate name
* @param {string} gateId
* @returns {string}
*/
function getGateName(gateId) {
const names = {
[GATES.RECOVERY_ESCALATION]: 'Recovery Escalation Gate',
[GATES.FALSE_POSITIVE_OVERRIDE]: 'False Positive Override Gate',
[GATES.DESTRUCTIVE_ACTION]: 'Destructive Action Approval Gate'
};
return names[gateId] || 'Unknown Gate';
}
/**
* Export all gates
*/
export default {
GATES,
loadGateConfig,
shouldTriggerGate,
formatGateDisplay,
logGateDecision
};