azcodr 1.5.2 → 2.1.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 (136) hide show
  1. package/.agents/hooks.json +42 -42
  2. package/.agents/hooks.json.example +42 -42
  3. package/.agents/mcp_config.json.example +29 -29
  4. package/.agents/scripts/safety_guard.sh +143 -34
  5. package/.agents/scripts/verify_completion.sh +90 -27
  6. package/.agents/skills/agentic-architect/SKILL.md +125 -125
  7. package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
  8. package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
  9. package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
  10. package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
  11. package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +402 -402
  12. package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
  13. package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
  14. package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
  15. package/.agents/skills/compliance-audit/SKILL.md +120 -120
  16. package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
  17. package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
  18. package/.agents/skills/lets-build/SKILL.md +173 -173
  19. package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
  20. package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
  21. package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
  22. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +419 -255
  23. package/.agents/skills/product-analyst/SKILL.md +154 -154
  24. package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
  25. package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
  26. package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
  27. package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
  28. package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
  29. package/.agents/skills/relentless-questioner/SKILL.md +128 -128
  30. package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
  31. package/.editorconfig +19 -19
  32. package/.github/workflows/ci.yml +167 -78
  33. package/.github/workflows/publish.yml +196 -0
  34. package/.gitignore +40 -25
  35. package/AGENTS.md +103 -102
  36. package/LICENSE +21 -21
  37. package/README.md +168 -165
  38. package/bin/azcodr.js +19 -228
  39. package/docs/knowledge/ubiquitous_language.md +31 -18
  40. package/docs/rules/agentic_configuration.md +259 -259
  41. package/docs/rules/api_architecture.md +179 -179
  42. package/docs/rules/authentication.md +76 -76
  43. package/docs/rules/authorization.md +75 -75
  44. package/docs/rules/caching.md +69 -69
  45. package/docs/rules/clean_code.md +62 -62
  46. package/docs/rules/cloud_native.md +41 -41
  47. package/docs/rules/cqrs.md +203 -203
  48. package/docs/rules/database_design.md +125 -125
  49. package/docs/rules/database_operations.md +69 -69
  50. package/docs/rules/design_patterns.md +98 -98
  51. package/docs/rules/devops_ci_cd.md +76 -76
  52. package/docs/rules/domain_driven_design.md +122 -122
  53. package/docs/rules/error_handling.md +54 -52
  54. package/docs/rules/feature_flags.md +59 -59
  55. package/docs/rules/frontend_architecture.md +157 -157
  56. package/docs/rules/multitenancy_architecture.md +98 -98
  57. package/docs/rules/product_ownership.md +127 -127
  58. package/docs/rules/project_management.md +49 -49
  59. package/docs/rules/relentless_questioning.md +52 -52
  60. package/docs/rules/requirements_engineering.md +98 -98
  61. package/docs/rules/security_compliance.md +53 -53
  62. package/docs/rules/server_driven_ui.md +88 -88
  63. package/docs/rules/test_driven_development.md +185 -185
  64. package/docs/rules/transactional_email.md +27 -27
  65. package/docs/rules/type_safety.md +65 -65
  66. package/docs/rules/ui_ux_architecture.md +150 -150
  67. package/docs/rules/workflow_state_machines.md +117 -117
  68. package/lib/cli-parse.d.ts +32 -0
  69. package/lib/cli-parse.d.ts.map +1 -0
  70. package/lib/cli-parse.js +55 -0
  71. package/lib/cli-parse.js.map +1 -0
  72. package/lib/cli-target.d.ts +66 -0
  73. package/lib/cli-target.d.ts.map +1 -0
  74. package/lib/cli-target.js +102 -0
  75. package/lib/cli-target.js.map +1 -0
  76. package/lib/cli.d.ts +40 -0
  77. package/lib/cli.d.ts.map +1 -0
  78. package/lib/cli.js +166 -0
  79. package/lib/cli.js.map +1 -0
  80. package/lib/errors.d.ts +39 -0
  81. package/lib/errors.d.ts.map +1 -0
  82. package/lib/errors.js +26 -0
  83. package/lib/errors.js.map +1 -0
  84. package/lib/git.d.ts +15 -0
  85. package/lib/git.d.ts.map +1 -0
  86. package/lib/git.js +32 -0
  87. package/lib/git.js.map +1 -0
  88. package/lib/guards.d.ts +35 -0
  89. package/lib/guards.d.ts.map +1 -0
  90. package/lib/guards.js +95 -0
  91. package/lib/guards.js.map +1 -0
  92. package/lib/index.d.ts +6 -134
  93. package/lib/index.d.ts.map +1 -0
  94. package/lib/index.js +4 -5
  95. package/lib/index.js.map +1 -0
  96. package/lib/links.d.ts +28 -0
  97. package/lib/links.d.ts.map +1 -0
  98. package/lib/links.js +129 -0
  99. package/lib/links.js.map +1 -0
  100. package/lib/permissions.d.ts +9 -0
  101. package/lib/permissions.d.ts.map +1 -0
  102. package/lib/permissions.js +44 -0
  103. package/lib/permissions.js.map +1 -0
  104. package/lib/repo.d.ts +20 -0
  105. package/lib/repo.d.ts.map +1 -0
  106. package/lib/repo.js +92 -0
  107. package/lib/repo.js.map +1 -0
  108. package/lib/scaffold.d.ts +80 -0
  109. package/lib/scaffold.d.ts.map +1 -0
  110. package/lib/scaffold.js +201 -448
  111. package/lib/scaffold.js.map +1 -0
  112. package/memory.md +135 -36
  113. package/package.json +75 -62
  114. package/scripts/test_coverage.js +66 -38
  115. package/scripts/validate/adr.js +155 -0
  116. package/scripts/validate/io.js +82 -0
  117. package/scripts/validate/links.js +166 -0
  118. package/scripts/validate/parity.js +122 -0
  119. package/scripts/validate/root.js +183 -0
  120. package/scripts/validate/rules.js +42 -0
  121. package/scripts/validate/skills.js +94 -0
  122. package/scripts/validate/text.js +27 -0
  123. package/scripts/validate-cli.js +12 -0
  124. package/scripts/validate.js +158 -258
  125. package/src/cli-parse.ts +77 -0
  126. package/src/cli-target.ts +167 -0
  127. package/src/cli.ts +240 -0
  128. package/src/errors.ts +35 -0
  129. package/src/git.ts +34 -0
  130. package/src/guards.ts +101 -0
  131. package/src/index.ts +39 -0
  132. package/src/links.ts +139 -0
  133. package/src/permissions.ts +42 -0
  134. package/src/repo.ts +94 -0
  135. package/src/scaffold.ts +273 -0
  136. package/.github/copilot-instructions.md +0 -1
@@ -1,402 +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
- 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
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