azcodr 1.5.1 → 1.5.2
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 +42 -42
- package/.agents/mcp_config.json.example +29 -29
- package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +1 -0
- package/.github/workflows/ci.yml +32 -10
- package/README.md +11 -0
- package/lib/scaffold.js +60 -11
- package/package.json +1 -1
- package/scripts/validate.js +258 -246
package/.agents/hooks.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"safety-guard": {
|
|
3
|
-
"enabled": true,
|
|
4
|
-
"PreToolUse": [
|
|
5
|
-
{
|
|
6
|
-
"matcher": "run_command",
|
|
7
|
-
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
"type": "command",
|
|
10
|
-
"command": "./.agents/scripts/safety_guard.sh",
|
|
11
|
-
"timeout": 15
|
|
12
|
-
}
|
|
13
|
-
]
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"post-tool-lint": {
|
|
18
|
-
"enabled": true,
|
|
19
|
-
"PostToolUse": [
|
|
20
|
-
{
|
|
21
|
-
"matcher": "run_command",
|
|
22
|
-
"hooks": [
|
|
23
|
-
{
|
|
24
|
-
"type": "command",
|
|
25
|
-
"command": "npm run lint",
|
|
26
|
-
"timeout": 30
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
},
|
|
32
|
-
"stop-verifier": {
|
|
33
|
-
"enabled": true,
|
|
34
|
-
"Stop": [
|
|
35
|
-
{
|
|
36
|
-
"type": "command",
|
|
37
|
-
"command": "./.agents/scripts/verify_completion.sh",
|
|
38
|
-
"timeout": 120
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"safety-guard": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "run_command",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "./.agents/scripts/safety_guard.sh",
|
|
11
|
+
"timeout": 15
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"post-tool-lint": {
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"PostToolUse": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "run_command",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "npm run lint",
|
|
26
|
+
"timeout": 30
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"stop-verifier": {
|
|
33
|
+
"enabled": true,
|
|
34
|
+
"Stop": [
|
|
35
|
+
{
|
|
36
|
+
"type": "command",
|
|
37
|
+
"command": "./.agents/scripts/verify_completion.sh",
|
|
38
|
+
"timeout": 120
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_notes": [
|
|
3
|
-
"Copy to .agents/mcp_config.json and replace placeholders before use.",
|
|
4
|
-
"Create ./data/ (shipped with .gitkeep) before starting the sqlite server.",
|
|
5
|
-
"Replace remote-service.url with your deployment; example.com is intentionally non-functional."
|
|
6
|
-
],
|
|
7
|
-
"mcpServers": {
|
|
8
|
-
"sqlite": {
|
|
9
|
-
"command": "npx",
|
|
10
|
-
"args": [
|
|
11
|
-
"-y",
|
|
12
|
-
"@modelcontextprotocol/server-sqlite",
|
|
13
|
-
"--db-path",
|
|
14
|
-
"./data/app.db"
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
"filesystem": {
|
|
18
|
-
"command": "npx",
|
|
19
|
-
"args": [
|
|
20
|
-
"-y",
|
|
21
|
-
"@modelcontextprotocol/server-filesystem",
|
|
22
|
-
"./docs"
|
|
23
|
-
]
|
|
24
|
-
},
|
|
25
|
-
"remote-service": {
|
|
26
|
-
"url": "https://mcp.example.com/sse"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"_notes": [
|
|
3
|
+
"Copy to .agents/mcp_config.json and replace placeholders before use.",
|
|
4
|
+
"Create ./data/ (shipped with .gitkeep) before starting the sqlite server.",
|
|
5
|
+
"Replace remote-service.url with your deployment; example.com is intentionally non-functional."
|
|
6
|
+
],
|
|
7
|
+
"mcpServers": {
|
|
8
|
+
"sqlite": {
|
|
9
|
+
"command": "npx",
|
|
10
|
+
"args": [
|
|
11
|
+
"-y",
|
|
12
|
+
"@modelcontextprotocol/server-sqlite",
|
|
13
|
+
"--db-path",
|
|
14
|
+
"./data/app.db"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"filesystem": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": [
|
|
20
|
+
"-y",
|
|
21
|
+
"@modelcontextprotocol/server-filesystem",
|
|
22
|
+
"./docs"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"remote-service": {
|
|
26
|
+
"url": "https://mcp.example.com/sse"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -173,6 +173,7 @@ COPILOT_FILE="${WORKSPACE_ROOT}/.github/copilot-instructions.md"
|
|
|
173
173
|
if [[ -d "${WORKSPACE_ROOT}/.github" ]]; then
|
|
174
174
|
if [[ -L "${COPILOT_FILE}" ]]; then
|
|
175
175
|
TARGET=$(readlink "${COPILOT_FILE}")
|
|
176
|
+
TARGET="${TARGET//\\//}"
|
|
176
177
|
if [[ "${TARGET}" == "../AGENTS.md" || "${TARGET}" == "${WORKSPACE_ROOT}/AGENTS.md" || "${TARGET}" == "AGENTS.md" ]]; then
|
|
177
178
|
log_pass ".github/copilot-instructions.md is a valid symlink to AGENTS.md."
|
|
178
179
|
else
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -13,15 +13,15 @@ jobs:
|
|
|
13
13
|
strategy:
|
|
14
14
|
fail-fast: false
|
|
15
15
|
matrix:
|
|
16
|
-
os: [ ubuntu-
|
|
16
|
+
os: [ ubuntu-24.04, macos-latest, windows-latest ]
|
|
17
17
|
node-version: [ 18.x, 20.x, 22.x, 24.x ]
|
|
18
18
|
|
|
19
19
|
steps:
|
|
20
20
|
- name: Checkout Repository
|
|
21
|
-
uses: actions/checkout@
|
|
21
|
+
uses: actions/checkout@v7
|
|
22
22
|
|
|
23
23
|
- name: Setup Node.js ${{ matrix.node-version }}
|
|
24
|
-
uses: actions/setup-node@
|
|
24
|
+
uses: actions/setup-node@v7
|
|
25
25
|
with:
|
|
26
26
|
node-version: ${{ matrix.node-version }}
|
|
27
27
|
|
|
@@ -36,21 +36,43 @@ jobs:
|
|
|
36
36
|
- name: Validate Agentic Architecture
|
|
37
37
|
run: npm run validate
|
|
38
38
|
|
|
39
|
+
- name: Run Test Suite
|
|
40
|
+
run: npm test
|
|
41
|
+
|
|
42
|
+
coverage:
|
|
43
|
+
name: 100.00% Coverage Gate (Node 24)
|
|
44
|
+
runs-on: ubuntu-24.04
|
|
45
|
+
steps:
|
|
46
|
+
- name: Checkout Repository
|
|
47
|
+
uses: actions/checkout@v7
|
|
48
|
+
|
|
49
|
+
- name: Setup Node.js 24.x
|
|
50
|
+
uses: actions/setup-node@v7
|
|
51
|
+
with:
|
|
52
|
+
node-version: 24.x
|
|
53
|
+
|
|
54
|
+
# Native 100% threshold flags require Node >=22.8.0 (see scripts/test_coverage.js),
|
|
55
|
+
# so the coverage gate runs once on the newest runtime instead of every matrix cell.
|
|
56
|
+
- name: Verify 100.00% Test Coverage Gates
|
|
57
|
+
run: npm run test:coverage
|
|
58
|
+
|
|
59
|
+
security:
|
|
60
|
+
name: Secret & Vulnerability Scans
|
|
61
|
+
runs-on: ubuntu-24.04
|
|
62
|
+
steps:
|
|
63
|
+
- name: Checkout Repository
|
|
64
|
+
uses: actions/checkout@v7
|
|
65
|
+
|
|
39
66
|
- name: Secret Scan (Gitleaks)
|
|
40
67
|
uses: gitleaks/gitleaks-action@v2
|
|
41
68
|
env:
|
|
42
69
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
43
70
|
|
|
44
71
|
- name: Filesystem Vulnerability Scan (Trivy)
|
|
45
|
-
uses: aquasecurity/trivy-action@
|
|
72
|
+
uses: aquasecurity/trivy-action@v0.36.0
|
|
46
73
|
with:
|
|
47
74
|
scan-type: fs
|
|
48
75
|
scan-ref: .
|
|
76
|
+
scanners: vuln
|
|
49
77
|
severity: HIGH,CRITICAL
|
|
50
78
|
exit-code: '1'
|
|
51
|
-
|
|
52
|
-
- name: Run Test Suite
|
|
53
|
-
run: npm test
|
|
54
|
-
|
|
55
|
-
- name: Verify 100.00% Test Coverage Gates
|
|
56
|
-
run: npm run test:coverage
|
package/README.md
CHANGED
|
@@ -152,3 +152,14 @@ Once confirmed, the agent automatically executes:
|
|
|
152
152
|
|
|
153
153
|
- 📖 **[Living Ubiquitous Language Glossary](./docs/knowledge/ubiquitous_language.md)**: Authoritative domain vocabulary contract.
|
|
154
154
|
- 📜 **[Lightweight ADR Ledger](./memory.md)**: Formal Architectural Decision Records and governing rules.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 🔒 Security & Supply-Chain Notes (Socket)
|
|
159
|
+
|
|
160
|
+
Socket flags two expected capabilities for `azcodr@1.5.1`:
|
|
161
|
+
|
|
162
|
+
- **Shell access (SUPPLY CHAIN RISK):** limited to `git` via `lib/scaffold.js:15` (`runGit`) using `execFileSync('git', argv, { shell: false })` with an allowlist of `rev-parse, init, branch, add, commit`. No shell strings, no network, `cwd` scoped to the scaffold target.
|
|
163
|
+
- **Filesystem access (SUPPLY CHAIN RISK):** scoped to `templateDir -> targetDir` via `lib/scaffold.js:34` (`assertInside`). Template allowlist is `TEMPLATE_ITEMS`; symlinks and `package.json` creation stay inside the target.
|
|
164
|
+
|
|
165
|
+
Initial-commit fallback identity is `Subodh Khanal <prosubodh@gmail.com>` via `GIT_AUTHOR_*` / `GIT_COMMITTER_*` env (no `git -c` shell flags).
|
package/lib/scaffold.js
CHANGED
|
@@ -4,6 +4,42 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const cp = require('node:child_process');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Socket-hardened process boundary (Supply Chain: shell access).
|
|
9
|
+
* Scaffolder must spawn `git`, but never via a shell string.
|
|
10
|
+
* Uses execFileSync with argv (shell:false) and an explicit subcommand allowlist.
|
|
11
|
+
* No network, no env exfiltration; cwd is constrained to targetDir callers.
|
|
12
|
+
*/
|
|
13
|
+
const GIT_ALLOWED_SUBCOMMANDS = new Set(['rev-parse', 'init', 'branch', 'add', 'commit']);
|
|
14
|
+
|
|
15
|
+
function runGit(args, options = {}) {
|
|
16
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
17
|
+
throw new Error('runGit requires a non-empty argv array');
|
|
18
|
+
}
|
|
19
|
+
if (!GIT_ALLOWED_SUBCOMMANDS.has(args[0])) {
|
|
20
|
+
throw new Error(`Blocked git subcommand: ${String(args[0])}`);
|
|
21
|
+
}
|
|
22
|
+
return cp.execFileSync('git', args, {
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
shell: false,
|
|
26
|
+
...options
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Filesystem scope guard (Supply Chain: filesystem access).
|
|
32
|
+
* Constrains all reads/writes to targetDir / templateDir.
|
|
33
|
+
*/
|
|
34
|
+
function assertInside(root, candidate, message) {
|
|
35
|
+
const resolvedRoot = path.resolve(root);
|
|
36
|
+
const resolvedCandidate = path.resolve(root, candidate);
|
|
37
|
+
const relative = path.relative(resolvedRoot, resolvedCandidate);
|
|
38
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
39
|
+
throw new Error(message || `Path escapes allowed root: ${candidate}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
7
43
|
const TEMPLATE_ITEMS = [
|
|
8
44
|
'AGENTS.md',
|
|
9
45
|
'memory.md',
|
|
@@ -201,6 +237,7 @@ function makeScriptsExecutable(targetDir, dryRun = false) {
|
|
|
201
237
|
|
|
202
238
|
/**
|
|
203
239
|
* Recursively copies template files into the target directory and sets up symlinks and permissions.
|
|
240
|
+
* Socket note: filesystem access is scoped to templateDir -> targetDir only (assertInside).
|
|
204
241
|
*/
|
|
205
242
|
function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
206
243
|
const { dryRun = false } = options;
|
|
@@ -223,6 +260,8 @@ function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
|
223
260
|
if (!fs.existsSync(srcPath)) continue;
|
|
224
261
|
|
|
225
262
|
const destPath = path.join(resolvedTarget, item);
|
|
263
|
+
assertInside(resolvedTemplate, srcPath);
|
|
264
|
+
assertInside(resolvedTarget, destPath);
|
|
226
265
|
actions.push(`copy: ${item} -> ${destPath}`);
|
|
227
266
|
|
|
228
267
|
if (!dryRun) {
|
|
@@ -288,16 +327,16 @@ function copyTemplate(targetDir, templateDir = getTemplateDir(), options = {}) {
|
|
|
288
327
|
|
|
289
328
|
/**
|
|
290
329
|
* Detects whether targetDir is already inside an existing Git worktree.
|
|
330
|
+
* Socket note: shell access is limited to `git rev-parse` via runGit (no shell).
|
|
291
331
|
*/
|
|
292
332
|
function isInsideGitWorkTree(targetDir) {
|
|
293
333
|
try {
|
|
294
334
|
const checkDir = fs.existsSync(targetDir) ? targetDir : path.dirname(targetDir);
|
|
295
|
-
const out =
|
|
335
|
+
const out = runGit(['rev-parse', '--is-inside-work-tree'], {
|
|
296
336
|
cwd: checkDir,
|
|
297
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
298
|
-
encoding: 'utf-8'
|
|
337
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
299
338
|
});
|
|
300
|
-
return out.trim() === 'true';
|
|
339
|
+
return String(out).trim() === 'true';
|
|
301
340
|
} catch {
|
|
302
341
|
return false;
|
|
303
342
|
}
|
|
@@ -305,6 +344,7 @@ function isInsideGitWorkTree(targetDir) {
|
|
|
305
344
|
|
|
306
345
|
/**
|
|
307
346
|
* Initializes a git repository in the target directory if not already inside one.
|
|
347
|
+
* Socket note: only allowlisted `git init/branch/add/commit` via execFileSync, cwd scoped.
|
|
308
348
|
*/
|
|
309
349
|
function initGit(targetDir, options = {}) {
|
|
310
350
|
const { noGit = false, dryRun = false } = options;
|
|
@@ -321,27 +361,34 @@ function initGit(targetDir, options = {}) {
|
|
|
321
361
|
|
|
322
362
|
try {
|
|
323
363
|
try {
|
|
324
|
-
|
|
364
|
+
runGit(['init', '-b', 'main', '-q'], { cwd: targetDir, stdio: 'ignore' });
|
|
325
365
|
} catch {
|
|
326
|
-
|
|
366
|
+
runGit(['init', '-q'], { cwd: targetDir, stdio: 'ignore' });
|
|
327
367
|
try {
|
|
328
|
-
|
|
368
|
+
runGit(['branch', '-m', 'main'], { cwd: targetDir, stdio: 'ignore' });
|
|
329
369
|
} catch {
|
|
330
370
|
// Non-critical if branch rename fails
|
|
331
371
|
}
|
|
332
372
|
}
|
|
333
373
|
|
|
334
374
|
try {
|
|
335
|
-
|
|
375
|
+
runGit(['add', '-A'], { cwd: targetDir, stdio: 'ignore' });
|
|
336
376
|
try {
|
|
337
|
-
|
|
377
|
+
runGit(['commit', '-q', '-m', 'chore: initial scaffold from azcodr template'], {
|
|
338
378
|
cwd: targetDir,
|
|
339
379
|
stdio: 'ignore'
|
|
340
380
|
});
|
|
341
381
|
} catch {
|
|
342
|
-
|
|
382
|
+
runGit(['commit', '-q', '-m', 'chore: initial scaffold from azcodr template'], {
|
|
343
383
|
cwd: targetDir,
|
|
344
|
-
stdio: 'ignore'
|
|
384
|
+
stdio: 'ignore',
|
|
385
|
+
env: {
|
|
386
|
+
...process.env,
|
|
387
|
+
GIT_AUTHOR_NAME: 'Subodh Khanal',
|
|
388
|
+
GIT_AUTHOR_EMAIL: 'prosubodh@gmail.com',
|
|
389
|
+
GIT_COMMITTER_NAME: 'Subodh Khanal',
|
|
390
|
+
GIT_COMMITTER_EMAIL: 'prosubodh@gmail.com'
|
|
391
|
+
}
|
|
345
392
|
});
|
|
346
393
|
}
|
|
347
394
|
} catch {
|
|
@@ -395,5 +442,7 @@ module.exports = {
|
|
|
395
442
|
isInsideGitWorkTree,
|
|
396
443
|
initGit,
|
|
397
444
|
getTemplateDir,
|
|
445
|
+
runGit,
|
|
446
|
+
assertInside,
|
|
398
447
|
TEMPLATE_ITEMS
|
|
399
448
|
};
|
package/package.json
CHANGED
package/scripts/validate.js
CHANGED
|
@@ -1,246 +1,258 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
|
|
7
|
-
function parseArgs(argv) {
|
|
8
|
-
let root = '';
|
|
9
|
-
for (const arg of argv) {
|
|
10
|
-
if (arg === '--fix') continue;
|
|
11
|
-
if (!root) root = arg;
|
|
12
|
-
}
|
|
13
|
-
return root || process.cwd();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function main() {
|
|
17
|
-
const workspaceRoot = path.resolve(parseArgs(process.argv.slice(2)));
|
|
18
|
-
let errors = 0;
|
|
19
|
-
let warnings = 0;
|
|
20
|
-
|
|
21
|
-
const pass = (msg) => console.log(` ✅ ${msg}`);
|
|
22
|
-
const warn = (msg) => { warnings += 1; console.log(` ⚠️ ${msg}`); };
|
|
23
|
-
const fail = (msg) => { errors += 1; console.log(` ❌ ${msg}`); };
|
|
24
|
-
|
|
25
|
-
console.log(`🔍 Validating Agentic Architecture in: ${workspaceRoot}`);
|
|
26
|
-
console.log('--------------------------------------------------------------');
|
|
27
|
-
|
|
28
|
-
console.log('1. Checking Root Configuration & Symlinks...');
|
|
29
|
-
const agentsFile = path.join(workspaceRoot, 'AGENTS.md');
|
|
30
|
-
if (!fs.existsSync(agentsFile)) {
|
|
31
|
-
fail(`Missing root AGENTS.md at ${agentsFile}`);
|
|
32
|
-
} else {
|
|
33
|
-
pass('AGENTS.md exists.');
|
|
34
|
-
const lines = fs.readFileSync(agentsFile, 'utf-8').split('\n').length;
|
|
35
|
-
if (lines <= 120) pass(`AGENTS.md line count is lean: ${lines} lines (<= 120).`);
|
|
36
|
-
else if (lines <= 150) warn(`AGENTS.md line count is getting large: ${lines} lines (warn > 120).`);
|
|
37
|
-
else fail(`AGENTS.md exceeds maximum line limit: ${lines} lines (max 150).`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const agentsContent = fs.existsSync(agentsFile) ? fs.readFileSync(agentsFile, 'utf-8') : '';
|
|
41
|
-
|
|
42
|
-
function isValidAgentsTarget(target) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
checkParity(
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (fs.existsSync(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
let root = '';
|
|
9
|
+
for (const arg of argv) {
|
|
10
|
+
if (arg === '--fix') continue;
|
|
11
|
+
if (!root) root = arg;
|
|
12
|
+
}
|
|
13
|
+
return root || process.cwd();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function main() {
|
|
17
|
+
const workspaceRoot = path.resolve(parseArgs(process.argv.slice(2)));
|
|
18
|
+
let errors = 0;
|
|
19
|
+
let warnings = 0;
|
|
20
|
+
|
|
21
|
+
const pass = (msg) => console.log(` ✅ ${msg}`);
|
|
22
|
+
const warn = (msg) => { warnings += 1; console.log(` ⚠️ ${msg}`); };
|
|
23
|
+
const fail = (msg) => { errors += 1; console.log(` ❌ ${msg}`); };
|
|
24
|
+
|
|
25
|
+
console.log(`🔍 Validating Agentic Architecture in: ${workspaceRoot}`);
|
|
26
|
+
console.log('--------------------------------------------------------------');
|
|
27
|
+
|
|
28
|
+
console.log('1. Checking Root Configuration & Symlinks...');
|
|
29
|
+
const agentsFile = path.join(workspaceRoot, 'AGENTS.md');
|
|
30
|
+
if (!fs.existsSync(agentsFile)) {
|
|
31
|
+
fail(`Missing root AGENTS.md at ${agentsFile}`);
|
|
32
|
+
} else {
|
|
33
|
+
pass('AGENTS.md exists.');
|
|
34
|
+
const lines = fs.readFileSync(agentsFile, 'utf-8').split('\n').length;
|
|
35
|
+
if (lines <= 120) pass(`AGENTS.md line count is lean: ${lines} lines (<= 120).`);
|
|
36
|
+
else if (lines <= 150) warn(`AGENTS.md line count is getting large: ${lines} lines (warn > 120).`);
|
|
37
|
+
else fail(`AGENTS.md exceeds maximum line limit: ${lines} lines (max 150).`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const agentsContent = fs.existsSync(agentsFile) ? fs.readFileSync(agentsFile, 'utf-8') : '';
|
|
41
|
+
|
|
42
|
+
function isValidAgentsTarget(target) {
|
|
43
|
+
const normalized = target.replace(/\\/g, '/');
|
|
44
|
+
const rootNormalized = agentsFile.replace(/\\/g, '/');
|
|
45
|
+
return normalized === 'AGENTS.md' || normalized === './AGENTS.md' || normalized === rootNormalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkParity(filePath, label, allowCopyFallback) {
|
|
49
|
+
let stat = null;
|
|
50
|
+
try { stat = fs.lstatSync(filePath); } catch { fail(`${label} is missing.`); return; }
|
|
51
|
+
if (stat.isSymbolicLink()) {
|
|
52
|
+
let target = '';
|
|
53
|
+
try { target = fs.readlinkSync(filePath); } catch { fail(`${label} readlink failed.`); return; }
|
|
54
|
+
const normalized = target.replace(/\\/g, '/');
|
|
55
|
+
const ok = label.endsWith('copilot-instructions.md')
|
|
56
|
+
? (normalized === '../AGENTS.md' || normalized === agentsFile.replace(/\\/g, '/') || normalized === 'AGENTS.md')
|
|
57
|
+
: isValidAgentsTarget(target);
|
|
58
|
+
if (ok) pass(`${label} is a valid symlink to AGENTS.md.`);
|
|
59
|
+
else fail(`${label} points to '${target}' instead of 'AGENTS.md'.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (stat.isFile()) {
|
|
63
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
64
|
+
const trimmed = content.trim();
|
|
65
|
+
if (trimmed === 'AGENTS.md' || trimmed === './AGENTS.md' || trimmed === agentsFile) {
|
|
66
|
+
pass(`${label} is a text pointer to AGENTS.md (symlink fallback).`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (label.endsWith('copilot-instructions.md') && content.includes('AGENTS.md')) {
|
|
70
|
+
pass(`${label} references AGENTS.md (symlink fallback).`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (allowCopyFallback && agentsContent && content === agentsContent) {
|
|
74
|
+
warn(`${label} is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk).`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (label === 'agents.md') {
|
|
78
|
+
try {
|
|
79
|
+
const entries = fs.readdirSync(workspaceRoot);
|
|
80
|
+
if (!entries.includes('agents.md') && entries.includes('AGENTS.md')) {
|
|
81
|
+
pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
} catch { /* fall through */ }
|
|
85
|
+
}
|
|
86
|
+
fail(`${label} is not a symbolic link.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
fail(`${label} is neither a symlink nor a regular file.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
checkParity(path.join(workspaceRoot, 'CLAUDE.md'), 'CLAUDE.md', true);
|
|
93
|
+
const lowerPath = path.join(workspaceRoot, 'agents.md');
|
|
94
|
+
try {
|
|
95
|
+
const entries = fs.readdirSync(workspaceRoot);
|
|
96
|
+
const hasUpper = entries.includes('AGENTS.md');
|
|
97
|
+
const hasLowerExact = entries.includes('agents.md');
|
|
98
|
+
if (hasUpper && !hasLowerExact) {
|
|
99
|
+
let appearsSame = false;
|
|
100
|
+
try { appearsSame = fs.existsSync(lowerPath); } catch { appearsSame = false; }
|
|
101
|
+
if (appearsSame) {
|
|
102
|
+
pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
|
|
103
|
+
} else {
|
|
104
|
+
// Case-sensitive filesystem gap (agents.md cannot be committed alongside
|
|
105
|
+
// AGENTS.md from case-insensitive systems): restore parity in place.
|
|
106
|
+
let created = false;
|
|
107
|
+
try { fs.symlinkSync('AGENTS.md', lowerPath); created = true; }
|
|
108
|
+
catch {
|
|
109
|
+
try { fs.writeFileSync(lowerPath, 'AGENTS.md\n', 'utf-8'); created = true; }
|
|
110
|
+
catch { created = false; }
|
|
111
|
+
}
|
|
112
|
+
if (created) pass('Created agents.md parity link to AGENTS.md (case-sensitive filesystem).');
|
|
113
|
+
checkParity(lowerPath, 'agents.md', true);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
checkParity(lowerPath, 'agents.md', true);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
checkParity(lowerPath, 'agents.md', true);
|
|
120
|
+
}
|
|
121
|
+
checkParity(path.join(workspaceRoot, 'GEMINI.md'), 'GEMINI.md', true);
|
|
122
|
+
checkParity(path.join(workspaceRoot, '.cursorrules'), '.cursorrules', true);
|
|
123
|
+
checkParity(path.join(workspaceRoot, '.windsurfrules'), '.windsurfrules', true);
|
|
124
|
+
const githubDir = path.join(workspaceRoot, '.github');
|
|
125
|
+
if (fs.existsSync(githubDir) && fs.statSync(githubDir).isDirectory()) {
|
|
126
|
+
const copilot = path.join(githubDir, 'copilot-instructions.md');
|
|
127
|
+
if (fs.existsSync(copilot)) {
|
|
128
|
+
checkParity(copilot, '.github/copilot-instructions.md', true);
|
|
129
|
+
} else {
|
|
130
|
+
warn('.github/copilot-instructions.md is missing (run scaffold to restore harness parity).');
|
|
131
|
+
}
|
|
132
|
+
if (!fs.existsSync(path.join(githubDir, 'workflows'))) {
|
|
133
|
+
warn('.github/workflows is missing (CI will not run in scaffolded projects).');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (fs.existsSync(path.join(workspaceRoot, '.gitignore'))) pass('.gitignore exists.');
|
|
138
|
+
else fail('Missing .gitignore');
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
console.log('2. Checking Progressive Disclosure Rules...');
|
|
142
|
+
const rulesDir = path.join(workspaceRoot, 'docs', 'rules');
|
|
143
|
+
if (!fs.existsSync(rulesDir)) {
|
|
144
|
+
fail(`Missing docs/rules directory at ${rulesDir}`);
|
|
145
|
+
} else {
|
|
146
|
+
const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md'));
|
|
147
|
+
let count = 0;
|
|
148
|
+
for (const name of files) {
|
|
149
|
+
count += 1;
|
|
150
|
+
const fp = path.join(rulesDir, name);
|
|
151
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
152
|
+
const stat = fs.statSync(fp);
|
|
153
|
+
if (!/^# /m.test(content)) fail(`Rule ${name} missing H1 header (# Title)`);
|
|
154
|
+
if (!/^> \*\*Core Mandate:\*\*/m.test(content)) warn(`Rule ${name} missing standardized '> **Core Mandate:**' summary`);
|
|
155
|
+
if (stat.size > 24000) warn(`Rule ${name} exceeds 24KB token-economy cap (${stat.size} bytes)`);
|
|
156
|
+
}
|
|
157
|
+
pass(`Validated ${count} modular rule files in docs/rules/.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log('3. Checking Specialized Skills (.agents/skills)...');
|
|
162
|
+
const skillsDir = path.join(workspaceRoot, '.agents', 'skills');
|
|
163
|
+
if (!fs.existsSync(skillsDir)) {
|
|
164
|
+
fail(`Missing .agents/skills directory at ${skillsDir}`);
|
|
165
|
+
} else {
|
|
166
|
+
const folders = fs.readdirSync(skillsDir).filter((f) => fs.statSync(path.join(skillsDir, f)).isDirectory());
|
|
167
|
+
let count = 0;
|
|
168
|
+
for (const skill of folders) {
|
|
169
|
+
count += 1;
|
|
170
|
+
const skillFile = path.join(skillsDir, skill, 'SKILL.md');
|
|
171
|
+
if (!fs.existsSync(skillFile)) { fail(`Skill '${skill}' missing SKILL.md`); continue; }
|
|
172
|
+
const content = fs.readFileSync(skillFile, 'utf-8');
|
|
173
|
+
const lines = content.split('\n');
|
|
174
|
+
if (lines[0].trim() !== '---') { fail(`Skill '${skill}' missing opening front matter delimiter (---)`); continue; }
|
|
175
|
+
const closing = lines.slice(1).findIndex((l) => l.trim() === '---');
|
|
176
|
+
if (closing === -1) fail(`Skill '${skill}' missing closing front matter delimiter (---)`);
|
|
177
|
+
const nameMatch = content.match(/^name:\s*(.+)$/m);
|
|
178
|
+
if (!nameMatch || nameMatch[1].trim() !== skill) fail(`Skill '${skill}' front matter 'name:' does not match directory name`);
|
|
179
|
+
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
180
|
+
const desc = descMatch ? descMatch[1].trim() : '';
|
|
181
|
+
if (!desc) {
|
|
182
|
+
fail(`Skill '${skill}' missing front matter 'description:'`);
|
|
183
|
+
} else {
|
|
184
|
+
if (!/^Use when/.test(desc)) warn(`Skill '${skill}' description should start with imperative 'Use when...'`);
|
|
185
|
+
if (!/do not use/i.test(desc)) warn(`Skill '${skill}' description should specify negative boundaries ('Do not use for...')`);
|
|
186
|
+
if (desc.length > 1024) fail(`Skill '${skill}' description exceeds 1024 chars (${desc.length} chars)`);
|
|
187
|
+
}
|
|
188
|
+
if (lines.length > 500) warn(`Skill '${skill}' exceeds 500 lines (${lines.length} lines). Offload details to references/.`);
|
|
189
|
+
else pass(`Skill '${skill}': ${lines.length} lines, description valid (${desc.length} chars).`);
|
|
190
|
+
if (!/What NOT to do/i.test(content) && !/Gotchas/i.test(content)) {
|
|
191
|
+
warn(`Skill '${skill}' missing mandatory 'Gotchas & What NOT to Do' section`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
pass(`Validated ${count} skills in .agents/skills/.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log('4. Checking Markdown Internal Links & Cross-References...');
|
|
199
|
+
const broken = [];
|
|
200
|
+
let totalLinks = 0;
|
|
201
|
+
function walk(dir) {
|
|
202
|
+
let entries = [];
|
|
203
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
if (entry.name === '.git' || entry.name === 'node_modules') continue;
|
|
206
|
+
const full = path.join(dir, entry.name);
|
|
207
|
+
if (entry.isDirectory()) walk(full);
|
|
208
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) checkFile(full);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function checkFile(filePath) {
|
|
212
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
213
|
+
const dir = path.dirname(filePath);
|
|
214
|
+
const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
215
|
+
let m;
|
|
216
|
+
while ((m = regex.exec(content)) !== null) {
|
|
217
|
+
const target = m[2].trim();
|
|
218
|
+
if (/^(https?:|mailto:|#|conversation:\/\/|file:\/\/)/.test(target)) continue;
|
|
219
|
+
const clean = target.split('#')[0];
|
|
220
|
+
if (!clean) continue;
|
|
221
|
+
totalLinks += 1;
|
|
222
|
+
if (!fs.existsSync(path.normalize(path.join(dir, clean)))) {
|
|
223
|
+
broken.push(`${path.relative(workspaceRoot, filePath)} -> ${target}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
walk(workspaceRoot);
|
|
228
|
+
if (broken.length === 0) pass(`Validated ${totalLinks} internal links across workspace (0 broken links).`);
|
|
229
|
+
else for (const b of broken) fail(`Broken markdown link: ${b}`);
|
|
230
|
+
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log('5. Checking Memory & ADR Ledger...');
|
|
233
|
+
const memoryFile = path.join(workspaceRoot, 'memory.md');
|
|
234
|
+
if (!fs.existsSync(memoryFile)) {
|
|
235
|
+
fail('Missing memory.md ADR ledger.');
|
|
236
|
+
} else {
|
|
237
|
+
const mem = fs.readFileSync(memoryFile, 'utf-8');
|
|
238
|
+
const withoutComments = mem.replace(/<!--[\s\S]*?-->/g, '');
|
|
239
|
+
if (!/^# /m.test(mem)) fail('memory.md missing H1 header');
|
|
240
|
+
else if (/#### ADR-/.test(withoutComments)) pass('memory.md contains ADR entries with valid envelope.');
|
|
241
|
+
else pass('memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build).');
|
|
242
|
+
if (/ADR-025|ADR-028/.test(withoutComments) && /azcodr/i.test(mem)) {
|
|
243
|
+
warn('memory.md may contain template-internal ADRs; fresh projects must start at ADR-001.');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log('--------------------------------------------------------------');
|
|
249
|
+
if (errors === 0) {
|
|
250
|
+
console.log(`🎉 SUCCESS: All agentic configurations are valid and healthy! (${warnings} warnings)`);
|
|
251
|
+
process.exit(0);
|
|
252
|
+
} else {
|
|
253
|
+
console.log(`🚨 FAILURE: Found ${errors} error(s) and ${warnings} warning(s) in agentic configurations.`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main();
|