#!/usr/bin/env bash # Update agent context files with information from plan.md # # This script maintains AI agent context files by parsing feature specifications # and updating agent-specific configuration files with project information. # # MAIN FUNCTIONS: # 1. Environment Validation # - Verifies git repository structure and branch information # - Checks for required plan.md files and templates # - Validates file permissions and accessibility # # 2. Plan Data Extraction # - Parses plan.md files to extract project metadata # - Identifies language/version, frameworks, databases, and project types # - Handles missing or incomplete specification data gracefully # # 3. Agent File Management # - Creates new agent context files from templates when needed # - Updates existing agent files with new project information # - Preserves manual additions and custom configurations # - Supports multiple AI agent formats and directory structures # # 4. Content Generation # - Generates language-specific build/test commands # - Creates appropriate project directory structures # - Updates technology stacks and recent changes sections # - Maintains consistent formatting and timestamps # # 5. Multi-Agent Support # - Handles agent-specific file paths and naming conventions # - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf # - Can update single agents or all existing agent files # - Creates default Claude file if no agent files exist # # Usage: ./update-agent-context.sh [agent_type] # Agent types: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf # Leave empty to update all existing agent files set -e # Enable strict error handling set -u set -o pipefail #============================================================================== # Configuration and Global Variables #============================================================================== # Get script directory and load common functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" # Get all paths and variables from common functions eval $(get_feature_paths) NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code AGENT_TYPE="${1:-}" # Agent-specific file paths CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" GEMINI_FILE="$REPO_ROOT/GEMINI.md" COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" QWEN_FILE="$REPO_ROOT/QWEN.md" AGENTS_FILE="$REPO_ROOT/AGENTS.md" WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" # Template file TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" # Global variables for parsed plan data NEW_LANG="" NEW_FRAMEWORK="" NEW_DB="" NEW_PROJECT_TYPE="" #============================================================================== # Utility Functions #============================================================================== log_info() { echo "INFO: $1" } log_success() { echo "✓ $1" } log_error() { echo "ERROR: $1" >&2 } log_warning() { echo "WARNING: $1" >&2 } # Cleanup function for temporary files cleanup() { local exit_code=$? rm -f /tmp/agent_update_*_$$ rm -f /tmp/manual_additions_$$ exit $exit_code } # Set up cleanup trap trap cleanup EXIT INT TERM #============================================================================== # Validation Functions #============================================================================== validate_environment() { # Check if we have a current branch/feature (git or non-git) if [[ -z "$CURRENT_BRANCH" ]]; then log_error "Unable to determine current feature" if [[ "$HAS_GIT" == "true" ]]; then log_info "Make sure you're on a feature branch" else log_info "Set SPECIFY_FEATURE environment variable or create a feature first" fi exit 1 fi # Check if plan.md exists if [[ ! -f "$NEW_PLAN" ]]; then log_error "No plan.md found at $NEW_PLAN" log_info "Make sure you're working on a feature with a corresponding spec directory" if [[ "$HAS_GIT" != "true" ]]; then log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" fi exit 1 fi # Check if template exists (needed for new files) if [[ ! -f "$TEMPLATE_FILE" ]]; then log_warning "Template file not found at $TEMPLATE_FILE" log_warning "Creating new agent files will fail" fi } #============================================================================== # Plan Parsing Functions #============================================================================== extract_plan_field() { local field_pattern="$1" local plan_file="$2" grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ head -1 | \ sed "s|^\*\*${field_pattern}\*\*: ||" | \ sed 's/^[ \t]*//;s/[ \t]*$//' | \ grep -v "NEEDS CLARIFICATION" | \ grep -v "^N/A$" || echo "" } parse_plan_data() { local plan_file="$1" if [[ ! -f "$plan_file" ]]; then log_error "Plan file not found: $plan_file" return 1 fi if [[ ! -r "$plan_file" ]]; then log_error "Plan file is not readable: $plan_file" return 1 fi log_info "Parsing plan data from $plan_file" NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") NEW_DB=$(extract_plan_field "Storage" "$plan_file") NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") # Log what we found if [[ -n "$NEW_LANG" ]]; then log_info "Found language: $NEW_LANG" else log_warning "No language information found in plan" fi if [[ -n "$NEW_FRAMEWORK" ]]; then log_info "Found framework: $NEW_FRAMEWORK" fi if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then log_info "Found database: $NEW_DB" fi if [[ -n "$NEW_PROJECT_TYPE" ]]; then log_info "Found project type: $NEW_PROJECT_TYPE" fi } format_technology_stack() { local lang="$1" local framework="$2" local parts=() # Add non-empty parts [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") # Join with proper formatting if [[ ${#parts[@]} -eq 0 ]]; then echo "" elif [[ ${#parts[@]} -eq 1 ]]; then echo "${parts[0]}" else # Join multiple parts with " + " local result="${parts[0]}" for ((i=1; i<${#parts[@]}; i++)); do result="$result + ${parts[i]}" done echo "$result" fi } #============================================================================== # Template and Content Generation Functions #============================================================================== get_project_structure() { local project_type="$1" if [[ "$project_type" == *"web"* ]]; then echo "backend/\\nfrontend/\\ntests/" else echo "src/\\ntests/" fi } get_commands_for_language() { local lang="$1" case "$lang" in *"Python"*) echo "cd src && pytest && ruff check ." ;; *"Rust"*) echo "cargo test && cargo clippy" ;; *"JavaScript"*|*"TypeScript"*) echo "npm test && npm run lint" ;; *) echo "# Add commands for $lang" ;; esac } get_language_conventions() { local lang="$1" echo "$lang: Follow standard conventions" } create_new_agent_file() { local target_file="$1" local temp_file="$2" local project_name="$3" local current_date="$4" if [[ ! -f "$TEMPLATE_FILE" ]]; then log_error "Template not found at $TEMPLATE_FILE" return 1 fi if [[ ! -r "$TEMPLATE_FILE" ]]; then log_error "Template file is not readable: $TEMPLATE_FILE" return 1 fi log_info "Creating new agent context file from template..." if ! cp "$TEMPLATE_FILE" "$temp_file"; then log_error "Failed to copy template file" return 1 fi # Replace template placeholders local project_structure project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") local commands commands=$(get_commands_for_language "$NEW_LANG") local language_conventions language_conventions=$(get_language_conventions "$NEW_LANG") # Perform substitutions with error checking using safer approach # Escape special characters for sed by using a different delimiter or escaping local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') # Build technology stack and recent change strings conditionally local tech_stack if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" elif [[ -n "$escaped_lang" ]]; then tech_stack="- $escaped_lang ($escaped_branch)" elif [[ -n "$escaped_framework" ]]; then tech_stack="- $escaped_framework ($escaped_branch)" else tech_stack="- ($escaped_branch)" fi local recent_change if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" elif [[ -n "$escaped_lang" ]]; then recent_change="- $escaped_branch: Added $escaped_lang" elif [[ -n "$escaped_framework" ]]; then recent_change="- $escaped_branch: Added $escaped_framework" else recent_change="- $escaped_branch: Added" fi local substitutions=( "s|\[PROJECT NAME\]|$project_name|" "s|\[DATE\]|$current_date|" "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" ) for substitution in "${substitutions[@]}"; do if ! sed -i.bak -e "$substitution" "$temp_file"; then log_error "Failed to perform substitution: $substitution" rm -f "$temp_file" "$temp_file.bak" return 1 fi done # Convert \n sequences to actual newlines newline=$(printf '\n') sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" # Clean up backup files rm -f "$temp_file.bak" "$temp_file.bak2" return 0 } update_existing_agent_file() { local target_file="$1" local current_date="$2" log_info "Updating existing agent context file..." # Use a single temporary file for atomic update local temp_file temp_file=$(mktemp) || { log_error "Failed to create temporary file" return 1 } # Process the file in one pass local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") local new_tech_entries=() local new_change_entry="" # Prepare new technology entries if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") fi if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") fi # Prepare new change entry if [[ -n "$tech_stack" ]]; then new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" fi # Process file line by line local in_tech_section=false local in_changes_section=false local tech_entries_added=false local changes_entries_added=false local existing_changes_count=0 while IFS= read -r line || [[ -n "$line" ]]; do # Handle Active Technologies section if [[ "$line" == "## Active Technologies" ]]; then echo "$line" >> "$temp_file" in_tech_section=true continue elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then # Add new tech entries before closing the section if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" tech_entries_added=true fi echo "$line" >> "$temp_file" in_tech_section=false continue elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then # Add new tech entries before empty line in tech section if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" tech_entries_added=true fi echo "$line" >> "$temp_file" continue fi # Handle Recent Changes section if [[ "$line" == "## Recent Changes" ]]; then echo "$line" >> "$temp_file" # Add new change entry right after the heading if [[ -n "$new_change_entry" ]]; then echo "$new_change_entry" >> "$temp_file" fi in_changes_section=true changes_entries_added=true continue elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then echo "$line" >> "$temp_file" in_changes_section=false continue elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then # Keep only first 2 existing changes if [[ $existing_changes_count -lt 2 ]]; then echo "$line" >> "$temp_file" ((existing_changes_count++)) fi continue fi # Update timestamp if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" else echo "$line" >> "$temp_file" fi done < "$target_file" # Post-loop check: if we're still in the Active Technologies section and haven't added new entries if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" fi # Move temp file to target atomically if ! mv "$temp_file" "$target_file"; then log_error "Failed to update target file" rm -f "$temp_file" return 1 fi return 0 } #============================================================================== # Main Agent File Update Function #============================================================================== update_agent_file() { local target_file="$1" local agent_name="$2" if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then log_error "update_agent_file requires target_file and agent_name parameters" return 1 fi log_info "Updating $agent_name context file: $target_file" local project_name project_name=$(basename "$REPO_ROOT") local current_date current_date=$(date +%Y-%m-%d) # Create directory if it doesn't exist local target_dir target_dir=$(dirname "$target_file") if [[ ! -d "$target_dir" ]]; then if ! mkdir -p "$target_dir"; then log_error "Failed to create directory: $target_dir" return 1 fi fi if [[ ! -f "$target_file" ]]; then # Create new file from template local temp_file temp_file=$(mktemp) || { log_error "Failed to create temporary file" return 1 } if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then if mv "$temp_file" "$target_file"; then log_success "Created new $agent_name context file" else log_error "Failed to move temporary file to $target_file" rm -f "$temp_file" return 1 fi else log_error "Failed to create new agent file" rm -f "$temp_file" return 1 fi else # Update existing file if [[ ! -r "$target_file" ]]; then log_error "Cannot read existing file: $target_file" return 1 fi if [[ ! -w "$target_file" ]]; then log_error "Cannot write to existing file: $target_file" return 1 fi if update_existing_agent_file "$target_file" "$current_date"; then log_success "Updated existing $agent_name context file" else log_error "Failed to update existing agent file" return 1 fi fi return 0 } #============================================================================== # Agent Selection and Processing #============================================================================== update_specific_agent() { local agent_type="$1" case "$agent_type" in claude) update_agent_file "$CLAUDE_FILE" "Claude Code" ;; gemini) update_agent_file "$GEMINI_FILE" "Gemini CLI" ;; copilot) update_agent_file "$COPILOT_FILE" "GitHub Copilot" ;; cursor) update_agent_file "$CURSOR_FILE" "Cursor IDE" ;; qwen) update_agent_file "$QWEN_FILE" "Qwen Code" ;; opencode) update_agent_file "$AGENTS_FILE" "opencode" ;; codex) update_agent_file "$AGENTS_FILE" "Codex CLI" ;; windsurf) update_agent_file "$WINDSURF_FILE" "Windsurf" ;; kilocode) update_agent_file "$KILOCODE_FILE" "Kilo Code" ;; auggie) update_agent_file "$AUGGIE_FILE" "Auggie CLI" ;; roo) update_agent_file "$ROO_FILE" "Roo Code" ;; *) log_error "Unknown agent type '$agent_type'" log_error "Expected: claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo" exit 1 ;; esac } update_all_existing_agents() { local found_agent=false # Check each possible agent file and update if it exists if [[ -f "$CLAUDE_FILE" ]]; then update_agent_file "$CLAUDE_FILE" "Claude Code" found_agent=true fi if [[ -f "$GEMINI_FILE" ]]; then update_agent_file "$GEMINI_FILE" "Gemini CLI" found_agent=true fi if [[ -f "$COPILOT_FILE" ]]; then update_agent_file "$COPILOT_FILE" "GitHub Copilot" found_agent=true fi if [[ -f "$CURSOR_FILE" ]]; then update_agent_file "$CURSOR_FILE" "Cursor IDE" found_agent=true fi if [[ -f "$QWEN_FILE" ]]; then update_agent_file "$QWEN_FILE" "Qwen Code" found_agent=true fi if [[ -f "$AGENTS_FILE" ]]; then update_agent_file "$AGENTS_FILE" "Codex/opencode" found_agent=true fi if [[ -f "$WINDSURF_FILE" ]]; then update_agent_file "$WINDSURF_FILE" "Windsurf" found_agent=true fi if [[ -f "$KILOCODE_FILE" ]]; then update_agent_file "$KILOCODE_FILE" "Kilo Code" found_agent=true fi if [[ -f "$AUGGIE_FILE" ]]; then update_agent_file "$AUGGIE_FILE" "Auggie CLI" found_agent=true fi if [[ -f "$ROO_FILE" ]]; then update_agent_file "$ROO_FILE" "Roo Code" found_agent=true fi # If no agent files exist, create a default Claude file if [[ "$found_agent" == false ]]; then log_info "No existing agent files found, creating default Claude file..." update_agent_file "$CLAUDE_FILE" "Claude Code" fi } print_summary() { echo log_info "Summary of changes:" if [[ -n "$NEW_LANG" ]]; then echo " - Added language: $NEW_LANG" fi if [[ -n "$NEW_FRAMEWORK" ]]; then echo " - Added framework: $NEW_FRAMEWORK" fi if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then echo " - Added database: $NEW_DB" fi echo log_info "Usage: $0 [claude|gemini|copilot|cursor|qwen|opencode|codex|windsurf|kilocode|auggie|roo]" } #============================================================================== # Main Execution #============================================================================== main() { # Validate environment before proceeding validate_environment log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" # Parse the plan file to extract project information if ! parse_plan_data "$NEW_PLAN"; then log_error "Failed to parse plan data" exit 1 fi # Process based on agent type argument local success=true if [[ -z "$AGENT_TYPE" ]]; then # No specific agent provided - update all existing agent files log_info "No agent specified, updating all existing agent files..." if ! update_all_existing_agents; then success=false fi else # Specific agent provided - update only that agent log_info "Updating specific agent: $AGENT_TYPE" if ! update_specific_agent "$AGENT_TYPE"; then success=false fi fi # Print summary print_summary if [[ "$success" == true ]]; then log_success "Agent context update completed successfully" exit 0 else log_error "Agent context update completed with errors" exit 1 fi } # Execute main function if script is run directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi