This repository has been archived on 2026-03-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
bootstrap/tools/bootstrap/agent-lint.sh
Jason Woltje 80c3680ccb feat: rename rails/ to tools/ and add service tool suites
Rename the `rails/` directory to `tools/` for agent discoverability —
agents frequently failed to locate helper scripts due to the non-intuitive
directory name. Add backward-compat symlink `rails/ → tools/`.

New tool suites:
- Authentik: auth-token, user-list, user-create, group-list, app-list,
  flow-list, admin-status (8 scripts)
- Coolify: team-list, project-list, service-list, service-status, deploy,
  env-set (7 scripts)
- Woodpecker: pipeline-list, pipeline-status, pipeline-trigger (3 stubs)
- GLPI: session-init, computer-list, ticket-list, ticket-create, user-list
  (6 scripts)
- Health: stack-health.sh — stack-wide connectivity check

Infrastructure:
- Shared credential loader at tools/_lib/credentials.sh
- install.sh creates symlink + chmod on tool scripts
- All ~253 rails/ path references updated across 68+ files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:51:39 -06:00

306 lines
8.9 KiB
Bash
Executable File

#!/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 <path> # 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 runtime context file (CLAUDE.md or RUNTIME.md)?
# 2. Has AGENTS.md?
# 3. Runtime context file references conditional context/guides?
# 4. Runtime context file 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 <path>] [--json] [--verbose] [--fix-hint] [--src-dir <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)
}
# Resolve runtime context file (CLAUDE.md or RUNTIME.md)
runtime_context_file() {
local dir="$1"
if [[ -f "$dir/CLAUDE.md" ]]; then
echo "$dir/CLAUDE.md"
return
fi
if [[ -f "$dir/RUNTIME.md" ]]; then
echo "$dir/RUNTIME.md"
return
fi
echo ""
}
# Check for runtime context file
check_runtime_context() {
[[ -n "$(runtime_context_file "$1")" ]]
}
# Check for AGENTS.md
check_agents_md() {
[[ -f "$1/AGENTS.md" ]]
}
# Check conditional loading/context (references guides or conditional section)
check_conditional_loading() {
local ctx
ctx="$(runtime_context_file "$1")"
[[ -n "$ctx" ]] && grep -qi "agent-guides\|~/.config/mosaic/guides\|conditional.*loading\|conditional.*documentation\|conditional.*context" "$ctx" 2>/dev/null
}
# Check quality gates
check_quality_gates() {
local ctx
ctx="$(runtime_context_file "$1")"
[[ -n "$ctx" ]] && grep -qi "quality.gates\|must pass before\|lint\|typecheck\|test" "$ctx" 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_runtime has_agents has_guides has_quality mono_status
local score=0 max_score=4
check_runtime_context "$dir" && has_runtime="OK" || has_runtime="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_runtime" == "OK" ]] && ((score++)) || true
[[ "$has_agents" == "OK" ]] && ((score++)) || true
[[ "$has_guides" == "OK" ]] && ((score++)) || true
[[ "$has_quality" == "OK" ]] && ((score++)) || true
if $JSON_OUTPUT; then
cat <<JSONEOF
{
"project": "$name",
"path": "$dir",
"runtime_context": "$has_runtime",
"agents_md": "$has_agents",
"conditional_loading": "$has_guides",
"quality_gates": "$has_quality",
"monorepo_sub_agents": "$mono_status",
"score": $score,
"max_score": $max_score
}
JSONEOF
else
# Color-code the status
local c_runtime c_agents c_guides c_quality
[[ "$has_runtime" == "OK" ]] && c_runtime="${GREEN} OK ${NC}" || c_runtime="${RED} MISS ${NC}"
[[ "$has_agents" == "OK" ]] && c_agents="${GREEN} OK ${NC}" || c_agents="${RED} MISS ${NC}"
[[ "$has_guides" == "OK" ]] && c_guides="${GREEN} OK ${NC}" || c_guides="${RED} MISS ${NC}"
[[ "$has_quality" == "OK" ]] && c_quality="${GREEN} OK ${NC}" || c_quality="${RED} MISS ${NC}"
local score_color="$RED"
[[ $score -ge 3 ]] && score_color="$YELLOW"
[[ $score -eq 4 ]] && score_color="$GREEN"
printf " %-35s %b %b %b %b ${score_color}%d/%d${NC}" \
"$name" "$c_runtime" "$c_agents" "$c_guides" "$c_quality" "$score" "$max_score"
# Show monorepo status if applicable
if [[ "$mono_status" != "N/A" && "$mono_status" != "OK" ]]; then
printf " ${YELLOW}(mono: %s)${NC}" "$mono_status"
fi
echo ""
fi
if $VERBOSE && ! $JSON_OUTPUT; then
[[ "$has_runtime" == "MISS" ]] && echo " ${DIM} Runtime context file missing (CLAUDE.md or RUNTIME.md)${NC}"
[[ "$has_agents" == "MISS" ]] && echo " ${DIM} AGENTS.md missing${NC}"
[[ "$has_guides" == "MISS" ]] && echo " ${DIM} No conditional context/loading section detected${NC}"
[[ "$has_quality" == "MISS" ]] && echo " ${DIM} No quality gates section${NC}"
if [[ "$mono_status" == MISS:* ]]; then
echo " ${DIM} Monorepo sub-AGENTS.md missing: ${mono_status#MISS:}${NC}"
fi
fi
if $FIX_HINT && ! $JSON_OUTPUT; then
if [[ "$has_runtime" == "MISS" || "$has_agents" == "MISS" ]]; then
echo " ${DIM}Fix: ~/.config/mosaic/tools/bootstrap/init-project.sh --name \"$name\" --type auto${NC}"
elif [[ "$has_guides" == "MISS" ]]; then
echo " ${DIM}Fix: ~/.config/mosaic/tools/bootstrap/agent-upgrade.sh $dir --section conditional-loading${NC}"
fi
fi
# Return score for summary
echo "$score" > /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" "RUNTIME" "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