#!/bin/bash # agent-upgrade.sh — Non-destructively upgrade agent configuration in projects # # Usage: # agent-upgrade.sh # 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 --section conditional-loading # Inject specific section # agent-upgrade.sh --create-agents # Create AGENTS.md if missing # agent-upgrade.sh --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 [|--all] [--dry-run] [--section ] [--create-agents] [--monorepo-scan]" echo "" echo "Options:" echo " --all Upgrade all projects in ~/src/" echo " --dry-run Preview changes without writing" echo " --section 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=".*#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