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.
@@ -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
@@ -13,15 +13,15 @@ jobs:
13
13
  strategy:
14
14
  fail-fast: false
15
15
  matrix:
16
- os: [ ubuntu-latest, macos-latest, windows-latest ]
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@v4
21
+ uses: actions/checkout@v7
22
22
 
23
23
  - name: Setup Node.js ${{ matrix.node-version }}
24
- uses: actions/setup-node@v4
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@0.28.0
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 = cp.execSync('git rev-parse --is-inside-work-tree', {
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
- cp.execSync('git init -b main -q', { cwd: targetDir, stdio: 'ignore' });
364
+ runGit(['init', '-b', 'main', '-q'], { cwd: targetDir, stdio: 'ignore' });
325
365
  } catch {
326
- cp.execSync('git init -q', { cwd: targetDir, stdio: 'ignore' });
366
+ runGit(['init', '-q'], { cwd: targetDir, stdio: 'ignore' });
327
367
  try {
328
- cp.execSync('git branch -m main', { cwd: targetDir, stdio: 'ignore' });
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
- cp.execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
375
+ runGit(['add', '-A'], { cwd: targetDir, stdio: 'ignore' });
336
376
  try {
337
- cp.execSync('git commit -q -m "chore: initial scaffold from azcodr template"', {
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
- cp.execSync('git -c user.name="azcodr" -c user.email="azcodr@local" commit -q -m "chore: initial scaffold from azcodr template"', {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azcodr",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Enterprise Architecture & Agentic Engineering Starter Template",
5
5
  "bin": {
6
6
  "azcodr": "bin/azcodr.js"
@@ -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
- return target === 'AGENTS.md' || target === './AGENTS.md' || target === agentsFile;
44
- }
45
-
46
- function checkParity(filePath, label, allowCopyFallback) {
47
- let stat = null;
48
- try { stat = fs.lstatSync(filePath); } catch { fail(`${label} is missing.`); return; }
49
- if (stat.isSymbolicLink()) {
50
- let target = '';
51
- try { target = fs.readlinkSync(filePath); } catch { fail(`${label} readlink failed.`); return; }
52
- const ok = label.endsWith('copilot-instructions.md')
53
- ? (target === '../AGENTS.md' || target === agentsFile || target === 'AGENTS.md')
54
- : isValidAgentsTarget(target);
55
- if (ok) pass(`${label} is a valid symlink to AGENTS.md.`);
56
- else fail(`${label} points to '${target}' instead of 'AGENTS.md'.`);
57
- return;
58
- }
59
- if (stat.isFile()) {
60
- const content = fs.readFileSync(filePath, 'utf-8');
61
- const trimmed = content.trim();
62
- if (trimmed === 'AGENTS.md' || trimmed === './AGENTS.md' || trimmed === agentsFile) {
63
- pass(`${label} is a text pointer to AGENTS.md (symlink fallback).`);
64
- return;
65
- }
66
- if (label.endsWith('copilot-instructions.md') && content.includes('AGENTS.md')) {
67
- pass(`${label} references AGENTS.md (symlink fallback).`);
68
- return;
69
- }
70
- if (allowCopyFallback && agentsContent && content === agentsContent) {
71
- warn(`${label} is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk).`);
72
- return;
73
- }
74
- if (label === 'agents.md') {
75
- try {
76
- const entries = fs.readdirSync(workspaceRoot);
77
- if (!entries.includes('agents.md') && entries.includes('AGENTS.md')) {
78
- pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
79
- return;
80
- }
81
- } catch { /* fall through */ }
82
- }
83
- fail(`${label} is not a symbolic link.`);
84
- return;
85
- }
86
- fail(`${label} is neither a symlink nor a regular file.`);
87
- }
88
-
89
- checkParity(path.join(workspaceRoot, 'CLAUDE.md'), 'CLAUDE.md', true);
90
- const lowerPath = path.join(workspaceRoot, 'agents.md');
91
- try {
92
- const entries = fs.readdirSync(workspaceRoot);
93
- const hasUpper = entries.includes('AGENTS.md');
94
- const hasLowerExact = entries.includes('agents.md');
95
- if (hasUpper && !hasLowerExact) {
96
- let appearsSame = false;
97
- try { appearsSame = fs.existsSync(lowerPath); } catch { appearsSame = false; }
98
- if (appearsSame) {
99
- pass('agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem).');
100
- } else {
101
- checkParity(lowerPath, 'agents.md', true);
102
- }
103
- } else {
104
- checkParity(lowerPath, 'agents.md', true);
105
- }
106
- } catch {
107
- checkParity(lowerPath, 'agents.md', true);
108
- }
109
- checkParity(path.join(workspaceRoot, 'GEMINI.md'), 'GEMINI.md', true);
110
- checkParity(path.join(workspaceRoot, '.cursorrules'), '.cursorrules', true);
111
- checkParity(path.join(workspaceRoot, '.windsurfrules'), '.windsurfrules', true);
112
- const githubDir = path.join(workspaceRoot, '.github');
113
- if (fs.existsSync(githubDir) && fs.statSync(githubDir).isDirectory()) {
114
- const copilot = path.join(githubDir, 'copilot-instructions.md');
115
- if (fs.existsSync(copilot)) {
116
- checkParity(copilot, '.github/copilot-instructions.md', true);
117
- } else {
118
- warn('.github/copilot-instructions.md is missing (run scaffold to restore harness parity).');
119
- }
120
- if (!fs.existsSync(path.join(githubDir, 'workflows'))) {
121
- warn('.github/workflows is missing (CI will not run in scaffolded projects).');
122
- }
123
- }
124
-
125
- if (fs.existsSync(path.join(workspaceRoot, '.gitignore'))) pass('.gitignore exists.');
126
- else fail('Missing .gitignore');
127
-
128
- console.log('');
129
- console.log('2. Checking Progressive Disclosure Rules...');
130
- const rulesDir = path.join(workspaceRoot, 'docs', 'rules');
131
- if (!fs.existsSync(rulesDir)) {
132
- fail(`Missing docs/rules directory at ${rulesDir}`);
133
- } else {
134
- const files = fs.readdirSync(rulesDir).filter((f) => f.endsWith('.md'));
135
- let count = 0;
136
- for (const name of files) {
137
- count += 1;
138
- const fp = path.join(rulesDir, name);
139
- const content = fs.readFileSync(fp, 'utf-8');
140
- const stat = fs.statSync(fp);
141
- if (!/^# /m.test(content)) fail(`Rule ${name} missing H1 header (# Title)`);
142
- if (!/^> \*\*Core Mandate:\*\*/m.test(content)) warn(`Rule ${name} missing standardized '> **Core Mandate:**' summary`);
143
- if (stat.size > 24000) warn(`Rule ${name} exceeds 24KB token-economy cap (${stat.size} bytes)`);
144
- }
145
- pass(`Validated ${count} modular rule files in docs/rules/.`);
146
- }
147
-
148
- console.log('');
149
- console.log('3. Checking Specialized Skills (.agents/skills)...');
150
- const skillsDir = path.join(workspaceRoot, '.agents', 'skills');
151
- if (!fs.existsSync(skillsDir)) {
152
- fail(`Missing .agents/skills directory at ${skillsDir}`);
153
- } else {
154
- const folders = fs.readdirSync(skillsDir).filter((f) => fs.statSync(path.join(skillsDir, f)).isDirectory());
155
- let count = 0;
156
- for (const skill of folders) {
157
- count += 1;
158
- const skillFile = path.join(skillsDir, skill, 'SKILL.md');
159
- if (!fs.existsSync(skillFile)) { fail(`Skill '${skill}' missing SKILL.md`); continue; }
160
- const content = fs.readFileSync(skillFile, 'utf-8');
161
- const lines = content.split('\n');
162
- if (lines[0].trim() !== '---') { fail(`Skill '${skill}' missing opening front matter delimiter (---)`); continue; }
163
- const closing = lines.slice(1).findIndex((l) => l.trim() === '---');
164
- if (closing === -1) fail(`Skill '${skill}' missing closing front matter delimiter (---)`);
165
- const nameMatch = content.match(/^name:\s*(.+)$/m);
166
- if (!nameMatch || nameMatch[1].trim() !== skill) fail(`Skill '${skill}' front matter 'name:' does not match directory name`);
167
- const descMatch = content.match(/^description:\s*(.+)$/m);
168
- const desc = descMatch ? descMatch[1].trim() : '';
169
- if (!desc) {
170
- fail(`Skill '${skill}' missing front matter 'description:'`);
171
- } else {
172
- if (!/^Use when/.test(desc)) warn(`Skill '${skill}' description should start with imperative 'Use when...'`);
173
- if (!/do not use/i.test(desc)) warn(`Skill '${skill}' description should specify negative boundaries ('Do not use for...')`);
174
- if (desc.length > 1024) fail(`Skill '${skill}' description exceeds 1024 chars (${desc.length} chars)`);
175
- }
176
- if (lines.length > 500) warn(`Skill '${skill}' exceeds 500 lines (${lines.length} lines). Offload details to references/.`);
177
- else pass(`Skill '${skill}': ${lines.length} lines, description valid (${desc.length} chars).`);
178
- if (!/What NOT to do/i.test(content) && !/Gotchas/i.test(content)) {
179
- warn(`Skill '${skill}' missing mandatory 'Gotchas & What NOT to Do' section`);
180
- }
181
- }
182
- pass(`Validated ${count} skills in .agents/skills/.`);
183
- }
184
-
185
- console.log('');
186
- console.log('4. Checking Markdown Internal Links & Cross-References...');
187
- const broken = [];
188
- let totalLinks = 0;
189
- function walk(dir) {
190
- let entries = [];
191
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
192
- for (const entry of entries) {
193
- if (entry.name === '.git' || entry.name === 'node_modules') continue;
194
- const full = path.join(dir, entry.name);
195
- if (entry.isDirectory()) walk(full);
196
- else if (entry.isFile() && entry.name.endsWith('.md')) checkFile(full);
197
- }
198
- }
199
- function checkFile(filePath) {
200
- const content = fs.readFileSync(filePath, 'utf-8');
201
- const dir = path.dirname(filePath);
202
- const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
203
- let m;
204
- while ((m = regex.exec(content)) !== null) {
205
- const target = m[2].trim();
206
- if (/^(https?:|mailto:|#|conversation:\/\/|file:\/\/)/.test(target)) continue;
207
- const clean = target.split('#')[0];
208
- if (!clean) continue;
209
- totalLinks += 1;
210
- if (!fs.existsSync(path.normalize(path.join(dir, clean)))) {
211
- broken.push(`${path.relative(workspaceRoot, filePath)} -> ${target}`);
212
- }
213
- }
214
- }
215
- walk(workspaceRoot);
216
- if (broken.length === 0) pass(`Validated ${totalLinks} internal links across workspace (0 broken links).`);
217
- else for (const b of broken) fail(`Broken markdown link: ${b}`);
218
-
219
- console.log('');
220
- console.log('5. Checking Memory & ADR Ledger...');
221
- const memoryFile = path.join(workspaceRoot, 'memory.md');
222
- if (!fs.existsSync(memoryFile)) {
223
- fail('Missing memory.md ADR ledger.');
224
- } else {
225
- const mem = fs.readFileSync(memoryFile, 'utf-8');
226
- const withoutComments = mem.replace(/<!--[\s\S]*?-->/g, '');
227
- if (!/^# /m.test(mem)) fail('memory.md missing H1 header');
228
- else if (/#### ADR-/.test(withoutComments)) pass('memory.md contains ADR entries with valid envelope.');
229
- else pass('memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build).');
230
- if (/ADR-025|ADR-028/.test(withoutComments) && /azcodr/i.test(mem)) {
231
- warn('memory.md may contain template-internal ADRs; fresh projects must start at ADR-001.');
232
- }
233
- }
234
-
235
- console.log('');
236
- console.log('--------------------------------------------------------------');
237
- if (errors === 0) {
238
- console.log(`🎉 SUCCESS: All agentic configurations are valid and healthy! (${warnings} warnings)`);
239
- process.exit(0);
240
- } else {
241
- console.log(`🚨 FAILURE: Found ${errors} error(s) and ${warnings} warning(s) in agentic configurations.`);
242
- process.exit(1);
243
- }
244
- }
245
-
246
- main();
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();