azcodr 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.agents/hooks.json.example +42 -42
  2. package/.agents/scripts/safety_guard.sh +143 -34
  3. package/.agents/scripts/verify_completion.sh +90 -27
  4. package/.agents/skills/agentic-architect/SKILL.md +125 -125
  5. package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
  6. package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
  7. package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
  8. package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
  9. package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -401
  10. package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
  11. package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
  12. package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
  13. package/.agents/skills/compliance-audit/SKILL.md +120 -120
  14. package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
  15. package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
  16. package/.agents/skills/lets-build/SKILL.md +173 -173
  17. package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
  18. package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
  19. package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
  20. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
  21. package/.agents/skills/product-analyst/SKILL.md +154 -154
  22. package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
  23. package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
  24. package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
  25. package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
  26. package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
  27. package/.agents/skills/relentless-questioner/SKILL.md +128 -128
  28. package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
  29. package/.editorconfig +19 -19
  30. package/.github/workflows/ci.yml +167 -56
  31. package/.github/workflows/publish.yml +200 -0
  32. package/.gitignore +40 -25
  33. package/AGENTS.md +103 -102
  34. package/LICENSE +21 -21
  35. package/README.md +168 -154
  36. package/bin/azcodr.js +14 -228
  37. package/docs/knowledge/ubiquitous_language.md +31 -18
  38. package/docs/rules/agentic_configuration.md +259 -259
  39. package/docs/rules/api_architecture.md +179 -179
  40. package/docs/rules/authentication.md +76 -76
  41. package/docs/rules/authorization.md +75 -75
  42. package/docs/rules/caching.md +69 -69
  43. package/docs/rules/clean_code.md +62 -62
  44. package/docs/rules/cloud_native.md +41 -41
  45. package/docs/rules/cqrs.md +203 -203
  46. package/docs/rules/database_design.md +125 -125
  47. package/docs/rules/database_operations.md +69 -69
  48. package/docs/rules/design_patterns.md +98 -98
  49. package/docs/rules/devops_ci_cd.md +76 -76
  50. package/docs/rules/domain_driven_design.md +122 -122
  51. package/docs/rules/error_handling.md +54 -52
  52. package/docs/rules/feature_flags.md +59 -59
  53. package/docs/rules/frontend_architecture.md +157 -157
  54. package/docs/rules/multitenancy_architecture.md +98 -98
  55. package/docs/rules/product_ownership.md +127 -127
  56. package/docs/rules/project_management.md +49 -49
  57. package/docs/rules/relentless_questioning.md +52 -52
  58. package/docs/rules/requirements_engineering.md +98 -98
  59. package/docs/rules/security_compliance.md +53 -53
  60. package/docs/rules/server_driven_ui.md +88 -88
  61. package/docs/rules/test_driven_development.md +185 -185
  62. package/docs/rules/transactional_email.md +27 -27
  63. package/docs/rules/type_safety.md +65 -65
  64. package/docs/rules/ui_ux_architecture.md +150 -150
  65. package/docs/rules/workflow_state_machines.md +117 -117
  66. package/lib/cli-parse.js +51 -0
  67. package/lib/cli-target.js +109 -0
  68. package/lib/cli.js +180 -0
  69. package/lib/errors.js +28 -0
  70. package/lib/git.js +29 -0
  71. package/lib/guards.js +96 -0
  72. package/lib/index.d.ts +199 -134
  73. package/lib/index.js +5 -5
  74. package/lib/links.js +123 -0
  75. package/lib/permissions.js +44 -0
  76. package/lib/repo.js +90 -0
  77. package/lib/scaffold.js +238 -399
  78. package/memory.md +119 -36
  79. package/package.json +65 -62
  80. package/scripts/test_coverage.js +66 -38
  81. package/scripts/validate/adr.js +151 -0
  82. package/scripts/validate/io.js +84 -0
  83. package/scripts/validate/links.js +167 -0
  84. package/scripts/validate/parity.js +124 -0
  85. package/scripts/validate/root.js +184 -0
  86. package/scripts/validate/rules.js +44 -0
  87. package/scripts/validate/skills.js +96 -0
  88. package/scripts/validate/text.js +29 -0
  89. package/scripts/validate-cli.js +13 -0
  90. package/scripts/validate.js +112 -218
  91. package/.github/copilot-instructions.md +0 -1
@@ -1,401 +1,402 @@
1
- #!/usr/bin/env bash
2
- # ==============================================================================
3
- # validate_agentic_configs.sh
4
- # Deterministic Validator for Agentic Files, Rules, and Skills
5
- # ==============================================================================
6
-
7
- set -euo pipefail
8
-
9
- WORKSPACE_ROOT=""
10
- FIX_MODE=false
11
-
12
- for arg in "$@"; do
13
- if [[ "${arg}" == "--fix" ]]; then
14
- FIX_MODE=true
15
- elif [[ -z "${WORKSPACE_ROOT}" ]]; then
16
- WORKSPACE_ROOT="${arg}"
17
- fi
18
- done
19
-
20
- if [[ -z "${WORKSPACE_ROOT}" ]]; then
21
- WORKSPACE_ROOT="$(pwd)"
22
- fi
23
-
24
- ERRORS=0
25
- WARNINGS=0
26
-
27
- echo "🔍 Validating Agentic Architecture in: ${WORKSPACE_ROOT}"
28
- echo "--------------------------------------------------------------"
29
-
30
- # Helper print functions
31
- log_pass() { echo " ✅ $1"; }
32
- log_warn() { echo " ⚠️ $1"; WARNINGS=$((WARNINGS + 1)); }
33
- log_fail() { echo " ❌ $1"; ERRORS=$((ERRORS + 1)); }
34
-
35
- # 1. Root AGENTS.md & Symlinks Verification
36
- echo "1. Checking Root Configuration & Symlinks..."
37
- AGENTS_FILE="${WORKSPACE_ROOT}/AGENTS.md"
38
- if [[ ! -f "${AGENTS_FILE}" ]]; then
39
- log_fail "Missing root AGENTS.md at ${AGENTS_FILE}"
40
- else
41
- log_pass "AGENTS.md exists."
42
-
43
- LINE_COUNT=$(wc -l < "${AGENTS_FILE}")
44
- if [[ ${LINE_COUNT} -le 120 ]]; then
45
- log_pass "AGENTS.md line count is lean: ${LINE_COUNT} lines (<= 120)."
46
- elif [[ ${LINE_COUNT} -le 150 ]]; then
47
- log_warn "AGENTS.md line count is getting large: ${LINE_COUNT} lines (warn > 120)."
48
- else
49
- log_fail "AGENTS.md exceeds maximum line limit: ${LINE_COUNT} lines (max 150)."
50
- fi
51
- fi
52
-
53
- # Helper to check if symlink target resolves to AGENTS.md
54
- is_valid_agents_target() {
55
- local target="$1"
56
- [[ "${target}" == "AGENTS.md" || "${target}" == "./AGENTS.md" || "${target}" == "${WORKSPACE_ROOT}/AGENTS.md" ]]
57
- }
58
-
59
- is_valid_text_pointer() {
60
- local file="$1"
61
- local content
62
- content=$(< "${file}")
63
- [[ "${content}" == "AGENTS.md" || "${content}" == "./AGENTS.md" || "${content}" == "${WORKSPACE_ROOT}/AGENTS.md" ]]
64
- }
65
-
66
- is_identical_copy() {
67
- local file="$1"
68
- [[ -f "${file}" ]] && [[ -f "${AGENTS_FILE}" ]] && cmp -s "${file}" "${AGENTS_FILE}"
69
- }
70
-
71
- # Check CLAUDE.md symlink
72
- CLAUDE_FILE="${WORKSPACE_ROOT}/CLAUDE.md"
73
- if [[ -L "${CLAUDE_FILE}" ]]; then
74
- TARGET=$(readlink "${CLAUDE_FILE}")
75
- if is_valid_agents_target "${TARGET}"; then
76
- log_pass "CLAUDE.md is a valid symlink to AGENTS.md."
77
- else
78
- log_fail "CLAUDE.md points to '${TARGET}' instead of 'AGENTS.md'."
79
- fi
80
- elif [[ -f "${CLAUDE_FILE}" ]] && is_valid_text_pointer "${CLAUDE_FILE}"; then
81
- log_pass "CLAUDE.md is a text pointer to AGENTS.md (symlink fallback)."
82
- elif is_identical_copy "${CLAUDE_FILE}"; then
83
- log_warn "CLAUDE.md is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
84
- else
85
- log_fail "CLAUDE.md is not a symbolic link."
86
- fi
87
-
88
- # Check agents.md symlink (case-insensitive filesystem aware)
89
- AGENTS_LOWER="${WORKSPACE_ROOT}/agents.md"
90
- IS_CASE_INSENSITIVE=false
91
- if [[ "$(uname -s)" == "Darwin" ]] || [[ "$(uname -s)" =~ (MINGW|MSYS|CYGWIN) ]] || [[ "${OS:-}" == "Windows_NT" ]]; then
92
- IS_CASE_INSENSITIVE=true
93
- elif [[ -f "${AGENTS_FILE}" ]] && [[ -f "${AGENTS_LOWER}" ]] && [[ ! -L "${AGENTS_LOWER}" ]]; then
94
- IS_CASE_INSENSITIVE=true
95
- fi
96
-
97
- if [[ "${IS_CASE_INSENSITIVE}" == "true" ]]; then
98
- log_pass "agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem)."
99
- else
100
- if [[ ! -L "${AGENTS_LOWER}" ]] && [[ ! -e "${AGENTS_LOWER}" ]] && [[ -f "${AGENTS_FILE}" ]]; then
101
- ln -sf "AGENTS.md" "${AGENTS_LOWER}"
102
- log_pass "Created agents.md symlink to AGENTS.md (case-sensitive filesystem parity)."
103
- fi
104
- if [[ -L "${AGENTS_LOWER}" ]]; then
105
- TARGET=$(readlink "${AGENTS_LOWER}")
106
- if is_valid_agents_target "${TARGET}"; then
107
- log_pass "agents.md is a valid symlink to AGENTS.md."
108
- else
109
- log_fail "agents.md points to '${TARGET}' instead of 'AGENTS.md'."
110
- fi
111
- elif [[ -f "${AGENTS_LOWER}" ]] && is_valid_text_pointer "${AGENTS_LOWER}"; then
112
- log_pass "agents.md is a text pointer to AGENTS.md (symlink fallback)."
113
- elif is_identical_copy "${AGENTS_LOWER}"; then
114
- log_warn "agents.md is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
115
- else
116
- log_fail "agents.md is not a symbolic link."
117
- fi
118
- fi
119
-
120
- # Check GEMINI.md symlink
121
- GEMINI_FILE="${WORKSPACE_ROOT}/GEMINI.md"
122
- if [[ -L "${GEMINI_FILE}" ]]; then
123
- TARGET=$(readlink "${GEMINI_FILE}")
124
- if is_valid_agents_target "${TARGET}"; then
125
- log_pass "GEMINI.md is a valid symlink to AGENTS.md."
126
- else
127
- log_fail "GEMINI.md points to '${TARGET}' instead of 'AGENTS.md'."
128
- fi
129
- elif [[ -f "${GEMINI_FILE}" ]] && is_valid_text_pointer "${GEMINI_FILE}"; then
130
- log_pass "GEMINI.md is a text pointer to AGENTS.md (symlink fallback)."
131
- elif is_identical_copy "${GEMINI_FILE}"; then
132
- log_warn "GEMINI.md is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
133
- else
134
- log_fail "GEMINI.md is not a symbolic link."
135
- fi
136
-
137
- # Check .cursorrules symlink
138
- CURSOR_FILE="${WORKSPACE_ROOT}/.cursorrules"
139
- if [[ -L "${CURSOR_FILE}" ]]; then
140
- TARGET=$(readlink "${CURSOR_FILE}")
141
- if is_valid_agents_target "${TARGET}"; then
142
- log_pass ".cursorrules is a valid symlink to AGENTS.md."
143
- else
144
- log_fail ".cursorrules points to '${TARGET}' instead of 'AGENTS.md'."
145
- fi
146
- elif [[ -f "${CURSOR_FILE}" ]] && is_valid_text_pointer "${CURSOR_FILE}"; then
147
- log_pass ".cursorrules is a text pointer to AGENTS.md (symlink fallback)."
148
- elif is_identical_copy "${CURSOR_FILE}"; then
149
- log_warn ".cursorrules is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
150
- else
151
- log_fail ".cursorrules is not a symbolic link."
152
- fi
153
-
154
- # Check .windsurfrules symlink
155
- WINDSURF_FILE="${WORKSPACE_ROOT}/.windsurfrules"
156
- if [[ -L "${WINDSURF_FILE}" ]]; then
157
- TARGET=$(readlink "${WINDSURF_FILE}")
158
- if is_valid_agents_target "${TARGET}"; then
159
- log_pass ".windsurfrules is a valid symlink to AGENTS.md."
160
- else
161
- log_fail ".windsurfrules points to '${TARGET}' instead of 'AGENTS.md'."
162
- fi
163
- elif [[ -f "${WINDSURF_FILE}" ]] && is_valid_text_pointer "${WINDSURF_FILE}"; then
164
- log_pass ".windsurfrules is a text pointer to AGENTS.md (symlink fallback)."
165
- elif is_identical_copy "${WINDSURF_FILE}"; then
166
- log_warn ".windsurfrules is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
167
- else
168
- log_fail ".windsurfrules is not a symbolic link."
169
- fi
170
-
171
- # Check .github/copilot-instructions.md symlink (if .github directory exists)
172
- COPILOT_FILE="${WORKSPACE_ROOT}/.github/copilot-instructions.md"
173
- if [[ -d "${WORKSPACE_ROOT}/.github" ]]; then
174
- if [[ -L "${COPILOT_FILE}" ]]; then
175
- TARGET=$(readlink "${COPILOT_FILE}")
176
- if [[ "${TARGET}" == "../AGENTS.md" || "${TARGET}" == "${WORKSPACE_ROOT}/AGENTS.md" || "${TARGET}" == "AGENTS.md" ]]; then
177
- log_pass ".github/copilot-instructions.md is a valid symlink to AGENTS.md."
178
- else
179
- log_fail ".github/copilot-instructions.md points to '${TARGET}' instead of '../AGENTS.md'."
180
- fi
181
- elif [[ -f "${COPILOT_FILE}" ]] && [[ "$(< "${COPILOT_FILE}")" == *"AGENTS.md"* ]]; then
182
- log_pass ".github/copilot-instructions.md references AGENTS.md (symlink fallback)."
183
- elif [[ -f "${COPILOT_FILE}" ]]; then
184
- log_warn ".github/copilot-instructions.md exists but is neither a symlink to ../AGENTS.md nor references AGENTS.md."
185
- fi
186
- fi
187
-
188
- # Check .gitignore exists
189
- GITIGNORE_FILE="${WORKSPACE_ROOT}/.gitignore"
190
- if [[ -f "${GITIGNORE_FILE}" ]]; then
191
- log_pass ".gitignore exists."
192
- else
193
- log_fail "Missing .gitignore at ${GITIGNORE_FILE}"
194
- fi
195
-
196
- # Check .github/workflows exists (CI must propagate to scaffolded projects)
197
- if [[ -d "${WORKSPACE_ROOT}/.github/workflows" ]]; then
198
- log_pass ".github/workflows exists."
199
- else
200
- log_warn ".github/workflows is missing (CI will not run in scaffolded projects)."
201
- fi
202
-
203
- # 2. Checking Progressive Disclosure Rules (docs/rules)
204
- echo ""
205
- echo "2. Checking Progressive Disclosure Rules..."
206
- RULES_DIR="${WORKSPACE_ROOT}/docs/rules"
207
- if [[ ! -d "${RULES_DIR}" ]]; then
208
- log_fail "Missing docs/rules directory at ${RULES_DIR}"
209
- else
210
- RULE_COUNT=0
211
- for rule_file in "${RULES_DIR}"/*.md; do
212
- [[ -e "${rule_file}" ]] || continue
213
- RULE_COUNT=$((RULE_COUNT + 1))
214
- RULE_NAME=$(basename "${rule_file}")
215
-
216
- # Check for title
217
- if ! grep -q "^# " "${rule_file}"; then
218
- log_fail "Rule ${RULE_NAME} missing H1 header (# Title)"
219
- fi
220
-
221
- # Check for Core Mandate blockquote
222
- if ! grep -q "^> \*\*Core Mandate:\*\*" "${rule_file}"; then
223
- log_warn "Rule ${RULE_NAME} missing standardized '> **Core Mandate:**' summary"
224
- fi
225
-
226
- # Token-economy size cap (24KB)
227
- RULE_SIZE=$(wc -c < "${rule_file}")
228
- if [[ ${RULE_SIZE} -gt 24000 ]]; then
229
- log_warn "Rule ${RULE_NAME} exceeds 24KB token-economy cap (${RULE_SIZE} bytes)"
230
- fi
231
- done
232
- log_pass "Validated ${RULE_COUNT} modular rule files in docs/rules/."
233
- fi
234
-
235
- # 3. Checking Skills Architecture (.agents/skills)
236
- echo ""
237
- echo "3. Checking Specialized Skills (.agents/skills)..."
238
- SKILLS_DIR="${WORKSPACE_ROOT}/.agents/skills"
239
- if [[ ! -d "${SKILLS_DIR}" ]]; then
240
- log_fail "Missing .agents/skills directory at ${SKILLS_DIR}"
241
- else
242
- SKILL_COUNT=0
243
- for skill_folder in "${SKILLS_DIR}"/*; do
244
- [[ -d "${skill_folder}" ]] || continue
245
- SKILL_NAME=$(basename "${skill_folder}")
246
- SKILL_FILE="${skill_folder}/SKILL.md"
247
- SKILL_COUNT=$((SKILL_COUNT + 1))
248
-
249
- if [[ ! -f "${SKILL_FILE}" ]]; then
250
- log_fail "Skill '${SKILL_NAME}' missing SKILL.md"
251
- continue
252
- fi
253
-
254
- # Check front matter existence
255
- if ! head -n 1 "${SKILL_FILE}" | grep -q "^---"; then
256
- log_fail "Skill '${SKILL_NAME}' missing opening front matter delimiter (---)"
257
- continue
258
- fi
259
-
260
- # Check closing front matter delimiter
261
- if ! awk 'NR > 1 && /^---[[:space:]]*$/ { found=1; exit } END { exit !found }' "${SKILL_FILE}"; then
262
- log_fail "Skill '${SKILL_NAME}' missing closing front matter delimiter (---)"
263
- fi
264
-
265
- # Check name field in front matter
266
- if ! grep -E "^name:[[:space:]]*${SKILL_NAME}" "${SKILL_FILE}" > /dev/null; then
267
- log_fail "Skill '${SKILL_NAME}' front matter 'name:' does not match directory name"
268
- fi
269
-
270
- # Check description
271
- DESC=$(grep -E "^description:" "${SKILL_FILE}" | sed -E 's/^description:[[:space:]]*//' || true)
272
- if [[ -z "${DESC}" ]]; then
273
- log_fail "Skill '${SKILL_NAME}' missing front matter 'description:'"
274
- else
275
- # Check imperative phrasing
276
- if [[ ! "${DESC}" =~ ^Use[[:space:]]when ]]; then
277
- log_warn "Skill '${SKILL_NAME}' description should start with imperative 'Use when...'"
278
- fi
279
-
280
- # Check negative boundary phrasing (Do not use / Do NOT use)
281
- if ! echo "${DESC}" | grep -qiE "(do not use|do NOT use)"; then
282
- log_warn "Skill '${SKILL_NAME}' description should specify negative boundaries ('Do not use for...')"
283
- fi
284
-
285
- # Check character length (< 1024)
286
- CHAR_LEN=${#DESC}
287
- if [[ ${CHAR_LEN} -gt 1024 ]]; then
288
- log_fail "Skill '${SKILL_NAME}' description exceeds 1024 chars (${CHAR_LEN} chars)"
289
- fi
290
- fi
291
-
292
- # Check body length (< 500 lines)
293
- SKILL_LINES=$(wc -l < "${SKILL_FILE}")
294
- if [[ ${SKILL_LINES} -gt 500 ]]; then
295
- log_warn "Skill '${SKILL_NAME}' exceeds 500 lines (${SKILL_LINES} lines). Offload details to references/."
296
- else
297
- log_pass "Skill '${SKILL_NAME}': ${SKILL_LINES} lines, description valid (${#DESC} chars)."
298
- fi
299
-
300
- # Check for Gotchas / What NOT to do section
301
- if ! grep -qi "What NOT to do" "${SKILL_FILE}" && ! grep -qi "Gotchas" "${SKILL_FILE}"; then
302
- log_warn "Skill '${SKILL_NAME}' missing mandatory 'Gotchas & What NOT to Do' section"
303
- fi
304
- done
305
- log_pass "Validated ${SKILL_COUNT} skills in .agents/skills/."
306
- fi
307
-
308
- # 4. Checking Markdown Internal Links & Cross-References
309
- echo ""
310
- echo "4. Checking Markdown Internal Links & Cross-References..."
311
- LINK_CHECK_RAW=$(node -e '
312
- const fs = require("fs");
313
- const path = require("path");
314
-
315
- const root = process.argv[1];
316
- const broken = [];
317
- let totalLinks = 0;
318
-
319
- function walk(dir) {
320
- const entries = fs.readdirSync(dir, { withFileTypes: true });
321
- for (const entry of entries) {
322
- if (entry.name === ".git" || entry.name === "node_modules") continue;
323
- const full = path.join(dir, entry.name);
324
- if (entry.isDirectory()) {
325
- walk(full);
326
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
327
- checkFile(full);
328
- }
329
- }
330
- }
331
-
332
- function checkFile(filePath) {
333
- const content = fs.readFileSync(filePath, "utf8");
334
- const dir = path.dirname(filePath);
335
- const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
336
- let match;
337
- while ((match = regex.exec(content)) !== null) {
338
- const target = match[2].trim();
339
- if (target.startsWith("http://") || target.startsWith("https://") || target.startsWith("mailto:") || target.startsWith("#") || target.startsWith("conversation://") || target.startsWith("file://")) {
340
- continue;
341
- }
342
- const cleanTarget = target.split("#")[0];
343
- if (!cleanTarget) continue;
344
- totalLinks++;
345
- const resolved = path.normalize(path.join(dir, cleanTarget));
346
- if (!fs.existsSync(resolved)) {
347
- broken.push(`${path.relative(root, filePath)} -> ${target}`);
348
- }
349
- }
350
- }
351
-
352
- walk(root);
353
- if (broken.length > 0) {
354
- console.log("BROKEN:" + broken.join("|"));
355
- process.exit(1);
356
- } else {
357
- console.log("OK:" + totalLinks);
358
- process.exit(0);
359
- }
360
- ' "${WORKSPACE_ROOT}" 2>&1) || true
361
-
362
- if [[ "${LINK_CHECK_RAW}" =~ ^OK:([0-9]+) ]]; then
363
- TOTAL_LINKS="${BASH_REMATCH[1]}"
364
- log_pass "Validated ${TOTAL_LINKS} internal links across workspace (0 broken links)."
365
- elif [[ "${LINK_CHECK_RAW}" =~ ^BROKEN:(.*) ]]; then
366
- BROKEN_LIST="${BASH_REMATCH[1]}"
367
- IFS='|' read -ra BROKEN_ITEMS <<< "${BROKEN_LIST}"
368
- for item in "${BROKEN_ITEMS[@]}"; do
369
- log_fail "Broken markdown link: ${item}"
370
- done
371
- else
372
- log_fail "Markdown link validation failed unexpectedly: ${LINK_CHECK_RAW}"
373
- fi
374
-
375
- # 5. Checking Memory & ADR Ledger
376
- echo ""
377
- echo "5. Checking Memory & ADR Ledger..."
378
- MEMORY_FILE="${WORKSPACE_ROOT}/memory.md"
379
- if [[ ! -f "${MEMORY_FILE}" ]]; then
380
- log_fail "Missing memory.md ADR ledger."
381
- else
382
- MEMORY_BODY=$(sed '/<!--/,/-->/d' "${MEMORY_FILE}")
383
- if ! grep -q "^# " "${MEMORY_FILE}"; then
384
- log_fail "memory.md missing H1 header"
385
- elif echo "${MEMORY_BODY}" | grep -q "#### ADR-"; then
386
- log_pass "memory.md contains ADR entries with valid envelope."
387
- else
388
- log_pass "memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build)."
389
- fi
390
- fi
391
-
392
- # 6. Summary Output
393
- echo ""
394
- echo "--------------------------------------------------------------"
395
- if [[ ${ERRORS} -eq 0 ]]; then
396
- echo "🎉 SUCCESS: All agentic configurations are valid and healthy! (${WARNINGS} warnings)"
397
- exit 0
398
- else
399
- echo "🚨 FAILURE: Found ${ERRORS} error(s) and ${WARNINGS} warning(s) in agentic configurations."
400
- exit 1
401
- fi
1
+ #!/usr/bin/env bash
2
+ # ==============================================================================
3
+ # validate_agentic_configs.sh
4
+ # Deterministic Validator for Agentic Files, Rules, and Skills
5
+ # ==============================================================================
6
+
7
+ set -euo pipefail
8
+
9
+ WORKSPACE_ROOT=""
10
+ FIX_MODE=false
11
+
12
+ for arg in "$@"; do
13
+ if [[ "${arg}" == "--fix" ]]; then
14
+ FIX_MODE=true
15
+ elif [[ -z "${WORKSPACE_ROOT}" ]]; then
16
+ WORKSPACE_ROOT="${arg}"
17
+ fi
18
+ done
19
+
20
+ if [[ -z "${WORKSPACE_ROOT}" ]]; then
21
+ WORKSPACE_ROOT="$(pwd)"
22
+ fi
23
+
24
+ ERRORS=0
25
+ WARNINGS=0
26
+
27
+ echo "🔍 Validating Agentic Architecture in: ${WORKSPACE_ROOT}"
28
+ echo "--------------------------------------------------------------"
29
+
30
+ # Helper print functions
31
+ log_pass() { echo " ✅ $1"; }
32
+ log_warn() { echo " ⚠️ $1"; WARNINGS=$((WARNINGS + 1)); }
33
+ log_fail() { echo " ❌ $1"; ERRORS=$((ERRORS + 1)); }
34
+
35
+ # 1. Root AGENTS.md & Symlinks Verification
36
+ echo "1. Checking Root Configuration & Symlinks..."
37
+ AGENTS_FILE="${WORKSPACE_ROOT}/AGENTS.md"
38
+ if [[ ! -f "${AGENTS_FILE}" ]]; then
39
+ log_fail "Missing root AGENTS.md at ${AGENTS_FILE}"
40
+ else
41
+ log_pass "AGENTS.md exists."
42
+
43
+ LINE_COUNT=$(wc -l < "${AGENTS_FILE}")
44
+ if [[ ${LINE_COUNT} -le 120 ]]; then
45
+ log_pass "AGENTS.md line count is lean: ${LINE_COUNT} lines (<= 120)."
46
+ elif [[ ${LINE_COUNT} -le 150 ]]; then
47
+ log_warn "AGENTS.md line count is getting large: ${LINE_COUNT} lines (warn > 120)."
48
+ else
49
+ log_fail "AGENTS.md exceeds maximum line limit: ${LINE_COUNT} lines (max 150)."
50
+ fi
51
+ fi
52
+
53
+ # Helper to check if symlink target resolves to AGENTS.md
54
+ is_valid_agents_target() {
55
+ local target="$1"
56
+ [[ "${target}" == "AGENTS.md" || "${target}" == "./AGENTS.md" || "${target}" == "${WORKSPACE_ROOT}/AGENTS.md" ]]
57
+ }
58
+
59
+ is_valid_text_pointer() {
60
+ local file="$1"
61
+ local content
62
+ content=$(< "${file}")
63
+ [[ "${content}" == "AGENTS.md" || "${content}" == "./AGENTS.md" || "${content}" == "${WORKSPACE_ROOT}/AGENTS.md" ]]
64
+ }
65
+
66
+ is_identical_copy() {
67
+ local file="$1"
68
+ [[ -f "${file}" ]] && [[ -f "${AGENTS_FILE}" ]] && cmp -s "${file}" "${AGENTS_FILE}"
69
+ }
70
+
71
+ # Check CLAUDE.md symlink
72
+ CLAUDE_FILE="${WORKSPACE_ROOT}/CLAUDE.md"
73
+ if [[ -L "${CLAUDE_FILE}" ]]; then
74
+ TARGET=$(readlink "${CLAUDE_FILE}")
75
+ if is_valid_agents_target "${TARGET}"; then
76
+ log_pass "CLAUDE.md is a valid symlink to AGENTS.md."
77
+ else
78
+ log_fail "CLAUDE.md points to '${TARGET}' instead of 'AGENTS.md'."
79
+ fi
80
+ elif [[ -f "${CLAUDE_FILE}" ]] && is_valid_text_pointer "${CLAUDE_FILE}"; then
81
+ log_pass "CLAUDE.md is a text pointer to AGENTS.md (symlink fallback)."
82
+ elif is_identical_copy "${CLAUDE_FILE}"; then
83
+ log_warn "CLAUDE.md is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
84
+ else
85
+ log_fail "CLAUDE.md is not a symbolic link."
86
+ fi
87
+
88
+ # Check agents.md symlink (case-insensitive filesystem aware)
89
+ AGENTS_LOWER="${WORKSPACE_ROOT}/agents.md"
90
+ IS_CASE_INSENSITIVE=false
91
+ if [[ "$(uname -s)" == "Darwin" ]] || [[ "$(uname -s)" =~ (MINGW|MSYS|CYGWIN) ]] || [[ "${OS:-}" == "Windows_NT" ]]; then
92
+ IS_CASE_INSENSITIVE=true
93
+ elif [[ -f "${AGENTS_FILE}" ]] && [[ -f "${AGENTS_LOWER}" ]] && [[ ! -L "${AGENTS_LOWER}" ]]; then
94
+ IS_CASE_INSENSITIVE=true
95
+ fi
96
+
97
+ if [[ "${IS_CASE_INSENSITIVE}" == "true" ]]; then
98
+ log_pass "agents.md is satisfied natively by AGENTS.md (case-insensitive filesystem)."
99
+ else
100
+ if [[ ! -L "${AGENTS_LOWER}" ]] && [[ ! -e "${AGENTS_LOWER}" ]] && [[ -f "${AGENTS_FILE}" ]]; then
101
+ ln -sf "AGENTS.md" "${AGENTS_LOWER}"
102
+ log_pass "Created agents.md symlink to AGENTS.md (case-sensitive filesystem parity)."
103
+ fi
104
+ if [[ -L "${AGENTS_LOWER}" ]]; then
105
+ TARGET=$(readlink "${AGENTS_LOWER}")
106
+ if is_valid_agents_target "${TARGET}"; then
107
+ log_pass "agents.md is a valid symlink to AGENTS.md."
108
+ else
109
+ log_fail "agents.md points to '${TARGET}' instead of 'AGENTS.md'."
110
+ fi
111
+ elif [[ -f "${AGENTS_LOWER}" ]] && is_valid_text_pointer "${AGENTS_LOWER}"; then
112
+ log_pass "agents.md is a text pointer to AGENTS.md (symlink fallback)."
113
+ elif is_identical_copy "${AGENTS_LOWER}"; then
114
+ log_warn "agents.md is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
115
+ else
116
+ log_fail "agents.md is not a symbolic link."
117
+ fi
118
+ fi
119
+
120
+ # Check GEMINI.md symlink
121
+ GEMINI_FILE="${WORKSPACE_ROOT}/GEMINI.md"
122
+ if [[ -L "${GEMINI_FILE}" ]]; then
123
+ TARGET=$(readlink "${GEMINI_FILE}")
124
+ if is_valid_agents_target "${TARGET}"; then
125
+ log_pass "GEMINI.md is a valid symlink to AGENTS.md."
126
+ else
127
+ log_fail "GEMINI.md points to '${TARGET}' instead of 'AGENTS.md'."
128
+ fi
129
+ elif [[ -f "${GEMINI_FILE}" ]] && is_valid_text_pointer "${GEMINI_FILE}"; then
130
+ log_pass "GEMINI.md is a text pointer to AGENTS.md (symlink fallback)."
131
+ elif is_identical_copy "${GEMINI_FILE}"; then
132
+ log_warn "GEMINI.md is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
133
+ else
134
+ log_fail "GEMINI.md is not a symbolic link."
135
+ fi
136
+
137
+ # Check .cursorrules symlink
138
+ CURSOR_FILE="${WORKSPACE_ROOT}/.cursorrules"
139
+ if [[ -L "${CURSOR_FILE}" ]]; then
140
+ TARGET=$(readlink "${CURSOR_FILE}")
141
+ if is_valid_agents_target "${TARGET}"; then
142
+ log_pass ".cursorrules is a valid symlink to AGENTS.md."
143
+ else
144
+ log_fail ".cursorrules points to '${TARGET}' instead of 'AGENTS.md'."
145
+ fi
146
+ elif [[ -f "${CURSOR_FILE}" ]] && is_valid_text_pointer "${CURSOR_FILE}"; then
147
+ log_pass ".cursorrules is a text pointer to AGENTS.md (symlink fallback)."
148
+ elif is_identical_copy "${CURSOR_FILE}"; then
149
+ log_warn ".cursorrules is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
150
+ else
151
+ log_fail ".cursorrules is not a symbolic link."
152
+ fi
153
+
154
+ # Check .windsurfrules symlink
155
+ WINDSURF_FILE="${WORKSPACE_ROOT}/.windsurfrules"
156
+ if [[ -L "${WINDSURF_FILE}" ]]; then
157
+ TARGET=$(readlink "${WINDSURF_FILE}")
158
+ if is_valid_agents_target "${TARGET}"; then
159
+ log_pass ".windsurfrules is a valid symlink to AGENTS.md."
160
+ else
161
+ log_fail ".windsurfrules points to '${TARGET}' instead of 'AGENTS.md'."
162
+ fi
163
+ elif [[ -f "${WINDSURF_FILE}" ]] && is_valid_text_pointer "${WINDSURF_FILE}"; then
164
+ log_pass ".windsurfrules is a text pointer to AGENTS.md (symlink fallback)."
165
+ elif is_identical_copy "${WINDSURF_FILE}"; then
166
+ log_warn ".windsurfrules is a byte-identical copy of AGENTS.md (Windows symlink fallback; drift risk)."
167
+ else
168
+ log_fail ".windsurfrules is not a symbolic link."
169
+ fi
170
+
171
+ # Check .github/copilot-instructions.md symlink (if .github directory exists)
172
+ COPILOT_FILE="${WORKSPACE_ROOT}/.github/copilot-instructions.md"
173
+ if [[ -d "${WORKSPACE_ROOT}/.github" ]]; then
174
+ if [[ -L "${COPILOT_FILE}" ]]; then
175
+ TARGET=$(readlink "${COPILOT_FILE}")
176
+ TARGET="${TARGET//\\//}"
177
+ if [[ "${TARGET}" == "../AGENTS.md" || "${TARGET}" == "${WORKSPACE_ROOT}/AGENTS.md" || "${TARGET}" == "AGENTS.md" ]]; then
178
+ log_pass ".github/copilot-instructions.md is a valid symlink to AGENTS.md."
179
+ else
180
+ log_fail ".github/copilot-instructions.md points to '${TARGET}' instead of '../AGENTS.md'."
181
+ fi
182
+ elif [[ -f "${COPILOT_FILE}" ]] && [[ "$(< "${COPILOT_FILE}")" == *"AGENTS.md"* ]]; then
183
+ log_pass ".github/copilot-instructions.md references AGENTS.md (symlink fallback)."
184
+ elif [[ -f "${COPILOT_FILE}" ]]; then
185
+ log_warn ".github/copilot-instructions.md exists but is neither a symlink to ../AGENTS.md nor references AGENTS.md."
186
+ fi
187
+ fi
188
+
189
+ # Check .gitignore exists
190
+ GITIGNORE_FILE="${WORKSPACE_ROOT}/.gitignore"
191
+ if [[ -f "${GITIGNORE_FILE}" ]]; then
192
+ log_pass ".gitignore exists."
193
+ else
194
+ log_fail "Missing .gitignore at ${GITIGNORE_FILE}"
195
+ fi
196
+
197
+ # Check .github/workflows exists (CI must propagate to scaffolded projects)
198
+ if [[ -d "${WORKSPACE_ROOT}/.github/workflows" ]]; then
199
+ log_pass ".github/workflows exists."
200
+ else
201
+ log_warn ".github/workflows is missing (CI will not run in scaffolded projects)."
202
+ fi
203
+
204
+ # 2. Checking Progressive Disclosure Rules (docs/rules)
205
+ echo ""
206
+ echo "2. Checking Progressive Disclosure Rules..."
207
+ RULES_DIR="${WORKSPACE_ROOT}/docs/rules"
208
+ if [[ ! -d "${RULES_DIR}" ]]; then
209
+ log_fail "Missing docs/rules directory at ${RULES_DIR}"
210
+ else
211
+ RULE_COUNT=0
212
+ for rule_file in "${RULES_DIR}"/*.md; do
213
+ [[ -e "${rule_file}" ]] || continue
214
+ RULE_COUNT=$((RULE_COUNT + 1))
215
+ RULE_NAME=$(basename "${rule_file}")
216
+
217
+ # Check for title
218
+ if ! grep -q "^# " "${rule_file}"; then
219
+ log_fail "Rule ${RULE_NAME} missing H1 header (# Title)"
220
+ fi
221
+
222
+ # Check for Core Mandate blockquote
223
+ if ! grep -q "^> \*\*Core Mandate:\*\*" "${rule_file}"; then
224
+ log_warn "Rule ${RULE_NAME} missing standardized '> **Core Mandate:**' summary"
225
+ fi
226
+
227
+ # Token-economy size cap (24KB)
228
+ RULE_SIZE=$(wc -c < "${rule_file}")
229
+ if [[ ${RULE_SIZE} -gt 24000 ]]; then
230
+ log_warn "Rule ${RULE_NAME} exceeds 24KB token-economy cap (${RULE_SIZE} bytes)"
231
+ fi
232
+ done
233
+ log_pass "Validated ${RULE_COUNT} modular rule files in docs/rules/."
234
+ fi
235
+
236
+ # 3. Checking Skills Architecture (.agents/skills)
237
+ echo ""
238
+ echo "3. Checking Specialized Skills (.agents/skills)..."
239
+ SKILLS_DIR="${WORKSPACE_ROOT}/.agents/skills"
240
+ if [[ ! -d "${SKILLS_DIR}" ]]; then
241
+ log_fail "Missing .agents/skills directory at ${SKILLS_DIR}"
242
+ else
243
+ SKILL_COUNT=0
244
+ for skill_folder in "${SKILLS_DIR}"/*; do
245
+ [[ -d "${skill_folder}" ]] || continue
246
+ SKILL_NAME=$(basename "${skill_folder}")
247
+ SKILL_FILE="${skill_folder}/SKILL.md"
248
+ SKILL_COUNT=$((SKILL_COUNT + 1))
249
+
250
+ if [[ ! -f "${SKILL_FILE}" ]]; then
251
+ log_fail "Skill '${SKILL_NAME}' missing SKILL.md"
252
+ continue
253
+ fi
254
+
255
+ # Check front matter existence
256
+ if ! head -n 1 "${SKILL_FILE}" | grep -q "^---"; then
257
+ log_fail "Skill '${SKILL_NAME}' missing opening front matter delimiter (---)"
258
+ continue
259
+ fi
260
+
261
+ # Check closing front matter delimiter
262
+ if ! awk 'NR > 1 && /^---[[:space:]]*$/ { found=1; exit } END { exit !found }' "${SKILL_FILE}"; then
263
+ log_fail "Skill '${SKILL_NAME}' missing closing front matter delimiter (---)"
264
+ fi
265
+
266
+ # Check name field in front matter
267
+ if ! grep -E "^name:[[:space:]]*${SKILL_NAME}" "${SKILL_FILE}" > /dev/null; then
268
+ log_fail "Skill '${SKILL_NAME}' front matter 'name:' does not match directory name"
269
+ fi
270
+
271
+ # Check description
272
+ DESC=$(grep -E "^description:" "${SKILL_FILE}" | sed -E 's/^description:[[:space:]]*//' || true)
273
+ if [[ -z "${DESC}" ]]; then
274
+ log_fail "Skill '${SKILL_NAME}' missing front matter 'description:'"
275
+ else
276
+ # Check imperative phrasing
277
+ if [[ ! "${DESC}" =~ ^Use[[:space:]]when ]]; then
278
+ log_warn "Skill '${SKILL_NAME}' description should start with imperative 'Use when...'"
279
+ fi
280
+
281
+ # Check negative boundary phrasing (Do not use / Do NOT use)
282
+ if ! echo "${DESC}" | grep -qiE "(do not use|do NOT use)"; then
283
+ log_warn "Skill '${SKILL_NAME}' description should specify negative boundaries ('Do not use for...')"
284
+ fi
285
+
286
+ # Check character length (< 1024)
287
+ CHAR_LEN=${#DESC}
288
+ if [[ ${CHAR_LEN} -gt 1024 ]]; then
289
+ log_fail "Skill '${SKILL_NAME}' description exceeds 1024 chars (${CHAR_LEN} chars)"
290
+ fi
291
+ fi
292
+
293
+ # Check body length (< 500 lines)
294
+ SKILL_LINES=$(wc -l < "${SKILL_FILE}")
295
+ if [[ ${SKILL_LINES} -gt 500 ]]; then
296
+ log_warn "Skill '${SKILL_NAME}' exceeds 500 lines (${SKILL_LINES} lines). Offload details to references/."
297
+ else
298
+ log_pass "Skill '${SKILL_NAME}': ${SKILL_LINES} lines, description valid (${#DESC} chars)."
299
+ fi
300
+
301
+ # Check for Gotchas / What NOT to do section
302
+ if ! grep -qi "What NOT to do" "${SKILL_FILE}" && ! grep -qi "Gotchas" "${SKILL_FILE}"; then
303
+ log_warn "Skill '${SKILL_NAME}' missing mandatory 'Gotchas & What NOT to Do' section"
304
+ fi
305
+ done
306
+ log_pass "Validated ${SKILL_COUNT} skills in .agents/skills/."
307
+ fi
308
+
309
+ # 4. Checking Markdown Internal Links & Cross-References
310
+ echo ""
311
+ echo "4. Checking Markdown Internal Links & Cross-References..."
312
+ LINK_CHECK_RAW=$(node -e '
313
+ const fs = require("fs");
314
+ const path = require("path");
315
+
316
+ const root = process.argv[1];
317
+ const broken = [];
318
+ let totalLinks = 0;
319
+
320
+ function walk(dir) {
321
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
322
+ for (const entry of entries) {
323
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
324
+ const full = path.join(dir, entry.name);
325
+ if (entry.isDirectory()) {
326
+ walk(full);
327
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
328
+ checkFile(full);
329
+ }
330
+ }
331
+ }
332
+
333
+ function checkFile(filePath) {
334
+ const content = fs.readFileSync(filePath, "utf8");
335
+ const dir = path.dirname(filePath);
336
+ const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
337
+ let match;
338
+ while ((match = regex.exec(content)) !== null) {
339
+ const target = match[2].trim();
340
+ if (target.startsWith("http://") || target.startsWith("https://") || target.startsWith("mailto:") || target.startsWith("#") || target.startsWith("conversation://") || target.startsWith("file://")) {
341
+ continue;
342
+ }
343
+ const cleanTarget = target.split("#")[0];
344
+ if (!cleanTarget) continue;
345
+ totalLinks++;
346
+ const resolved = path.normalize(path.join(dir, cleanTarget));
347
+ if (!fs.existsSync(resolved)) {
348
+ broken.push(`${path.relative(root, filePath)} -> ${target}`);
349
+ }
350
+ }
351
+ }
352
+
353
+ walk(root);
354
+ if (broken.length > 0) {
355
+ console.log("BROKEN:" + broken.join("|"));
356
+ process.exit(1);
357
+ } else {
358
+ console.log("OK:" + totalLinks);
359
+ process.exit(0);
360
+ }
361
+ ' "${WORKSPACE_ROOT}" 2>&1) || true
362
+
363
+ if [[ "${LINK_CHECK_RAW}" =~ ^OK:([0-9]+) ]]; then
364
+ TOTAL_LINKS="${BASH_REMATCH[1]}"
365
+ log_pass "Validated ${TOTAL_LINKS} internal links across workspace (0 broken links)."
366
+ elif [[ "${LINK_CHECK_RAW}" =~ ^BROKEN:(.*) ]]; then
367
+ BROKEN_LIST="${BASH_REMATCH[1]}"
368
+ IFS='|' read -ra BROKEN_ITEMS <<< "${BROKEN_LIST}"
369
+ for item in "${BROKEN_ITEMS[@]}"; do
370
+ log_fail "Broken markdown link: ${item}"
371
+ done
372
+ else
373
+ log_fail "Markdown link validation failed unexpectedly: ${LINK_CHECK_RAW}"
374
+ fi
375
+
376
+ # 5. Checking Memory & ADR Ledger
377
+ echo ""
378
+ echo "5. Checking Memory & ADR Ledger..."
379
+ MEMORY_FILE="${WORKSPACE_ROOT}/memory.md"
380
+ if [[ ! -f "${MEMORY_FILE}" ]]; then
381
+ log_fail "Missing memory.md ADR ledger."
382
+ else
383
+ MEMORY_BODY=$(sed '/<!--/,/-->/d' "${MEMORY_FILE}")
384
+ if ! grep -q "^# " "${MEMORY_FILE}"; then
385
+ log_fail "memory.md missing H1 header"
386
+ elif echo "${MEMORY_BODY}" | grep -q "#### ADR-"; then
387
+ log_pass "memory.md contains ADR entries with valid envelope."
388
+ else
389
+ log_pass "memory.md is a clean slate (no ADRs yet; record ADR-001 during /lets-build)."
390
+ fi
391
+ fi
392
+
393
+ # 6. Summary Output
394
+ echo ""
395
+ echo "--------------------------------------------------------------"
396
+ if [[ ${ERRORS} -eq 0 ]]; then
397
+ echo "🎉 SUCCESS: All agentic configurations are valid and healthy! (${WARNINGS} warnings)"
398
+ exit 0
399
+ else
400
+ echo "🚨 FAILURE: Found ${ERRORS} error(s) and ${WARNINGS} warning(s) in agentic configurations."
401
+ exit 1
402
+ fi