From 739cb2ab4827918421617eb1d83c25bc95c97ae2 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 13:43:31 -0800 Subject: [PATCH] docs: add hooks guide, expand planner agent, add Django example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add hooks/README.md: comprehensive hook documentation with input schema, customization guide, 4 ready-to-use hook recipes, and cross-platform notes - Expand planner agent with full worked example (Stripe subscriptions plan) and sizing/phasing guidance (119 → 212 lines) - Add Django REST API example config (DRF + Celery + pytest + Factory Boy) - Update README directory tree with new files --- README.md | 2 + agents/planner.md | 93 ++++++++++ examples/django-api-CLAUDE.md | 308 ++++++++++++++++++++++++++++++++++ hooks/README.md | 198 ++++++++++++++++++++++ 4 files changed, 601 insertions(+) create mode 100644 examples/django-api-CLAUDE.md create mode 100644 hooks/README.md diff --git a/README.md b/README.md index 8c6dc3bc..e81c5125 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ everything-claude-code/ | |-- golang/ # Go specific | |-- hooks/ # Trigger-based automations +| |-- README.md # Hook documentation, recipes, and customization guide | |-- hooks.json # All hooks config (PreToolUse, PostToolUse, Stop, etc.) | |-- memory-persistence/ # Session lifecycle hooks (Longform Guide) | |-- strategic-compact/ # Compaction suggestions (Longform Guide) @@ -320,6 +321,7 @@ everything-claude-code/ | |-- user-CLAUDE.md # Example user-level config | |-- saas-nextjs-CLAUDE.md # Real-world SaaS (Next.js + Supabase + Stripe) | |-- go-microservice-CLAUDE.md # Real-world Go microservice (gRPC + PostgreSQL) +| |-- django-api-CLAUDE.md # Real-world Django REST API (DRF + Celery) | |-- mcp-configs/ # MCP server configurations | |-- mcp-servers.json # GitHub, Supabase, Vercel, Railway, etc. diff --git a/agents/planner.md b/agents/planner.md index 495dfc03..4150bd60 100644 --- a/agents/planner.md +++ b/agents/planner.md @@ -98,6 +98,85 @@ Create detailed steps with: 6. **Think Incrementally**: Each step should be verifiable 7. **Document Decisions**: Explain why, not just what +## Worked Example: Adding Stripe Subscriptions + +Here is a complete plan showing the level of detail expected: + +```markdown +# Implementation Plan: Stripe Subscription Billing + +## Overview +Add subscription billing with free/pro/enterprise tiers. Users upgrade via +Stripe Checkout, and webhook events keep subscription status in sync. + +## Requirements +- Three tiers: Free (default), Pro ($29/mo), Enterprise ($99/mo) +- Stripe Checkout for payment flow +- Webhook handler for subscription lifecycle events +- Feature gating based on subscription tier + +## Architecture Changes +- New table: `subscriptions` (user_id, stripe_customer_id, stripe_subscription_id, status, tier) +- New API route: `app/api/checkout/route.ts` — creates Stripe Checkout session +- New API route: `app/api/webhooks/stripe/route.ts` — handles Stripe events +- New middleware: check subscription tier for gated features +- New component: `PricingTable` — displays tiers with upgrade buttons + +## Implementation Steps + +### Phase 1: Database & Backend (2 files) +1. **Create subscription migration** (File: supabase/migrations/004_subscriptions.sql) + - Action: CREATE TABLE subscriptions with RLS policies + - Why: Store billing state server-side, never trust client + - Dependencies: None + - Risk: Low + +2. **Create Stripe webhook handler** (File: src/app/api/webhooks/stripe/route.ts) + - Action: Handle checkout.session.completed, customer.subscription.updated, + customer.subscription.deleted events + - Why: Keep subscription status in sync with Stripe + - Dependencies: Step 1 (needs subscriptions table) + - Risk: High — webhook signature verification is critical + +### Phase 2: Checkout Flow (2 files) +3. **Create checkout API route** (File: src/app/api/checkout/route.ts) + - Action: Create Stripe Checkout session with price_id and success/cancel URLs + - Why: Server-side session creation prevents price tampering + - Dependencies: Step 1 + - Risk: Medium — must validate user is authenticated + +4. **Build pricing page** (File: src/components/PricingTable.tsx) + - Action: Display three tiers with feature comparison and upgrade buttons + - Why: User-facing upgrade flow + - Dependencies: Step 3 + - Risk: Low + +### Phase 3: Feature Gating (1 file) +5. **Add tier-based middleware** (File: src/middleware.ts) + - Action: Check subscription tier on protected routes, redirect free users + - Why: Enforce tier limits server-side + - Dependencies: Steps 1-2 (needs subscription data) + - Risk: Medium — must handle edge cases (expired, past_due) + +## Testing Strategy +- Unit tests: Webhook event parsing, tier checking logic +- Integration tests: Checkout session creation, webhook processing +- E2E tests: Full upgrade flow (Stripe test mode) + +## Risks & Mitigations +- **Risk**: Webhook events arrive out of order + - Mitigation: Use event timestamps, idempotent updates +- **Risk**: User upgrades but webhook fails + - Mitigation: Poll Stripe as fallback, show "processing" state + +## Success Criteria +- [ ] User can upgrade from Free to Pro via Stripe Checkout +- [ ] Webhook correctly syncs subscription status +- [ ] Free users cannot access Pro features +- [ ] Downgrade/cancellation works correctly +- [ ] All tests pass with 80%+ coverage +``` + ## When Planning Refactors 1. Identify code smells and technical debt @@ -106,6 +185,17 @@ Create detailed steps with: 4. Create backwards-compatible changes when possible 5. Plan for gradual migration if needed +## Sizing and Phasing + +When the feature is large, break it into independently deliverable phases: + +- **Phase 1**: Minimum viable — smallest slice that provides value +- **Phase 2**: Core experience — complete happy path +- **Phase 3**: Edge cases — error handling, edge cases, polish +- **Phase 4**: Optimization — performance, monitoring, analytics + +Each phase should be mergeable independently. Avoid plans that require all phases to complete before anything works. + ## Red Flags to Check - Large functions (>50 lines) @@ -115,5 +205,8 @@ Create detailed steps with: - Hardcoded values - Missing tests - Performance bottlenecks +- Plans with no testing strategy +- Steps without clear file paths +- Phases that cannot be delivered independently **Remember**: A great plan is specific, actionable, and considers both the happy path and edge cases. The best plans enable confident, incremental implementation. diff --git a/examples/django-api-CLAUDE.md b/examples/django-api-CLAUDE.md new file mode 100644 index 00000000..efa5f541 --- /dev/null +++ b/examples/django-api-CLAUDE.md @@ -0,0 +1,308 @@ +# Django REST API — Project CLAUDE.md + +> Real-world example for a Django REST Framework API with PostgreSQL and Celery. +> Copy this to your project root and customize for your service. + +## Project Overview + +**Stack:** Python 3.12+, Django 5.x, Django REST Framework, PostgreSQL, Celery + Redis, pytest, Docker Compose + +**Architecture:** Domain-driven design with apps per business domain. DRF for API layer, Celery for async tasks, pytest for testing. All endpoints return JSON — no template rendering. + +## Critical Rules + +### Python Conventions + +- Type hints on all function signatures — use `from __future__ import annotations` +- No `print()` statements — use `logging.getLogger(__name__)` +- f-strings for string formatting, never `%` or `.format()` +- Use `pathlib.Path` not `os.path` for file operations +- Imports sorted with isort: stdlib, third-party, local (enforced by ruff) + +### Database + +- All queries use Django ORM — raw SQL only with `.raw()` and parameterized queries +- Migrations committed to git — never use `--fake` in production +- Use `select_related()` and `prefetch_related()` to prevent N+1 queries +- All models must have `created_at` and `updated_at` auto-fields +- Indexes on any field used in `filter()`, `order_by()`, or `WHERE` clauses + +```python +# BAD: N+1 query +orders = Order.objects.all() +for order in orders: + print(order.customer.name) # hits DB for each order + +# GOOD: Single query with join +orders = Order.objects.select_related("customer").all() +``` + +### Authentication + +- JWT via `djangorestframework-simplejwt` — access token (15 min) + refresh token (7 days) +- Permission classes on every view — never rely on default +- Use `IsAuthenticated` as base, add custom permissions for object-level access +- Token blacklisting enabled for logout + +### Serializers + +- Use `ModelSerializer` for simple CRUD, `Serializer` for complex validation +- Separate read and write serializers when input/output shapes differ +- Validate at serializer level, not in views — views should be thin + +```python +class CreateOrderSerializer(serializers.Serializer): + product_id = serializers.UUIDField() + quantity = serializers.IntegerField(min_value=1, max_value=100) + + def validate_product_id(self, value): + if not Product.objects.filter(id=value, active=True).exists(): + raise serializers.ValidationError("Product not found or inactive") + return value + +class OrderDetailSerializer(serializers.ModelSerializer): + customer = CustomerSerializer(read_only=True) + product = ProductSerializer(read_only=True) + + class Meta: + model = Order + fields = ["id", "customer", "product", "quantity", "total", "status", "created_at"] +``` + +### Error Handling + +- Use DRF exception handler for consistent error responses +- Custom exceptions for business logic in `core/exceptions.py` +- Never expose internal error details to clients + +```python +# core/exceptions.py +from rest_framework.exceptions import APIException + +class InsufficientStockError(APIException): + status_code = 409 + default_detail = "Insufficient stock for this order" + default_code = "insufficient_stock" +``` + +### Code Style + +- No emojis in code or comments +- Max line length: 120 characters (enforced by ruff) +- Classes: PascalCase, functions/variables: snake_case, constants: UPPER_SNAKE_CASE +- Views are thin — business logic lives in service functions or model methods + +## File Structure + +``` +config/ + settings/ + base.py # Shared settings + local.py # Dev overrides (DEBUG=True) + production.py # Production settings + urls.py # Root URL config + celery.py # Celery app configuration +apps/ + accounts/ # User auth, registration, profile + models.py + serializers.py + views.py + services.py # Business logic + tests/ + test_views.py + test_services.py + factories.py # Factory Boy factories + orders/ # Order management + models.py + serializers.py + views.py + services.py + tasks.py # Celery tasks + tests/ + products/ # Product catalog + models.py + serializers.py + views.py + tests/ +core/ + exceptions.py # Custom API exceptions + permissions.py # Shared permission classes + pagination.py # Custom pagination + middleware.py # Request logging, timing + tests/ +``` + +## Key Patterns + +### Service Layer + +```python +# apps/orders/services.py +from django.db import transaction + +def create_order(*, customer, product_id: uuid.UUID, quantity: int) -> Order: + """Create an order with stock validation and payment hold.""" + product = Product.objects.select_for_update().get(id=product_id) + + if product.stock < quantity: + raise InsufficientStockError() + + with transaction.atomic(): + order = Order.objects.create( + customer=customer, + product=product, + quantity=quantity, + total=product.price * quantity, + ) + product.stock -= quantity + product.save(update_fields=["stock", "updated_at"]) + + # Async: send confirmation email + send_order_confirmation.delay(order.id) + return order +``` + +### View Pattern + +```python +# apps/orders/views.py +class OrderViewSet(viewsets.ModelViewSet): + permission_classes = [IsAuthenticated] + pagination_class = StandardPagination + + def get_serializer_class(self): + if self.action == "create": + return CreateOrderSerializer + return OrderDetailSerializer + + def get_queryset(self): + return ( + Order.objects + .filter(customer=self.request.user) + .select_related("product", "customer") + .order_by("-created_at") + ) + + def perform_create(self, serializer): + order = create_order( + customer=self.request.user, + product_id=serializer.validated_data["product_id"], + quantity=serializer.validated_data["quantity"], + ) + serializer.instance = order +``` + +### Test Pattern (pytest + Factory Boy) + +```python +# apps/orders/tests/factories.py +import factory +from apps.accounts.tests.factories import UserFactory +from apps.products.tests.factories import ProductFactory + +class OrderFactory(factory.django.DjangoModelFactory): + class Meta: + model = "orders.Order" + + customer = factory.SubFactory(UserFactory) + product = factory.SubFactory(ProductFactory, stock=100) + quantity = 1 + total = factory.LazyAttribute(lambda o: o.product.price * o.quantity) + +# apps/orders/tests/test_views.py +import pytest +from rest_framework.test import APIClient + +@pytest.mark.django_db +class TestCreateOrder: + def setup_method(self): + self.client = APIClient() + self.user = UserFactory() + self.client.force_authenticate(self.user) + + def test_create_order_success(self): + product = ProductFactory(price=29_99, stock=10) + response = self.client.post("/api/orders/", { + "product_id": str(product.id), + "quantity": 2, + }) + assert response.status_code == 201 + assert response.data["total"] == 59_98 + + def test_create_order_insufficient_stock(self): + product = ProductFactory(stock=0) + response = self.client.post("/api/orders/", { + "product_id": str(product.id), + "quantity": 1, + }) + assert response.status_code == 409 + + def test_create_order_unauthenticated(self): + self.client.force_authenticate(None) + response = self.client.post("/api/orders/", {}) + assert response.status_code == 401 +``` + +## Environment Variables + +```bash +# Django +SECRET_KEY= +DEBUG=False +ALLOWED_HOSTS=api.example.com + +# Database +DATABASE_URL=postgres://user:pass@localhost:5432/myapp + +# Redis (Celery broker + cache) +REDIS_URL=redis://localhost:6379/0 + +# JWT +JWT_ACCESS_TOKEN_LIFETIME=15 # minutes +JWT_REFRESH_TOKEN_LIFETIME=10080 # minutes (7 days) + +# Email +EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend +EMAIL_HOST=smtp.example.com +``` + +## Testing Strategy + +```bash +# Run all tests +pytest --cov=apps --cov-report=term-missing + +# Run specific app tests +pytest apps/orders/tests/ -v + +# Run with parallel execution +pytest -n auto + +# Only failing tests from last run +pytest --lf +``` + +## ECC Workflow + +```bash +# Planning +/plan "Add order refund system with Stripe integration" + +# Development with TDD +/tdd # pytest-based TDD workflow + +# Review +/python-review # Python-specific code review +/security-scan # Django security audit +/code-review # General quality check + +# Verification +/verify # Build, lint, test, security scan +``` + +## Git Workflow + +- `feat:` new features, `fix:` bug fixes, `refactor:` code changes +- Feature branches from `main`, PRs required +- CI: ruff (lint + format), mypy (types), pytest (tests), safety (dep check) +- Deploy: Docker image, managed via Kubernetes or Railway diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 00000000..13726e8d --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,198 @@ +# Hooks + +Hooks are event-driven automations that fire before or after Claude Code tool executions. They enforce code quality, catch mistakes early, and automate repetitive checks. + +## How Hooks Work + +``` +User request → Claude picks a tool → PreToolUse hook runs → Tool executes → PostToolUse hook runs +``` + +- **PreToolUse** hooks run before the tool executes. They can **block** (exit code 2) or **warn** (stderr without blocking). +- **PostToolUse** hooks run after the tool completes. They can analyze output but cannot block. +- **Stop** hooks run after each Claude response. +- **SessionStart/SessionEnd** hooks run at session lifecycle boundaries. +- **PreCompact** hooks run before context compaction, useful for saving state. + +## Hooks in This Plugin + +### PreToolUse Hooks + +| Hook | Matcher | Behavior | Exit Code | +|------|---------|----------|-----------| +| **Dev server blocker** | `Bash` | Blocks `npm run dev` etc. outside tmux — ensures log access | 2 (blocks) | +| **Tmux reminder** | `Bash` | Suggests tmux for long-running commands (npm test, cargo build, docker) | 0 (warns) | +| **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) | +| **Doc file blocker** | `Write` | Blocks creation of random `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING) | 2 (blocks) | +| **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) | + +### PostToolUse Hooks + +| Hook | Matcher | What It Does | +|------|---------|-------------| +| **PR logger** | `Bash` | Logs PR URL and review command after `gh pr create` | +| **Build analysis** | `Bash` | Background analysis after build commands (async, non-blocking) | +| **Prettier format** | `Edit` | Auto-formats JS/TS files with Prettier after edits | +| **TypeScript check** | `Edit` | Runs `tsc --noEmit` after editing `.ts`/`.tsx` files | +| **console.log warning** | `Edit` | Warns about `console.log` statements in edited files | + +### Lifecycle Hooks + +| Hook | Event | What It Does | +|------|-------|-------------| +| **Session start** | `SessionStart` | Loads previous context and detects package manager | +| **Pre-compact** | `PreCompact` | Saves state before context compaction | +| **Console.log audit** | `Stop` | Checks all modified files for `console.log` after each response | +| **Session end** | `SessionEnd` | Persists session state for next session | +| **Pattern extraction** | `SessionEnd` | Evaluates session for extractable patterns (continuous learning) | + +## Customizing Hooks + +### Disabling a Hook + +Remove or comment out the hook entry in `hooks.json`. If installed as a plugin, override in your `~/.claude/settings.json`: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write", + "hooks": [], + "description": "Override: allow all .md file creation" + } + ] + } +} +``` + +### Writing Your Own Hook + +Hooks are shell commands that receive tool input as JSON on stdin and must output JSON on stdout. + +**Basic structure:** + +```javascript +// my-hook.js +let data = ''; +process.stdin.on('data', chunk => data += chunk); +process.stdin.on('end', () => { + const input = JSON.parse(data); + + // Access tool info + const toolName = input.tool_name; // "Edit", "Bash", "Write", etc. + const toolInput = input.tool_input; // Tool-specific parameters + const toolOutput = input.tool_output; // Only available in PostToolUse + + // Warn (non-blocking): write to stderr + console.error('[Hook] Warning message shown to Claude'); + + // Block (PreToolUse only): exit with code 2 + // process.exit(2); + + // Always output the original data to stdout + console.log(data); +}); +``` + +**Exit codes:** +- `0` — Success (continue execution) +- `2` — Block the tool call (PreToolUse only) +- Other non-zero — Error (logged but does not block) + +### Hook Input Schema + +```typescript +interface HookInput { + tool_name: string; // "Bash", "Edit", "Write", "Read", etc. + tool_input: { + command?: string; // Bash: the command being run + file_path?: string; // Edit/Write/Read: target file + old_string?: string; // Edit: text being replaced + new_string?: string; // Edit: replacement text + content?: string; // Write: file content + }; + tool_output?: { // PostToolUse only + output?: string; // Command/tool output + }; +} +``` + +### Async Hooks + +For hooks that should not block the main flow (e.g., background analysis): + +```json +{ + "type": "command", + "command": "node my-slow-hook.js", + "async": true, + "timeout": 30 +} +``` + +Async hooks run in the background. They cannot block tool execution. + +## Common Hook Recipes + +### Warn about TODO comments + +```json +{ + "matcher": "Edit", + "hooks": [{ + "type": "command", + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}console.log(d)})\"" + }], + "description": "Warn when adding TODO/FIXME comments" +} +``` + +### Block large file creation + +```json +{ + "matcher": "Write", + "hooks": [{ + "type": "command", + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const c=i.tool_input?.content||'';const lines=c.split('\\n').length;if(lines>800){console.error('[Hook] BLOCKED: File exceeds 800 lines ('+lines+' lines)');console.error('[Hook] Split into smaller, focused modules');process.exit(2)}console.log(d)})\"" + }], + "description": "Block creation of files larger than 800 lines" +} +``` + +### Auto-format Python files with ruff + +```json +{ + "matcher": "Edit", + "hooks": [{ + "type": "command", + "command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/\\.py$/.test(p)){const{execFileSync}=require('child_process');try{execFileSync('ruff',['format',p],{stdio:'pipe'})}catch(e){}}console.log(d)})\"" + }], + "description": "Auto-format Python files with ruff after edits" +} +``` + +### Require test files alongside new source files + +```json +{ + "matcher": "Write", + "hooks": [{ + "type": "command", + "command": "node -e \"const fs=require('fs');let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const p=i.tool_input?.file_path||'';if(/src\\/.*\\.(ts|js)$/.test(p)&&!/\\.test\\.|\\.spec\\./.test(p)){const testPath=p.replace(/\\.(ts|js)$/,'.test.$1');if(!fs.existsSync(testPath)){console.error('[Hook] No test file found for: '+p);console.error('[Hook] Expected: '+testPath);console.error('[Hook] Consider writing tests first (/tdd)')}}console.log(d)})\"" + }], + "description": "Remind to create tests when adding new source files" +} +``` + +## Cross-Platform Notes + +All hooks in this plugin use Node.js (`node -e` or `node script.js`) for maximum compatibility across Windows, macOS, and Linux. Avoid bash-specific syntax in hooks. + +## Related + +- [rules/common/hooks.md](../rules/common/hooks.md) — Hook architecture guidelines +- [skills/strategic-compact/](../skills/strategic-compact/) — Strategic compaction skill +- [scripts/hooks/](../scripts/hooks/) — Hook script implementations