Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
333 lines
9.6 KiB
Bash
Executable File
333 lines
9.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# agent-upgrade.sh — Non-destructively upgrade agent configuration in projects
|
|
#
|
|
# Usage:
|
|
# agent-upgrade.sh <project-path> # Upgrade one project
|
|
# agent-upgrade.sh --all # Upgrade all projects in ~/src/
|
|
# agent-upgrade.sh --all --dry-run # Preview what would change
|
|
# agent-upgrade.sh <path> --section conditional-loading # Inject specific section
|
|
# agent-upgrade.sh <path> --create-agents # Create AGENTS.md if missing
|
|
# agent-upgrade.sh <path> --monorepo-scan # Create sub-AGENTS.md for monorepo dirs
|
|
#
|
|
# Safety:
|
|
# - Creates .bak backup before any modification
|
|
# - Append-only — never modifies existing sections
|
|
# - --dry-run shows what would change without writing
|
|
|
|
set -euo pipefail
|
|
|
|
# Defaults
|
|
SRC_DIR="$HOME/src"
|
|
FRAGMENTS_DIR="$HOME/.config/mosaic/templates/agent/fragments"
|
|
TEMPLATES_DIR="$HOME/.config/mosaic/templates/agent"
|
|
DRY_RUN=false
|
|
ALL_PROJECTS=false
|
|
TARGET_PATH=""
|
|
SECTION_ONLY=""
|
|
CREATE_AGENTS=false
|
|
MONOREPO_SCAN=false
|
|
|
|
# Exclusion patterns (same as agent-lint.sh)
|
|
EXCLUDE_PATTERNS=(
|
|
"_worktrees"
|
|
".backup"
|
|
"_old"
|
|
"_bak"
|
|
"junk"
|
|
"traefik"
|
|
"infrastructure"
|
|
)
|
|
|
|
# Colors
|
|
GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m'
|
|
NC='\033[0m' BOLD='\033[1m' DIM='\033[2m'
|
|
|
|
# Parse args
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--all) ALL_PROJECTS=true; shift ;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--section) SECTION_ONLY="$2"; shift 2 ;;
|
|
--create-agents) CREATE_AGENTS=true; shift ;;
|
|
--monorepo-scan) MONOREPO_SCAN=true; shift ;;
|
|
--src-dir) SRC_DIR="$2"; shift 2 ;;
|
|
-h|--help)
|
|
echo "Usage: agent-upgrade.sh [<project-path>|--all] [--dry-run] [--section <name>] [--create-agents] [--monorepo-scan]"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --all Upgrade all projects in ~/src/"
|
|
echo " --dry-run Preview changes without writing"
|
|
echo " --section <name> Inject only a specific fragment (conditional-loading, commit-format, secrets, multi-agent, code-review, campsite-rule)"
|
|
echo " --create-agents Create AGENTS.md if missing"
|
|
echo " --monorepo-scan Create sub-AGENTS.md for monorepo directories"
|
|
exit 0
|
|
;;
|
|
*)
|
|
if [[ -d "$1" ]]; then
|
|
TARGET_PATH="$1"
|
|
else
|
|
echo "Unknown option or invalid path: $1"
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if ! $ALL_PROJECTS && [[ -z "$TARGET_PATH" ]]; then
|
|
echo "Error: Specify a project path or use --all"
|
|
exit 1
|
|
fi
|
|
|
|
# Helpers
|
|
is_coding_project() {
|
|
local dir="$1"
|
|
[[ -f "$dir/package.json" ]] || \
|
|
[[ -f "$dir/pyproject.toml" ]] || \
|
|
[[ -f "$dir/Cargo.toml" ]] || \
|
|
[[ -f "$dir/go.mod" ]] || \
|
|
[[ -f "$dir/pom.xml" ]] || \
|
|
[[ -f "$dir/build.gradle" ]]
|
|
}
|
|
|
|
is_excluded() {
|
|
local dir_name
|
|
dir_name=$(basename "$1")
|
|
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
|
[[ "$dir_name" == *"$pattern"* ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
has_section() {
|
|
local file="$1"
|
|
local pattern="$2"
|
|
[[ -f "$file" ]] && grep -qi "$pattern" "$file" 2>/dev/null
|
|
}
|
|
|
|
runtime_context_file() {
|
|
local project_dir="$1"
|
|
if [[ -f "$project_dir/CLAUDE.md" ]]; then
|
|
echo "$project_dir/CLAUDE.md"
|
|
return
|
|
fi
|
|
if [[ -f "$project_dir/RUNTIME.md" ]]; then
|
|
echo "$project_dir/RUNTIME.md"
|
|
return
|
|
fi
|
|
echo "$project_dir/CLAUDE.md"
|
|
}
|
|
|
|
backup_file() {
|
|
local file="$1"
|
|
if [[ -f "$file" ]] && ! $DRY_RUN; then
|
|
cp "$file" "${file}.bak"
|
|
fi
|
|
}
|
|
|
|
# Inject a fragment into CLAUDE.md if the section doesn't exist
|
|
inject_fragment() {
|
|
local project_dir="$1"
|
|
local fragment_name="$2"
|
|
local ctx_file
|
|
ctx_file="$(runtime_context_file "$project_dir")"
|
|
local fragment_file="$FRAGMENTS_DIR/$fragment_name.md"
|
|
|
|
if [[ ! -f "$fragment_file" ]]; then
|
|
echo -e " ${RED}Fragment not found: $fragment_file${NC}"
|
|
return 1
|
|
fi
|
|
|
|
# Determine detection pattern for this fragment
|
|
local detect_pattern
|
|
case "$fragment_name" in
|
|
conditional-loading) detect_pattern="agent-guides\|~/.config/mosaic/guides\|Conditional.*Loading\|Conditional.*Documentation\|Conditional.*Context" ;;
|
|
commit-format) detect_pattern="<type>.*#issue\|Types:.*feat.*fix" ;;
|
|
secrets) detect_pattern="NEVER hardcode secrets\|\.env.example.*committed" ;;
|
|
multi-agent) detect_pattern="Multi-Agent Coordination\|pull --rebase.*before" ;;
|
|
code-review) detect_pattern="codex-code-review\|codex-security-review\|Code Review" ;;
|
|
campsite-rule) detect_pattern="Campsite Rule\|Touching it makes it yours\|was already there.*NEVER" ;;
|
|
*) echo "Unknown fragment: $fragment_name"; return 1 ;;
|
|
esac
|
|
|
|
if [[ ! -f "$ctx_file" ]]; then
|
|
echo -e " ${YELLOW}No runtime context file (CLAUDE.md/RUNTIME.md) — skipping fragment injection${NC}"
|
|
return 0
|
|
fi
|
|
|
|
if has_section "$ctx_file" "$detect_pattern"; then
|
|
echo -e " ${DIM}$fragment_name already present${NC}"
|
|
return 0
|
|
fi
|
|
|
|
if $DRY_RUN; then
|
|
echo -e " ${GREEN}Would inject: $fragment_name${NC}"
|
|
else
|
|
backup_file "$ctx_file"
|
|
echo "" >> "$ctx_file"
|
|
cat "$fragment_file" >> "$ctx_file"
|
|
echo "" >> "$ctx_file"
|
|
echo -e " ${GREEN}Injected: $fragment_name${NC}"
|
|
fi
|
|
}
|
|
|
|
# Create AGENTS.md from template
|
|
create_agents_md() {
|
|
local project_dir="$1"
|
|
local agents_md="$project_dir/AGENTS.md"
|
|
|
|
if [[ -f "$agents_md" ]]; then
|
|
echo -e " ${DIM}AGENTS.md already exists${NC}"
|
|
return 0
|
|
fi
|
|
|
|
local project_name
|
|
project_name=$(basename "$project_dir")
|
|
|
|
# Detect project type for quality gates
|
|
local quality_gates="# Add quality gate commands here"
|
|
if [[ -f "$project_dir/package.json" ]]; then
|
|
quality_gates="npm run lint && npm run typecheck && npm test"
|
|
if grep -q '"pnpm"' "$project_dir/package.json" 2>/dev/null || [[ -f "$project_dir/pnpm-lock.yaml" ]]; then
|
|
quality_gates="pnpm lint && pnpm typecheck && pnpm test"
|
|
fi
|
|
elif [[ -f "$project_dir/pyproject.toml" ]]; then
|
|
quality_gates="uv run ruff check src/ tests/ && uv run mypy src/ && uv run pytest --cov"
|
|
fi
|
|
|
|
if $DRY_RUN; then
|
|
echo -e " ${GREEN}Would create: AGENTS.md${NC}"
|
|
else
|
|
# Use generic AGENTS.md template with substitutions
|
|
sed -e "s/\${PROJECT_NAME}/$project_name/g" \
|
|
-e "s/\${QUALITY_GATES}/$quality_gates/g" \
|
|
-e "s/\${TASK_PREFIX}/${project_name^^}/g" \
|
|
-e "s|\${SOURCE_DIR}|src|g" \
|
|
"$TEMPLATES_DIR/AGENTS.md.template" > "$agents_md"
|
|
echo -e " ${GREEN}Created: AGENTS.md${NC}"
|
|
fi
|
|
}
|
|
|
|
# Create sub-AGENTS.md for monorepo directories
|
|
create_sub_agents() {
|
|
local project_dir="$1"
|
|
|
|
if ! is_monorepo "$project_dir"; then
|
|
echo -e " ${DIM}Not a monorepo — skipping sub-AGENTS scan${NC}"
|
|
return 0
|
|
fi
|
|
|
|
local created=0
|
|
for subdir_type in apps packages services plugins; do
|
|
if [[ -d "$project_dir/$subdir_type" ]]; then
|
|
for subdir in "$project_dir/$subdir_type"/*/; do
|
|
[[ -d "$subdir" ]] || continue
|
|
# Only if it has its own manifest
|
|
if [[ -f "$subdir/package.json" ]] || [[ -f "$subdir/pyproject.toml" ]]; then
|
|
if [[ ! -f "$subdir/AGENTS.md" ]]; then
|
|
local dir_name
|
|
dir_name=$(basename "$subdir")
|
|
if $DRY_RUN; then
|
|
echo -e " ${GREEN}Would create: $subdir_type/$dir_name/AGENTS.md${NC}"
|
|
else
|
|
sed -e "s/\${DIRECTORY_NAME}/$dir_name/g" \
|
|
-e "s/\${DIRECTORY_PURPOSE}/Part of the $subdir_type layer./g" \
|
|
"$TEMPLATES_DIR/sub-agents.md.template" > "${subdir}AGENTS.md"
|
|
echo -e " ${GREEN}Created: $subdir_type/$dir_name/AGENTS.md${NC}"
|
|
fi
|
|
((created++)) || true
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
|
|
if [[ $created -eq 0 ]]; then
|
|
echo -e " ${DIM}All monorepo sub-AGENTS.md present${NC}"
|
|
fi
|
|
}
|
|
|
|
# Upgrade a single project
|
|
upgrade_project() {
|
|
local dir="$1"
|
|
local name
|
|
name=$(basename "$dir")
|
|
|
|
echo -e "\n${BOLD}$name${NC} ${DIM}($dir)${NC}"
|
|
|
|
if [[ -n "$SECTION_ONLY" ]]; then
|
|
inject_fragment "$dir" "$SECTION_ONLY"
|
|
return
|
|
fi
|
|
|
|
# Always try conditional-loading (highest impact)
|
|
inject_fragment "$dir" "conditional-loading"
|
|
|
|
# Try other fragments if runtime context exists
|
|
if [[ -f "$dir/CLAUDE.md" || -f "$dir/RUNTIME.md" ]]; then
|
|
inject_fragment "$dir" "commit-format"
|
|
inject_fragment "$dir" "secrets"
|
|
inject_fragment "$dir" "multi-agent"
|
|
inject_fragment "$dir" "code-review"
|
|
inject_fragment "$dir" "campsite-rule"
|
|
fi
|
|
|
|
# Create AGENTS.md if missing (always unless --section was used)
|
|
if $CREATE_AGENTS || [[ -z "$SECTION_ONLY" ]]; then
|
|
create_agents_md "$dir"
|
|
fi
|
|
|
|
# Monorepo sub-AGENTS.md
|
|
if $MONOREPO_SCAN || [[ -z "$SECTION_ONLY" ]]; then
|
|
create_sub_agents "$dir"
|
|
fi
|
|
}
|
|
|
|
# Main
|
|
main() {
|
|
local projects=()
|
|
|
|
if $ALL_PROJECTS; then
|
|
for dir in "$SRC_DIR"/*/; do
|
|
[[ -d "$dir" ]] || continue
|
|
is_excluded "$dir" && continue
|
|
is_coding_project "$dir" && projects+=("${dir%/}")
|
|
done
|
|
else
|
|
projects=("$TARGET_PATH")
|
|
fi
|
|
|
|
if [[ ${#projects[@]} -eq 0 ]]; then
|
|
echo "No coding projects found."
|
|
exit 0
|
|
fi
|
|
|
|
local mode="LIVE"
|
|
$DRY_RUN && mode="DRY RUN"
|
|
|
|
echo -e "${BOLD}Agent Configuration Upgrade — $(date +%Y-%m-%d) [$mode]${NC}"
|
|
echo "========================================================"
|
|
|
|
for dir in "${projects[@]}"; do
|
|
upgrade_project "$dir"
|
|
done
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Done.${NC}"
|
|
if $DRY_RUN; then
|
|
echo -e "${DIM}Run without --dry-run to apply changes.${NC}"
|
|
else
|
|
echo -e "${DIM}Backups saved as .bak files. Run agent-lint.sh to verify.${NC}"
|
|
fi
|
|
}
|
|
|
|
main
|