#!/usr/bin/env bash set -euo pipefail usage() { cat < Check every AGENTS.md file in a repo (root and nested) for structural completeness against the agents.md spec's common-sections checklist (setup/build, code style, testing, security, commit/PR conventions). Missing individual sections are informational (not every repo needs every section) — only an empty or entirely unfilled file is a hard failure. Arguments: repo-root Path to the repository root to scan. Exit codes: 0 No FAIL findings (INFO/SUGGESTION may still be printed) 1 One or more FAIL findings EOF } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 fi if [[ $# -lt 1 ]]; then echo "Error: repo-root is required." >&2 echo "" >&2 usage >&2 exit 1 fi python3 -u - "$1" <<'PYTHON' import sys import os import re PLACEHOLDER_RE = re.compile(r'(?i)FILL IN:|TODO:\s*write|lorem ipsum') COMMON_SECTIONS = [ ("setup/build commands", re.compile(r'(?im)^#{1,3}\s*(setup|install|build|getting started)')), ("code style", re.compile(r'(?im)^#{1,3}\s*(code style|style guide|conventions)')), ("testing instructions", re.compile(r'(?im)^#{1,3}\s*(test|testing)')), ("security considerations", re.compile(r'(?im)^#{1,3}\s*security')), ("commit/PR conventions", re.compile(r'(?im)^#{1,3}\s*(commit|pr|pull request)')), ] repo_root = os.path.abspath(sys.argv[1]) if not os.path.isdir(repo_root): print(f"Error: '{repo_root}' is not a directory.", file=sys.stderr) sys.exit(1) EXCLUDE_DIRS = {".git", "node_modules", "vendor", ".venv", "venv", "dist", "build"} def find_agents_md(root): results = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS and not d.startswith(".")] for fname in filenames: if fname == "AGENTS.md": results.append(os.path.join(dirpath, fname)) return sorted(results) has_fail = False file_contents = {} # rel path -> content, for the duplication pass below for fpath in find_agents_md(repo_root): rel = os.path.relpath(fpath, repo_root) with open(fpath, encoding="utf-8", errors="replace") as f: content = f.read() file_contents[rel] = content if not content.strip(): has_fail = True print(f"FAIL AGENTS.md is empty — {rel}") print(" Why: An empty file provides no instructions and gives agents nothing to act on.") print(" Fix: Add at least a project overview and setup/test commands, per the agents.md common-sections checklist.") print() continue if PLACEHOLDER_RE.search(content): has_fail = True print(f"FAIL Unfilled placeholder content — {rel}") print(" Why: A 'FILL IN:' or template stub left in place means the file has no repo-specific instructions yet.") print(" Fix: Replace the placeholder with real, repo-specific content.") print() continue for label, pattern in COMMON_SECTIONS: if not pattern.search(content): print(f"INFO No {label} section — {rel}") print(f" Note: The agents.md common-sections checklist includes {label}; not every repo needs every section, but confirm this omission is deliberate.") print() # --- Nested-vs-root duplication check --- root_content = file_contents.get("AGENTS.md") if root_content: root_lines = {ln.strip() for ln in root_content.splitlines() if ln.strip()} for rel, content in file_contents.items(): if rel == "AGENTS.md": continue nested_lines = [ln.strip() for ln in content.splitlines() if ln.strip()] if not nested_lines: continue overlap = sum(1 for ln in nested_lines if ln in root_lines) ratio = overlap / len(nested_lines) if ratio >= 0.7: print(f"SUGGESTION Nested AGENTS.md largely duplicates the root file — {rel}") print(f" Why: {ratio:.0%} of this file's content lines already appear in the root AGENTS.md; per the spec's nearest-file-wins precedence, nested files don't inherit from the root, but they also shouldn't just restate it.") print(f" Fix: Trim {rel} down to only what's specific to this package/directory.") print() if has_fail: sys.exit(1) sys.exit(0) PYTHON