1#!/usr/bin/env bash
2# Incrementally update agent context files based on new feature plan
3# Supports: CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md
4# O(1) operation - only reads current context file and new plan.md
5
6set -e
7
8REPO_ROOT=$(git rev-parse --show-toplevel)
9CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
10FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
11NEW_PLAN="$FEATURE_DIR/plan.md"
12
13# Determine which agent context files to update
14CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
15GEMINI_FILE="$REPO_ROOT/GEMINI.md"
16COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
17
18# Allow override via argument
19AGENT_TYPE="$1"
20
21if [ ! -f "$NEW_PLAN" ]; then
22 echo "ERROR: No plan.md found at $NEW_PLAN"
23 exit 1
24fi
25
26echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
27
28# Extract tech from new plan
29NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
30NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
31NEW_TESTING=$(grep "^**Testing**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Testing**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
32NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "")
33NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "")
34
35# Function to update a single agent context file
36update_agent_file() {
37 local target_file="$1"
38 local agent_name="$2"
39
40 echo "Updating $agent_name context file: $target_file"
41
42 # Create temp file for new context
43 local temp_file=$(mktemp)
44
45 # If file doesn't exist, create from template
46 if [ ! -f "$target_file" ]; then
47 echo "Creating new $agent_name context file..."
48
49 # Check if this is the SDD repo itself
50 if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then
51 cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file"
52 else
53 echo "ERROR: Template not found at $REPO_ROOT/templates/agent-file-template.md"
54 return 1
55 fi
56
57 # Replace placeholders
58 sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file"
59 sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file"
60 sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file"
61
62 # Add project structure based on type
63 if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then
64 sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file"
65 else
66 sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file"
67 fi
68
69 # Add minimal commands
70 if [[ "$NEW_LANG" == *"Python"* ]]; then
71 COMMANDS="cd src && pytest && ruff check ."
72 elif [[ "$NEW_LANG" == *"Rust"* ]]; then
73 COMMANDS="cargo test && cargo clippy"
74 elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then
75 COMMANDS="npm test && npm run lint"
76 else
77 COMMANDS="# Add commands for $NEW_LANG"
78 fi
79 sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file"
80
81 # Add code style
82 sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"
83
84 # Add recent changes
85 sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file"
86
87 rm "$temp_file.bak"
88 else
89 echo "Updating existing $agent_name context file..."
90
91 # Extract manual additions
92 local manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1)
93 local manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$target_file" | cut -d: -f1)
94
95 if [ ! -z "$manual_start" ] && [ ! -z "$manual_end" ]; then
96 sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt
97 fi
98
99 # Parse existing file and create updated version
100 python3 - << EOF
101import re
102import sys
103from datetime import datetime
104
105# Read existing file
106with open("$target_file", 'r') as f:
107 content = f.read()
108
109# Check if new tech already exists
110tech_section = re.search(r'## Active Technologies\n(.*?)\n\n', content, re.DOTALL)
111if tech_section:
112 existing_tech = tech_section.group(1)
113
114 # Add new tech if not already present
115 new_additions = []
116 if "$NEW_LANG" and "$NEW_LANG" not in existing_tech:
117 new_additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)")
118 if "$NEW_DB" and "$NEW_DB" not in existing_tech and "$NEW_DB" != "N/A":
119 new_additions.append(f"- $NEW_DB ($CURRENT_BRANCH)")
120
121 if new_additions:
122 updated_tech = existing_tech + "\n" + "\n".join(new_additions)
123 content = content.replace(tech_section.group(0), f"## Active Technologies\n{updated_tech}\n\n")
124
125# Update project structure if needed
126if "$NEW_PROJECT_TYPE" == "web" and "frontend/" not in content:
127 struct_section = re.search(r'## Project Structure\n\`\`\`\n(.*?)\n\`\`\`', content, re.DOTALL)
128 if struct_section:
129 updated_struct = struct_section.group(1) + "\nfrontend/src/ # Web UI"
130 content = re.sub(r'(## Project Structure\n\`\`\`\n).*?(\n\`\`\`)',
131 f'\\1{updated_struct}\\2', content, flags=re.DOTALL)
132
133# Add new commands if language is new
134if "$NEW_LANG" and f"# {NEW_LANG}" not in content:
135 commands_section = re.search(r'## Commands\n\`\`\`bash\n(.*?)\n\`\`\`', content, re.DOTALL)
136 if not commands_section:
137 commands_section = re.search(r'## Commands\n(.*?)\n\n', content, re.DOTALL)
138
139 if commands_section:
140 new_commands = commands_section.group(1)
141 if "Python" in "$NEW_LANG":
142 new_commands += "\ncd src && pytest && ruff check ."
143 elif "Rust" in "$NEW_LANG":
144 new_commands += "\ncargo test && cargo clippy"
145 elif "JavaScript" in "$NEW_LANG" or "TypeScript" in "$NEW_LANG":
146 new_commands += "\nnpm test && npm run lint"
147
148 if "```bash" in content:
149 content = re.sub(r'(## Commands\n\`\`\`bash\n).*?(\n\`\`\`)',
150 f'\\1{new_commands}\\2', content, flags=re.DOTALL)
151 else:
152 content = re.sub(r'(## Commands\n).*?(\n\n)',
153 f'\\1{new_commands}\\2', content, flags=re.DOTALL)
154
155# Update recent changes (keep only last 3)
156changes_section = re.search(r'## Recent Changes\n(.*?)(\n\n|$)', content, re.DOTALL)
157if changes_section:
158 changes = changes_section.group(1).strip().split('\n')
159 changes.insert(0, f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK")
160 # Keep only last 3
161 changes = changes[:3]
162 content = re.sub(r'(## Recent Changes\n).*?(\n\n|$)',
163 f'\\1{chr(10).join(changes)}\\2', content, flags=re.DOTALL)
164
165# Update date
166content = re.sub(r'Last updated: \d{4}-\d{2}-\d{2}',
167 f'Last updated: {datetime.now().strftime("%Y-%m-%d")}', content)
168
169# Write to temp file
170with open("$temp_file", 'w') as f:
171 f.write(content)
172EOF
173
174 # Restore manual additions if they exist
175 if [ -f /tmp/manual_additions.txt ]; then
176 # Remove old manual section from temp file
177 sed -i.bak '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/d' "$temp_file"
178 # Append manual additions
179 cat /tmp/manual_additions.txt >> "$temp_file"
180 rm /tmp/manual_additions.txt "$temp_file.bak"
181 fi
182 fi
183
184 # Move temp file to final location
185 mv "$temp_file" "$target_file"
186 echo "✅ $agent_name context file updated successfully"
187}
188
189# Update files based on argument or detect existing files
190case "$AGENT_TYPE" in
191 "claude")
192 update_agent_file "$CLAUDE_FILE" "Claude Code"
193 ;;
194 "gemini")
195 update_agent_file "$GEMINI_FILE" "Gemini CLI"
196 ;;
197 "copilot")
198 update_agent_file "$COPILOT_FILE" "GitHub Copilot"
199 ;;
200 "")
201 # Update all existing files
202 [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"
203 [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"
204 [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"
205
206 # If no files exist, create based on current directory or ask user
207 if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then
208 echo "No agent context files found. Creating Claude Code context file by default."
209 update_agent_file "$CLAUDE_FILE" "Claude Code"
210 fi
211 ;;
212 *)
213 echo "ERROR: Unknown agent type '$AGENT_TYPE'. Use: claude, gemini, copilot, or leave empty for all."
214 exit 1
215 ;;
216esac
217echo ""
218echo "Summary of changes:"
219if [ ! -z "$NEW_LANG" ]; then
220 echo "- Added language: $NEW_LANG"
221fi
222if [ ! -z "$NEW_FRAMEWORK" ]; then
223 echo "- Added framework: $NEW_FRAMEWORK"
224fi
225if [ ! -z "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ]; then
226 echo "- Added database: $NEW_DB"
227fi
228
229echo ""
230echo "Usage: $0 [claude|gemini|copilot]"
231echo " - No argument: Update all existing agent context files"
232echo " - claude: Update only CLAUDE.md"
233echo " - gemini: Update only GEMINI.md"
234echo " - copilot: Update only .github/copilot-instructions.md"