#!/usr/bin/env bash set -euo pipefail usage() { cat < Check every AGENTS.md file in a repo (root and nested) for drift: package manager scripts and file paths referenced in the text that no longer exist in the repo. Catches the failure mode that matters most in practice — an agent running a documented command that was renamed or deleted. Arguments: repo-root Path to the repository root to scan. Exit codes: 0 No FAIL findings (INFO may still be printed, e.g. no package.json found) 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 import json 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) def load_package_scripts(root): pkg_path = os.path.join(root, "package.json") if not os.path.isfile(pkg_path): return None try: with open(pkg_path, encoding="utf-8") as f: data = json.load(f) except (json.JSONDecodeError, OSError): return None return set(data.get("scripts", {}).keys()) def load_make_targets(root): make_path = os.path.join(root, "Makefile") if not os.path.isfile(make_path): return None with open(make_path, encoding="utf-8", errors="replace") as f: content = f.read() return set(re.findall(r'(?m)^([a-zA-Z0-9_-]+)\s*:(?!=)', content)) NPM_RUN_RE = re.compile(r'\b(?:npm|pnpm|yarn)\s+run\s+([a-zA-Z0-9:_-]+)') MAKE_RE = re.compile(r'\bmake\s+([a-zA-Z0-9_-]+)') # Backticked relative file paths, e.g. `scripts/bootstrap.sh`, `src/index.ts`. # Requires a path separator and file extension to avoid matching bare commands/words. PATH_RE = re.compile(r'`([A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)+\.[A-Za-z0-9]+)`') has_fail = False package_scripts = load_package_scripts(repo_root) make_targets = load_make_targets(repo_root) 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() for m in NPM_RUN_RE.finditer(content): script_name = m.group(1) if package_scripts is None: print(f"INFO Cannot verify referenced script '{script_name}' — {rel}") print(f" Note: AGENTS.md references an npm/pnpm/yarn script, but no package.json was found at the repo root to check it against.") print() elif script_name not in package_scripts: has_fail = True print(f"FAIL Referenced script '{script_name}' not found in package.json — {rel}") print(f" Why: AGENTS.md tells agents to run '{script_name}', but package.json has no matching \"scripts\" entry — the command will fail.") print(f" Fix: Update AGENTS.md to reference an existing script, or add '{script_name}' to package.json's scripts.") print() for m in MAKE_RE.finditer(content): target_name = m.group(1) if make_targets is None: print(f"INFO Cannot verify referenced make target '{target_name}' — {rel}") print(f" Note: AGENTS.md references a make target, but no Makefile was found at the repo root to check it against.") print() elif target_name not in make_targets: has_fail = True print(f"FAIL Referenced make target '{target_name}' not found in Makefile — {rel}") print(f" Why: AGENTS.md tells agents to run 'make {target_name}', but the Makefile has no matching target — the command will fail.") print(f" Fix: Update AGENTS.md to reference an existing target, or add '{target_name}' to the Makefile.") print() file_dir = os.path.dirname(fpath) for m in PATH_RE.finditer(content): candidate = m.group(1) resolved = ( os.path.isfile(os.path.join(repo_root, candidate)) or os.path.isfile(os.path.join(file_dir, candidate)) or os.path.isdir(os.path.join(repo_root, candidate)) or os.path.isdir(os.path.join(file_dir, candidate)) ) if not resolved: has_fail = True print(f"FAIL Referenced path '{candidate}' does not exist — {rel}") print(f" Why: AGENTS.md points agents to '{candidate}', but it isn't present in the repo (checked relative to repo root and to the AGENTS.md's own directory).") print(f" Fix: Update AGENTS.md to reference the correct path, or restore/create '{candidate}'.") print() if has_fail: sys.exit(1) sys.exit(0) PYTHON