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>
306 lines
8.9 KiB
Bash
Executable File
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
|