#!/bin/bash # agent-lint.sh — Audit agent configuration across all coding projects # # Usage: # agent-lint.sh # Scan all projects in ~/src/ # agent-lint.sh --project # Scan single project # agent-lint.sh --json # Output JSON for jarvis-brain # agent-lint.sh --verbose # Show per-check details # agent-lint.sh --fix-hint # Show fix commands for failures # # Checks per project: # 1. Has CLAUDE.md? # 2. Has AGENTS.md? # 3. CLAUDE.md references conditional context/guides? # 4. CLAUDE.md has quality gates? # 5. For monorepos: sub-directories have AGENTS.md? set -euo pipefail # Defaults SRC_DIR="$HOME/src" SINGLE_PROJECT="" JSON_OUTPUT=false VERBOSE=false FIX_HINT=false # Exclusion patterns (not coding projects) EXCLUDE_PATTERNS=( "_worktrees" ".backup" "_old" "_bak" "junk" "traefik" "infrastructure" ) # Parse args while [[ $# -gt 0 ]]; do case "$1" in --project) SINGLE_PROJECT="$2"; shift 2 ;; --json) JSON_OUTPUT=true; shift ;; --verbose) VERBOSE=true; shift ;; --fix-hint) FIX_HINT=true; shift ;; --src-dir) SRC_DIR="$2"; shift 2 ;; -h|--help) echo "Usage: agent-lint.sh [--project ] [--json] [--verbose] [--fix-hint] [--src-dir ]" exit 0 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done # Colors (disabled for JSON mode) if $JSON_OUTPUT; then GREEN="" RED="" YELLOW="" NC="" BOLD="" DIM="" else GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' NC='\033[0m' BOLD='\033[1m' DIM='\033[2m' fi # Determine if a directory is a coding project is_coding_project() { local dir="$1" [[ -f "$dir/package.json" ]] || \ [[ -f "$dir/pyproject.toml" ]] || \ [[ -f "$dir/Cargo.toml" ]] || \ [[ -f "$dir/go.mod" ]] || \ [[ -f "$dir/Makefile" && -f "$dir/src/main.rs" ]] || \ [[ -f "$dir/pom.xml" ]] || \ [[ -f "$dir/build.gradle" ]] } # Check if directory should be excluded is_excluded() { local dir_name dir_name=$(basename "$1") for pattern in "${EXCLUDE_PATTERNS[@]}"; do if [[ "$dir_name" == *"$pattern"* ]]; then return 0 fi done return 1 } # Detect if project is a monorepo is_monorepo() { local dir="$1" [[ -f "$dir/pnpm-workspace.yaml" ]] || \ [[ -f "$dir/turbo.json" ]] || \ [[ -f "$dir/lerna.json" ]] || \ (grep -q '"workspaces"' "$dir/package.json" 2>/dev/null) } # Check for CLAUDE.md check_claude_md() { [[ -f "$1/CLAUDE.md" ]] } # Check for AGENTS.md check_agents_md() { [[ -f "$1/AGENTS.md" ]] } # Check conditional loading/context (references guides or conditional section) check_conditional_loading() { local claude_md="$1/CLAUDE.md" [[ -f "$claude_md" ]] && grep -qi "agent-guides\|~/.mosaic/guides\|conditional.*loading\|conditional.*documentation\|conditional.*context" "$claude_md" 2>/dev/null } # Check quality gates check_quality_gates() { local claude_md="$1/CLAUDE.md" [[ -f "$claude_md" ]] && grep -qi "quality.gates\|must pass before\|lint\|typecheck\|test" "$claude_md" 2>/dev/null } # Check monorepo sub-AGENTS.md check_monorepo_sub_agents() { local dir="$1" local missing=() if ! is_monorepo "$dir"; then echo "N/A" return fi # Check apps/, packages/, services/, plugins/ directories for subdir_type in apps packages services plugins; do if [[ -d "$dir/$subdir_type" ]]; then for subdir in "$dir/$subdir_type"/*/; do [[ -d "$subdir" ]] || continue # Only check if it has its own manifest if [[ -f "$subdir/package.json" ]] || [[ -f "$subdir/pyproject.toml" ]]; then if [[ ! -f "$subdir/AGENTS.md" ]]; then missing+=("$(basename "$subdir")") fi fi done fi done if [[ ${#missing[@]} -eq 0 ]]; then echo "OK" else echo "MISS:${missing[*]}" fi } # Lint a single project lint_project() { local dir="$1" local name name=$(basename "$dir") local has_claude has_agents has_guides has_quality mono_status local score=0 max_score=4 check_claude_md "$dir" && has_claude="OK" || has_claude="MISS" check_agents_md "$dir" && has_agents="OK" || has_agents="MISS" check_conditional_loading "$dir" && has_guides="OK" || has_guides="MISS" check_quality_gates "$dir" && has_quality="OK" || has_quality="MISS" mono_status=$(check_monorepo_sub_agents "$dir") [[ "$has_claude" == "OK" ]] && ((score++)) || true [[ "$has_agents" == "OK" ]] && ((score++)) || true [[ "$has_guides" == "OK" ]] && ((score++)) || true [[ "$has_quality" == "OK" ]] && ((score++)) || true if $JSON_OUTPUT; then cat < /tmp/agent-lint-score-$$ } # Main main() { local projects=() local total=0 passing=0 total_score=0 if [[ -n "$SINGLE_PROJECT" ]]; then projects=("$SINGLE_PROJECT") else for dir in "$SRC_DIR"/*/; do [[ -d "$dir" ]] || continue is_excluded "$dir" && continue is_coding_project "$dir" && projects+=("${dir%/}") done fi if [[ ${#projects[@]} -eq 0 ]]; then echo "No coding projects found." exit 0 fi if $JSON_OUTPUT; then echo '{ "audit_date": "'$(date -I)'", "projects": [' local first=true for dir in "${projects[@]}"; do $first || echo "," first=false lint_project "$dir" done echo '] }' else echo "" echo -e "${BOLD}Agent Configuration Audit — $(date +%Y-%m-%d)${NC}" echo "========================================================" printf " %-35s %s %s %s %s %s\n" \ "Project" "CLAUDE" "AGENTS" "Guides" "Quality" "Score" echo " -----------------------------------------------------------------------" for dir in "${projects[@]}"; do lint_project "$dir" local score score=$(cat /tmp/agent-lint-score-$$ 2>/dev/null || echo 0) ((total++)) || true ((total_score += score)) || true [[ $score -eq 4 ]] && ((passing++)) || true done rm -f /tmp/agent-lint-score-$$ echo " -----------------------------------------------------------------------" local need_attention=$((total - passing)) echo "" echo -e " ${BOLD}Summary:${NC} $total projects | ${GREEN}$passing pass${NC} | ${RED}$need_attention need attention${NC}" echo "" if [[ $need_attention -gt 0 ]] && ! $FIX_HINT; then echo -e " ${DIM}Run with --fix-hint for suggested fixes${NC}" echo -e " ${DIM}Run with --verbose for per-check details${NC}" echo "" fi fi } main