azcodr 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "safety-guard": {
3
- "enabled": false,
3
+ "enabled": true,
4
4
  "PreToolUse": [
5
5
  {
6
6
  "matcher": "run_command",
@@ -14,8 +14,23 @@
14
14
  }
15
15
  ]
16
16
  },
17
+ "architectural-guard": {
18
+ "enabled": true,
19
+ "PreToolUse": [
20
+ {
21
+ "matcher": "run_command",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "./.agents/scripts/agent_guard.js",
26
+ "timeout": 15
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ },
17
32
  "post-tool-lint": {
18
- "enabled": false,
33
+ "enabled": true,
19
34
  "PostToolUse": [
20
35
  {
21
36
  "matcher": "run_command",
@@ -30,7 +45,7 @@
30
45
  ]
31
46
  },
32
47
  "stop-verifier": {
33
- "enabled": false,
48
+ "enabled": true,
34
49
  "Stop": [
35
50
  {
36
51
  "type": "command",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "safety-guard": {
3
- "enabled": false,
3
+ "enabled": true,
4
4
  "PreToolUse": [
5
5
  {
6
6
  "matcher": "run_command",
@@ -14,8 +14,23 @@
14
14
  }
15
15
  ]
16
16
  },
17
+ "architectural-guard": {
18
+ "enabled": true,
19
+ "PreToolUse": [
20
+ {
21
+ "matcher": "run_command",
22
+ "hooks": [
23
+ {
24
+ "type": "command",
25
+ "command": "./.agents/scripts/agent_guard.js",
26
+ "timeout": 15
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ },
17
32
  "post-tool-lint": {
18
- "enabled": false,
33
+ "enabled": true,
19
34
  "PostToolUse": [
20
35
  {
21
36
  "matcher": "run_command",
@@ -30,7 +45,7 @@
30
45
  ]
31
46
  },
32
47
  "stop-verifier": {
33
- "enabled": false,
48
+ "enabled": true,
34
49
  "Stop": [
35
50
  {
36
51
  "type": "command",
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Command safety guard inspecting shell commands against a destructive denylist.
3
+ */
4
+ export const DESTRUCTIVE_RULES = [
5
+ {
6
+ pattern: /\brm\s+(-[a-z-]*[rf][a-z-]*\s+)+(\/|\/\*|~|~\/\*|\$home|\$\{home\}|\.|\.\.|\.\/|[a-z]:\\|[a-z]:\/)/i,
7
+ reason: 'destructive rm targeting root, home, drive, or the current directory'
8
+ },
9
+ {
10
+ pattern: /(^|\s)rm\s+(-[a-z-]*\s+)*(\/|~|[a-z]:\\|[a-z]:\/)($|\s)/i,
11
+ reason: 'destructive rm targeting root or home'
12
+ },
13
+ {
14
+ pattern: /\brm\s+--recursive.*--force\s+(\/|~|\$home|\.)/i,
15
+ reason: 'destructive rm using long-form flags'
16
+ },
17
+ {
18
+ pattern: /\brm\s+.*--no-preserve-root/i,
19
+ reason: 'rm --no-preserve-root is forbidden under agent execution'
20
+ },
21
+ {
22
+ pattern: /\b(wipefs\s+-a|shred\s+-u|find\s+.*-delete)\b/i,
23
+ reason: 'irreversible disk or mass-delete operation'
24
+ },
25
+ {
26
+ pattern: /\bgit\s+push\s+.*(-f|--force|--mirror)\b/i,
27
+ reason: 'force-push or mirror-push can destroy remote history'
28
+ },
29
+ {
30
+ pattern: /\bgit\s+push\b.*--force-with-lease\b/i,
31
+ reason: 'force-push variant detected'
32
+ },
33
+ {
34
+ pattern: /\bgit\s+(reset\s+--hard|clean\s+-[a-z]*[fd])/i,
35
+ reason: 'destructive git worktree operation (reset --hard / clean)'
36
+ },
37
+ {
38
+ pattern: /\bgit\s+checkout\s+--\s+\./i,
39
+ reason: 'git checkout -- . discards all uncommitted work'
40
+ },
41
+ {
42
+ pattern: /\bgit\s+branch\s+-D\b/i,
43
+ reason: 'force branch delete'
44
+ },
45
+ {
46
+ pattern: /\b(drop\s+(database|schema|table)|truncate\s+(table\s+)?[a-z_])/i,
47
+ reason: 'destructive SQL DDL'
48
+ },
49
+ {
50
+ pattern: /\b(delete\s+from|update\s+[a-z_]+\s+set|alter\s+table)\b/i,
51
+ reason: 'destructive SQL DML'
52
+ },
53
+ {
54
+ pattern: /\b(grant\s+all|revoke\s+all)\b/i,
55
+ reason: 'privilege escalation in SQL'
56
+ },
57
+ {
58
+ pattern: /\b(mkfs|dd\s+if=|fdisk|parted\s+\/dev)\b/i,
59
+ reason: 'raw disk operation'
60
+ },
61
+ {
62
+ pattern: /(:[\s]*\([\s]*\)[\s]*\{|:[\s]*\|[\s]*:)/,
63
+ reason: 'fork bomb'
64
+ },
65
+ {
66
+ pattern: /\b(curl|wget)\s+.*\|\s*(sudo\s+)?(ba)?sh\b/i,
67
+ reason: 'piping download directly into shell execution'
68
+ },
69
+ {
70
+ pattern: /\b(base64\s+-d|base64\s+--decode)\s+.*\|/i,
71
+ reason: 'decoding encoded payload into a pipe'
72
+ },
73
+ {
74
+ pattern: /\b(npm|yarn|pnpm)\s+publish\b/i,
75
+ reason: 'package publish must run via release workflow, not ad-hoc tool call'
76
+ }
77
+ ];
78
+ export function normalizeCommand(raw) {
79
+ return raw.replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim();
80
+ }
81
+ /**
82
+ * Checks a command string against the safety denylist.
83
+ *
84
+ * @param command - Raw shell command string.
85
+ * @returns Violation details if blocked, or clean outcome.
86
+ */
87
+ export function inspectCommand(command) {
88
+ const normalized = normalizeCommand(command);
89
+ if (!normalized)
90
+ return { blocked: false };
91
+ for (const rule of DESTRUCTIVE_RULES) {
92
+ if (rule.pattern.test(normalized)) {
93
+ return {
94
+ blocked: true,
95
+ reason: rule.reason,
96
+ command: normalized.slice(0, 300)
97
+ };
98
+ }
99
+ }
100
+ return { blocked: false, command: normalized };
101
+ }
102
+ export default { DESTRUCTIVE_RULES, normalizeCommand, inspectCommand };
103
+ //# sourceMappingURL=agent-guard-command.js.map
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const DEFAULT_MAX_FILE_LINES = 300;
4
+ export function countLines(text) {
5
+ if (!text)
6
+ return 0;
7
+ return text.split('\n').length;
8
+ }
9
+ export function readExistingLines(filePath) {
10
+ if (!fs.existsSync(filePath))
11
+ return 0;
12
+ try {
13
+ const content = fs.readFileSync(filePath, 'utf-8');
14
+ return countLines(content);
15
+ }
16
+ catch {
17
+ return 0;
18
+ }
19
+ }
20
+ function calculateProjectedLines(currentLines, input) {
21
+ if (input.incomingContent !== undefined) {
22
+ return countLines(input.incomingContent);
23
+ }
24
+ if (input.replacementContent !== undefined) {
25
+ const addedLines = countLines(input.replacementContent);
26
+ const removedLines = input.targetContent !== undefined ? countLines(input.targetContent) : 1;
27
+ return Math.max(0, currentLines + addedLines - removedLines);
28
+ }
29
+ return currentLines;
30
+ }
31
+ function evaluateLineThresholds(params) {
32
+ const { filePath, currentLines, projectedLines, maxLines } = params;
33
+ const base = { currentLines, projectedLines, maxLines };
34
+ const relPath = path.basename(filePath);
35
+ if (currentLines > maxLines) {
36
+ if (projectedLines >= currentLines) {
37
+ return {
38
+ ...base,
39
+ blocked: true,
40
+ reason: `Refactor-Before-Add violation: '${relPath}' currently has ${currentLines} lines (limit: ${maxLines}). Adding or maintaining lines in an overflowing file is blocked. Refactor into modular units under green tests first.`
41
+ };
42
+ }
43
+ return { ...base, blocked: false };
44
+ }
45
+ if (projectedLines > maxLines) {
46
+ return {
47
+ ...base,
48
+ blocked: true,
49
+ reason: `File line budget exceeded: '${relPath}' would grow to ${projectedLines} lines (limit: ${maxLines}). Decompose into modular components before appending more code.`
50
+ };
51
+ }
52
+ return { ...base, blocked: false };
53
+ }
54
+ /**
55
+ * Checks a proposed file write or edit against Refactor-Before-Add rules.
56
+ *
57
+ * @param input - File path and proposed content.
58
+ * @returns Decision result indicating whether the write is blocked.
59
+ */
60
+ export function inspectFileWrite(input) {
61
+ const maxLines = input.maxLines ?? DEFAULT_MAX_FILE_LINES;
62
+ const currentLines = readExistingLines(input.filePath);
63
+ const projectedLines = calculateProjectedLines(currentLines, input);
64
+ return evaluateLineThresholds({
65
+ filePath: input.filePath,
66
+ currentLines,
67
+ projectedLines,
68
+ maxLines
69
+ });
70
+ }
71
+ export default {
72
+ DEFAULT_MAX_FILE_LINES,
73
+ countLines,
74
+ readExistingLines,
75
+ inspectFileWrite
76
+ };
77
+ //# sourceMappingURL=agent-guard-file.js.map
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function isTestFile(filePath) {
4
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
5
+ if (/(^|\/)(tests?|__tests__)(\/|$)/i.test(norm)) {
6
+ return true;
7
+ }
8
+ return /\.(test|spec)\.[a-z0-9]+$/i.test(norm);
9
+ }
10
+ export function isNonCodeFile(filePath) {
11
+ const ext = path.extname(filePath).toLowerCase();
12
+ const nonCodeExts = new Set(['.md', '.json', '.yml', '.yaml', '.txt', '.toml', '.lock', '.svg', '.png']);
13
+ return nonCodeExts.has(ext);
14
+ }
15
+ export function isProductionFile(filePath) {
16
+ if (isNonCodeFile(filePath))
17
+ return false;
18
+ if (isTestFile(filePath))
19
+ return false;
20
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
21
+ return norm.includes('src/') || norm.includes('lib/') || norm.includes('app/');
22
+ }
23
+ export function readTddState(stateFile) {
24
+ if (!fs.existsSync(stateFile)) {
25
+ return { lastFailingTestRecorded: false };
26
+ }
27
+ try {
28
+ const raw = fs.readFileSync(stateFile, 'utf-8');
29
+ return JSON.parse(raw);
30
+ }
31
+ catch {
32
+ return { lastFailingTestRecorded: false };
33
+ }
34
+ }
35
+ export function writeTddState(stateFile, state) {
36
+ try {
37
+ const dir = path.dirname(stateFile);
38
+ if (!fs.existsSync(dir))
39
+ fs.mkdirSync(dir, { recursive: true });
40
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf-8');
41
+ }
42
+ catch {
43
+ // Fail-safe write: do not crash agent if state cannot be persisted
44
+ }
45
+ }
46
+ /**
47
+ * Checks whether an edit to a file is allowed under RED-before-GREEN TDD rules.
48
+ *
49
+ * @param filePath - Path to file being edited.
50
+ * @param state - Current session TDD state.
51
+ * @param enforceTestFirst - Whether test-first enforcement is strictly enabled.
52
+ * @returns Decision result indicating whether edit is blocked.
53
+ */
54
+ export function inspectTddRequirement(filePath, state, enforceTestFirst = false) {
55
+ const prod = isProductionFile(filePath);
56
+ if (!enforceTestFirst || !prod) {
57
+ return { blocked: false, isProductionFile: prod };
58
+ }
59
+ if (!state.lastFailingTestRecorded) {
60
+ const baseName = path.basename(filePath);
61
+ return {
62
+ blocked: true,
63
+ isProductionFile: true,
64
+ reason: `Test-First (RED-before-GREEN) requirement: Production file '${baseName}' cannot be edited before a failing test (RED) is recorded. Author a failing unit or acceptance test first, verify failure output, then edit production code.`
65
+ };
66
+ }
67
+ return { blocked: false, isProductionFile: true };
68
+ }
69
+ export default {
70
+ isTestFile,
71
+ isNonCodeFile,
72
+ isProductionFile,
73
+ readTddState,
74
+ writeTddState,
75
+ inspectTddRequirement
76
+ };
77
+ //# sourceMappingURL=agent-guard-tdd.js.map
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { inspectCommand } from './agent-guard-command.js';
3
+ import { inspectFileWrite } from './agent-guard-file.js';
4
+ import { inspectTddRequirement, readTddState, writeTddState } from './agent-guard-tdd.js';
5
+ function parseEnvelope(raw) {
6
+ try {
7
+ const trimmed = raw.trim();
8
+ if (!trimmed.startsWith('{'))
9
+ return null;
10
+ return JSON.parse(trimmed);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function extractKey(input, keys) {
17
+ for (const k of keys) {
18
+ if (typeof input[k] === 'string')
19
+ return input[k];
20
+ }
21
+ return undefined;
22
+ }
23
+ function extractFilePath(input) {
24
+ return extractKey(input, ['TargetFile', 'file_path', 'path', 'filePath']);
25
+ }
26
+ function extractCommand(input) {
27
+ return extractKey(input, ['CommandLine', 'command', 'cmd']);
28
+ }
29
+ function inspectPreCommand(cmd) {
30
+ const violation = inspectCommand(cmd);
31
+ if (violation.blocked) {
32
+ return { allowed: false, reason: violation.reason, source: 'command' };
33
+ }
34
+ return { allowed: true };
35
+ }
36
+ function inspectPreFile(filePath, input, options) {
37
+ const incoming = extractKey(input, ['CodeContent', 'content']);
38
+ const replacement = extractKey(input, ['ReplacementContent', 'replacement']);
39
+ const target = extractKey(input, ['TargetContent', 'target']);
40
+ const fileDecision = inspectFileWrite({
41
+ filePath,
42
+ incomingContent: incoming,
43
+ replacementContent: replacement,
44
+ targetContent: target,
45
+ maxLines: options.maxFileLines
46
+ });
47
+ if (fileDecision.blocked) {
48
+ return { allowed: false, reason: fileDecision.reason, source: 'file' };
49
+ }
50
+ const root = options.workspaceRoot ?? process.cwd();
51
+ const statePath = path.join(root, '.agents', '.session-state.json');
52
+ const state = readTddState(statePath);
53
+ const tddDecision = inspectTddRequirement(filePath, state, options.enforceTestFirst);
54
+ if (tddDecision.blocked) {
55
+ return { allowed: false, reason: tddDecision.reason, source: 'tdd' };
56
+ }
57
+ return { allowed: true };
58
+ }
59
+ /**
60
+ * Inspects a tool-use request before execution (PreToolUse hook).
61
+ *
62
+ * @param rawInput - Raw command string, JSON envelope, or argv slice.
63
+ * @param options - Guard configuration options.
64
+ * @returns Guard decision indicating whether tool execution is permitted.
65
+ */
66
+ export function inspectPreTool(rawInput, options = {}) {
67
+ const env = parseEnvelope(rawInput);
68
+ if (env && env.tool_input) {
69
+ const cmd = extractCommand(env.tool_input);
70
+ if (cmd)
71
+ return inspectPreCommand(cmd);
72
+ const targetFile = extractFilePath(env.tool_input);
73
+ if (targetFile)
74
+ return inspectPreFile(targetFile, env.tool_input, options);
75
+ }
76
+ return inspectPreCommand(rawInput);
77
+ }
78
+ export { inspectCommand, inspectFileWrite, inspectTddRequirement, readTddState, writeTddState };
79
+ export default { inspectPreTool, inspectCommand, inspectFileWrite, inspectTddRequirement };
80
+ //# sourceMappingURL=agent-guard.js.map
@@ -0,0 +1,158 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const DEFAULT_BOUNDARY_RULES = [
4
+ {
5
+ fromLayer: 'domain',
6
+ disallowImportsFrom: ['infrastructure', 'infra', 'adapters', 'cli', 'transport', 'controllers']
7
+ },
8
+ {
9
+ fromLayer: 'core',
10
+ disallowImportsFrom: ['infrastructure', 'infra', 'adapters', 'presentation', 'ui']
11
+ }
12
+ ];
13
+ function isSkippedDir(name) {
14
+ return name.startsWith('.') || name === 'node_modules' || name === 'lib';
15
+ }
16
+ function isEligibleSource(name) {
17
+ const isScript = name.endsWith('.ts') || name.endsWith('.js');
18
+ if (!isScript || name.endsWith('.d.ts'))
19
+ return false;
20
+ return !name.includes('.test.') && !name.includes('.spec.');
21
+ }
22
+ export function findSourceFiles(dir) {
23
+ if (!fs.existsSync(dir))
24
+ return [];
25
+ const results = [];
26
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (isSkippedDir(entry.name))
29
+ continue;
30
+ const full = path.join(dir, entry.name);
31
+ if (entry.isDirectory()) {
32
+ results.push(...findSourceFiles(full));
33
+ }
34
+ else if (isEligibleSource(entry.name)) {
35
+ results.push(path.resolve(full));
36
+ }
37
+ }
38
+ return results.sort();
39
+ }
40
+ function resolveImportTarget(sourceFile, specifier) {
41
+ const dir = path.dirname(sourceFile);
42
+ const candidate = path.resolve(dir, specifier);
43
+ if (fs.existsSync(candidate))
44
+ return candidate;
45
+ if (specifier.endsWith('.js')) {
46
+ const tsCandidate = path.resolve(dir, specifier.slice(0, -3) + '.ts');
47
+ if (fs.existsSync(tsCandidate))
48
+ return tsCandidate;
49
+ }
50
+ return null;
51
+ }
52
+ export function extractLocalImports(filePath) {
53
+ if (!fs.existsSync(filePath))
54
+ return [];
55
+ const content = fs.readFileSync(filePath, 'utf-8');
56
+ const imports = [];
57
+ const regex = /(?:import|export)\s+(?:(?:[\s\S]*?from\s+)|)['"](\.[^'"]+)['"]|import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
58
+ for (const match of content.matchAll(regex)) {
59
+ const spec = match[1] ?? match[2];
60
+ if (spec) {
61
+ const resolved = resolveImportTarget(filePath, spec);
62
+ if (resolved && !imports.includes(resolved)) {
63
+ imports.push(resolved);
64
+ }
65
+ }
66
+ }
67
+ return imports.sort();
68
+ }
69
+ export function buildDependencyGraph(files) {
70
+ const graph = new Map();
71
+ for (const file of files) {
72
+ graph.set(file, extractLocalImports(file));
73
+ }
74
+ return graph;
75
+ }
76
+ export function detectDependencyCycles(graph) {
77
+ const cycles = [];
78
+ const visited = new Set();
79
+ const inStack = new Set();
80
+ const pathStack = [];
81
+ function dfs(node) {
82
+ visited.add(node);
83
+ inStack.add(node);
84
+ pathStack.push(node);
85
+ for (const neighbor of graph.get(node) ?? []) {
86
+ if (!visited.has(neighbor)) {
87
+ dfs(neighbor);
88
+ }
89
+ else if (inStack.has(neighbor)) {
90
+ const startIdx = pathStack.indexOf(neighbor);
91
+ if (startIdx !== -1) {
92
+ cycles.push([...pathStack.slice(startIdx), neighbor]);
93
+ }
94
+ }
95
+ }
96
+ inStack.delete(node);
97
+ pathStack.pop();
98
+ }
99
+ for (const node of graph.keys()) {
100
+ if (!visited.has(node))
101
+ dfs(node);
102
+ }
103
+ return cycles;
104
+ }
105
+ function hasLayerSegment(pathStr, segment) {
106
+ return new RegExp(`(^|[/\\\\])${segment}([/\\\\]|$)`, 'i').test(pathStr);
107
+ }
108
+ export function detectBoundaryViolations(graph, rules = DEFAULT_BOUNDARY_RULES) {
109
+ const violations = [];
110
+ for (const [file, imports] of graph.entries()) {
111
+ for (const rule of rules) {
112
+ if (hasLayerSegment(file, rule.fromLayer)) {
113
+ for (const imported of imports) {
114
+ for (const disallowed of rule.disallowImportsFrom) {
115
+ if (hasLayerSegment(imported, disallowed)) {
116
+ violations.push({
117
+ file,
118
+ importedFile: imported,
119
+ fromLayer: rule.fromLayer,
120
+ toLayer: disallowed
121
+ });
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ return violations;
129
+ }
130
+ /**
131
+ * Inspects a source tree for architectural boundary violations and circular dependency cycles.
132
+ *
133
+ * @param sourceDir - Directory containing source code (e.g. src/).
134
+ * @param rules - Architectural boundary rules.
135
+ * @returns Report summarizing cycle and boundary findings.
136
+ */
137
+ export function inspectBoundaries(sourceDir, rules = DEFAULT_BOUNDARY_RULES) {
138
+ const files = findSourceFiles(sourceDir);
139
+ const graph = buildDependencyGraph(files);
140
+ const cycles = detectDependencyCycles(graph);
141
+ const violations = detectBoundaryViolations(graph, rules);
142
+ return {
143
+ moduleCount: files.length,
144
+ cycles,
145
+ violations,
146
+ ok: cycles.length === 0 && violations.length === 0
147
+ };
148
+ }
149
+ export default {
150
+ DEFAULT_BOUNDARY_RULES,
151
+ findSourceFiles,
152
+ extractLocalImports,
153
+ buildDependencyGraph,
154
+ detectDependencyCycles,
155
+ detectBoundaryViolations,
156
+ inspectBoundaries
157
+ };
158
+ //# sourceMappingURL=boundaries.js.map
@@ -14,10 +14,39 @@ import process from 'node:process';
14
14
  import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
 
16
16
  const selfDir = path.dirname(fileURLToPath(import.meta.url));
17
- const guardModulePath = path.resolve(selfDir, '../../lib/agent-guard.js');
17
+
18
+ // Resolution order (first existing file wins):
19
+ // 1. AZCODR_GUARD_ENGINE override (tests, embedders).
20
+ // 2. ../lib/agent-guard.js — vendored engine shipped inside `.agents/`
21
+ // (present in scaffolded projects; `.agents` is copied recursively).
22
+ // 3. ../../lib/agent-guard.js — azcodr repo dev layout (fallback only).
23
+ const guardCandidates = [
24
+ process.env.AZCODR_GUARD_ENGINE,
25
+ path.resolve(selfDir, '../lib/agent-guard.js'),
26
+ path.resolve(selfDir, '../../lib/agent-guard.js')
27
+ ].filter(Boolean);
18
28
 
19
29
  async function getGuardModule() {
20
- return import(pathToFileURL(guardModulePath).href);
30
+ const tried = [];
31
+ for (const candidate of guardCandidates) {
32
+ if (!fs.existsSync(candidate)) {
33
+ tried.push(candidate);
34
+ continue;
35
+ }
36
+ try {
37
+ return await import(pathToFileURL(candidate).href);
38
+ } catch (err) {
39
+ tried.push(`${candidate} (unreadable: ${err.message})`);
40
+ }
41
+ }
42
+ // Fail CLOSED: a guard with no engine must be loud, never a silent allow.
43
+ // (Unparseable tool payloads still fail open inside inspectPreTool per
44
+ // ADR-005; a missing engine is a broken install, not an ambiguous input.)
45
+ console.error(
46
+ '🚨 Architectural Guard misconfigured: engine module not found, refusing to fail open. Tried:\n' +
47
+ tried.map((t) => ` - ${t}`).join('\n')
48
+ );
49
+ process.exit(2);
21
50
  }
22
51
 
23
52
  async function readStdin(timeoutMs = 1500) {
@@ -60,4 +89,7 @@ async function main() {
60
89
  process.exit(0);
61
90
  }
62
91
 
63
- main().catch(() => process.exit(0));
92
+ main().catch((err) => {
93
+ console.error(`🚨 Architectural Guard crashed: ${err?.message || err}`);
94
+ process.exit(2);
95
+ });