Development Patterns & Solutions¶
This document captures proven patterns and solutions for clean design and robust development. It serves as a quick reference for recurring challenges.
Pattern Curation Philosophy¶
This document maintains 14 foundational patterns that apply across most amplihack development.
Patterns are kept when they:
- Solve recurring problems (used 3+ times in real PRs)
- Apply broadly across multiple agent types and scenarios
- Represent non-obvious solutions with working code
- Prevent costly errors or enable critical capabilities
Patterns are removed when they:
- Become project-specific (better suited for PROJECT.md or DISCOVERIES.md)
- Are one-time solutions (preserved in git history)
- Are obvious applications of existing patterns
- Haven't been referenced in 6+ months
Trust in Emergence: Removed patterns can re-emerge when needed. See git history for context: git log -p .claude/context/PATTERNS.md
This refactoring (2024-11): Reduced from 24 to 14 patterns (74% reduction) based on usage analysis and philosophy compliance. Removed patterns include: CI Failure Rapid Diagnosis, Incremental Processing, Configuration Single Source of Truth, Parallel Task Execution (covered in CLAUDE.md), Multi-Layer Security Sanitization, Reflection-Driven Self-Improvement, Unified Validation Flow, Modular User Visibility, and others that were either too specific or better documented elsewhere.
Core Architecture Patterns¶
Pattern: Bricks & Studs Module Design with Clear Public API¶
Philosophy Reference: See @.claude/context/PHILOSOPHY.md "The Brick Philosophy for AI Development" for the philosophical foundation of this pattern.
Challenge: Modules become tightly coupled, making them hard to regenerate or replace.
Solution: Design modules as self-contained "bricks" with clear "studs" (public API) defined via __all__.
"""Module docstring documents philosophy and public API.
Philosophy:
- Single responsibility
- Standard library only (when possible)
- Self-contained and regeneratable
Public API (the "studs"):
MainClass: Primary functionality
helper_function: Utility function
CONSTANT: Configuration value
"""
# ... implementation ...
__all__ = ["MainClass", "helper_function", "CONSTANT"]
Module Structure:
module_name/
├── __init__.py # Public interface via __all__
├── README.md # Contract specification
├── core.py # Implementation
├── tests/ # Test the contract
└── examples/ # Working examples
Key Points:
- Module docstring documents philosophy and public API
__all__defines the public interface explicitly- Standard library only for core utilities (avoid circular dependencies)
- Tests verify the contract, not implementation details
Pattern: Zero-BS Implementation¶
Philosophy Reference: See @.claude/context/PHILOSOPHY.md "Zero-BS Implementations" section for the core principle behind this pattern.
Challenge: Avoiding stub code and placeholders that serve no purpose.
Solution: Every function must work or not exist.
# BAD - Stub that does nothing
def process_payment(amount):
# TODO: Implement Stripe integration
raise NotImplementedError("Coming soon")
# GOOD - Working implementation
def process_payment(amount, payments_file="payments.json"):
"""Record payment locally - fully functional."""
payment = {
"amount": amount,
"timestamp": datetime.now().isoformat(),
"id": str(uuid.uuid4())
}
payments = []
if Path(payments_file).exists():
payments = json.loads(Path(payments_file).read_text())
payments.append(payment)
Path(payments_file).write_text(json.dumps(payments, indent=2))
return payment
Key Points:
- Every function must work or not exist
- Use files instead of external services initially
- No TODOs without working code
- Start simple, add complexity when needed
API & Integration Patterns¶
Pattern: API Validation Before Implementation¶
Challenge: Invalid API calls cause immediate failures. Wrong model names, missing imports, or incorrect types lead to 20-30 min debug cycles.
Solution: Validate APIs before implementation using official documentation.
Validation Checklist:
- Model/LLM APIs: Check model name format, verify parameters, test minimal example
- Imports/Libraries: Verify module exists, check function signatures
- Services/Config: Verify endpoints, check response format
- Error Handling: Plan for rate limits, timeouts, specific error types
# WRONG - assumptions without validation
client = Anthropic()
message = client.messages.create(
model="claude-3-5-sonnet-20241022", # ❌ Not verified
max_tokens="1024", # ❌ Wrong type
messages=[{"role": "user", "content": prompt}]
)
# RIGHT - validated against docs
VALID_MODELS = ["claude-3-opus-20240229", "claude-3-sonnet-20241022"]
model = "claude-3-sonnet-20241022" # ✓ Verified
max_tokens = 1024 # ✓ Correct type
if model not in VALID_MODELS:
raise ValueError(f"Invalid model: {model}")
try:
message = client.messages.create(
model=model,
max_tokens=max_tokens,
messages=[{"role": "user", "content": prompt}]
)
except Exception as e:
raise RuntimeError(f"API call failed: {e}")
Key Points:
- 5-10 min validation prevents 20-30 min debug cycles
- Use official documentation as source of truth
- Test imports and minimal examples before full implementation
Pattern: Claude Code SDK Integration¶
Challenge: Integrating Claude Code SDK requires proper environment setup and timeout handling.
Solution:
import asyncio
from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions
async def extract_with_claude_sdk(prompt: str, timeout_seconds: int = 120):
"""Extract using Claude Code SDK with proper timeout handling"""
try:
async with asyncio.timeout(timeout_seconds):
async with ClaudeSDKClient(
options=ClaudeCodeOptions(
system_prompt="Extract information...",
max_turns=1,
)
) as client:
await client.query(prompt)
response = ""
async for message in client.receive_response():
if hasattr(message, "content"):
content = getattr(message, "content", [])
if isinstance(content, list):
for block in content:
if hasattr(block, "text"):
response += getattr(block, "text", "")
return response
except asyncio.TimeoutError:
print(f"Claude Code SDK timed out after {timeout_seconds} seconds")
return ""
Key Points:
- 120-second timeout is optimal
- SDK only works in Claude Code environment
- Handle markdown in responses
Error Handling & Reliability Patterns¶
Pattern: Safe Subprocess Wrapper with Comprehensive Error Handling¶
Challenge: Subprocess calls fail with cryptic error messages. Different error types need different user guidance.
Solution: Create a safe subprocess wrapper with user-friendly, actionable error messages.
def safe_subprocess_call(
cmd: List[str],
context: str,
timeout: Optional[int] = 30,
) -> Tuple[int, str, str]:
"""Safely execute subprocess with comprehensive error handling."""
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout
)
return result.returncode, result.stdout, result.stderr
except FileNotFoundError:
cmd_name = cmd[0] if cmd else "command"
error_msg = f"Command not found: {cmd_name}\n"
if context:
error_msg += f"Context: {context}\n"
error_msg += "Please ensure the tool is installed and in your PATH."
return 127, "", error_msg
except subprocess.TimeoutExpired:
cmd_name = cmd[0] if cmd else "command"
error_msg = f"Command timed out after {timeout}s: {cmd_name}\n"
if context:
error_msg += f"Context: {context}\n"
return 124, "", error_msg
except Exception as e:
cmd_name = cmd[0] if cmd else "command"
error_msg = f"Unexpected error running {cmd_name}: {str(e)}\n"
if context:
error_msg += f"Context: {context}\n"
return 1, "", error_msg
Key Points:
- Standard exit codes (127 for command not found)
- Context parameter is critical - always tell users what operation failed
- User-friendly messages with actionable guidance
- No exceptions propagate
Pattern: Fail-Fast Prerequisite Checking¶
Challenge: Users start using a tool, get cryptic errors mid-workflow when dependencies are missing.
Solution: Check all prerequisites at startup with clear, actionable error messages.
@dataclass
class ToolCheckResult:
tool: str
available: bool
path: Optional[str] = None
version: Optional[str] = None
error: Optional[str] = None
class PrerequisiteChecker:
REQUIRED_TOOLS = {
"node": "--version",
"npm": "--version",
"uv": "--version",
}
def check_and_report(self) -> bool:
"""Check prerequisites and print report if any are missing."""
result = self.check_all_prerequisites()
if result.all_available:
return True
print(self.format_missing_prerequisites(result.missing_tools))
return False
class Launcher:
def prepare_launch(self) -> bool:
"""Check prerequisites FIRST before any other operations"""
checker = PrerequisiteChecker()
if not checker.check_and_report():
return False
return self._setup_environment()
Key Points:
- Check at entry point before any operations
- Check all at once - show all issues
- Structured results with dataclasses
- Never auto-install - user control first
Pattern: Resilient Batch Processing¶
Challenge: Processing large batches where individual items might fail.
Solution:
class ResilientProcessor:
async def process_batch(self, items):
results = {"succeeded": [], "failed": []}
for item in items:
try:
result = await self.process_item(item)
results["succeeded"].append(result)
self.save_results(results) # Save after every item
except Exception as e:
results["failed"].append({
"item": item,
"error": str(e),
"timestamp": datetime.now().isoformat()
})
continue # Continue processing other items
return results
Key Points:
- Save after every item - never lose progress
- Continue on failure - don't let one failure stop the batch
- Track failure reasons
Testing & Validation Patterns¶
Pattern: TDD Testing Pyramid for System Utilities¶
Challenge: Testing system utilities that interact with external tools while maintaining fast execution.
Solution: Follow testing pyramid with 60% unit tests, 30% integration tests, 10% E2E tests.
"""Tests for module - TDD approach.
Testing pyramid:
- 60% Unit tests (fast, heavily mocked)
- 30% Integration tests (multiple components)
- 10% E2E tests (complete workflows)
"""
# UNIT TESTS (60%)
class TestPlatformDetection:
def test_detect_macos(self):
with patch("platform.system", return_value="Darwin"):
checker = PrerequisiteChecker()
assert checker.platform == Platform.MACOS
# INTEGRATION TESTS (30%)
class TestPrerequisiteIntegration:
def test_full_check_workflow(self):
checker = PrerequisiteChecker()
with patch("shutil.which") as mock_which:
mock_which.side_effect = lambda x: f"/usr/bin/{x}"
result = checker.check_all_prerequisites()
assert result.all_available is True
# E2E TESTS (10%)
class TestEndToEnd:
def test_complete_workflow_with_guidance(self):
checker = PrerequisiteChecker()
result = checker.check_all_prerequisites()
message = checker.format_missing_prerequisites(result.missing_tools)
assert "prerequisite" in message.lower()
Key Points:
- 60% unit tests for speed
- Strategic mocking of external dependencies
- E2E tests for complete workflows
- All tests run in seconds
Environment & Platform Patterns¶
Pattern: Platform-Specific Installation Guidance¶
Challenge: Users on different platforms need different installation commands.
Solution: Detect platform automatically and provide exact installation commands.
class Platform(Enum):
MACOS = "macos"
LINUX = "linux"
WSL = "wsl"
WINDOWS = "windows"
class PrerequisiteChecker:
INSTALL_COMMANDS = {
Platform.MACOS: {
"node": "brew install node",
"git": "brew install git",
},
Platform.LINUX: {
"node": "# Ubuntu/Debian:\nsudo apt install nodejs\n# Fedora:\nsudo dnf install nodejs",
},
}
def get_install_command(self, tool: str) -> str:
platform_commands = self.INSTALL_COMMANDS.get(self.platform, {})
return platform_commands.get(tool, f"Please install {tool} manually")
Key Points:
- Automatic platform detection (including WSL)
- Multiple package managers for Linux
- Documentation links for complex installations
Pattern: Graceful Environment Adaptation¶
Challenge: Different behavior needed in different environments (UVX, normal, testing).
Solution: Detect environment automatically and adapt through configuration objects.
class EnvironmentAdapter:
def detect_environment(self) -> str:
if self._is_uvx_environment():
return "uvx"
elif self._is_testing_environment():
return "testing"
else:
return "normal"
def get_config(self) -> Dict[str, Any]:
env = self.detect_environment()
configs = {
"uvx": {"use_add_dir": True, "timeout_multiplier": 1.5},
"normal": {"use_add_dir": False, "timeout_multiplier": 1.0},
"testing": {"use_add_dir": False, "timeout_multiplier": 0.5},
}
config = configs.get(env, configs["normal"])
self._apply_env_overrides() # Allow env variable overrides
return config
Key Points:
- Automatic environment detection
- Configuration objects over scattered conditionals
- Environment variable overrides for customization
Performance & Optimization Patterns¶
Pattern: Intelligent Caching with Lifecycle Management¶
Challenge: Expensive operations repeated unnecessarily, but naive caching leads to memory leaks.
Solution: Smart caching with invalidation strategies.
from functools import lru_cache
import threading
class SmartCache:
@lru_cache(maxsize=128)
def expensive_operation(self, input_data: str) -> str:
return self._compute_expensive_result(input_data)
def invalidate_cache(self) -> None:
with self._lock:
self.expensive_operation.cache_clear()
def get_cache_stats(self) -> Dict[str, Any]:
cache_info = self.expensive_operation.cache_info()
return {
"hits": cache_info.hits,
"misses": cache_info.misses,
"hit_rate": cache_info.hits / max(1, cache_info.hits + cache_info.misses)
}
Key Points:
- lru_cache for automatic size management
- Thread safety is essential
- Provide invalidation methods
- Track cache performance
File I/O & Async Patterns¶
Pattern: File I/O with Cloud Sync Resilience¶
Challenge: File operations fail mysteriously when directories are synced with cloud services.
Solution:
def write_with_retry(filepath: Path, data: str, max_retries: int = 3):
"""Write file with exponential backoff for cloud sync issues"""
retry_delay = 0.1
for attempt in range(max_retries):
try:
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_text(data)
return
except OSError as e:
if e.errno == 5 and attempt < max_retries - 1:
if attempt == 0:
print("File I/O error - retrying. May be cloud sync issue.")
time.sleep(retry_delay)
retry_delay *= 2
else:
raise
Key Points:
- Exponential backoff for cloud sync
- Inform user about delays
- Create parent directories
Pattern: System Metadata vs User Content Classification in Git Operations¶
Challenge: Git-aware operations treat framework-generated metadata files (like .version, .state) as user content, causing false conflict warnings when files are auto-updated by the system.
Solution: Explicitly categorize and filter system-generated files based on semantic purpose, not just directory structure.
from pathlib import Path
from typing import Set, List
class GitAwareFileFilter:
"""Distinguish system metadata from user content in git operations"""
# System-generated files that should never trigger conflicts
SYSTEM_METADATA = {
".version", # Framework version tracking
".state", # Runtime state
"settings.json", # Auto-generated settings
"*.pyc", # Compiled bytecode
"__pycache__", # Python cache
".pytest_cache", # Test cache
}
def _filter_conflicts(
self, uncommitted_files: List[str], essential_dirs: List[str]
) -> List[str]:
"""Filter git status to exclude system metadata"""
conflicts = []
for file_path in uncommitted_files:
if file_path.startswith(".claude/"):
relative_path = file_path[8:] # Strip ".claude/"
# Skip system-generated metadata - safe to overwrite
if relative_path in self.SYSTEM_METADATA:
continue
# Check if file is in essential directories (user content)
for essential_dir in essential_dirs:
if (
relative_path.startswith(essential_dir + "/")
or relative_path == essential_dir
):
conflicts.append(file_path)
break
return conflicts
Usage in conflict detection:
class ConflictChecker:
def check_conflicts(self, source_dir: Path, essential_dirs: List[str]) -> List[Path]:
"""Check for REAL conflicts - ignore system metadata"""
result = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True, text=True, cwd=source_dir
)
uncommitted = self._parse_git_status(result.stdout)
user_changes = self._filter_conflicts(uncommitted, essential_dirs)
if user_changes:
raise ConflictError(
f"Uncommitted user content: {user_changes}\n"
f"(System metadata changes are normal and ignored)"
)
Key Points:
- Semantic categorization: Filter by PURPOSE (system vs user), not location
- Root-level awareness: Don't assume all root files are user content
- Clear error messages: Tell users when conflicts are real vs system noise
- Philosophy alignment: Ruthlessly simple - add explicit exclusion list
- Common pitfall: Only checking subdirectories and missing root-level system files
Origin: Discovered investigating
.versionfile causing false conflicts during UVX deployment. See DISCOVERIES.md (2025-12-01).
Pattern: Async Context Management¶
Challenge: Nested asyncio event loops cause hangs.
Solution: Design APIs to be fully async or fully sync, not both.
# WRONG - Creates nested event loops
class Service:
def process(self, data):
return asyncio.run(self._async_process(data)) # Creates new loop
# RIGHT - Pure async throughout
class Service:
async def process(self, data):
return await self._async_process(data) # No new loop
Key Points:
- Never mix sync/async APIs
- Avoid asyncio.run() in libraries
- Let caller manage the event loop
Documentation & Investigation Patterns¶
Pattern: Documentation Discovery Before Code Analysis¶
Challenge: Agents dive into code without checking if documentation already explains the system.
Solution: Always perform documentation discovery before code analysis.
Process:
- Search for documentation files (README, ARCHITECTURE, docs/)
- Filter by relevance using keywords
- Read top 5 most relevant files
- Establish documentation baseline
- Use docs to guide code analysis
Before analyzing [TOPIC], discover existing documentation:
1. Glob: **/README.md, **/ARCHITECTURE.md, **/docs/**/\*.md
2. Grep: Search for keywords related to TOPIC
3. Read: Top 5 most relevant files
4. Establish: What docs claim vs what exists
5. Analyze: Verify code matches docs, identify gaps
Key Points:
- Always discover docs first (30-second limit)
- Identify doc/code discrepancies
- Graceful degradation for missing docs
Decision-Making Patterns¶
Pattern: Cross-Domain Pattern Applicability Analysis¶
Challenge: Teams import "industry best practices" from other domains without validating applicability, leading to unnecessary complexity.
Solution: Five-phase framework for evaluating pattern adoption from other domains.
Phase 1: Threat Model Match
- Identify actual failure modes in YOUR system
- Identify pattern's target failure modes
- Verify failure modes match
- If mismatch, REJECT pattern
Phase 2: Mechanism Appropriateness
- Does pattern assume adversarial nodes? (Usually wrong for AI agents)
- Does pattern optimize for network communication? (Usually irrelevant for AI)
- Does pattern solve YOUR domain's specific problem?
Phase 3: Complexity Justification
If ratio < 3.0, seek simpler alternatives.
Phase 4: Domain Validation
- Research pattern's origin domain
- Verify target domain shares those characteristics
- Check for successful applications in similar contexts
Phase 5: Alternative Exploration
- Can simpler mechanisms achieve same benefits?
- Can you get 80% of benefit with 20% of complexity?
Key Points:
- Threat model mismatch is primary source of inappropriate pattern adoption
- Distributed systems patterns rarely map to AI agent systems
- "Industry best practice" without context validation is a red flag
- Default to ruthless simplicity unless complexity clearly justified
Origin: Discovered evaluating PBZFT vs N-Version Programming. PBZFT would be 6-9x more complex with zero benefit. See DISCOVERIES.md (2025-10-20).
Multi-Model AI Patterns¶
Pattern: Multi-Model Validation Anti-Pattern (STOP Gates)¶
Challenge: Validation checkpoints in AI guidance can trigger model-specific responses, helping one model while breaking another.
Problem: STOP gates added to improve Opus caused Sonnet degradation:
- Opus 4.5: STOP gates help (20/22 → 22/22 steps) ✅
- Sonnet 4.5: STOP gates break (22/22 → 8/22 steps) ❌
- Same text, opposite outcomes
Solution: Remove validation checkpoints, use flow language instead.
Example - Bad (STOP Gates):
## Step 1: Create GitHub Issue
Create an issue for your feature.
## STOP - Verify Issue Created
Before proceeding to Step 2, confirm:
- [ ] GitHub issue created
- [ ] Issue number recorded
Only proceed after verification complete.
## Step 2: Create Branch
...
Example - Good (Flow Language):
## Step 1: Create GitHub Issue
Create an issue for your feature.
## Step 2: Create Branch
After creating the issue, create a feature branch...
Why This Works:
- Provides clear structure without interruption points
- Uses flow language ("After X, do Y") not interruption language ("STOP before Y")
- Allows continuous autonomous execution
- Works for both models
Empirical Evidence (Issue #1755, 6/8 benchmarks complete):
| Model | With STOP Gates | Without STOP Gates (V2) |
|---|---|---|
| Sonnet | 8/22 steps (36%) | 22/22 steps (100%) |
| Opus | 22/22 steps | ~20/22 steps (maintains baseline) |
Performance Results:
- Sonnet V2: -16% cost improvement
- Opus V2: -21% cost improvement
- Removing gates IMPROVES performance (STOP Gate Paradox)
Key Points:
- Different models interpret "STOP" differently
- Opus: Treats as checkpoint, proceeds
- Sonnet: Treats as permission gate, asks user
- High-salience language ("STOP", "MUST", ALL CAPS) risky
- Always test multi-model before deploying guidance changes
When to Use Flow Language:
- "After X, proceed to Y" ✅
- "When X completes, Y begins" ✅
- "Following X, continue with Y" ✅
When to AVOID Interruption Language:
- "STOP before Y" ❌
- "Only proceed after X" ❌
- "Wait for confirmation before Y" ❌
Related: Issue #1755, DISCOVERIES.md (2025-12-01) Validation: 75% complete (6/8 benchmarks), both models tested Impact: \(20K-\)406K annual savings from removing STOP gates
Remember¶
These patterns represent proven solutions from real development challenges:
- Check this document first - Don't reinvent solutions
- Update when learning - Keep patterns current
- Include context - Explain why, not just how
- Show working code - Examples should be copy-pasteable
- Document gotchas - Save others from the same pain