azcodr 2.3.0 → 2.5.1
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.
- package/.agents/hooks.json +15 -0
- package/.agents/hooks.json.example +15 -0
- package/.agents/lib/agent-guard-command.js +103 -0
- package/.agents/lib/agent-guard-file.js +77 -0
- package/.agents/lib/agent-guard-tdd.js +77 -0
- package/.agents/lib/agent-guard.js +80 -0
- package/.agents/scripts/agent_guard.js +35 -3
- package/.agents/skills/clean-code-refactor/SKILL.md +1 -0
- package/.agents/skills/lets-build/SKILL.md +3 -2
- package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +159 -0
- package/.agents/skills/relentless-questioner/SKILL.md +1 -1
- package/.github/workflows/ci.yml +13 -0
- package/.github/workflows/publish.yml +7 -0
- package/AGENTS.md +3 -3
- package/CODE_OF_CONDUCT.md +122 -0
- package/CONTRIBUTING.md +16 -4
- package/README.md +77 -4
- package/docs/generated-enforcement-analysis.md +141 -0
- package/docs/rules/clean_code.md +3 -2
- package/docs/rules/test_driven_development.md +8 -5
- package/lib/cli-boundaries.d.ts +18 -0
- package/lib/cli-boundaries.d.ts.map +1 -0
- package/lib/cli-boundaries.js +46 -0
- package/lib/cli-boundaries.js.map +1 -0
- package/lib/cli-parse.d.ts +2 -1
- package/lib/cli-parse.d.ts.map +1 -1
- package/lib/cli-parse.js +15 -3
- package/lib/cli-parse.js.map +1 -1
- package/lib/cli.d.ts +3 -0
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +18 -4
- package/lib/cli.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/scaffold.js +1 -1
- package/lib/scaffold.js.map +1 -1
- package/memory.md +32 -0
- package/package.json +9 -3
- package/scripts/validate/toolchain.js +60 -0
- package/scripts/validate.js +12 -2
- package/src/cli-boundaries.ts +67 -0
- package/src/cli-parse.ts +19 -5
- package/src/cli.ts +20 -4
- package/src/index.ts +1 -1
- package/src/scaffold.ts +1 -1
package/.agents/hooks.json
CHANGED
|
@@ -14,6 +14,21 @@
|
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
|
+
"architectural-guard": {
|
|
18
|
+
"enabled": false,
|
|
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
33
|
"enabled": false,
|
|
19
34
|
"PostToolUse": [
|
|
@@ -14,6 +14,21 @@
|
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
|
+
"architectural-guard": {
|
|
18
|
+
"enabled": false,
|
|
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
33
|
"enabled": false,
|
|
19
34
|
"PostToolUse": [
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
92
|
+
main().catch((err) => {
|
|
93
|
+
console.error(`🚨 Architectural Guard crashed: ${err?.message || err}`);
|
|
94
|
+
process.exit(2);
|
|
95
|
+
});
|
|
@@ -51,6 +51,7 @@ Target concrete flaws:
|
|
|
51
51
|
### Step 5: Verify Continuous Green State
|
|
52
52
|
- Run tests after every single atomic change. Use the workspace's own command — do not assume one exists. Verify with the package manifest first (`node -p "JSON.stringify(require('./package.json').scripts)"`); for this template that is `npm test`, with `npm run test:coverage` for the coverage gate.
|
|
53
53
|
- Ensure coverage remains at **100.00%**.
|
|
54
|
+
- Verify architectural boundaries: Run `npx azcodr boundaries` to guarantee the refactoring introduced 0 circular dependencies and 0 layer boundary breaches.
|
|
54
55
|
|
|
55
56
|
---
|
|
56
57
|
|
|
@@ -100,7 +100,7 @@ Upon user confirmation:
|
|
|
100
100
|
- *Extension:* `manifest.json`, `src/background/index.ts`, `src/content/index.ts`, `src/popup/index.html`.
|
|
101
101
|
- *Game / Engine:* `src/core/`, `src/ecs/`, asset manifest, frame loop entrypoint.
|
|
102
102
|
- *CLI:* `src/cmd/`, `src/core/`, CLI entrypoint with exit code handling.
|
|
103
|
-
3. Generate build manifests (`Cargo.toml`, `package.json`, `go.mod`, `pyproject.toml`),
|
|
103
|
+
3. Generate build manifests (`Cargo.toml`, `package.json`, `go.mod`, `pyproject.toml`) for the recorded language, install the pinned linter from the emitted gate configuration (`eslint.config.js`, `ruff.toml`, `clippy.toml`, `.golangci.yml`, `checkstyle.xml`, `.editorconfig` CA block, `.clang-tidy` — written deterministically by `bootstrap_workspace.sh`, never invented). Node profiles arrive with `package.json` lint already rewired to `eslint .`; other profiles need the lint entry wired by hand. Prove the gate in Phase 5, and keep the boundary smoke test (`scripts/smoke_test.sh`).
|
|
104
104
|
4. **Replace Starter README with Project-Specific README**:
|
|
105
105
|
Generate a clean, project-specific `README.md` using [references/project_readme_template.md](./references/project_readme_template.md), completely replacing meta-template content with the project's actual name, mission, stack highlights, quickstart commands, and directory tree.
|
|
106
106
|
|
|
@@ -114,6 +114,7 @@ Upon user confirmation:
|
|
|
114
114
|
2. Execute toolchain dependency checks, build commands, and health/smoke tests:
|
|
115
115
|
- Compile code and verify zero compiler or lint errors.
|
|
116
116
|
- Verify boundary verification smoke test (`scripts/smoke_test.sh`).
|
|
117
|
+
- **Prove enforcement is live:** enable the shipped hooks in `.agents/hooks.json` (`safety-guard`, `architectural-guard`), then deliberately attempt a blocked action (e.g. append lines to a file already over budget, or a denied command) and confirm the hook rejects it before re-disabling nothing — hooks stay enabled. A guard that has never blocked anything is a rumor, not a control.
|
|
117
118
|
3. **Mandatory Handover to Domain Analysis (STOP & PIVOT):**
|
|
118
119
|
- **`lets-build` IS NOW COMPLETE.** Do NOT proceed to write domain business entities, repositories, or application features.
|
|
119
120
|
- Present the bootstrapped technical skeleton to the user.
|
|
@@ -160,7 +161,7 @@ Upon user confirmation:
|
|
|
160
161
|
|
|
161
162
|
## 4. Quality & Verification
|
|
162
163
|
- **Testing Strategy:** Outside-In TDD with Nano-Cycles (Uncle Bob's 3 Laws)
|
|
163
|
-
- **Code Health Gates:** 100
|
|
164
|
+
- **Code Health Gates:** 100% coverage on all metrics + mutation kill gate on guards, zero lint errors
|
|
164
165
|
- **DevSecOps:** <Semgrep / Trivy / Gitleaks / None>
|
|
165
166
|
```
|
|
166
167
|
|
|
@@ -324,6 +324,165 @@ EOF
|
|
|
324
324
|
;;
|
|
325
325
|
esac
|
|
326
326
|
|
|
327
|
+
# ------------------------------------------------------------------------------
|
|
328
|
+
# DETERMINISTIC TOOLCHAIN FILES. Directories alone enforce nothing: until this
|
|
329
|
+
# section existed, every language produced zero gate configurations and the
|
|
330
|
+
# starter `lint` script was an echo placeholder, so all enforcement was
|
|
331
|
+
# agent-authored. Each profile below emits its pinned gate configuration with
|
|
332
|
+
# values mirroring docs/rules/clean_code.md (300/30/10/3); the agent installs
|
|
333
|
+
# the named tool and proves the gate in Phase 5. Files are created only when
|
|
334
|
+
# absent, so re-runs never clobber agent-authored configs. Build manifests
|
|
335
|
+
# (Cargo.toml, go.mod, csproj, ...) stay with the agent: they carry project
|
|
336
|
+
# naming and version decisions no script may invent.
|
|
337
|
+
# ------------------------------------------------------------------------------
|
|
338
|
+
write_unless_exists() {
|
|
339
|
+
local target="$1"
|
|
340
|
+
if [[ -f "${target}" ]]; then
|
|
341
|
+
echo " keeping existing ${target}"
|
|
342
|
+
return
|
|
343
|
+
fi
|
|
344
|
+
mkdir -p "$(dirname "${target}")"
|
|
345
|
+
cat > "${target}"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
append_unless_present() {
|
|
349
|
+
local target="$1"
|
|
350
|
+
local marker="$2"
|
|
351
|
+
if [[ -f "${target}" ]] && grep -qF "${marker}" "${target}"; then
|
|
352
|
+
echo " keeping existing ${target} gates"
|
|
353
|
+
return
|
|
354
|
+
fi
|
|
355
|
+
cat >> "${target}"
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
echo "6. Emitting deterministic toolchain configs for '${LANGUAGE}'..."
|
|
359
|
+
case "${LANGUAGE}" in
|
|
360
|
+
typescript|javascript|deno|bun)
|
|
361
|
+
write_unless_exists "${WORKSPACE_ROOT}/eslint.config.js" << 'EOF'
|
|
362
|
+
// Deterministic fitness functions (azcodr clean_code.md section 5).
|
|
363
|
+
// Install the pinned tool, then prove the gate: npm run lint
|
|
364
|
+
export default [
|
|
365
|
+
{
|
|
366
|
+
files: ['src/**/*.{js,ts}', 'tests/**/*.{js,ts}'],
|
|
367
|
+
rules: {
|
|
368
|
+
'max-lines': ['error', 300],
|
|
369
|
+
'max-lines-per-function': ['error', 30],
|
|
370
|
+
complexity: ['error', 10],
|
|
371
|
+
'max-params': ['error', 3]
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
];
|
|
375
|
+
EOF
|
|
376
|
+
;;
|
|
377
|
+
python)
|
|
378
|
+
write_unless_exists "${WORKSPACE_ROOT}/ruff.toml" << 'EOF'
|
|
379
|
+
# Deterministic fitness functions (azcodr clean_code.md section 5).
|
|
380
|
+
# Install the pinned tool, then prove the gate: ruff check .
|
|
381
|
+
[lint]
|
|
382
|
+
select = ["E", "F", "C901", "PLR0912", "PLR0913", "PLR0915"]
|
|
383
|
+
[lint.mccabe]
|
|
384
|
+
max-complexity = 10
|
|
385
|
+
[lint.pylint]
|
|
386
|
+
max-args = 3
|
|
387
|
+
max-statements = 30
|
|
388
|
+
# NOTE: ruff has no file-length rule; the 300-line file cap is enforced by
|
|
389
|
+
# the project's lint entry (see lets-build Phase 5 proof).
|
|
390
|
+
EOF
|
|
391
|
+
;;
|
|
392
|
+
rust)
|
|
393
|
+
write_unless_exists "${WORKSPACE_ROOT}/clippy.toml" << 'EOF'
|
|
394
|
+
# Deterministic fitness functions (azcodr clean_code.md section 5).
|
|
395
|
+
# Enforce with: cargo clippy -- -D clippy::too_many_lines -D clippy::cognitive_complexity -D clippy::too_many_arguments
|
|
396
|
+
too-many-lines-threshold = 30
|
|
397
|
+
cognitive-complexity-threshold = 10
|
|
398
|
+
too-many-arguments-threshold = 3
|
|
399
|
+
# NOTE: clippy has no file-length lint; the 300-line file cap is enforced by
|
|
400
|
+
# the project's lint entry (see lets-build Phase 5 proof).
|
|
401
|
+
EOF
|
|
402
|
+
;;
|
|
403
|
+
go)
|
|
404
|
+
write_unless_exists "${WORKSPACE_ROOT}/.golangci.yml" << 'EOF'
|
|
405
|
+
# Deterministic fitness functions (azcodr clean_code.md section 5).
|
|
406
|
+
# Install the pinned tool, then prove the gate: golangci-lint run ./...
|
|
407
|
+
linters:
|
|
408
|
+
enable: [funlen, gocyclo]
|
|
409
|
+
linters-settings:
|
|
410
|
+
funlen:
|
|
411
|
+
lines: 30
|
|
412
|
+
statements: 25
|
|
413
|
+
gocyclo:
|
|
414
|
+
min-complexity: 10
|
|
415
|
+
# NOTE: no golangci-native file-length check; the 300-line file cap is
|
|
416
|
+
# enforced by the project's lint entry (see lets-build Phase 5 proof).
|
|
417
|
+
EOF
|
|
418
|
+
;;
|
|
419
|
+
java)
|
|
420
|
+
write_unless_exists "${WORKSPACE_ROOT}/checkstyle.xml" << 'EOF'
|
|
421
|
+
<?xml version="1.0"?>
|
|
422
|
+
<!-- Deterministic fitness functions (azcodr clean_code.md section 5). -->
|
|
423
|
+
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
|
|
424
|
+
<module name="Checker">
|
|
425
|
+
<module name="FileLength">
|
|
426
|
+
<property name="max" value="300"/>
|
|
427
|
+
</module>
|
|
428
|
+
<module name="TreeWalker">
|
|
429
|
+
<module name="MethodLength">
|
|
430
|
+
<property name="max" value="30"/>
|
|
431
|
+
</module>
|
|
432
|
+
<module name="CyclomaticComplexity">
|
|
433
|
+
<property name="max" value="10"/>
|
|
434
|
+
</module>
|
|
435
|
+
<module name="ParameterNumber">
|
|
436
|
+
<property name="max" value="3"/>
|
|
437
|
+
</module>
|
|
438
|
+
</module>
|
|
439
|
+
</module>
|
|
440
|
+
EOF
|
|
441
|
+
;;
|
|
442
|
+
csharp)
|
|
443
|
+
append_unless_present "${WORKSPACE_ROOT}/.editorconfig" "azcodr fitness functions" << 'EOF'
|
|
444
|
+
|
|
445
|
+
# --- azcodr fitness functions (CA gates; exact numbers in lint entry) ---
|
|
446
|
+
[*.cs]
|
|
447
|
+
dotnet_diagnostic.CA1501.severity = error
|
|
448
|
+
dotnet_diagnostic.CA1502.severity = error
|
|
449
|
+
EOF
|
|
450
|
+
;;
|
|
451
|
+
cpp|c)
|
|
452
|
+
write_unless_exists "${WORKSPACE_ROOT}/.clang-tidy" << 'EOF'
|
|
453
|
+
# Deterministic fitness functions (azcodr clean_code.md section 5).
|
|
454
|
+
Checks: 'readability-function-size,readability-function-cognitive-complexity'
|
|
455
|
+
CheckOptions:
|
|
456
|
+
- { key: readability-function-size.LineThreshold, value: 30 }
|
|
457
|
+
- { key: readability-function-size.ParameterThreshold, value: 3 }
|
|
458
|
+
- { key: readability-function-cognitive-complexity.Threshold, value: 10 }
|
|
459
|
+
# NOTE: clang-tidy has no file-length check; the 300-line file cap is
|
|
460
|
+
# enforced by the project's lint entry (see lets-build Phase 5 proof).
|
|
461
|
+
EOF
|
|
462
|
+
;;
|
|
463
|
+
generic)
|
|
464
|
+
echo " language undecided: no toolchain configs emitted (re-run with a language profile)"
|
|
465
|
+
;;
|
|
466
|
+
esac
|
|
467
|
+
|
|
468
|
+
# For Node profiles the starter package.json ships a lint placeholder that
|
|
469
|
+
# always succeeds. Point it at the emitted config so `npm run lint` fails
|
|
470
|
+
# until the pinned eslint is installed (fail-closed) instead of echoing.
|
|
471
|
+
# Only the placeholder is ever replaced; agent-wired entries are sacred.
|
|
472
|
+
case "${LANGUAGE}" in
|
|
473
|
+
typescript|javascript|deno|bun)
|
|
474
|
+
STARTER_PKG="${WORKSPACE_ROOT}/package.json"
|
|
475
|
+
if [[ -f "${STARTER_PKG}" ]] && grep -q 'No linter configured yet' "${STARTER_PKG}"; then
|
|
476
|
+
if command -v node >/dev/null 2>&1; then
|
|
477
|
+
node -e 'const fs=require("fs");const p=process.argv[1];const j=JSON.parse(fs.readFileSync(p,"utf8"));j.scripts=j.scripts||{};j.scripts.lint="eslint .";fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n");' "${STARTER_PKG}"
|
|
478
|
+
echo " wired package.json lint -> eslint ."
|
|
479
|
+
else
|
|
480
|
+
echo " node not found: leaving starter lint placeholder (agent wires it in Phase 4)"
|
|
481
|
+
fi
|
|
482
|
+
fi
|
|
483
|
+
;;
|
|
484
|
+
esac
|
|
485
|
+
|
|
327
486
|
# ------------------------------------------------------------------------------
|
|
328
487
|
# memory.md is an APPEND-ONLY LEDGER. It is never rewritten automatically.
|
|
329
488
|
#
|
|
@@ -119,7 +119,7 @@ Synthesize the answers into an unambiguous **Feature Alignment Specification (FA
|
|
|
119
119
|
When [Action]
|
|
120
120
|
Then [Observable Outcome]
|
|
121
121
|
```
|
|
122
|
-
- **Test Strategy:** [Contract / Integration / Unit tests required
|
|
122
|
+
- **Test Strategy:** [Contract / Integration / Unit tests required; 100% on all metrics, assertion-free tests rejected]
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
---
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -54,6 +54,13 @@ jobs:
|
|
|
54
54
|
node --version
|
|
55
55
|
npm --version
|
|
56
56
|
|
|
57
|
+
- name: Verify TypeScript Compilation & lib/ Parity
|
|
58
|
+
shell: bash
|
|
59
|
+
run: |
|
|
60
|
+
npm run build
|
|
61
|
+
git diff --exit-code lib/
|
|
62
|
+
npm run typecheck
|
|
63
|
+
|
|
57
64
|
- name: Run Syntax & Lint Checks
|
|
58
65
|
run: npm run lint
|
|
59
66
|
|
|
@@ -84,6 +91,12 @@ jobs:
|
|
|
84
91
|
- name: Verify Coverage Gates
|
|
85
92
|
run: npm run test:coverage
|
|
86
93
|
|
|
94
|
+
- name: Verify Mutation Score Gate (100% Mutant Kill)
|
|
95
|
+
run: npm run test:mutation
|
|
96
|
+
|
|
97
|
+
- name: Verify Empirical Drift Benchmark
|
|
98
|
+
run: node benchmark/run-benchmark.js
|
|
99
|
+
|
|
87
100
|
- name: Upload Coverage Report
|
|
88
101
|
uses: actions/upload-artifact@v4
|
|
89
102
|
if: always()
|