Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 110 additions & 6 deletions src/integrations/hermes-agent/bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ let ProcessorManager = null;
let StrRayStateManager = null;
let featuresConfigLoader = null;
let runQualityGateWithLogging = null;
let enforcerValidators = null;
let frameworkReady = false;
let frameworkLoadAttempted = false;

Expand Down Expand Up @@ -162,6 +163,25 @@ async function loadFramework(projectRoot) {
}
}

// RuleEnforcer — full code analysis (security, quality, architecture)
if (!enforcerValidators) {
const rePath = join(distDir, "enforcement", "index.js");
if (existsSync(rePath)) {
try {
const reModule = await import(rePath);
// Use ValidatorRegistry directly to bypass RuleExecutor dependency chain
// which cascades failures when validators lack full project context
const ValidatorRegistry = reModule.ValidatorRegistry;
if (ValidatorRegistry) {
enforcerValidators = new ValidatorRegistry();
// Validators are pre-registered in constructor — no registerAllValidators needed
}
} catch (e) {
logToActivity(join(projectRoot, "logs", "framework"), `ValidatorRegistry load skipped: ${e.message}`);
}
}
}

frameworkReady = !!(runQualityGateWithLogging || ProcessorManager);
if (frameworkReady) return true;
} catch (e) {
Expand Down Expand Up @@ -433,21 +453,102 @@ async function handleValidate(input, projectRoot, logDir) {
}

async function handleCodexCheck(input, projectRoot, logDir) {
const { code, focusAreas } = input;
logToActivity(logDir, `codex-check: code_length=${code?.length || 0} focus=${focusAreas}`);
const { code, focusAreas, operation } = input;
const codeLen = code?.length || 0;
logToActivity(logDir, `codex-check: code_length=${codeLen} focus=${focusAreas} operation=${operation}`);

// Check code against debug patterns via quality gate
// Collect violations from both systems
const allViolations = [];
const allChecks = [];
let enforcerRan = false;

// Phase 1: Quality gate (fast meta-checks + basic patterns)
const qualityResult = await runQualityGateCheck(
{ tool: "write", args: { content: code } },
projectRoot,
logDir,
);

if (qualityResult.checks) {
allChecks.push(...qualityResult.checks);
}
if (qualityResult.violations?.length) {
allViolations.push(...qualityResult.violations);
}

// Phase 2: Enforcement validators (deep code analysis — security, quality, architecture)
// Use ValidatorRegistry directly instead of RuleEnforcer.validateOperation()
// to avoid the dependency chain cascade that blocks validators on snippet analysis.
// Only run content-analysis validators — skip project-level validators that
// require full context (docs, tests, CI, package.json) and always fail on snippets.
const SNIPPET_SAFE_RULES = new Set([
"security-by-design",
"input-validation",
"clean-debug-logs",
"console-log-usage",
"no-duplicate-code",
"loop-safety",
"no-over-engineering",
"single-responsibility",
"error-resolution",
"module-system-consistency",
]);

if (enforcerValidators && codeLen > 0) {
try {
const ctx = { operation: "write", newCode: code, files: [] };
const validators = enforcerValidators.getAllValidators();
let enforcerViolations = 0;

for (const v of validators) {
// Only run snippet-safe validators, or respect focus areas
if (!SNIPPET_SAFE_RULES.has(v.ruleId)) {
// Still run if focus area explicitly requests this category
if (focusAreas && focusAreas !== "all" && Array.isArray(focusAreas)) {
if (focusAreas.includes(v.category)) {
// Fall through to validate
} else {
continue;
}
} else {
continue;
}
}

try {
const result = await v.validate(ctx);
if (!result.passed) {
enforcerViolations++;
allViolations.push({
id: v.ruleId,
severity: v.severity || "error",
message: result.message,
suggestions: result.suggestions,
});
allChecks.push({ id: v.ruleId, passed: false, message: result.message });
}
} catch {
// Skip broken validators gracefully
}
}

enforcerRan = true;
logToActivity(logDir, `codex-check: Validators ran against ${validators.length} rules (${SNIPPET_SAFE_RULES.size} snippet-safe), ${enforcerViolations} violations`);
} catch (e) {
logToActivity(logDir, `codex-check: Validator error: ${e.message}`);
}
} else if (!enforcerValidators) {
logToActivity(logDir, `codex-check: Validators not available, quality gate only`);
}

const passed = allViolations.length === 0;

return {
passed: qualityResult.passed,
violations: qualityResult.violations,
checks: qualityResult.checks,
passed,
violations: allViolations,
checks: allChecks,
focusAreas: focusAreas || "all",
enforcerRan,
};
}

Expand Down Expand Up @@ -615,6 +716,9 @@ async function main() {
i++;
} else if (!argv[i].startsWith("-") && !positionalCommand && KNOWN_COMMANDS.has(argv[i])) {
positionalCommand = argv[i];
} else if (!argv[i].startsWith("-") && positionalCommand && !positionalPayload) {
// Inline JSON payload after command: node bridge.mjs hooks '{"action":"install"}'
positionalPayload = argv[i];
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/integrations/hermes-agent/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ def setUp(self):
"post_processor_runs": 6,
"bridge_calls": 15,
"bridge_errors": 0,
"subagent_dispatches": 3,
"subagent_validations": 1,
"subagent_blocks": 0,
}

def test_stats(self):
Expand Down Expand Up @@ -1182,7 +1185,7 @@ def test_v2_1_log_message(self):
ctx = MagicMock()
with self.assertLogs("strray-hermes", level="INFO") as cm:
pi.register(ctx)
self.assertTrue(any("v2.1" in m for m in cm.output))
self.assertTrue(any("v2.2" in m for m in cm.output))
self.assertTrue(any("4 tools" in m for m in cm.output))
self.assertTrue(any("5 hooks" in m for m in cm.output))

Expand Down
Loading