From d70bab85e33af7a03b78c70dba7a7ce3b01d1b17 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 25 Feb 2026 10:45:29 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Cursor,=20Codex,=20and=20OpenCode?= =?UTF-8?q?=20harnesses=20=E2=80=94=20maximize=20every=20AI=20coding=20too?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AGENTS.md: universal cross-tool file read by Claude Code, Cursor, Codex, and OpenCode - .cursor/: 15 hook events via hooks.json, 16 hook scripts with DRY adapter pattern, 29 rules (9 common + 20 language-specific) with Cursor YAML frontmatter - .codex/: reference config.toml, Codex-specific AGENTS.md supplement, 10 skills ported to .agents/skills/ with openai.yaml metadata - .opencode/: 3 new tools (format-code, lint-check, git-summary), 3 new hooks (shell.env, experimental.session.compacting, permission.ask), expanded instructions, version bumped to 1.6.0 - README: fixed Cursor section, added Codex section, added cross-tool parity table - install.sh: now copies hooks.json + hooks/ for --target cursor --- .agents/skills/api-design/SKILL.md | 523 ++++++++++++++ .agents/skills/api-design/agents/openai.yaml | 7 + .agents/skills/backend-patterns/SKILL.md | 598 ++++++++++++++++ .../backend-patterns/agents/openai.yaml | 7 + .agents/skills/coding-standards/SKILL.md | 530 +++++++++++++++ .../coding-standards/agents/openai.yaml | 7 + .agents/skills/e2e-testing/SKILL.md | 326 +++++++++ .agents/skills/e2e-testing/agents/openai.yaml | 7 + .agents/skills/eval-harness/SKILL.md | 236 +++++++ .../skills/eval-harness/agents/openai.yaml | 7 + .agents/skills/frontend-patterns/SKILL.md | 642 ++++++++++++++++++ .../frontend-patterns/agents/openai.yaml | 7 + .agents/skills/security-review/SKILL.md | 495 ++++++++++++++ .../skills/security-review/agents/openai.yaml | 7 + .agents/skills/strategic-compact/SKILL.md | 103 +++ .../strategic-compact/agents/openai.yaml | 7 + .agents/skills/tdd-workflow/SKILL.md | 410 +++++++++++ .../skills/tdd-workflow/agents/openai.yaml | 7 + .agents/skills/verification-loop/SKILL.md | 126 ++++ .../verification-loop/agents/openai.yaml | 7 + .codex/AGENTS.md | 55 ++ .codex/config.toml | 80 +++ .cursor/hooks.json | 109 +++ .cursor/hooks/adapter.js | 62 ++ .cursor/hooks/after-file-edit.js | 17 + .cursor/hooks/after-mcp-execution.js | 12 + .cursor/hooks/after-shell-execution.js | 26 + .cursor/hooks/after-tab-file-edit.js | 12 + .cursor/hooks/before-mcp-execution.js | 11 + .cursor/hooks/before-read-file.js | 13 + .cursor/hooks/before-shell-execution.js | 27 + .cursor/hooks/before-submit-prompt.js | 23 + .cursor/hooks/before-tab-file-read.js | 13 + .cursor/hooks/pre-compact.js | 7 + .cursor/hooks/session-end.js | 9 + .cursor/hooks/session-start.js | 8 + .cursor/hooks/stop.js | 7 + .cursor/hooks/subagent-start.js | 10 + .cursor/hooks/subagent-stop.js | 10 + .cursor/rules/common-agents.md | 53 ++ .cursor/rules/common-coding-style.md | 52 ++ .cursor/rules/common-development-workflow.md | 33 + .cursor/rules/common-git-workflow.md | 28 + .cursor/rules/common-hooks.md | 34 + .cursor/rules/common-patterns.md | 35 + .cursor/rules/common-performance.md | 59 ++ .cursor/rules/common-security.md | 33 + .cursor/rules/common-testing.md | 33 + .cursor/rules/golang-coding-style.md | 31 + .cursor/rules/golang-hooks.md | 16 + .cursor/rules/golang-patterns.md | 44 ++ .cursor/rules/golang-security.md | 33 + .cursor/rules/golang-testing.md | 30 + .cursor/rules/python-coding-style.md | 42 ++ .cursor/rules/python-hooks.md | 19 + .cursor/rules/python-patterns.md | 39 ++ .cursor/rules/python-security.md | 30 + .cursor/rules/python-testing.md | 38 ++ .cursor/rules/swift-coding-style.md | 47 ++ .cursor/rules/swift-hooks.md | 20 + .cursor/rules/swift-patterns.md | 66 ++ .cursor/rules/swift-security.md | 33 + .cursor/rules/swift-testing.md | 45 ++ .cursor/rules/typescript-coding-style.md | 63 ++ .cursor/rules/typescript-hooks.md | 20 + .cursor/rules/typescript-patterns.md | 50 ++ .cursor/rules/typescript-security.md | 26 + .cursor/rules/typescript-testing.md | 16 + .opencode/index.ts | 13 +- .opencode/opencode.json | 10 +- .opencode/package.json | 2 +- .opencode/plugins/ecc-hooks.ts | 136 +++- .opencode/tools/format-code.ts | 66 ++ .opencode/tools/git-summary.ts | 56 ++ .opencode/tools/index.ts | 3 + .opencode/tools/lint-check.ts | 74 ++ AGENTS.md | 136 ++++ README.md | 141 +++- .../sessions/2026-01-17-debugging-memory.tmp | 54 -- examples/sessions/2026-01-19-refactor-api.tmp | 43 -- examples/sessions/2026-01-20-feature-auth.tmp | 76 --- install.sh | 11 + package.json | 2 +- 83 files changed, 6249 insertions(+), 212 deletions(-) create mode 100644 .agents/skills/api-design/SKILL.md create mode 100644 .agents/skills/api-design/agents/openai.yaml create mode 100644 .agents/skills/backend-patterns/SKILL.md create mode 100644 .agents/skills/backend-patterns/agents/openai.yaml create mode 100644 .agents/skills/coding-standards/SKILL.md create mode 100644 .agents/skills/coding-standards/agents/openai.yaml create mode 100644 .agents/skills/e2e-testing/SKILL.md create mode 100644 .agents/skills/e2e-testing/agents/openai.yaml create mode 100644 .agents/skills/eval-harness/SKILL.md create mode 100644 .agents/skills/eval-harness/agents/openai.yaml create mode 100644 .agents/skills/frontend-patterns/SKILL.md create mode 100644 .agents/skills/frontend-patterns/agents/openai.yaml create mode 100644 .agents/skills/security-review/SKILL.md create mode 100644 .agents/skills/security-review/agents/openai.yaml create mode 100644 .agents/skills/strategic-compact/SKILL.md create mode 100644 .agents/skills/strategic-compact/agents/openai.yaml create mode 100644 .agents/skills/tdd-workflow/SKILL.md create mode 100644 .agents/skills/tdd-workflow/agents/openai.yaml create mode 100644 .agents/skills/verification-loop/SKILL.md create mode 100644 .agents/skills/verification-loop/agents/openai.yaml create mode 100644 .codex/AGENTS.md create mode 100644 .codex/config.toml create mode 100644 .cursor/hooks.json create mode 100644 .cursor/hooks/adapter.js create mode 100644 .cursor/hooks/after-file-edit.js create mode 100644 .cursor/hooks/after-mcp-execution.js create mode 100644 .cursor/hooks/after-shell-execution.js create mode 100644 .cursor/hooks/after-tab-file-edit.js create mode 100644 .cursor/hooks/before-mcp-execution.js create mode 100644 .cursor/hooks/before-read-file.js create mode 100644 .cursor/hooks/before-shell-execution.js create mode 100644 .cursor/hooks/before-submit-prompt.js create mode 100644 .cursor/hooks/before-tab-file-read.js create mode 100644 .cursor/hooks/pre-compact.js create mode 100644 .cursor/hooks/session-end.js create mode 100644 .cursor/hooks/session-start.js create mode 100644 .cursor/hooks/stop.js create mode 100644 .cursor/hooks/subagent-start.js create mode 100644 .cursor/hooks/subagent-stop.js create mode 100644 .cursor/rules/common-agents.md create mode 100644 .cursor/rules/common-coding-style.md create mode 100644 .cursor/rules/common-development-workflow.md create mode 100644 .cursor/rules/common-git-workflow.md create mode 100644 .cursor/rules/common-hooks.md create mode 100644 .cursor/rules/common-patterns.md create mode 100644 .cursor/rules/common-performance.md create mode 100644 .cursor/rules/common-security.md create mode 100644 .cursor/rules/common-testing.md create mode 100644 .cursor/rules/golang-coding-style.md create mode 100644 .cursor/rules/golang-hooks.md create mode 100644 .cursor/rules/golang-patterns.md create mode 100644 .cursor/rules/golang-security.md create mode 100644 .cursor/rules/golang-testing.md create mode 100644 .cursor/rules/python-coding-style.md create mode 100644 .cursor/rules/python-hooks.md create mode 100644 .cursor/rules/python-patterns.md create mode 100644 .cursor/rules/python-security.md create mode 100644 .cursor/rules/python-testing.md create mode 100644 .cursor/rules/swift-coding-style.md create mode 100644 .cursor/rules/swift-hooks.md create mode 100644 .cursor/rules/swift-patterns.md create mode 100644 .cursor/rules/swift-security.md create mode 100644 .cursor/rules/swift-testing.md create mode 100644 .cursor/rules/typescript-coding-style.md create mode 100644 .cursor/rules/typescript-hooks.md create mode 100644 .cursor/rules/typescript-patterns.md create mode 100644 .cursor/rules/typescript-security.md create mode 100644 .cursor/rules/typescript-testing.md create mode 100644 .opencode/tools/format-code.ts create mode 100644 .opencode/tools/git-summary.ts create mode 100644 .opencode/tools/lint-check.ts create mode 100644 AGENTS.md delete mode 100644 examples/sessions/2026-01-17-debugging-memory.tmp delete mode 100644 examples/sessions/2026-01-19-refactor-api.tmp delete mode 100644 examples/sessions/2026-01-20-feature-auth.tmp diff --git a/.agents/skills/api-design/SKILL.md b/.agents/skills/api-design/SKILL.md new file mode 100644 index 00000000..a45aca06 --- /dev/null +++ b/.agents/skills/api-design/SKILL.md @@ -0,0 +1,523 @@ +--- +name: api-design +description: REST API design patterns including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs. +origin: ECC +--- + +# API Design Patterns + +Conventions and best practices for designing consistent, developer-friendly REST APIs. + +## When to Activate + +- Designing new API endpoints +- Reviewing existing API contracts +- Adding pagination, filtering, or sorting +- Implementing error handling for APIs +- Planning API versioning strategy +- Building public or partner-facing APIs + +## Resource Design + +### URL Structure + +``` +# Resources are nouns, plural, lowercase, kebab-case +GET /api/v1/users +GET /api/v1/users/:id +POST /api/v1/users +PUT /api/v1/users/:id +PATCH /api/v1/users/:id +DELETE /api/v1/users/:id + +# Sub-resources for relationships +GET /api/v1/users/:id/orders +POST /api/v1/users/:id/orders + +# Actions that don't map to CRUD (use verbs sparingly) +POST /api/v1/orders/:id/cancel +POST /api/v1/auth/login +POST /api/v1/auth/refresh +``` + +### Naming Rules + +``` +# GOOD +/api/v1/team-members # kebab-case for multi-word resources +/api/v1/orders?status=active # query params for filtering +/api/v1/users/123/orders # nested resources for ownership + +# BAD +/api/v1/getUsers # verb in URL +/api/v1/user # singular (use plural) +/api/v1/team_members # snake_case in URLs +/api/v1/users/123/getOrders # verb in nested resource +``` + +## HTTP Methods and Status Codes + +### Method Semantics + +| Method | Idempotent | Safe | Use For | +|--------|-----------|------|---------| +| GET | Yes | Yes | Retrieve resources | +| POST | No | No | Create resources, trigger actions | +| PUT | Yes | No | Full replacement of a resource | +| PATCH | No* | No | Partial update of a resource | +| DELETE | Yes | No | Remove a resource | + +*PATCH can be made idempotent with proper implementation + +### Status Code Reference + +``` +# Success +200 OK — GET, PUT, PATCH (with response body) +201 Created — POST (include Location header) +204 No Content — DELETE, PUT (no response body) + +# Client Errors +400 Bad Request — Validation failure, malformed JSON +401 Unauthorized — Missing or invalid authentication +403 Forbidden — Authenticated but not authorized +404 Not Found — Resource doesn't exist +409 Conflict — Duplicate entry, state conflict +422 Unprocessable Entity — Semantically invalid (valid JSON, bad data) +429 Too Many Requests — Rate limit exceeded + +# Server Errors +500 Internal Server Error — Unexpected failure (never expose details) +502 Bad Gateway — Upstream service failed +503 Service Unavailable — Temporary overload, include Retry-After +``` + +### Common Mistakes + +``` +# BAD: 200 for everything +{ "status": 200, "success": false, "error": "Not found" } + +# GOOD: Use HTTP status codes semantically +HTTP/1.1 404 Not Found +{ "error": { "code": "not_found", "message": "User not found" } } + +# BAD: 500 for validation errors +# GOOD: 400 or 422 with field-level details + +# BAD: 200 for created resources +# GOOD: 201 with Location header +HTTP/1.1 201 Created +Location: /api/v1/users/abc-123 +``` + +## Response Format + +### Success Response + +```json +{ + "data": { + "id": "abc-123", + "email": "alice@example.com", + "name": "Alice", + "created_at": "2025-01-15T10:30:00Z" + } +} +``` + +### Collection Response (with Pagination) + +```json +{ + "data": [ + { "id": "abc-123", "name": "Alice" }, + { "id": "def-456", "name": "Bob" } + ], + "meta": { + "total": 142, + "page": 1, + "per_page": 20, + "total_pages": 8 + }, + "links": { + "self": "/api/v1/users?page=1&per_page=20", + "next": "/api/v1/users?page=2&per_page=20", + "last": "/api/v1/users?page=8&per_page=20" + } +} +``` + +### Error Response + +```json +{ + "error": { + "code": "validation_error", + "message": "Request validation failed", + "details": [ + { + "field": "email", + "message": "Must be a valid email address", + "code": "invalid_format" + }, + { + "field": "age", + "message": "Must be between 0 and 150", + "code": "out_of_range" + } + ] + } +} +``` + +### Response Envelope Variants + +```typescript +// Option A: Envelope with data wrapper (recommended for public APIs) +interface ApiResponse { + data: T; + meta?: PaginationMeta; + links?: PaginationLinks; +} + +interface ApiError { + error: { + code: string; + message: string; + details?: FieldError[]; + }; +} + +// Option B: Flat response (simpler, common for internal APIs) +// Success: just return the resource directly +// Error: return error object +// Distinguish by HTTP status code +``` + +## Pagination + +### Offset-Based (Simple) + +``` +GET /api/v1/users?page=2&per_page=20 + +# Implementation +SELECT * FROM users +ORDER BY created_at DESC +LIMIT 20 OFFSET 20; +``` + +**Pros:** Easy to implement, supports "jump to page N" +**Cons:** Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts + +### Cursor-Based (Scalable) + +``` +GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20 + +# Implementation +SELECT * FROM users +WHERE id > :cursor_id +ORDER BY id ASC +LIMIT 21; -- fetch one extra to determine has_next +``` + +```json +{ + "data": [...], + "meta": { + "has_next": true, + "next_cursor": "eyJpZCI6MTQzfQ" + } +} +``` + +**Pros:** Consistent performance regardless of position, stable with concurrent inserts +**Cons:** Cannot jump to arbitrary page, cursor is opaque + +### When to Use Which + +| Use Case | Pagination Type | +|----------|----------------| +| Admin dashboards, small datasets (<10K) | Offset | +| Infinite scroll, feeds, large datasets | Cursor | +| Public APIs | Cursor (default) with offset (optional) | +| Search results | Offset (users expect page numbers) | + +## Filtering, Sorting, and Search + +### Filtering + +``` +# Simple equality +GET /api/v1/orders?status=active&customer_id=abc-123 + +# Comparison operators (use bracket notation) +GET /api/v1/products?price[gte]=10&price[lte]=100 +GET /api/v1/orders?created_at[after]=2025-01-01 + +# Multiple values (comma-separated) +GET /api/v1/products?category=electronics,clothing + +# Nested fields (dot notation) +GET /api/v1/orders?customer.country=US +``` + +### Sorting + +``` +# Single field (prefix - for descending) +GET /api/v1/products?sort=-created_at + +# Multiple fields (comma-separated) +GET /api/v1/products?sort=-featured,price,-created_at +``` + +### Full-Text Search + +``` +# Search query parameter +GET /api/v1/products?q=wireless+headphones + +# Field-specific search +GET /api/v1/users?email=alice +``` + +### Sparse Fieldsets + +``` +# Return only specified fields (reduces payload) +GET /api/v1/users?fields=id,name,email +GET /api/v1/orders?fields=id,total,status&include=customer.name +``` + +## Authentication and Authorization + +### Token-Based Auth + +``` +# Bearer token in Authorization header +GET /api/v1/users +Authorization: Bearer eyJhbGciOiJIUzI1NiIs... + +# API key (for server-to-server) +GET /api/v1/data +X-API-Key: sk_live_abc123 +``` + +### Authorization Patterns + +```typescript +// Resource-level: check ownership +app.get("/api/v1/orders/:id", async (req, res) => { + const order = await Order.findById(req.params.id); + if (!order) return res.status(404).json({ error: { code: "not_found" } }); + if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } }); + return res.json({ data: order }); +}); + +// Role-based: check permissions +app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => { + await User.delete(req.params.id); + return res.status(204).send(); +}); +``` + +## Rate Limiting + +### Headers + +``` +HTTP/1.1 200 OK +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1640000000 + +# When exceeded +HTTP/1.1 429 Too Many Requests +Retry-After: 60 +{ + "error": { + "code": "rate_limit_exceeded", + "message": "Rate limit exceeded. Try again in 60 seconds." + } +} +``` + +### Rate Limit Tiers + +| Tier | Limit | Window | Use Case | +|------|-------|--------|----------| +| Anonymous | 30/min | Per IP | Public endpoints | +| Authenticated | 100/min | Per user | Standard API access | +| Premium | 1000/min | Per API key | Paid API plans | +| Internal | 10000/min | Per service | Service-to-service | + +## Versioning + +### URL Path Versioning (Recommended) + +``` +/api/v1/users +/api/v2/users +``` + +**Pros:** Explicit, easy to route, cacheable +**Cons:** URL changes between versions + +### Header Versioning + +``` +GET /api/users +Accept: application/vnd.myapp.v2+json +``` + +**Pros:** Clean URLs +**Cons:** Harder to test, easy to forget + +### Versioning Strategy + +``` +1. Start with /api/v1/ — don't version until you need to +2. Maintain at most 2 active versions (current + previous) +3. Deprecation timeline: + - Announce deprecation (6 months notice for public APIs) + - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT + - Return 410 Gone after sunset date +4. Non-breaking changes don't need a new version: + - Adding new fields to responses + - Adding new optional query parameters + - Adding new endpoints +5. Breaking changes require a new version: + - Removing or renaming fields + - Changing field types + - Changing URL structure + - Changing authentication method +``` + +## Implementation Patterns + +### TypeScript (Next.js API Route) + +```typescript +import { z } from "zod"; +import { NextRequest, NextResponse } from "next/server"; + +const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1).max(100), +}); + +export async function POST(req: NextRequest) { + const body = await req.json(); + const parsed = createUserSchema.safeParse(body); + + if (!parsed.success) { + return NextResponse.json({ + error: { + code: "validation_error", + message: "Request validation failed", + details: parsed.error.issues.map(i => ({ + field: i.path.join("."), + message: i.message, + code: i.code, + })), + }, + }, { status: 422 }); + } + + const user = await createUser(parsed.data); + + return NextResponse.json( + { data: user }, + { + status: 201, + headers: { Location: `/api/v1/users/${user.id}` }, + }, + ); +} +``` + +### Python (Django REST Framework) + +```python +from rest_framework import serializers, viewsets, status +from rest_framework.response import Response + +class CreateUserSerializer(serializers.Serializer): + email = serializers.EmailField() + name = serializers.CharField(max_length=100) + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "email", "name", "created_at"] + +class UserViewSet(viewsets.ModelViewSet): + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def get_serializer_class(self): + if self.action == "create": + return CreateUserSerializer + return UserSerializer + + def create(self, request): + serializer = CreateUserSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = UserService.create(**serializer.validated_data) + return Response( + {"data": UserSerializer(user).data}, + status=status.HTTP_201_CREATED, + headers={"Location": f"/api/v1/users/{user.id}"}, + ) +``` + +### Go (net/http) + +```go +func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { + var req CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body") + return + } + + if err := req.Validate(); err != nil { + writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error()) + return + } + + user, err := h.service.Create(r.Context(), req) + if err != nil { + switch { + case errors.Is(err, domain.ErrEmailTaken): + writeError(w, http.StatusConflict, "email_taken", "Email already registered") + default: + writeError(w, http.StatusInternalServerError, "internal_error", "Internal error") + } + return + } + + w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID)) + writeJSON(w, http.StatusCreated, map[string]any{"data": user}) +} +``` + +## API Design Checklist + +Before shipping a new endpoint: + +- [ ] Resource URL follows naming conventions (plural, kebab-case, no verbs) +- [ ] Correct HTTP method used (GET for reads, POST for creates, etc.) +- [ ] Appropriate status codes returned (not 200 for everything) +- [ ] Input validated with schema (Zod, Pydantic, Bean Validation) +- [ ] Error responses follow standard format with codes and messages +- [ ] Pagination implemented for list endpoints (cursor or offset) +- [ ] Authentication required (or explicitly marked as public) +- [ ] Authorization checked (user can only access their own resources) +- [ ] Rate limiting configured +- [ ] Response does not leak internal details (stack traces, SQL errors) +- [ ] Consistent naming with existing endpoints (camelCase vs snake_case) +- [ ] Documented (OpenAPI/Swagger spec updated) diff --git a/.agents/skills/api-design/agents/openai.yaml b/.agents/skills/api-design/agents/openai.yaml new file mode 100644 index 00000000..b83fe25f --- /dev/null +++ b/.agents/skills/api-design/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "API Design" + short_description: "REST API design patterns and best practices" + brand_color: "#F97316" + default_prompt: "Design REST API: resources, status codes, pagination" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/backend-patterns/SKILL.md b/.agents/skills/backend-patterns/SKILL.md new file mode 100644 index 00000000..42c0cbee --- /dev/null +++ b/.agents/skills/backend-patterns/SKILL.md @@ -0,0 +1,598 @@ +--- +name: backend-patterns +description: Backend architecture patterns, API design, database optimization, and server-side best practices for Node.js, Express, and Next.js API routes. +origin: ECC +--- + +# Backend Development Patterns + +Backend architecture patterns and best practices for scalable server-side applications. + +## When to Activate + +- Designing REST or GraphQL API endpoints +- Implementing repository, service, or controller layers +- Optimizing database queries (N+1, indexing, connection pooling) +- Adding caching (Redis, in-memory, HTTP cache headers) +- Setting up background jobs or async processing +- Structuring error handling and validation for APIs +- Building middleware (auth, logging, rate limiting) + +## API Design Patterns + +### RESTful API Structure + +```typescript +// ✅ Resource-based URLs +GET /api/markets # List resources +GET /api/markets/:id # Get single resource +POST /api/markets # Create resource +PUT /api/markets/:id # Replace resource +PATCH /api/markets/:id # Update resource +DELETE /api/markets/:id # Delete resource + +// ✅ Query parameters for filtering, sorting, pagination +GET /api/markets?status=active&sort=volume&limit=20&offset=0 +``` + +### Repository Pattern + +```typescript +// Abstract data access logic +interface MarketRepository { + findAll(filters?: MarketFilters): Promise + findById(id: string): Promise + create(data: CreateMarketDto): Promise + update(id: string, data: UpdateMarketDto): Promise + delete(id: string): Promise +} + +class SupabaseMarketRepository implements MarketRepository { + async findAll(filters?: MarketFilters): Promise { + let query = supabase.from('markets').select('*') + + if (filters?.status) { + query = query.eq('status', filters.status) + } + + if (filters?.limit) { + query = query.limit(filters.limit) + } + + const { data, error } = await query + + if (error) throw new Error(error.message) + return data + } + + // Other methods... +} +``` + +### Service Layer Pattern + +```typescript +// Business logic separated from data access +class MarketService { + constructor(private marketRepo: MarketRepository) {} + + async searchMarkets(query: string, limit: number = 10): Promise { + // Business logic + const embedding = await generateEmbedding(query) + const results = await this.vectorSearch(embedding, limit) + + // Fetch full data + const markets = await this.marketRepo.findByIds(results.map(r => r.id)) + + // Sort by similarity + return markets.sort((a, b) => { + const scoreA = results.find(r => r.id === a.id)?.score || 0 + const scoreB = results.find(r => r.id === b.id)?.score || 0 + return scoreA - scoreB + }) + } + + private async vectorSearch(embedding: number[], limit: number) { + // Vector search implementation + } +} +``` + +### Middleware Pattern + +```typescript +// Request/response processing pipeline +export function withAuth(handler: NextApiHandler): NextApiHandler { + return async (req, res) => { + const token = req.headers.authorization?.replace('Bearer ', '') + + if (!token) { + return res.status(401).json({ error: 'Unauthorized' }) + } + + try { + const user = await verifyToken(token) + req.user = user + return handler(req, res) + } catch (error) { + return res.status(401).json({ error: 'Invalid token' }) + } + } +} + +// Usage +export default withAuth(async (req, res) => { + // Handler has access to req.user +}) +``` + +## Database Patterns + +### Query Optimization + +```typescript +// ✅ GOOD: Select only needed columns +const { data } = await supabase + .from('markets') + .select('id, name, status, volume') + .eq('status', 'active') + .order('volume', { ascending: false }) + .limit(10) + +// ❌ BAD: Select everything +const { data } = await supabase + .from('markets') + .select('*') +``` + +### N+1 Query Prevention + +```typescript +// ❌ BAD: N+1 query problem +const markets = await getMarkets() +for (const market of markets) { + market.creator = await getUser(market.creator_id) // N queries +} + +// ✅ GOOD: Batch fetch +const markets = await getMarkets() +const creatorIds = markets.map(m => m.creator_id) +const creators = await getUsers(creatorIds) // 1 query +const creatorMap = new Map(creators.map(c => [c.id, c])) + +markets.forEach(market => { + market.creator = creatorMap.get(market.creator_id) +}) +``` + +### Transaction Pattern + +```typescript +async function createMarketWithPosition( + marketData: CreateMarketDto, + positionData: CreatePositionDto +) { + // Use Supabase transaction + const { data, error } = await supabase.rpc('create_market_with_position', { + market_data: marketData, + position_data: positionData + }) + + if (error) throw new Error('Transaction failed') + return data +} + +// SQL function in Supabase +CREATE OR REPLACE FUNCTION create_market_with_position( + market_data jsonb, + position_data jsonb +) +RETURNS jsonb +LANGUAGE plpgsql +AS $$ +BEGIN + -- Start transaction automatically + INSERT INTO markets VALUES (market_data); + INSERT INTO positions VALUES (position_data); + RETURN jsonb_build_object('success', true); +EXCEPTION + WHEN OTHERS THEN + -- Rollback happens automatically + RETURN jsonb_build_object('success', false, 'error', SQLERRM); +END; +$$; +``` + +## Caching Strategies + +### Redis Caching Layer + +```typescript +class CachedMarketRepository implements MarketRepository { + constructor( + private baseRepo: MarketRepository, + private redis: RedisClient + ) {} + + async findById(id: string): Promise { + // Check cache first + const cached = await this.redis.get(`market:${id}`) + + if (cached) { + return JSON.parse(cached) + } + + // Cache miss - fetch from database + const market = await this.baseRepo.findById(id) + + if (market) { + // Cache for 5 minutes + await this.redis.setex(`market:${id}`, 300, JSON.stringify(market)) + } + + return market + } + + async invalidateCache(id: string): Promise { + await this.redis.del(`market:${id}`) + } +} +``` + +### Cache-Aside Pattern + +```typescript +async function getMarketWithCache(id: string): Promise { + const cacheKey = `market:${id}` + + // Try cache + const cached = await redis.get(cacheKey) + if (cached) return JSON.parse(cached) + + // Cache miss - fetch from DB + const market = await db.markets.findUnique({ where: { id } }) + + if (!market) throw new Error('Market not found') + + // Update cache + await redis.setex(cacheKey, 300, JSON.stringify(market)) + + return market +} +``` + +## Error Handling Patterns + +### Centralized Error Handler + +```typescript +class ApiError extends Error { + constructor( + public statusCode: number, + public message: string, + public isOperational = true + ) { + super(message) + Object.setPrototypeOf(this, ApiError.prototype) + } +} + +export function errorHandler(error: unknown, req: Request): Response { + if (error instanceof ApiError) { + return NextResponse.json({ + success: false, + error: error.message + }, { status: error.statusCode }) + } + + if (error instanceof z.ZodError) { + return NextResponse.json({ + success: false, + error: 'Validation failed', + details: error.errors + }, { status: 400 }) + } + + // Log unexpected errors + console.error('Unexpected error:', error) + + return NextResponse.json({ + success: false, + error: 'Internal server error' + }, { status: 500 }) +} + +// Usage +export async function GET(request: Request) { + try { + const data = await fetchData() + return NextResponse.json({ success: true, data }) + } catch (error) { + return errorHandler(error, request) + } +} +``` + +### Retry with Exponential Backoff + +```typescript +async function fetchWithRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + let lastError: Error + + for (let i = 0; i < maxRetries; i++) { + try { + return await fn() + } catch (error) { + lastError = error as Error + + if (i < maxRetries - 1) { + // Exponential backoff: 1s, 2s, 4s + const delay = Math.pow(2, i) * 1000 + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + } + + throw lastError! +} + +// Usage +const data = await fetchWithRetry(() => fetchFromAPI()) +``` + +## Authentication & Authorization + +### JWT Token Validation + +```typescript +import jwt from 'jsonwebtoken' + +interface JWTPayload { + userId: string + email: string + role: 'admin' | 'user' +} + +export function verifyToken(token: string): JWTPayload { + try { + const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload + return payload + } catch (error) { + throw new ApiError(401, 'Invalid token') + } +} + +export async function requireAuth(request: Request) { + const token = request.headers.get('authorization')?.replace('Bearer ', '') + + if (!token) { + throw new ApiError(401, 'Missing authorization token') + } + + return verifyToken(token) +} + +// Usage in API route +export async function GET(request: Request) { + const user = await requireAuth(request) + + const data = await getDataForUser(user.userId) + + return NextResponse.json({ success: true, data }) +} +``` + +### Role-Based Access Control + +```typescript +type Permission = 'read' | 'write' | 'delete' | 'admin' + +interface User { + id: string + role: 'admin' | 'moderator' | 'user' +} + +const rolePermissions: Record = { + admin: ['read', 'write', 'delete', 'admin'], + moderator: ['read', 'write', 'delete'], + user: ['read', 'write'] +} + +export function hasPermission(user: User, permission: Permission): boolean { + return rolePermissions[user.role].includes(permission) +} + +export function requirePermission(permission: Permission) { + return (handler: (request: Request, user: User) => Promise) => { + return async (request: Request) => { + const user = await requireAuth(request) + + if (!hasPermission(user, permission)) { + throw new ApiError(403, 'Insufficient permissions') + } + + return handler(request, user) + } + } +} + +// Usage - HOF wraps the handler +export const DELETE = requirePermission('delete')( + async (request: Request, user: User) => { + // Handler receives authenticated user with verified permission + return new Response('Deleted', { status: 200 }) + } +) +``` + +## Rate Limiting + +### Simple In-Memory Rate Limiter + +```typescript +class RateLimiter { + private requests = new Map() + + async checkLimit( + identifier: string, + maxRequests: number, + windowMs: number + ): Promise { + const now = Date.now() + const requests = this.requests.get(identifier) || [] + + // Remove old requests outside window + const recentRequests = requests.filter(time => now - time < windowMs) + + if (recentRequests.length >= maxRequests) { + return false // Rate limit exceeded + } + + // Add current request + recentRequests.push(now) + this.requests.set(identifier, recentRequests) + + return true + } +} + +const limiter = new RateLimiter() + +export async function GET(request: Request) { + const ip = request.headers.get('x-forwarded-for') || 'unknown' + + const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min + + if (!allowed) { + return NextResponse.json({ + error: 'Rate limit exceeded' + }, { status: 429 }) + } + + // Continue with request +} +``` + +## Background Jobs & Queues + +### Simple Queue Pattern + +```typescript +class JobQueue { + private queue: T[] = [] + private processing = false + + async add(job: T): Promise { + this.queue.push(job) + + if (!this.processing) { + this.process() + } + } + + private async process(): Promise { + this.processing = true + + while (this.queue.length > 0) { + const job = this.queue.shift()! + + try { + await this.execute(job) + } catch (error) { + console.error('Job failed:', error) + } + } + + this.processing = false + } + + private async execute(job: T): Promise { + // Job execution logic + } +} + +// Usage for indexing markets +interface IndexJob { + marketId: string +} + +const indexQueue = new JobQueue() + +export async function POST(request: Request) { + const { marketId } = await request.json() + + // Add to queue instead of blocking + await indexQueue.add({ marketId }) + + return NextResponse.json({ success: true, message: 'Job queued' }) +} +``` + +## Logging & Monitoring + +### Structured Logging + +```typescript +interface LogContext { + userId?: string + requestId?: string + method?: string + path?: string + [key: string]: unknown +} + +class Logger { + log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) { + const entry = { + timestamp: new Date().toISOString(), + level, + message, + ...context + } + + console.log(JSON.stringify(entry)) + } + + info(message: string, context?: LogContext) { + this.log('info', message, context) + } + + warn(message: string, context?: LogContext) { + this.log('warn', message, context) + } + + error(message: string, error: Error, context?: LogContext) { + this.log('error', message, { + ...context, + error: error.message, + stack: error.stack + }) + } +} + +const logger = new Logger() + +// Usage +export async function GET(request: Request) { + const requestId = crypto.randomUUID() + + logger.info('Fetching markets', { + requestId, + method: 'GET', + path: '/api/markets' + }) + + try { + const markets = await fetchMarkets() + return NextResponse.json({ success: true, data: markets }) + } catch (error) { + logger.error('Failed to fetch markets', error as Error, { requestId }) + return NextResponse.json({ error: 'Internal error' }, { status: 500 }) + } +} +``` + +**Remember**: Backend patterns enable scalable, maintainable server-side applications. Choose patterns that fit your complexity level. diff --git a/.agents/skills/backend-patterns/agents/openai.yaml b/.agents/skills/backend-patterns/agents/openai.yaml new file mode 100644 index 00000000..5fb47c63 --- /dev/null +++ b/.agents/skills/backend-patterns/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Backend Patterns" + short_description: "API design, database, and server-side patterns" + brand_color: "#F59E0B" + default_prompt: "Apply backend patterns: API design, repository, caching" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/coding-standards/SKILL.md b/.agents/skills/coding-standards/SKILL.md new file mode 100644 index 00000000..70d36239 --- /dev/null +++ b/.agents/skills/coding-standards/SKILL.md @@ -0,0 +1,530 @@ +--- +name: coding-standards +description: Universal coding standards, best practices, and patterns for TypeScript, JavaScript, React, and Node.js development. +origin: ECC +--- + +# Coding Standards & Best Practices + +Universal coding standards applicable across all projects. + +## When to Activate + +- Starting a new project or module +- Reviewing code for quality and maintainability +- Refactoring existing code to follow conventions +- Enforcing naming, formatting, or structural consistency +- Setting up linting, formatting, or type-checking rules +- Onboarding new contributors to coding conventions + +## Code Quality Principles + +### 1. Readability First +- Code is read more than written +- Clear variable and function names +- Self-documenting code preferred over comments +- Consistent formatting + +### 2. KISS (Keep It Simple, Stupid) +- Simplest solution that works +- Avoid over-engineering +- No premature optimization +- Easy to understand > clever code + +### 3. DRY (Don't Repeat Yourself) +- Extract common logic into functions +- Create reusable components +- Share utilities across modules +- Avoid copy-paste programming + +### 4. YAGNI (You Aren't Gonna Need It) +- Don't build features before they're needed +- Avoid speculative generality +- Add complexity only when required +- Start simple, refactor when needed + +## TypeScript/JavaScript Standards + +### Variable Naming + +```typescript +// ✅ GOOD: Descriptive names +const marketSearchQuery = 'election' +const isUserAuthenticated = true +const totalRevenue = 1000 + +// ❌ BAD: Unclear names +const q = 'election' +const flag = true +const x = 1000 +``` + +### Function Naming + +```typescript +// ✅ GOOD: Verb-noun pattern +async function fetchMarketData(marketId: string) { } +function calculateSimilarity(a: number[], b: number[]) { } +function isValidEmail(email: string): boolean { } + +// ❌ BAD: Unclear or noun-only +async function market(id: string) { } +function similarity(a, b) { } +function email(e) { } +``` + +### Immutability Pattern (CRITICAL) + +```typescript +// ✅ ALWAYS use spread operator +const updatedUser = { + ...user, + name: 'New Name' +} + +const updatedArray = [...items, newItem] + +// ❌ NEVER mutate directly +user.name = 'New Name' // BAD +items.push(newItem) // BAD +``` + +### Error Handling + +```typescript +// ✅ GOOD: Comprehensive error handling +async function fetchData(url: string) { + try { + const response = await fetch(url) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + return await response.json() + } catch (error) { + console.error('Fetch failed:', error) + throw new Error('Failed to fetch data') + } +} + +// ❌ BAD: No error handling +async function fetchData(url) { + const response = await fetch(url) + return response.json() +} +``` + +### Async/Await Best Practices + +```typescript +// ✅ GOOD: Parallel execution when possible +const [users, markets, stats] = await Promise.all([ + fetchUsers(), + fetchMarkets(), + fetchStats() +]) + +// ❌ BAD: Sequential when unnecessary +const users = await fetchUsers() +const markets = await fetchMarkets() +const stats = await fetchStats() +``` + +### Type Safety + +```typescript +// ✅ GOOD: Proper types +interface Market { + id: string + name: string + status: 'active' | 'resolved' | 'closed' + created_at: Date +} + +function getMarket(id: string): Promise { + // Implementation +} + +// ❌ BAD: Using 'any' +function getMarket(id: any): Promise { + // Implementation +} +``` + +## React Best Practices + +### Component Structure + +```typescript +// ✅ GOOD: Functional component with types +interface ButtonProps { + children: React.ReactNode + onClick: () => void + disabled?: boolean + variant?: 'primary' | 'secondary' +} + +export function Button({ + children, + onClick, + disabled = false, + variant = 'primary' +}: ButtonProps) { + return ( + + ) +} + +// ❌ BAD: No types, unclear structure +export function Button(props) { + return +} +``` + +### Custom Hooks + +```typescript +// ✅ GOOD: Reusable custom hook +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} + +// Usage +const debouncedQuery = useDebounce(searchQuery, 500) +``` + +### State Management + +```typescript +// ✅ GOOD: Proper state updates +const [count, setCount] = useState(0) + +// Functional update for state based on previous state +setCount(prev => prev + 1) + +// ❌ BAD: Direct state reference +setCount(count + 1) // Can be stale in async scenarios +``` + +### Conditional Rendering + +```typescript +// ✅ GOOD: Clear conditional rendering +{isLoading && } +{error && } +{data && } + +// ❌ BAD: Ternary hell +{isLoading ? : error ? : data ? : null} +``` + +## API Design Standards + +### REST API Conventions + +``` +GET /api/markets # List all markets +GET /api/markets/:id # Get specific market +POST /api/markets # Create new market +PUT /api/markets/:id # Update market (full) +PATCH /api/markets/:id # Update market (partial) +DELETE /api/markets/:id # Delete market + +# Query parameters for filtering +GET /api/markets?status=active&limit=10&offset=0 +``` + +### Response Format + +```typescript +// ✅ GOOD: Consistent response structure +interface ApiResponse { + success: boolean + data?: T + error?: string + meta?: { + total: number + page: number + limit: number + } +} + +// Success response +return NextResponse.json({ + success: true, + data: markets, + meta: { total: 100, page: 1, limit: 10 } +}) + +// Error response +return NextResponse.json({ + success: false, + error: 'Invalid request' +}, { status: 400 }) +``` + +### Input Validation + +```typescript +import { z } from 'zod' + +// ✅ GOOD: Schema validation +const CreateMarketSchema = z.object({ + name: z.string().min(1).max(200), + description: z.string().min(1).max(2000), + endDate: z.string().datetime(), + categories: z.array(z.string()).min(1) +}) + +export async function POST(request: Request) { + const body = await request.json() + + try { + const validated = CreateMarketSchema.parse(body) + // Proceed with validated data + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ + success: false, + error: 'Validation failed', + details: error.errors + }, { status: 400 }) + } + } +} +``` + +## File Organization + +### Project Structure + +``` +src/ +├── app/ # Next.js App Router +│ ├── api/ # API routes +│ ├── markets/ # Market pages +│ └── (auth)/ # Auth pages (route groups) +├── components/ # React components +│ ├── ui/ # Generic UI components +│ ├── forms/ # Form components +│ └── layouts/ # Layout components +├── hooks/ # Custom React hooks +├── lib/ # Utilities and configs +│ ├── api/ # API clients +│ ├── utils/ # Helper functions +│ └── constants/ # Constants +├── types/ # TypeScript types +└── styles/ # Global styles +``` + +### File Naming + +``` +components/Button.tsx # PascalCase for components +hooks/useAuth.ts # camelCase with 'use' prefix +lib/formatDate.ts # camelCase for utilities +types/market.types.ts # camelCase with .types suffix +``` + +## Comments & Documentation + +### When to Comment + +```typescript +// ✅ GOOD: Explain WHY, not WHAT +// Use exponential backoff to avoid overwhelming the API during outages +const delay = Math.min(1000 * Math.pow(2, retryCount), 30000) + +// Deliberately using mutation here for performance with large arrays +items.push(newItem) + +// ❌ BAD: Stating the obvious +// Increment counter by 1 +count++ + +// Set name to user's name +name = user.name +``` + +### JSDoc for Public APIs + +```typescript +/** + * Searches markets using semantic similarity. + * + * @param query - Natural language search query + * @param limit - Maximum number of results (default: 10) + * @returns Array of markets sorted by similarity score + * @throws {Error} If OpenAI API fails or Redis unavailable + * + * @example + * ```typescript + * const results = await searchMarkets('election', 5) + * console.log(results[0].name) // "Trump vs Biden" + * ``` + */ +export async function searchMarkets( + query: string, + limit: number = 10 +): Promise { + // Implementation +} +``` + +## Performance Best Practices + +### Memoization + +```typescript +import { useMemo, useCallback } from 'react' + +// ✅ GOOD: Memoize expensive computations +const sortedMarkets = useMemo(() => { + return markets.sort((a, b) => b.volume - a.volume) +}, [markets]) + +// ✅ GOOD: Memoize callbacks +const handleSearch = useCallback((query: string) => { + setSearchQuery(query) +}, []) +``` + +### Lazy Loading + +```typescript +import { lazy, Suspense } from 'react' + +// ✅ GOOD: Lazy load heavy components +const HeavyChart = lazy(() => import('./HeavyChart')) + +export function Dashboard() { + return ( + }> + + + ) +} +``` + +### Database Queries + +```typescript +// ✅ GOOD: Select only needed columns +const { data } = await supabase + .from('markets') + .select('id, name, status') + .limit(10) + +// ❌ BAD: Select everything +const { data } = await supabase + .from('markets') + .select('*') +``` + +## Testing Standards + +### Test Structure (AAA Pattern) + +```typescript +test('calculates similarity correctly', () => { + // Arrange + const vector1 = [1, 0, 0] + const vector2 = [0, 1, 0] + + // Act + const similarity = calculateCosineSimilarity(vector1, vector2) + + // Assert + expect(similarity).toBe(0) +}) +``` + +### Test Naming + +```typescript +// ✅ GOOD: Descriptive test names +test('returns empty array when no markets match query', () => { }) +test('throws error when OpenAI API key is missing', () => { }) +test('falls back to substring search when Redis unavailable', () => { }) + +// ❌ BAD: Vague test names +test('works', () => { }) +test('test search', () => { }) +``` + +## Code Smell Detection + +Watch for these anti-patterns: + +### 1. Long Functions +```typescript +// ❌ BAD: Function > 50 lines +function processMarketData() { + // 100 lines of code +} + +// ✅ GOOD: Split into smaller functions +function processMarketData() { + const validated = validateData() + const transformed = transformData(validated) + return saveData(transformed) +} +``` + +### 2. Deep Nesting +```typescript +// ❌ BAD: 5+ levels of nesting +if (user) { + if (user.isAdmin) { + if (market) { + if (market.isActive) { + if (hasPermission) { + // Do something + } + } + } + } +} + +// ✅ GOOD: Early returns +if (!user) return +if (!user.isAdmin) return +if (!market) return +if (!market.isActive) return +if (!hasPermission) return + +// Do something +``` + +### 3. Magic Numbers +```typescript +// ❌ BAD: Unexplained numbers +if (retryCount > 3) { } +setTimeout(callback, 500) + +// ✅ GOOD: Named constants +const MAX_RETRIES = 3 +const DEBOUNCE_DELAY_MS = 500 + +if (retryCount > MAX_RETRIES) { } +setTimeout(callback, DEBOUNCE_DELAY_MS) +``` + +**Remember**: Code quality is not negotiable. Clear, maintainable code enables rapid development and confident refactoring. diff --git a/.agents/skills/coding-standards/agents/openai.yaml b/.agents/skills/coding-standards/agents/openai.yaml new file mode 100644 index 00000000..b0dda0ef --- /dev/null +++ b/.agents/skills/coding-standards/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Coding Standards" + short_description: "Universal coding standards and best practices" + brand_color: "#3B82F6" + default_prompt: "Apply standards: immutability, error handling, type safety" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/e2e-testing/SKILL.md b/.agents/skills/e2e-testing/SKILL.md new file mode 100644 index 00000000..05631991 --- /dev/null +++ b/.agents/skills/e2e-testing/SKILL.md @@ -0,0 +1,326 @@ +--- +name: e2e-testing +description: Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies. +origin: ECC +--- + +# E2E Testing Patterns + +Comprehensive Playwright patterns for building stable, fast, and maintainable E2E test suites. + +## Test File Organization + +``` +tests/ +├── e2e/ +│ ├── auth/ +│ │ ├── login.spec.ts +│ │ ├── logout.spec.ts +│ │ └── register.spec.ts +│ ├── features/ +│ │ ├── browse.spec.ts +│ │ ├── search.spec.ts +│ │ └── create.spec.ts +│ └── api/ +│ └── endpoints.spec.ts +├── fixtures/ +│ ├── auth.ts +│ └── data.ts +└── playwright.config.ts +``` + +## Page Object Model (POM) + +```typescript +import { Page, Locator } from '@playwright/test' + +export class ItemsPage { + readonly page: Page + readonly searchInput: Locator + readonly itemCards: Locator + readonly createButton: Locator + + constructor(page: Page) { + this.page = page + this.searchInput = page.locator('[data-testid="search-input"]') + this.itemCards = page.locator('[data-testid="item-card"]') + this.createButton = page.locator('[data-testid="create-btn"]') + } + + async goto() { + await this.page.goto('/items') + await this.page.waitForLoadState('networkidle') + } + + async search(query: string) { + await this.searchInput.fill(query) + await this.page.waitForResponse(resp => resp.url().includes('/api/search')) + await this.page.waitForLoadState('networkidle') + } + + async getItemCount() { + return await this.itemCards.count() + } +} +``` + +## Test Structure + +```typescript +import { test, expect } from '@playwright/test' +import { ItemsPage } from '../../pages/ItemsPage' + +test.describe('Item Search', () => { + let itemsPage: ItemsPage + + test.beforeEach(async ({ page }) => { + itemsPage = new ItemsPage(page) + await itemsPage.goto() + }) + + test('should search by keyword', async ({ page }) => { + await itemsPage.search('test') + + const count = await itemsPage.getItemCount() + expect(count).toBeGreaterThan(0) + + await expect(itemsPage.itemCards.first()).toContainText(/test/i) + await page.screenshot({ path: 'artifacts/search-results.png' }) + }) + + test('should handle no results', async ({ page }) => { + await itemsPage.search('xyznonexistent123') + + await expect(page.locator('[data-testid="no-results"]')).toBeVisible() + expect(await itemsPage.getItemCount()).toBe(0) + }) +}) +``` + +## Playwright Configuration + +```typescript +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report' }], + ['junit', { outputFile: 'playwright-results.xml' }], + ['json', { outputFile: 'playwright-results.json' }] + ], + use: { + baseURL: process.env.BASE_URL || 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + actionTimeout: 10000, + navigationTimeout: 30000, + }, + projects: [ + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } }, + ], + webServer: { + command: 'npm run dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}) +``` + +## Flaky Test Patterns + +### Quarantine + +```typescript +test('flaky: complex search', async ({ page }) => { + test.fixme(true, 'Flaky - Issue #123') + // test code... +}) + +test('conditional skip', async ({ page }) => { + test.skip(process.env.CI, 'Flaky in CI - Issue #123') + // test code... +}) +``` + +### Identify Flakiness + +```bash +npx playwright test tests/search.spec.ts --repeat-each=10 +npx playwright test tests/search.spec.ts --retries=3 +``` + +### Common Causes & Fixes + +**Race conditions:** +```typescript +// Bad: assumes element is ready +await page.click('[data-testid="button"]') + +// Good: auto-wait locator +await page.locator('[data-testid="button"]').click() +``` + +**Network timing:** +```typescript +// Bad: arbitrary timeout +await page.waitForTimeout(5000) + +// Good: wait for specific condition +await page.waitForResponse(resp => resp.url().includes('/api/data')) +``` + +**Animation timing:** +```typescript +// Bad: click during animation +await page.click('[data-testid="menu-item"]') + +// Good: wait for stability +await page.locator('[data-testid="menu-item"]').waitFor({ state: 'visible' }) +await page.waitForLoadState('networkidle') +await page.locator('[data-testid="menu-item"]').click() +``` + +## Artifact Management + +### Screenshots + +```typescript +await page.screenshot({ path: 'artifacts/after-login.png' }) +await page.screenshot({ path: 'artifacts/full-page.png', fullPage: true }) +await page.locator('[data-testid="chart"]').screenshot({ path: 'artifacts/chart.png' }) +``` + +### Traces + +```typescript +await browser.startTracing(page, { + path: 'artifacts/trace.json', + screenshots: true, + snapshots: true, +}) +// ... test actions ... +await browser.stopTracing() +``` + +### Video + +```typescript +// In playwright.config.ts +use: { + video: 'retain-on-failure', + videosPath: 'artifacts/videos/' +} +``` + +## CI/CD Integration + +```yaml +# .github/workflows/e2e.yml +name: E2E Tests +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npx playwright install --with-deps + - run: npx playwright test + env: + BASE_URL: ${{ vars.STAGING_URL }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 +``` + +## Test Report Template + +```markdown +# E2E Test Report + +**Date:** YYYY-MM-DD HH:MM +**Duration:** Xm Ys +**Status:** PASSING / FAILING + +## Summary +- Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C + +## Failed Tests + +### test-name +**File:** `tests/e2e/feature.spec.ts:45` +**Error:** Expected element to be visible +**Screenshot:** artifacts/failed.png +**Recommended Fix:** [description] + +## Artifacts +- HTML Report: playwright-report/index.html +- Screenshots: artifacts/*.png +- Videos: artifacts/videos/*.webm +- Traces: artifacts/*.zip +``` + +## Wallet / Web3 Testing + +```typescript +test('wallet connection', async ({ page, context }) => { + // Mock wallet provider + await context.addInitScript(() => { + window.ethereum = { + isMetaMask: true, + request: async ({ method }) => { + if (method === 'eth_requestAccounts') + return ['0x1234567890123456789012345678901234567890'] + if (method === 'eth_chainId') return '0x1' + } + } + }) + + await page.goto('/') + await page.locator('[data-testid="connect-wallet"]').click() + await expect(page.locator('[data-testid="wallet-address"]')).toContainText('0x1234') +}) +``` + +## Financial / Critical Flow Testing + +```typescript +test('trade execution', async ({ page }) => { + // Skip on production — real money + test.skip(process.env.NODE_ENV === 'production', 'Skip on production') + + await page.goto('/markets/test-market') + await page.locator('[data-testid="position-yes"]').click() + await page.locator('[data-testid="trade-amount"]').fill('1.0') + + // Verify preview + const preview = page.locator('[data-testid="trade-preview"]') + await expect(preview).toContainText('1.0') + + // Confirm and wait for blockchain + await page.locator('[data-testid="confirm-trade"]').click() + await page.waitForResponse( + resp => resp.url().includes('/api/trade') && resp.status() === 200, + { timeout: 30000 } + ) + + await expect(page.locator('[data-testid="trade-success"]')).toBeVisible() +}) +``` diff --git a/.agents/skills/e2e-testing/agents/openai.yaml b/.agents/skills/e2e-testing/agents/openai.yaml new file mode 100644 index 00000000..cdedda78 --- /dev/null +++ b/.agents/skills/e2e-testing/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "E2E Testing" + short_description: "Playwright end-to-end testing" + brand_color: "#06B6D4" + default_prompt: "Generate Playwright E2E tests with Page Object Model" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/eval-harness/SKILL.md b/.agents/skills/eval-harness/SKILL.md new file mode 100644 index 00000000..d320670c --- /dev/null +++ b/.agents/skills/eval-harness/SKILL.md @@ -0,0 +1,236 @@ +--- +name: eval-harness +description: Formal evaluation framework for Claude Code sessions implementing eval-driven development (EDD) principles +origin: ECC +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# Eval Harness Skill + +A formal evaluation framework for Claude Code sessions, implementing eval-driven development (EDD) principles. + +## When to Activate + +- Setting up eval-driven development (EDD) for AI-assisted workflows +- Defining pass/fail criteria for Claude Code task completion +- Measuring agent reliability with pass@k metrics +- Creating regression test suites for prompt or agent changes +- Benchmarking agent performance across model versions + +## Philosophy + +Eval-Driven Development treats evals as the "unit tests of AI development": +- Define expected behavior BEFORE implementation +- Run evals continuously during development +- Track regressions with each change +- Use pass@k metrics for reliability measurement + +## Eval Types + +### Capability Evals +Test if Claude can do something it couldn't before: +```markdown +[CAPABILITY EVAL: feature-name] +Task: Description of what Claude should accomplish +Success Criteria: + - [ ] Criterion 1 + - [ ] Criterion 2 + - [ ] Criterion 3 +Expected Output: Description of expected result +``` + +### Regression Evals +Ensure changes don't break existing functionality: +```markdown +[REGRESSION EVAL: feature-name] +Baseline: SHA or checkpoint name +Tests: + - existing-test-1: PASS/FAIL + - existing-test-2: PASS/FAIL + - existing-test-3: PASS/FAIL +Result: X/Y passed (previously Y/Y) +``` + +## Grader Types + +### 1. Code-Based Grader +Deterministic checks using code: +```bash +# Check if file contains expected pattern +grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL" + +# Check if tests pass +npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL" + +# Check if build succeeds +npm run build && echo "PASS" || echo "FAIL" +``` + +### 2. Model-Based Grader +Use Claude to evaluate open-ended outputs: +```markdown +[MODEL GRADER PROMPT] +Evaluate the following code change: +1. Does it solve the stated problem? +2. Is it well-structured? +3. Are edge cases handled? +4. Is error handling appropriate? + +Score: 1-5 (1=poor, 5=excellent) +Reasoning: [explanation] +``` + +### 3. Human Grader +Flag for manual review: +```markdown +[HUMAN REVIEW REQUIRED] +Change: Description of what changed +Reason: Why human review is needed +Risk Level: LOW/MEDIUM/HIGH +``` + +## Metrics + +### pass@k +"At least one success in k attempts" +- pass@1: First attempt success rate +- pass@3: Success within 3 attempts +- Typical target: pass@3 > 90% + +### pass^k +"All k trials succeed" +- Higher bar for reliability +- pass^3: 3 consecutive successes +- Use for critical paths + +## Eval Workflow + +### 1. Define (Before Coding) +```markdown +## EVAL DEFINITION: feature-xyz + +### Capability Evals +1. Can create new user account +2. Can validate email format +3. Can hash password securely + +### Regression Evals +1. Existing login still works +2. Session management unchanged +3. Logout flow intact + +### Success Metrics +- pass@3 > 90% for capability evals +- pass^3 = 100% for regression evals +``` + +### 2. Implement +Write code to pass the defined evals. + +### 3. Evaluate +```bash +# Run capability evals +[Run each capability eval, record PASS/FAIL] + +# Run regression evals +npm test -- --testPathPattern="existing" + +# Generate report +``` + +### 4. Report +```markdown +EVAL REPORT: feature-xyz +======================== + +Capability Evals: + create-user: PASS (pass@1) + validate-email: PASS (pass@2) + hash-password: PASS (pass@1) + Overall: 3/3 passed + +Regression Evals: + login-flow: PASS + session-mgmt: PASS + logout-flow: PASS + Overall: 3/3 passed + +Metrics: + pass@1: 67% (2/3) + pass@3: 100% (3/3) + +Status: READY FOR REVIEW +``` + +## Integration Patterns + +### Pre-Implementation +``` +/eval define feature-name +``` +Creates eval definition file at `.claude/evals/feature-name.md` + +### During Implementation +``` +/eval check feature-name +``` +Runs current evals and reports status + +### Post-Implementation +``` +/eval report feature-name +``` +Generates full eval report + +## Eval Storage + +Store evals in project: +``` +.claude/ + evals/ + feature-xyz.md # Eval definition + feature-xyz.log # Eval run history + baseline.json # Regression baselines +``` + +## Best Practices + +1. **Define evals BEFORE coding** - Forces clear thinking about success criteria +2. **Run evals frequently** - Catch regressions early +3. **Track pass@k over time** - Monitor reliability trends +4. **Use code graders when possible** - Deterministic > probabilistic +5. **Human review for security** - Never fully automate security checks +6. **Keep evals fast** - Slow evals don't get run +7. **Version evals with code** - Evals are first-class artifacts + +## Example: Adding Authentication + +```markdown +## EVAL: add-authentication + +### Phase 1: Define (10 min) +Capability Evals: +- [ ] User can register with email/password +- [ ] User can login with valid credentials +- [ ] Invalid credentials rejected with proper error +- [ ] Sessions persist across page reloads +- [ ] Logout clears session + +Regression Evals: +- [ ] Public routes still accessible +- [ ] API responses unchanged +- [ ] Database schema compatible + +### Phase 2: Implement (varies) +[Write code] + +### Phase 3: Evaluate +Run: /eval check add-authentication + +### Phase 4: Report +EVAL REPORT: add-authentication +============================== +Capability: 5/5 passed (pass@3: 100%) +Regression: 3/3 passed (pass^3: 100%) +Status: SHIP IT +``` diff --git a/.agents/skills/eval-harness/agents/openai.yaml b/.agents/skills/eval-harness/agents/openai.yaml new file mode 100644 index 00000000..e017e6a7 --- /dev/null +++ b/.agents/skills/eval-harness/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Eval Harness" + short_description: "Eval-driven development with pass/fail criteria" + brand_color: "#EC4899" + default_prompt: "Set up eval-driven development with pass/fail criteria" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/frontend-patterns/SKILL.md b/.agents/skills/frontend-patterns/SKILL.md new file mode 100644 index 00000000..7ce38802 --- /dev/null +++ b/.agents/skills/frontend-patterns/SKILL.md @@ -0,0 +1,642 @@ +--- +name: frontend-patterns +description: Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices. +origin: ECC +--- + +# Frontend Development Patterns + +Modern frontend patterns for React, Next.js, and performant user interfaces. + +## When to Activate + +- Building React components (composition, props, rendering) +- Managing state (useState, useReducer, Zustand, Context) +- Implementing data fetching (SWR, React Query, server components) +- Optimizing performance (memoization, virtualization, code splitting) +- Working with forms (validation, controlled inputs, Zod schemas) +- Handling client-side routing and navigation +- Building accessible, responsive UI patterns + +## Component Patterns + +### Composition Over Inheritance + +```typescript +// ✅ GOOD: Component composition +interface CardProps { + children: React.ReactNode + variant?: 'default' | 'outlined' +} + +export function Card({ children, variant = 'default' }: CardProps) { + return
{children}
+} + +export function CardHeader({ children }: { children: React.ReactNode }) { + return
{children}
+} + +export function CardBody({ children }: { children: React.ReactNode }) { + return
{children}
+} + +// Usage + + Title + Content + +``` + +### Compound Components + +```typescript +interface TabsContextValue { + activeTab: string + setActiveTab: (tab: string) => void +} + +const TabsContext = createContext(undefined) + +export function Tabs({ children, defaultTab }: { + children: React.ReactNode + defaultTab: string +}) { + const [activeTab, setActiveTab] = useState(defaultTab) + + return ( + + {children} + + ) +} + +export function TabList({ children }: { children: React.ReactNode }) { + return
{children}
+} + +export function Tab({ id, children }: { id: string, children: React.ReactNode }) { + const context = useContext(TabsContext) + if (!context) throw new Error('Tab must be used within Tabs') + + return ( + + ) +} + +// Usage + + + Overview + Details + + +``` + +### Render Props Pattern + +```typescript +interface DataLoaderProps { + url: string + children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode +} + +export function DataLoader({ url, children }: DataLoaderProps) { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then(setData) + .catch(setError) + .finally(() => setLoading(false)) + }, [url]) + + return <>{children(data, loading, error)} +} + +// Usage + url="/api/markets"> + {(markets, loading, error) => { + if (loading) return + if (error) return + return + }} + +``` + +## Custom Hooks Patterns + +### State Management Hook + +```typescript +export function useToggle(initialValue = false): [boolean, () => void] { + const [value, setValue] = useState(initialValue) + + const toggle = useCallback(() => { + setValue(v => !v) + }, []) + + return [value, toggle] +} + +// Usage +const [isOpen, toggleOpen] = useToggle() +``` + +### Async Data Fetching Hook + +```typescript +interface UseQueryOptions { + onSuccess?: (data: T) => void + onError?: (error: Error) => void + enabled?: boolean +} + +export function useQuery( + key: string, + fetcher: () => Promise, + options?: UseQueryOptions +) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + const refetch = useCallback(async () => { + setLoading(true) + setError(null) + + try { + const result = await fetcher() + setData(result) + options?.onSuccess?.(result) + } catch (err) { + const error = err as Error + setError(error) + options?.onError?.(error) + } finally { + setLoading(false) + } + }, [fetcher, options]) + + useEffect(() => { + if (options?.enabled !== false) { + refetch() + } + }, [key, refetch, options?.enabled]) + + return { data, error, loading, refetch } +} + +// Usage +const { data: markets, loading, error, refetch } = useQuery( + 'markets', + () => fetch('/api/markets').then(r => r.json()), + { + onSuccess: data => console.log('Fetched', data.length, 'markets'), + onError: err => console.error('Failed:', err) + } +) +``` + +### Debounce Hook + +```typescript +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} + +// Usage +const [searchQuery, setSearchQuery] = useState('') +const debouncedQuery = useDebounce(searchQuery, 500) + +useEffect(() => { + if (debouncedQuery) { + performSearch(debouncedQuery) + } +}, [debouncedQuery]) +``` + +## State Management Patterns + +### Context + Reducer Pattern + +```typescript +interface State { + markets: Market[] + selectedMarket: Market | null + loading: boolean +} + +type Action = + | { type: 'SET_MARKETS'; payload: Market[] } + | { type: 'SELECT_MARKET'; payload: Market } + | { type: 'SET_LOADING'; payload: boolean } + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'SET_MARKETS': + return { ...state, markets: action.payload } + case 'SELECT_MARKET': + return { ...state, selectedMarket: action.payload } + case 'SET_LOADING': + return { ...state, loading: action.payload } + default: + return state + } +} + +const MarketContext = createContext<{ + state: State + dispatch: Dispatch +} | undefined>(undefined) + +export function MarketProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(reducer, { + markets: [], + selectedMarket: null, + loading: false + }) + + return ( + + {children} + + ) +} + +export function useMarkets() { + const context = useContext(MarketContext) + if (!context) throw new Error('useMarkets must be used within MarketProvider') + return context +} +``` + +## Performance Optimization + +### Memoization + +```typescript +// ✅ useMemo for expensive computations +const sortedMarkets = useMemo(() => { + return markets.sort((a, b) => b.volume - a.volume) +}, [markets]) + +// ✅ useCallback for functions passed to children +const handleSearch = useCallback((query: string) => { + setSearchQuery(query) +}, []) + +// ✅ React.memo for pure components +export const MarketCard = React.memo(({ market }) => { + return ( +
+

{market.name}

+

{market.description}

+
+ ) +}) +``` + +### Code Splitting & Lazy Loading + +```typescript +import { lazy, Suspense } from 'react' + +// ✅ Lazy load heavy components +const HeavyChart = lazy(() => import('./HeavyChart')) +const ThreeJsBackground = lazy(() => import('./ThreeJsBackground')) + +export function Dashboard() { + return ( +
+ }> + + + + + + +
+ ) +} +``` + +### Virtualization for Long Lists + +```typescript +import { useVirtualizer } from '@tanstack/react-virtual' + +export function VirtualMarketList({ markets }: { markets: Market[] }) { + const parentRef = useRef(null) + + const virtualizer = useVirtualizer({ + count: markets.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 100, // Estimated row height + overscan: 5 // Extra items to render + }) + + return ( +
+
+ {virtualizer.getVirtualItems().map(virtualRow => ( +
+ +
+ ))} +
+
+ ) +} +``` + +## Form Handling Patterns + +### Controlled Form with Validation + +```typescript +interface FormData { + name: string + description: string + endDate: string +} + +interface FormErrors { + name?: string + description?: string + endDate?: string +} + +export function CreateMarketForm() { + const [formData, setFormData] = useState({ + name: '', + description: '', + endDate: '' + }) + + const [errors, setErrors] = useState({}) + + const validate = (): boolean => { + const newErrors: FormErrors = {} + + if (!formData.name.trim()) { + newErrors.name = 'Name is required' + } else if (formData.name.length > 200) { + newErrors.name = 'Name must be under 200 characters' + } + + if (!formData.description.trim()) { + newErrors.description = 'Description is required' + } + + if (!formData.endDate) { + newErrors.endDate = 'End date is required' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validate()) return + + try { + await createMarket(formData) + // Success handling + } catch (error) { + // Error handling + } + } + + return ( +
+ setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Market name" + /> + {errors.name && {errors.name}} + + {/* Other fields */} + + +
+ ) +} +``` + +## Error Boundary Pattern + +```typescript +interface ErrorBoundaryState { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { + hasError: false, + error: null + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error boundary caught:', error, errorInfo) + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ) + } + + return this.props.children + } +} + +// Usage + + + +``` + +## Animation Patterns + +### Framer Motion Animations + +```typescript +import { motion, AnimatePresence } from 'framer-motion' + +// ✅ List animations +export function AnimatedMarketList({ markets }: { markets: Market[] }) { + return ( + + {markets.map(market => ( + + + + ))} + + ) +} + +// ✅ Modal animations +export function Modal({ isOpen, onClose, children }: ModalProps) { + return ( + + {isOpen && ( + <> + + + {children} + + + )} + + ) +} +``` + +## Accessibility Patterns + +### Keyboard Navigation + +```typescript +export function Dropdown({ options, onSelect }: DropdownProps) { + const [isOpen, setIsOpen] = useState(false) + const [activeIndex, setActiveIndex] = useState(0) + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setActiveIndex(i => Math.min(i + 1, options.length - 1)) + break + case 'ArrowUp': + e.preventDefault() + setActiveIndex(i => Math.max(i - 1, 0)) + break + case 'Enter': + e.preventDefault() + onSelect(options[activeIndex]) + setIsOpen(false) + break + case 'Escape': + setIsOpen(false) + break + } + } + + return ( +
+ {/* Dropdown implementation */} +
+ ) +} +``` + +### Focus Management + +```typescript +export function Modal({ isOpen, onClose, children }: ModalProps) { + const modalRef = useRef(null) + const previousFocusRef = useRef(null) + + useEffect(() => { + if (isOpen) { + // Save currently focused element + previousFocusRef.current = document.activeElement as HTMLElement + + // Focus modal + modalRef.current?.focus() + } else { + // Restore focus when closing + previousFocusRef.current?.focus() + } + }, [isOpen]) + + return isOpen ? ( +
e.key === 'Escape' && onClose()} + > + {children} +
+ ) : null +} +``` + +**Remember**: Modern frontend patterns enable maintainable, performant user interfaces. Choose patterns that fit your project complexity. diff --git a/.agents/skills/frontend-patterns/agents/openai.yaml b/.agents/skills/frontend-patterns/agents/openai.yaml new file mode 100644 index 00000000..00f85995 --- /dev/null +++ b/.agents/skills/frontend-patterns/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Frontend Patterns" + short_description: "React and Next.js patterns and best practices" + brand_color: "#8B5CF6" + default_prompt: "Apply React/Next.js patterns and best practices" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/security-review/SKILL.md b/.agents/skills/security-review/SKILL.md new file mode 100644 index 00000000..35403243 --- /dev/null +++ b/.agents/skills/security-review/SKILL.md @@ -0,0 +1,495 @@ +--- +name: security-review +description: Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns. +origin: ECC +--- + +# Security Review Skill + +This skill ensures all code follows security best practices and identifies potential vulnerabilities. + +## When to Activate + +- Implementing authentication or authorization +- Handling user input or file uploads +- Creating new API endpoints +- Working with secrets or credentials +- Implementing payment features +- Storing or transmitting sensitive data +- Integrating third-party APIs + +## Security Checklist + +### 1. Secrets Management + +#### ❌ NEVER Do This +```typescript +const apiKey = "sk-proj-xxxxx" // Hardcoded secret +const dbPassword = "password123" // In source code +``` + +#### ✅ ALWAYS Do This +```typescript +const apiKey = process.env.OPENAI_API_KEY +const dbUrl = process.env.DATABASE_URL + +// Verify secrets exist +if (!apiKey) { + throw new Error('OPENAI_API_KEY not configured') +} +``` + +#### Verification Steps +- [ ] No hardcoded API keys, tokens, or passwords +- [ ] All secrets in environment variables +- [ ] `.env.local` in .gitignore +- [ ] No secrets in git history +- [ ] Production secrets in hosting platform (Vercel, Railway) + +### 2. Input Validation + +#### Always Validate User Input +```typescript +import { z } from 'zod' + +// Define validation schema +const CreateUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1).max(100), + age: z.number().int().min(0).max(150) +}) + +// Validate before processing +export async function createUser(input: unknown) { + try { + const validated = CreateUserSchema.parse(input) + return await db.users.create(validated) + } catch (error) { + if (error instanceof z.ZodError) { + return { success: false, errors: error.errors } + } + throw error + } +} +``` + +#### File Upload Validation +```typescript +function validateFileUpload(file: File) { + // Size check (5MB max) + const maxSize = 5 * 1024 * 1024 + if (file.size > maxSize) { + throw new Error('File too large (max 5MB)') + } + + // Type check + const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] + if (!allowedTypes.includes(file.type)) { + throw new Error('Invalid file type') + } + + // Extension check + const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'] + const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0] + if (!extension || !allowedExtensions.includes(extension)) { + throw new Error('Invalid file extension') + } + + return true +} +``` + +#### Verification Steps +- [ ] All user inputs validated with schemas +- [ ] File uploads restricted (size, type, extension) +- [ ] No direct use of user input in queries +- [ ] Whitelist validation (not blacklist) +- [ ] Error messages don't leak sensitive info + +### 3. SQL Injection Prevention + +#### ❌ NEVER Concatenate SQL +```typescript +// DANGEROUS - SQL Injection vulnerability +const query = `SELECT * FROM users WHERE email = '${userEmail}'` +await db.query(query) +``` + +#### ✅ ALWAYS Use Parameterized Queries +```typescript +// Safe - parameterized query +const { data } = await supabase + .from('users') + .select('*') + .eq('email', userEmail) + +// Or with raw SQL +await db.query( + 'SELECT * FROM users WHERE email = $1', + [userEmail] +) +``` + +#### Verification Steps +- [ ] All database queries use parameterized queries +- [ ] No string concatenation in SQL +- [ ] ORM/query builder used correctly +- [ ] Supabase queries properly sanitized + +### 4. Authentication & Authorization + +#### JWT Token Handling +```typescript +// ❌ WRONG: localStorage (vulnerable to XSS) +localStorage.setItem('token', token) + +// ✅ CORRECT: httpOnly cookies +res.setHeader('Set-Cookie', + `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`) +``` + +#### Authorization Checks +```typescript +export async function deleteUser(userId: string, requesterId: string) { + // ALWAYS verify authorization first + const requester = await db.users.findUnique({ + where: { id: requesterId } + }) + + if (requester.role !== 'admin') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 403 } + ) + } + + // Proceed with deletion + await db.users.delete({ where: { id: userId } }) +} +``` + +#### Row Level Security (Supabase) +```sql +-- Enable RLS on all tables +ALTER TABLE users ENABLE ROW LEVEL SECURITY; + +-- Users can only view their own data +CREATE POLICY "Users view own data" + ON users FOR SELECT + USING (auth.uid() = id); + +-- Users can only update their own data +CREATE POLICY "Users update own data" + ON users FOR UPDATE + USING (auth.uid() = id); +``` + +#### Verification Steps +- [ ] Tokens stored in httpOnly cookies (not localStorage) +- [ ] Authorization checks before sensitive operations +- [ ] Row Level Security enabled in Supabase +- [ ] Role-based access control implemented +- [ ] Session management secure + +### 5. XSS Prevention + +#### Sanitize HTML +```typescript +import DOMPurify from 'isomorphic-dompurify' + +// ALWAYS sanitize user-provided HTML +function renderUserContent(html: string) { + const clean = DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], + ALLOWED_ATTR: [] + }) + return
+} +``` + +#### Content Security Policy +```typescript +// next.config.js +const securityHeaders = [ + { + key: 'Content-Security-Policy', + value: ` + default-src 'self'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + img-src 'self' data: https:; + font-src 'self'; + connect-src 'self' https://api.example.com; + `.replace(/\s{2,}/g, ' ').trim() + } +] +``` + +#### Verification Steps +- [ ] User-provided HTML sanitized +- [ ] CSP headers configured +- [ ] No unvalidated dynamic content rendering +- [ ] React's built-in XSS protection used + +### 6. CSRF Protection + +#### CSRF Tokens +```typescript +import { csrf } from '@/lib/csrf' + +export async function POST(request: Request) { + const token = request.headers.get('X-CSRF-Token') + + if (!csrf.verify(token)) { + return NextResponse.json( + { error: 'Invalid CSRF token' }, + { status: 403 } + ) + } + + // Process request +} +``` + +#### SameSite Cookies +```typescript +res.setHeader('Set-Cookie', + `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`) +``` + +#### Verification Steps +- [ ] CSRF tokens on state-changing operations +- [ ] SameSite=Strict on all cookies +- [ ] Double-submit cookie pattern implemented + +### 7. Rate Limiting + +#### API Rate Limiting +```typescript +import rateLimit from 'express-rate-limit' + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // 100 requests per window + message: 'Too many requests' +}) + +// Apply to routes +app.use('/api/', limiter) +``` + +#### Expensive Operations +```typescript +// Aggressive rate limiting for searches +const searchLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 10, // 10 requests per minute + message: 'Too many search requests' +}) + +app.use('/api/search', searchLimiter) +``` + +#### Verification Steps +- [ ] Rate limiting on all API endpoints +- [ ] Stricter limits on expensive operations +- [ ] IP-based rate limiting +- [ ] User-based rate limiting (authenticated) + +### 8. Sensitive Data Exposure + +#### Logging +```typescript +// ❌ WRONG: Logging sensitive data +console.log('User login:', { email, password }) +console.log('Payment:', { cardNumber, cvv }) + +// ✅ CORRECT: Redact sensitive data +console.log('User login:', { email, userId }) +console.log('Payment:', { last4: card.last4, userId }) +``` + +#### Error Messages +```typescript +// ❌ WRONG: Exposing internal details +catch (error) { + return NextResponse.json( + { error: error.message, stack: error.stack }, + { status: 500 } + ) +} + +// ✅ CORRECT: Generic error messages +catch (error) { + console.error('Internal error:', error) + return NextResponse.json( + { error: 'An error occurred. Please try again.' }, + { status: 500 } + ) +} +``` + +#### Verification Steps +- [ ] No passwords, tokens, or secrets in logs +- [ ] Error messages generic for users +- [ ] Detailed errors only in server logs +- [ ] No stack traces exposed to users + +### 9. Blockchain Security (Solana) + +#### Wallet Verification +```typescript +import { verify } from '@solana/web3.js' + +async function verifyWalletOwnership( + publicKey: string, + signature: string, + message: string +) { + try { + const isValid = verify( + Buffer.from(message), + Buffer.from(signature, 'base64'), + Buffer.from(publicKey, 'base64') + ) + return isValid + } catch (error) { + return false + } +} +``` + +#### Transaction Verification +```typescript +async function verifyTransaction(transaction: Transaction) { + // Verify recipient + if (transaction.to !== expectedRecipient) { + throw new Error('Invalid recipient') + } + + // Verify amount + if (transaction.amount > maxAmount) { + throw new Error('Amount exceeds limit') + } + + // Verify user has sufficient balance + const balance = await getBalance(transaction.from) + if (balance < transaction.amount) { + throw new Error('Insufficient balance') + } + + return true +} +``` + +#### Verification Steps +- [ ] Wallet signatures verified +- [ ] Transaction details validated +- [ ] Balance checks before transactions +- [ ] No blind transaction signing + +### 10. Dependency Security + +#### Regular Updates +```bash +# Check for vulnerabilities +npm audit + +# Fix automatically fixable issues +npm audit fix + +# Update dependencies +npm update + +# Check for outdated packages +npm outdated +``` + +#### Lock Files +```bash +# ALWAYS commit lock files +git add package-lock.json + +# Use in CI/CD for reproducible builds +npm ci # Instead of npm install +``` + +#### Verification Steps +- [ ] Dependencies up to date +- [ ] No known vulnerabilities (npm audit clean) +- [ ] Lock files committed +- [ ] Dependabot enabled on GitHub +- [ ] Regular security updates + +## Security Testing + +### Automated Security Tests +```typescript +// Test authentication +test('requires authentication', async () => { + const response = await fetch('/api/protected') + expect(response.status).toBe(401) +}) + +// Test authorization +test('requires admin role', async () => { + const response = await fetch('/api/admin', { + headers: { Authorization: `Bearer ${userToken}` } + }) + expect(response.status).toBe(403) +}) + +// Test input validation +test('rejects invalid input', async () => { + const response = await fetch('/api/users', { + method: 'POST', + body: JSON.stringify({ email: 'not-an-email' }) + }) + expect(response.status).toBe(400) +}) + +// Test rate limiting +test('enforces rate limits', async () => { + const requests = Array(101).fill(null).map(() => + fetch('/api/endpoint') + ) + + const responses = await Promise.all(requests) + const tooManyRequests = responses.filter(r => r.status === 429) + + expect(tooManyRequests.length).toBeGreaterThan(0) +}) +``` + +## Pre-Deployment Security Checklist + +Before ANY production deployment: + +- [ ] **Secrets**: No hardcoded secrets, all in env vars +- [ ] **Input Validation**: All user inputs validated +- [ ] **SQL Injection**: All queries parameterized +- [ ] **XSS**: User content sanitized +- [ ] **CSRF**: Protection enabled +- [ ] **Authentication**: Proper token handling +- [ ] **Authorization**: Role checks in place +- [ ] **Rate Limiting**: Enabled on all endpoints +- [ ] **HTTPS**: Enforced in production +- [ ] **Security Headers**: CSP, X-Frame-Options configured +- [ ] **Error Handling**: No sensitive data in errors +- [ ] **Logging**: No sensitive data logged +- [ ] **Dependencies**: Up to date, no vulnerabilities +- [ ] **Row Level Security**: Enabled in Supabase +- [ ] **CORS**: Properly configured +- [ ] **File Uploads**: Validated (size, type) +- [ ] **Wallet Signatures**: Verified (if blockchain) + +## Resources + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Next.js Security](https://nextjs.org/docs/security) +- [Supabase Security](https://supabase.com/docs/guides/auth) +- [Web Security Academy](https://portswigger.net/web-security) + +--- + +**Remember**: Security is not optional. One vulnerability can compromise the entire platform. When in doubt, err on the side of caution. diff --git a/.agents/skills/security-review/agents/openai.yaml b/.agents/skills/security-review/agents/openai.yaml new file mode 100644 index 00000000..9af83023 --- /dev/null +++ b/.agents/skills/security-review/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Security Review" + short_description: "Comprehensive security checklist and vulnerability detection" + brand_color: "#EF4444" + default_prompt: "Run security checklist: secrets, input validation, injection prevention" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/strategic-compact/SKILL.md b/.agents/skills/strategic-compact/SKILL.md new file mode 100644 index 00000000..67bbb31e --- /dev/null +++ b/.agents/skills/strategic-compact/SKILL.md @@ -0,0 +1,103 @@ +--- +name: strategic-compact +description: Suggests manual context compaction at logical intervals to preserve context through task phases rather than arbitrary auto-compaction. +origin: ECC +--- + +# Strategic Compact Skill + +Suggests manual `/compact` at strategic points in your workflow rather than relying on arbitrary auto-compaction. + +## When to Activate + +- Running long sessions that approach context limits (200K+ tokens) +- Working on multi-phase tasks (research → plan → implement → test) +- Switching between unrelated tasks within the same session +- After completing a major milestone and starting new work +- When responses slow down or become less coherent (context pressure) + +## Why Strategic Compaction? + +Auto-compaction triggers at arbitrary points: +- Often mid-task, losing important context +- No awareness of logical task boundaries +- Can interrupt complex multi-step operations + +Strategic compaction at logical boundaries: +- **After exploration, before execution** — Compact research context, keep implementation plan +- **After completing a milestone** — Fresh start for next phase +- **Before major context shifts** — Clear exploration context before different task + +## How It Works + +The `suggest-compact.js` script runs on PreToolUse (Edit/Write) and: + +1. **Tracks tool calls** — Counts tool invocations in session +2. **Threshold detection** — Suggests at configurable threshold (default: 50 calls) +3. **Periodic reminders** — Reminds every 25 calls after threshold + +## Hook Setup + +Add to your `~/.claude/settings.json`: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit", + "hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }] + }, + { + "matcher": "Write", + "hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }] + } + ] + } +} +``` + +## Configuration + +Environment variables: +- `COMPACT_THRESHOLD` — Tool calls before first suggestion (default: 50) + +## Compaction Decision Guide + +Use this table to decide when to compact: + +| Phase Transition | Compact? | Why | +|-----------------|----------|-----| +| Research → Planning | Yes | Research context is bulky; plan is the distilled output | +| Planning → Implementation | Yes | Plan is in TodoWrite or a file; free up context for code | +| Implementation → Testing | Maybe | Keep if tests reference recent code; compact if switching focus | +| Debugging → Next feature | Yes | Debug traces pollute context for unrelated work | +| Mid-implementation | No | Losing variable names, file paths, and partial state is costly | +| After a failed approach | Yes | Clear the dead-end reasoning before trying a new approach | + +## What Survives Compaction + +Understanding what persists helps you compact with confidence: + +| Persists | Lost | +|----------|------| +| CLAUDE.md instructions | Intermediate reasoning and analysis | +| TodoWrite task list | File contents you previously read | +| Memory files (`~/.claude/memory/`) | Multi-step conversation context | +| Git state (commits, branches) | Tool call history and counts | +| Files on disk | Nuanced user preferences stated verbally | + +## Best Practices + +1. **Compact after planning** — Once plan is finalized in TodoWrite, compact to start fresh +2. **Compact after debugging** — Clear error-resolution context before continuing +3. **Don't compact mid-implementation** — Preserve context for related changes +4. **Read the suggestion** — The hook tells you *when*, you decide *if* +5. **Write before compacting** — Save important context to files or memory before compacting +6. **Use `/compact` with a summary** — Add a custom message: `/compact Focus on implementing auth middleware next` + +## Related + +- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) — Token optimization section +- Memory persistence hooks — For state that survives compaction +- `continuous-learning` skill — Extracts patterns before session ends diff --git a/.agents/skills/strategic-compact/agents/openai.yaml b/.agents/skills/strategic-compact/agents/openai.yaml new file mode 100644 index 00000000..19ecabf8 --- /dev/null +++ b/.agents/skills/strategic-compact/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Strategic Compact" + short_description: "Context management via strategic compaction" + brand_color: "#14B8A6" + default_prompt: "Suggest task boundary compaction for context management" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/tdd-workflow/SKILL.md b/.agents/skills/tdd-workflow/SKILL.md new file mode 100644 index 00000000..90c0a6df --- /dev/null +++ b/.agents/skills/tdd-workflow/SKILL.md @@ -0,0 +1,410 @@ +--- +name: tdd-workflow +description: Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests. +origin: ECC +--- + +# Test-Driven Development Workflow + +This skill ensures all code development follows TDD principles with comprehensive test coverage. + +## When to Activate + +- Writing new features or functionality +- Fixing bugs or issues +- Refactoring existing code +- Adding API endpoints +- Creating new components + +## Core Principles + +### 1. Tests BEFORE Code +ALWAYS write tests first, then implement code to make tests pass. + +### 2. Coverage Requirements +- Minimum 80% coverage (unit + integration + E2E) +- All edge cases covered +- Error scenarios tested +- Boundary conditions verified + +### 3. Test Types + +#### Unit Tests +- Individual functions and utilities +- Component logic +- Pure functions +- Helpers and utilities + +#### Integration Tests +- API endpoints +- Database operations +- Service interactions +- External API calls + +#### E2E Tests (Playwright) +- Critical user flows +- Complete workflows +- Browser automation +- UI interactions + +## TDD Workflow Steps + +### Step 1: Write User Journeys +``` +As a [role], I want to [action], so that [benefit] + +Example: +As a user, I want to search for markets semantically, +so that I can find relevant markets even without exact keywords. +``` + +### Step 2: Generate Test Cases +For each user journey, create comprehensive test cases: + +```typescript +describe('Semantic Search', () => { + it('returns relevant markets for query', async () => { + // Test implementation + }) + + it('handles empty query gracefully', async () => { + // Test edge case + }) + + it('falls back to substring search when Redis unavailable', async () => { + // Test fallback behavior + }) + + it('sorts results by similarity score', async () => { + // Test sorting logic + }) +}) +``` + +### Step 3: Run Tests (They Should Fail) +```bash +npm test +# Tests should fail - we haven't implemented yet +``` + +### Step 4: Implement Code +Write minimal code to make tests pass: + +```typescript +// Implementation guided by tests +export async function searchMarkets(query: string) { + // Implementation here +} +``` + +### Step 5: Run Tests Again +```bash +npm test +# Tests should now pass +``` + +### Step 6: Refactor +Improve code quality while keeping tests green: +- Remove duplication +- Improve naming +- Optimize performance +- Enhance readability + +### Step 7: Verify Coverage +```bash +npm run test:coverage +# Verify 80%+ coverage achieved +``` + +## Testing Patterns + +### Unit Test Pattern (Jest/Vitest) +```typescript +import { render, screen, fireEvent } from '@testing-library/react' +import { Button } from './Button' + +describe('Button Component', () => { + it('renders with correct text', () => { + render() + expect(screen.getByText('Click me')).toBeInTheDocument() + }) + + it('calls onClick when clicked', () => { + const handleClick = jest.fn() + render() + + fireEvent.click(screen.getByRole('button')) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + it('is disabled when disabled prop is true', () => { + render() + expect(screen.getByRole('button')).toBeDisabled() + }) +}) +``` + +### API Integration Test Pattern +```typescript +import { NextRequest } from 'next/server' +import { GET } from './route' + +describe('GET /api/markets', () => { + it('returns markets successfully', async () => { + const request = new NextRequest('http://localhost/api/markets') + const response = await GET(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.success).toBe(true) + expect(Array.isArray(data.data)).toBe(true) + }) + + it('validates query parameters', async () => { + const request = new NextRequest('http://localhost/api/markets?limit=invalid') + const response = await GET(request) + + expect(response.status).toBe(400) + }) + + it('handles database errors gracefully', async () => { + // Mock database failure + const request = new NextRequest('http://localhost/api/markets') + // Test error handling + }) +}) +``` + +### E2E Test Pattern (Playwright) +```typescript +import { test, expect } from '@playwright/test' + +test('user can search and filter markets', async ({ page }) => { + // Navigate to markets page + await page.goto('/') + await page.click('a[href="/markets"]') + + // Verify page loaded + await expect(page.locator('h1')).toContainText('Markets') + + // Search for markets + await page.fill('input[placeholder="Search markets"]', 'election') + + // Wait for debounce and results + await page.waitForTimeout(600) + + // Verify search results displayed + const results = page.locator('[data-testid="market-card"]') + await expect(results).toHaveCount(5, { timeout: 5000 }) + + // Verify results contain search term + const firstResult = results.first() + await expect(firstResult).toContainText('election', { ignoreCase: true }) + + // Filter by status + await page.click('button:has-text("Active")') + + // Verify filtered results + await expect(results).toHaveCount(3) +}) + +test('user can create a new market', async ({ page }) => { + // Login first + await page.goto('/creator-dashboard') + + // Fill market creation form + await page.fill('input[name="name"]', 'Test Market') + await page.fill('textarea[name="description"]', 'Test description') + await page.fill('input[name="endDate"]', '2025-12-31') + + // Submit form + await page.click('button[type="submit"]') + + // Verify success message + await expect(page.locator('text=Market created successfully')).toBeVisible() + + // Verify redirect to market page + await expect(page).toHaveURL(/\/markets\/test-market/) +}) +``` + +## Test File Organization + +``` +src/ +├── components/ +│ ├── Button/ +│ │ ├── Button.tsx +│ │ ├── Button.test.tsx # Unit tests +│ │ └── Button.stories.tsx # Storybook +│ └── MarketCard/ +│ ├── MarketCard.tsx +│ └── MarketCard.test.tsx +├── app/ +│ └── api/ +│ └── markets/ +│ ├── route.ts +│ └── route.test.ts # Integration tests +└── e2e/ + ├── markets.spec.ts # E2E tests + ├── trading.spec.ts + └── auth.spec.ts +``` + +## Mocking External Services + +### Supabase Mock +```typescript +jest.mock('@/lib/supabase', () => ({ + supabase: { + from: jest.fn(() => ({ + select: jest.fn(() => ({ + eq: jest.fn(() => Promise.resolve({ + data: [{ id: 1, name: 'Test Market' }], + error: null + })) + })) + })) + } +})) +``` + +### Redis Mock +```typescript +jest.mock('@/lib/redis', () => ({ + searchMarketsByVector: jest.fn(() => Promise.resolve([ + { slug: 'test-market', similarity_score: 0.95 } + ])), + checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true })) +})) +``` + +### OpenAI Mock +```typescript +jest.mock('@/lib/openai', () => ({ + generateEmbedding: jest.fn(() => Promise.resolve( + new Array(1536).fill(0.1) // Mock 1536-dim embedding + )) +})) +``` + +## Test Coverage Verification + +### Run Coverage Report +```bash +npm run test:coverage +``` + +### Coverage Thresholds +```json +{ + "jest": { + "coverageThresholds": { + "global": { + "branches": 80, + "functions": 80, + "lines": 80, + "statements": 80 + } + } + } +} +``` + +## Common Testing Mistakes to Avoid + +### ❌ WRONG: Testing Implementation Details +```typescript +// Don't test internal state +expect(component.state.count).toBe(5) +``` + +### ✅ CORRECT: Test User-Visible Behavior +```typescript +// Test what users see +expect(screen.getByText('Count: 5')).toBeInTheDocument() +``` + +### ❌ WRONG: Brittle Selectors +```typescript +// Breaks easily +await page.click('.css-class-xyz') +``` + +### ✅ CORRECT: Semantic Selectors +```typescript +// Resilient to changes +await page.click('button:has-text("Submit")') +await page.click('[data-testid="submit-button"]') +``` + +### ❌ WRONG: No Test Isolation +```typescript +// Tests depend on each other +test('creates user', () => { /* ... */ }) +test('updates same user', () => { /* depends on previous test */ }) +``` + +### ✅ CORRECT: Independent Tests +```typescript +// Each test sets up its own data +test('creates user', () => { + const user = createTestUser() + // Test logic +}) + +test('updates user', () => { + const user = createTestUser() + // Update logic +}) +``` + +## Continuous Testing + +### Watch Mode During Development +```bash +npm test -- --watch +# Tests run automatically on file changes +``` + +### Pre-Commit Hook +```bash +# Runs before every commit +npm test && npm run lint +``` + +### CI/CD Integration +```yaml +# GitHub Actions +- name: Run Tests + run: npm test -- --coverage +- name: Upload Coverage + uses: codecov/codecov-action@v3 +``` + +## Best Practices + +1. **Write Tests First** - Always TDD +2. **One Assert Per Test** - Focus on single behavior +3. **Descriptive Test Names** - Explain what's tested +4. **Arrange-Act-Assert** - Clear test structure +5. **Mock External Dependencies** - Isolate unit tests +6. **Test Edge Cases** - Null, undefined, empty, large +7. **Test Error Paths** - Not just happy paths +8. **Keep Tests Fast** - Unit tests < 50ms each +9. **Clean Up After Tests** - No side effects +10. **Review Coverage Reports** - Identify gaps + +## Success Metrics + +- 80%+ code coverage achieved +- All tests passing (green) +- No skipped or disabled tests +- Fast test execution (< 30s for unit tests) +- E2E tests cover critical user flows +- Tests catch bugs before production + +--- + +**Remember**: Tests are not optional. They are the safety net that enables confident refactoring, rapid development, and production reliability. diff --git a/.agents/skills/tdd-workflow/agents/openai.yaml b/.agents/skills/tdd-workflow/agents/openai.yaml new file mode 100644 index 00000000..425c7d1c --- /dev/null +++ b/.agents/skills/tdd-workflow/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "TDD Workflow" + short_description: "Test-driven development with 80%+ coverage" + brand_color: "#22C55E" + default_prompt: "Follow TDD: write tests first, implement, verify 80%+ coverage" +policy: + allow_implicit_invocation: true diff --git a/.agents/skills/verification-loop/SKILL.md b/.agents/skills/verification-loop/SKILL.md new file mode 100644 index 00000000..1933545d --- /dev/null +++ b/.agents/skills/verification-loop/SKILL.md @@ -0,0 +1,126 @@ +--- +name: verification-loop +description: "A comprehensive verification system for Claude Code sessions." +origin: ECC +--- + +# Verification Loop Skill + +A comprehensive verification system for Claude Code sessions. + +## When to Use + +Invoke this skill: +- After completing a feature or significant code change +- Before creating a PR +- When you want to ensure quality gates pass +- After refactoring + +## Verification Phases + +### Phase 1: Build Verification +```bash +# Check if project builds +npm run build 2>&1 | tail -20 +# OR +pnpm build 2>&1 | tail -20 +``` + +If build fails, STOP and fix before continuing. + +### Phase 2: Type Check +```bash +# TypeScript projects +npx tsc --noEmit 2>&1 | head -30 + +# Python projects +pyright . 2>&1 | head -30 +``` + +Report all type errors. Fix critical ones before continuing. + +### Phase 3: Lint Check +```bash +# JavaScript/TypeScript +npm run lint 2>&1 | head -30 + +# Python +ruff check . 2>&1 | head -30 +``` + +### Phase 4: Test Suite +```bash +# Run tests with coverage +npm run test -- --coverage 2>&1 | tail -50 + +# Check coverage threshold +# Target: 80% minimum +``` + +Report: +- Total tests: X +- Passed: X +- Failed: X +- Coverage: X% + +### Phase 5: Security Scan +```bash +# Check for secrets +grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10 +grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10 + +# Check for console.log +grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10 +``` + +### Phase 6: Diff Review +```bash +# Show what changed +git diff --stat +git diff HEAD~1 --name-only +``` + +Review each changed file for: +- Unintended changes +- Missing error handling +- Potential edge cases + +## Output Format + +After running all phases, produce a verification report: + +``` +VERIFICATION REPORT +================== + +Build: [PASS/FAIL] +Types: [PASS/FAIL] (X errors) +Lint: [PASS/FAIL] (X warnings) +Tests: [PASS/FAIL] (X/Y passed, Z% coverage) +Security: [PASS/FAIL] (X issues) +Diff: [X files changed] + +Overall: [READY/NOT READY] for PR + +Issues to Fix: +1. ... +2. ... +``` + +## Continuous Mode + +For long sessions, run verification every 15 minutes or after major changes: + +```markdown +Set a mental checkpoint: +- After completing each function +- After finishing a component +- Before moving to next task + +Run: /verify +``` + +## Integration with Hooks + +This skill complements PostToolUse hooks but provides deeper verification. +Hooks catch issues immediately; this skill provides comprehensive review. diff --git a/.agents/skills/verification-loop/agents/openai.yaml b/.agents/skills/verification-loop/agents/openai.yaml new file mode 100644 index 00000000..644a9ad5 --- /dev/null +++ b/.agents/skills/verification-loop/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Verification Loop" + short_description: "Build, test, lint, typecheck verification" + brand_color: "#10B981" + default_prompt: "Run verification: build, test, lint, typecheck, security" +policy: + allow_implicit_invocation: true diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md new file mode 100644 index 00000000..97fdd57a --- /dev/null +++ b/.codex/AGENTS.md @@ -0,0 +1,55 @@ +# ECC for Codex CLI + +This supplements the root `AGENTS.md` with Codex-specific guidance. + +## Model Recommendations + +| Task Type | Recommended Model | +|-----------|------------------| +| Routine coding, tests, formatting | o4-mini | +| Complex features, architecture | o3 | +| Debugging, refactoring | o4-mini | +| Security review | o3 | + +## Skills Discovery + +Skills are auto-loaded from `.agents/skills/`. Each skill contains: +- `SKILL.md` — Detailed instructions and workflow +- `agents/openai.yaml` — Codex interface metadata + +Available skills: +- tdd-workflow — Test-driven development with 80%+ coverage +- security-review — Comprehensive security checklist +- coding-standards — Universal coding standards +- frontend-patterns — React/Next.js patterns +- backend-patterns — API design, database, caching +- e2e-testing — Playwright E2E tests +- eval-harness — Eval-driven development +- strategic-compact — Context management +- api-design — REST API design patterns +- verification-loop — Build, test, lint, typecheck, security + +## MCP Servers + +Configure in `~/.codex/config.toml` under `[mcp_servers]`. See `.codex/config.toml` for reference configuration with GitHub, Context7, Memory, and Sequential Thinking servers. + +## Key Differences from Claude Code + +| Feature | Claude Code | Codex CLI | +|---------|------------|-----------| +| Hooks | 8+ event types | Not yet supported | +| Context file | CLAUDE.md + AGENTS.md | AGENTS.md only | +| Skills | Skills loaded via plugin | `.agents/skills/` directory | +| Commands | `/slash` commands | Instruction-based | +| Agents | Subagent Task tool | Single agent model | +| Security | Hook-based enforcement | Instruction + sandbox | +| MCP | Full support | Command-based only | + +## Security Without Hooks + +Since Codex lacks hooks, security enforcement is instruction-based: +1. Always validate inputs at system boundaries +2. Never hardcode secrets — use environment variables +3. Run `npm audit` / `pip audit` before committing +4. Review `git diff` before every push +5. Use `sandbox_mode = "workspace-write"` in config diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 00000000..87d9747a --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,80 @@ +# Everything Claude Code (ECC) — Codex CLI Reference Configuration +# +# Copy this file to ~/.codex/config.toml to apply globally. +# Or keep it in your project root for project-level config. +# +# Docs: https://github.com/openai/codex + +# Model selection +model = "o4-mini" +model_provider = "openai" + +# Permissions +[permissions] +# "untrusted" = no writes, "on-request" = ask per action, "never" = full auto +approval_policy = "on-request" +# "off", "workspace-read", "workspace-write", "danger-full-access" +sandbox_mode = "workspace-write" + +# Notifications (macOS) +[notify] +command = "terminal-notifier -title 'Codex ECC' -message 'Task completed!' -sound default" +on_complete = true + +# History - persistent instructions applied to every session +[history] +# These are prepended to every conversation +persistent_instructions = """ +Follow ECC principles: +1. Test-Driven Development (TDD) - write tests first, 80%+ coverage required +2. Immutability - always create new objects, never mutate +3. Security-First - validate all inputs, no hardcoded secrets +4. Conventional commits: feat|fix|refactor|docs|test|chore|perf|ci: description +5. File organization: many small files (200-400 lines, 800 max) +6. Error handling: handle at every level, never swallow errors +7. Input validation: schema-based validation at system boundaries +""" + +# MCP Servers +# Codex supports command-based MCP servers +[mcp_servers.github] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-github"] + +[mcp_servers.context7] +command = "npx" +args = ["-y", "@upstash/context7-mcp@latest"] + +[mcp_servers.memory] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-memory"] + +[mcp_servers.sequential-thinking] +command = "npx" +args = ["-y", "@modelcontextprotocol/server-sequential-thinking"] + +# Additional MCP servers (uncomment as needed): +# [mcp_servers.supabase] +# command = "npx" +# args = ["-y", "supabase-mcp-server@latest", "--read-only"] +# +# [mcp_servers.firecrawl] +# command = "npx" +# args = ["-y", "firecrawl-mcp"] +# +# [mcp_servers.railway] +# command = "npx" +# args = ["-y", "@anthropic/railway-mcp"] + +# Features +[features] +web_search_request = true + +# Profiles — switch with CODEX_PROFILE= +[profiles.strict] +approval_policy = "on-request" +sandbox_mode = "workspace-read" + +[profiles.yolo] +approval_policy = "never" +sandbox_mode = "workspace-write" diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 00000000..f46121b8 --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,109 @@ +{ + "hooks": { + "sessionStart": [ + { + "command": "node .cursor/hooks/session-start.js", + "event": "sessionStart", + "description": "Load previous context and detect environment" + } + ], + "sessionEnd": [ + { + "command": "node .cursor/hooks/session-end.js", + "event": "sessionEnd", + "description": "Persist session state and evaluate patterns" + } + ], + "beforeShellExecution": [ + { + "command": "node .cursor/hooks/before-shell-execution.js", + "event": "beforeShellExecution", + "description": "Tmux dev server blocker, tmux reminder, git push review" + } + ], + "afterShellExecution": [ + { + "command": "node .cursor/hooks/after-shell-execution.js", + "event": "afterShellExecution", + "description": "PR URL logging, build analysis" + } + ], + "afterFileEdit": [ + { + "command": "node .cursor/hooks/after-file-edit.js", + "event": "afterFileEdit", + "description": "Auto-format, TypeScript check, console.log warning" + } + ], + "beforeMCPExecution": [ + { + "command": "node .cursor/hooks/before-mcp-execution.js", + "event": "beforeMCPExecution", + "description": "MCP audit logging and untrusted server warning" + } + ], + "afterMCPExecution": [ + { + "command": "node .cursor/hooks/after-mcp-execution.js", + "event": "afterMCPExecution", + "description": "MCP result logging" + } + ], + "beforeReadFile": [ + { + "command": "node .cursor/hooks/before-read-file.js", + "event": "beforeReadFile", + "description": "Warn when reading sensitive files (.env, .key, .pem)" + } + ], + "beforeSubmitPrompt": [ + { + "command": "node .cursor/hooks/before-submit-prompt.js", + "event": "beforeSubmitPrompt", + "description": "Detect secrets in prompts (sk-, ghp_, AKIA patterns)" + } + ], + "subagentStart": [ + { + "command": "node .cursor/hooks/subagent-start.js", + "event": "subagentStart", + "description": "Log agent spawning for observability" + } + ], + "subagentStop": [ + { + "command": "node .cursor/hooks/subagent-stop.js", + "event": "subagentStop", + "description": "Log agent completion" + } + ], + "beforeTabFileRead": [ + { + "command": "node .cursor/hooks/before-tab-file-read.js", + "event": "beforeTabFileRead", + "description": "Block Tab from reading secrets (.env, .key, .pem, credentials)" + } + ], + "afterTabFileEdit": [ + { + "command": "node .cursor/hooks/after-tab-file-edit.js", + "event": "afterTabFileEdit", + "description": "Auto-format Tab edits" + } + ], + "preCompact": [ + { + "command": "node .cursor/hooks/pre-compact.js", + "event": "preCompact", + "description": "Save state before context compaction" + } + ], + "stop": [ + { + "command": "node .cursor/hooks/stop.js", + "event": "stop", + "description": "Console.log audit on all modified files" + } + ] + } +} diff --git a/.cursor/hooks/adapter.js b/.cursor/hooks/adapter.js new file mode 100644 index 00000000..23ba4723 --- /dev/null +++ b/.cursor/hooks/adapter.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node +/** + * Cursor-to-Claude Code Hook Adapter + * Transforms Cursor stdin JSON to Claude Code hook format, + * then delegates to existing scripts/hooks/*.js + */ + +const { execFileSync } = require('child_process'); +const path = require('path'); + +const MAX_STDIN = 1024 * 1024; + +function readStdin() { + return new Promise((resolve) => { + let data = ''; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', chunk => { + if (data.length < MAX_STDIN) data += chunk.substring(0, MAX_STDIN - data.length); + }); + process.stdin.on('end', () => resolve(data)); + }); +} + +function getPluginRoot() { + return path.resolve(__dirname, '..', '..'); +} + +function transformToClaude(cursorInput, overrides = {}) { + return { + tool_input: { + command: cursorInput.command || cursorInput.args?.command || '', + file_path: cursorInput.path || cursorInput.file || '', + ...overrides.tool_input, + }, + tool_output: { + output: cursorInput.output || cursorInput.result || '', + ...overrides.tool_output, + }, + _cursor: { + conversation_id: cursorInput.conversation_id, + hook_event_name: cursorInput.hook_event_name, + workspace_roots: cursorInput.workspace_roots, + model: cursorInput.model, + }, + }; +} + +function runExistingHook(scriptName, stdinData) { + const scriptPath = path.join(getPluginRoot(), 'scripts', 'hooks', scriptName); + try { + execFileSync('node', [scriptPath], { + input: typeof stdinData === 'string' ? stdinData : JSON.stringify(stdinData), + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 15000, + cwd: process.cwd(), + }); + } catch (e) { + if (e.status === 2) process.exit(2); // Forward blocking exit code + } +} + +module.exports = { readStdin, getPluginRoot, transformToClaude, runExistingHook }; diff --git a/.cursor/hooks/after-file-edit.js b/.cursor/hooks/after-file-edit.js new file mode 100644 index 00000000..f4c39e49 --- /dev/null +++ b/.cursor/hooks/after-file-edit.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node +const { readStdin, runExistingHook, transformToClaude } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const claudeInput = transformToClaude(input, { + tool_input: { file_path: input.path || input.file || '' } + }); + const claudeStr = JSON.stringify(claudeInput); + + // Run format, typecheck, and console.log warning sequentially + runExistingHook('post-edit-format.js', claudeStr); + runExistingHook('post-edit-typecheck.js', claudeStr); + runExistingHook('post-edit-console-warn.js', claudeStr); + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/after-mcp-execution.js b/.cursor/hooks/after-mcp-execution.js new file mode 100644 index 00000000..d09a780e --- /dev/null +++ b/.cursor/hooks/after-mcp-execution.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const server = input.server || input.mcp_server || 'unknown'; + const tool = input.tool || input.mcp_tool || 'unknown'; + const success = input.error ? 'FAILED' : 'OK'; + console.error(`[ECC] MCP result: ${server}/${tool} - ${success}`); + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/after-shell-execution.js b/.cursor/hooks/after-shell-execution.js new file mode 100644 index 00000000..bdccc973 --- /dev/null +++ b/.cursor/hooks/after-shell-execution.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const cmd = input.command || ''; + const output = input.output || input.result || ''; + + // PR creation logging + if (/gh pr create/.test(cmd)) { + const m = output.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+/); + if (m) { + console.error('[ECC] PR created: ' + m[0]); + const repo = m[0].replace(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+/, '$1'); + const pr = m[0].replace(/.+\/pull\/(\d+)/, '$1'); + console.error('[ECC] To review: gh pr review ' + pr + ' --repo ' + repo); + } + } + + // Build completion notice + if (/(npm run build|pnpm build|yarn build)/.test(cmd)) { + console.error('[ECC] Build completed'); + } + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/after-tab-file-edit.js b/.cursor/hooks/after-tab-file-edit.js new file mode 100644 index 00000000..355876c0 --- /dev/null +++ b/.cursor/hooks/after-tab-file-edit.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node +const { readStdin, runExistingHook, transformToClaude } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const claudeInput = transformToClaude(input, { + tool_input: { file_path: input.path || input.file || '' } + }); + runExistingHook('post-edit-format.js', JSON.stringify(claudeInput)); + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/before-mcp-execution.js b/.cursor/hooks/before-mcp-execution.js new file mode 100644 index 00000000..38c56c24 --- /dev/null +++ b/.cursor/hooks/before-mcp-execution.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const server = input.server || input.mcp_server || 'unknown'; + const tool = input.tool || input.mcp_tool || 'unknown'; + console.error(`[ECC] MCP invocation: ${server}/${tool}`); + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/before-read-file.js b/.cursor/hooks/before-read-file.js new file mode 100644 index 00000000..e221530c --- /dev/null +++ b/.cursor/hooks/before-read-file.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const filePath = input.path || input.file || ''; + if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) { + console.error('[ECC] WARNING: Reading sensitive file: ' + filePath); + console.error('[ECC] Ensure this data is not exposed in outputs'); + } + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/before-shell-execution.js b/.cursor/hooks/before-shell-execution.js new file mode 100644 index 00000000..bb5448d9 --- /dev/null +++ b/.cursor/hooks/before-shell-execution.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const cmd = input.command || ''; + + // 1. Block dev server outside tmux + if (process.platform !== 'win32' && /(npm run dev\b|pnpm( run)? dev\b|yarn dev\b|bun run dev\b)/.test(cmd)) { + console.error('[ECC] BLOCKED: Dev server must run in tmux for log access'); + console.error('[ECC] Use: tmux new-session -d -s dev "npm run dev"'); + process.exit(2); + } + + // 2. Tmux reminder for long-running commands + if (process.platform !== 'win32' && !process.env.TMUX && + /(npm (install|test)|pnpm (install|test)|yarn (install|test)?|bun (install|test)|cargo build|make\b|docker\b|pytest|vitest|playwright)/.test(cmd)) { + console.error('[ECC] Consider running in tmux for session persistence'); + } + + // 3. Git push review reminder + if (/git push/.test(cmd)) { + console.error('[ECC] Review changes before push: git diff origin/main...HEAD'); + } + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/before-submit-prompt.js b/.cursor/hooks/before-submit-prompt.js new file mode 100644 index 00000000..f48426ae --- /dev/null +++ b/.cursor/hooks/before-submit-prompt.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const prompt = input.prompt || input.content || input.message || ''; + const secretPatterns = [ + /sk-[a-zA-Z0-9]{20,}/, // OpenAI API keys + /ghp_[a-zA-Z0-9]{36,}/, // GitHub personal access tokens + /AKIA[A-Z0-9]{16}/, // AWS access keys + /xox[bpsa]-[a-zA-Z0-9-]+/, // Slack tokens + /-----BEGIN (RSA |EC )?PRIVATE KEY-----/, // Private keys + ]; + for (const pattern of secretPatterns) { + if (pattern.test(prompt)) { + console.error('[ECC] WARNING: Potential secret detected in prompt!'); + console.error('[ECC] Remove secrets before submitting. Use environment variables instead.'); + break; + } + } + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/before-tab-file-read.js b/.cursor/hooks/before-tab-file-read.js new file mode 100644 index 00000000..f4ea79a4 --- /dev/null +++ b/.cursor/hooks/before-tab-file-read.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const filePath = input.path || input.file || ''; + if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) { + console.error('[ECC] BLOCKED: Tab cannot read sensitive file: ' + filePath); + process.exit(2); + } + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/pre-compact.js b/.cursor/hooks/pre-compact.js new file mode 100644 index 00000000..1b2156e1 --- /dev/null +++ b/.cursor/hooks/pre-compact.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +const { readStdin, runExistingHook, transformToClaude } = require('./adapter'); +readStdin().then(raw => { + const claudeInput = JSON.parse(raw || '{}'); + runExistingHook('pre-compact.js', transformToClaude(claudeInput)); + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/session-end.js b/.cursor/hooks/session-end.js new file mode 100644 index 00000000..5b759287 --- /dev/null +++ b/.cursor/hooks/session-end.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node +const { readStdin, runExistingHook, transformToClaude } = require('./adapter'); +readStdin().then(raw => { + const input = JSON.parse(raw); + const claudeInput = transformToClaude(input); + runExistingHook('session-end.js', claudeInput); + runExistingHook('evaluate-session.js', claudeInput); + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/session-start.js b/.cursor/hooks/session-start.js new file mode 100644 index 00000000..bdd9121a --- /dev/null +++ b/.cursor/hooks/session-start.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node +const { readStdin, runExistingHook, transformToClaude } = require('./adapter'); +readStdin().then(raw => { + const input = JSON.parse(raw); + const claudeInput = transformToClaude(input); + runExistingHook('session-start.js', claudeInput); + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/stop.js b/.cursor/hooks/stop.js new file mode 100644 index 00000000..d2bbbc3c --- /dev/null +++ b/.cursor/hooks/stop.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +const { readStdin, runExistingHook, transformToClaude } = require('./adapter'); +readStdin().then(raw => { + const claudeInput = JSON.parse(raw || '{}'); + runExistingHook('check-console-log.js', transformToClaude(claudeInput)); + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/subagent-start.js b/.cursor/hooks/subagent-start.js new file mode 100644 index 00000000..c03c9cd5 --- /dev/null +++ b/.cursor/hooks/subagent-start.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const agent = input.agent_name || input.agent || 'unknown'; + console.error(`[ECC] Agent spawned: ${agent}`); + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/hooks/subagent-stop.js b/.cursor/hooks/subagent-stop.js new file mode 100644 index 00000000..d6cf681b --- /dev/null +++ b/.cursor/hooks/subagent-stop.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node +const { readStdin } = require('./adapter'); +readStdin().then(raw => { + try { + const input = JSON.parse(raw); + const agent = input.agent_name || input.agent || 'unknown'; + console.error(`[ECC] Agent completed: ${agent}`); + } catch {} + process.stdout.write(raw); +}).catch(() => process.exit(0)); diff --git a/.cursor/rules/common-agents.md b/.cursor/rules/common-agents.md new file mode 100644 index 00000000..c5b17463 --- /dev/null +++ b/.cursor/rules/common-agents.md @@ -0,0 +1,53 @@ +--- +description: "Agent orchestration: available agents, parallel execution, multi-perspective analysis" +alwaysApply: true +--- +# Agent Orchestration + +## Available Agents + +Located in `~/.claude/agents/`: + +| Agent | Purpose | When to Use | +|-------|---------|-------------| +| planner | Implementation planning | Complex features, refactoring | +| architect | System design | Architectural decisions | +| tdd-guide | Test-driven development | New features, bug fixes | +| code-reviewer | Code review | After writing code | +| security-reviewer | Security analysis | Before commits | +| build-error-resolver | Fix build errors | When build fails | +| e2e-runner | E2E testing | Critical user flows | +| refactor-cleaner | Dead code cleanup | Code maintenance | +| doc-updater | Documentation | Updating docs | + +## Immediate Agent Usage + +No user prompt needed: +1. Complex feature requests - Use **planner** agent +2. Code just written/modified - Use **code-reviewer** agent +3. Bug fix or new feature - Use **tdd-guide** agent +4. Architectural decision - Use **architect** agent + +## Parallel Task Execution + +ALWAYS use parallel Task execution for independent operations: + +```markdown +# GOOD: Parallel execution +Launch 3 agents in parallel: +1. Agent 1: Security analysis of auth module +2. Agent 2: Performance review of cache system +3. Agent 3: Type checking of utilities + +# BAD: Sequential when unnecessary +First agent 1, then agent 2, then agent 3 +``` + +## Multi-Perspective Analysis + +For complex problems, use split role sub-agents: +- Factual reviewer +- Senior engineer +- Security expert +- Consistency reviewer +- Redundancy checker diff --git a/.cursor/rules/common-coding-style.md b/.cursor/rules/common-coding-style.md new file mode 100644 index 00000000..d7e2745b --- /dev/null +++ b/.cursor/rules/common-coding-style.md @@ -0,0 +1,52 @@ +--- +description: "ECC coding style: immutability, file organization, error handling, validation" +alwaysApply: true +--- +# Coding Style + +## Immutability (CRITICAL) + +ALWAYS create new objects, NEVER mutate existing ones: + +``` +// Pseudocode +WRONG: modify(original, field, value) → changes original in-place +CORRECT: update(original, field, value) → returns new copy with change +``` + +Rationale: Immutable data prevents hidden side effects, makes debugging easier, and enables safe concurrency. + +## File Organization + +MANY SMALL FILES > FEW LARGE FILES: +- High cohesion, low coupling +- 200-400 lines typical, 800 max +- Extract utilities from large modules +- Organize by feature/domain, not by type + +## Error Handling + +ALWAYS handle errors comprehensively: +- Handle errors explicitly at every level +- Provide user-friendly error messages in UI-facing code +- Log detailed error context on the server side +- Never silently swallow errors + +## Input Validation + +ALWAYS validate at system boundaries: +- Validate all user input before processing +- Use schema-based validation where available +- Fail fast with clear error messages +- Never trust external data (API responses, user input, file content) + +## Code Quality Checklist + +Before marking work complete: +- [ ] Code is readable and well-named +- [ ] Functions are small (<50 lines) +- [ ] Files are focused (<800 lines) +- [ ] No deep nesting (>4 levels) +- [ ] Proper error handling +- [ ] No hardcoded values (use constants or config) +- [ ] No mutation (immutable patterns used) diff --git a/.cursor/rules/common-development-workflow.md b/.cursor/rules/common-development-workflow.md new file mode 100644 index 00000000..a218b94c --- /dev/null +++ b/.cursor/rules/common-development-workflow.md @@ -0,0 +1,33 @@ +--- +description: "Development workflow: plan, TDD, review, commit pipeline" +alwaysApply: true +--- +# Development Workflow + +> This rule extends the git workflow rule with the full feature development process that happens before git operations. + +The Feature Implementation Workflow describes the development pipeline: planning, TDD, code review, and then committing to git. + +## Feature Implementation Workflow + +1. **Plan First** + - Use **planner** agent to create implementation plan + - Identify dependencies and risks + - Break down into phases + +2. **TDD Approach** + - Use **tdd-guide** agent + - Write tests first (RED) + - Implement to pass tests (GREEN) + - Refactor (IMPROVE) + - Verify 80%+ coverage + +3. **Code Review** + - Use **code-reviewer** agent immediately after writing code + - Address CRITICAL and HIGH issues + - Fix MEDIUM issues when possible + +4. **Commit & Push** + - Detailed commit messages + - Follow conventional commits format + - See the git workflow rule for commit message format and PR process diff --git a/.cursor/rules/common-git-workflow.md b/.cursor/rules/common-git-workflow.md new file mode 100644 index 00000000..b275023e --- /dev/null +++ b/.cursor/rules/common-git-workflow.md @@ -0,0 +1,28 @@ +--- +description: "Git workflow: conventional commits, PR process" +alwaysApply: true +--- +# Git Workflow + +## Commit Message Format +``` +: + + +``` + +Types: feat, fix, refactor, docs, test, chore, perf, ci + +Note: Attribution disabled globally via ~/.claude/settings.json. + +## Pull Request Workflow + +When creating PRs: +1. Analyze full commit history (not just latest commit) +2. Use `git diff [base-branch]...HEAD` to see all changes +3. Draft comprehensive PR summary +4. Include test plan with TODOs +5. Push with `-u` flag if new branch + +> For the full development process (planning, TDD, code review) before git operations, +> see the development workflow rule. diff --git a/.cursor/rules/common-hooks.md b/.cursor/rules/common-hooks.md new file mode 100644 index 00000000..0f9586fd --- /dev/null +++ b/.cursor/rules/common-hooks.md @@ -0,0 +1,34 @@ +--- +description: "Hooks system: types, auto-accept permissions, TodoWrite best practices" +alwaysApply: true +--- +# Hooks System + +## Hook Types + +- **PreToolUse**: Before tool execution (validation, parameter modification) +- **PostToolUse**: After tool execution (auto-format, checks) +- **Stop**: When session ends (final verification) + +## Auto-Accept Permissions + +Use with caution: +- Enable for trusted, well-defined plans +- Disable for exploratory work +- Never use dangerously-skip-permissions flag +- Configure `allowedTools` in `~/.claude.json` instead + +## TodoWrite Best Practices + +Use TodoWrite tool to: +- Track progress on multi-step tasks +- Verify understanding of instructions +- Enable real-time steering +- Show granular implementation steps + +Todo list reveals: +- Out of order steps +- Missing items +- Extra unnecessary items +- Wrong granularity +- Misinterpreted requirements diff --git a/.cursor/rules/common-patterns.md b/.cursor/rules/common-patterns.md new file mode 100644 index 00000000..7304709a --- /dev/null +++ b/.cursor/rules/common-patterns.md @@ -0,0 +1,35 @@ +--- +description: "Common patterns: repository, API response, skeleton projects" +alwaysApply: true +--- +# Common Patterns + +## Skeleton Projects + +When implementing new functionality: +1. Search for battle-tested skeleton projects +2. Use parallel agents to evaluate options: + - Security assessment + - Extensibility analysis + - Relevance scoring + - Implementation planning +3. Clone best match as foundation +4. Iterate within proven structure + +## Design Patterns + +### Repository Pattern + +Encapsulate data access behind a consistent interface: +- Define standard operations: findAll, findById, create, update, delete +- Concrete implementations handle storage details (database, API, file, etc.) +- Business logic depends on the abstract interface, not the storage mechanism +- Enables easy swapping of data sources and simplifies testing with mocks + +### API Response Format + +Use a consistent envelope for all API responses: +- Include a success/status indicator +- Include the data payload (nullable on error) +- Include an error message field (nullable on success) +- Include metadata for paginated responses (total, page, limit) diff --git a/.cursor/rules/common-performance.md b/.cursor/rules/common-performance.md new file mode 100644 index 00000000..dc65dccb --- /dev/null +++ b/.cursor/rules/common-performance.md @@ -0,0 +1,59 @@ +--- +description: "Performance: model selection, context management, build troubleshooting" +alwaysApply: true +--- +# Performance Optimization + +## Model Selection Strategy + +**Haiku 4.5** (90% of Sonnet capability, 3x cost savings): +- Lightweight agents with frequent invocation +- Pair programming and code generation +- Worker agents in multi-agent systems + +**Sonnet 4.6** (Best coding model): +- Main development work +- Orchestrating multi-agent workflows +- Complex coding tasks + +**Opus 4.5** (Deepest reasoning): +- Complex architectural decisions +- Maximum reasoning requirements +- Research and analysis tasks + +## Context Window Management + +Avoid last 20% of context window for: +- Large-scale refactoring +- Feature implementation spanning multiple files +- Debugging complex interactions + +Lower context sensitivity tasks: +- Single-file edits +- Independent utility creation +- Documentation updates +- Simple bug fixes + +## Extended Thinking + Plan Mode + +Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning. + +Control extended thinking via: +- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux) +- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json` +- **Budget cap**: `export MAX_THINKING_TOKENS=10000` +- **Verbose mode**: Ctrl+O to see thinking output + +For complex tasks requiring deep reasoning: +1. Ensure extended thinking is enabled (on by default) +2. Enable **Plan Mode** for structured approach +3. Use multiple critique rounds for thorough analysis +4. Use split role sub-agents for diverse perspectives + +## Build Troubleshooting + +If build fails: +1. Use **build-error-resolver** agent +2. Analyze error messages +3. Fix incrementally +4. Verify after each fix diff --git a/.cursor/rules/common-security.md b/.cursor/rules/common-security.md new file mode 100644 index 00000000..a45737c6 --- /dev/null +++ b/.cursor/rules/common-security.md @@ -0,0 +1,33 @@ +--- +description: "Security: mandatory checks, secret management, response protocol" +alwaysApply: true +--- +# Security Guidelines + +## Mandatory Security Checks + +Before ANY commit: +- [ ] No hardcoded secrets (API keys, passwords, tokens) +- [ ] All user inputs validated +- [ ] SQL injection prevention (parameterized queries) +- [ ] XSS prevention (sanitized HTML) +- [ ] CSRF protection enabled +- [ ] Authentication/authorization verified +- [ ] Rate limiting on all endpoints +- [ ] Error messages don't leak sensitive data + +## Secret Management + +- NEVER hardcode secrets in source code +- ALWAYS use environment variables or a secret manager +- Validate that required secrets are present at startup +- Rotate any secrets that may have been exposed + +## Security Response Protocol + +If security issue found: +1. STOP immediately +2. Use **security-reviewer** agent +3. Fix CRITICAL issues before continuing +4. Rotate any exposed secrets +5. Review entire codebase for similar issues diff --git a/.cursor/rules/common-testing.md b/.cursor/rules/common-testing.md new file mode 100644 index 00000000..bc6cd86d --- /dev/null +++ b/.cursor/rules/common-testing.md @@ -0,0 +1,33 @@ +--- +description: "Testing requirements: 80% coverage, TDD workflow, test types" +alwaysApply: true +--- +# Testing Requirements + +## Minimum Test Coverage: 80% + +Test Types (ALL required): +1. **Unit Tests** - Individual functions, utilities, components +2. **Integration Tests** - API endpoints, database operations +3. **E2E Tests** - Critical user flows (framework chosen per language) + +## Test-Driven Development + +MANDATORY workflow: +1. Write test first (RED) +2. Run test - it should FAIL +3. Write minimal implementation (GREEN) +4. Run test - it should PASS +5. Refactor (IMPROVE) +6. Verify coverage (80%+) + +## Troubleshooting Test Failures + +1. Use **tdd-guide** agent +2. Check test isolation +3. Verify mocks are correct +4. Fix implementation, not tests (unless tests are wrong) + +## Agent Support + +- **tdd-guide** - Use PROACTIVELY for new features, enforces write-tests-first diff --git a/.cursor/rules/golang-coding-style.md b/.cursor/rules/golang-coding-style.md new file mode 100644 index 00000000..412bb77d --- /dev/null +++ b/.cursor/rules/golang-coding-style.md @@ -0,0 +1,31 @@ +--- +description: "Go coding style extending common rules" +globs: ["**/*.go", "**/go.mod", "**/go.sum"] +alwaysApply: false +--- +# Go Coding Style + +> This file extends the common coding style rule with Go specific content. + +## Formatting + +- **gofmt** and **goimports** are mandatory -- no style debates + +## Design Principles + +- Accept interfaces, return structs +- Keep interfaces small (1-3 methods) + +## Error Handling + +Always wrap errors with context: + +```go +if err != nil { + return fmt.Errorf("failed to create user: %w", err) +} +``` + +## Reference + +See skill: `golang-patterns` for comprehensive Go idioms and patterns. diff --git a/.cursor/rules/golang-hooks.md b/.cursor/rules/golang-hooks.md new file mode 100644 index 00000000..4edadb8e --- /dev/null +++ b/.cursor/rules/golang-hooks.md @@ -0,0 +1,16 @@ +--- +description: "Go hooks extending common rules" +globs: ["**/*.go", "**/go.mod", "**/go.sum"] +alwaysApply: false +--- +# Go Hooks + +> This file extends the common hooks rule with Go specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **gofmt/goimports**: Auto-format `.go` files after edit +- **go vet**: Run static analysis after editing `.go` files +- **staticcheck**: Run extended static checks on modified packages diff --git a/.cursor/rules/golang-patterns.md b/.cursor/rules/golang-patterns.md new file mode 100644 index 00000000..d03fbb97 --- /dev/null +++ b/.cursor/rules/golang-patterns.md @@ -0,0 +1,44 @@ +--- +description: "Go patterns extending common rules" +globs: ["**/*.go", "**/go.mod", "**/go.sum"] +alwaysApply: false +--- +# Go Patterns + +> This file extends the common patterns rule with Go specific content. + +## Functional Options + +```go +type Option func(*Server) + +func WithPort(port int) Option { + return func(s *Server) { s.port = port } +} + +func NewServer(opts ...Option) *Server { + s := &Server{port: 8080} + for _, opt := range opts { + opt(s) + } + return s +} +``` + +## Small Interfaces + +Define interfaces where they are used, not where they are implemented. + +## Dependency Injection + +Use constructor functions to inject dependencies: + +```go +func NewUserService(repo UserRepository, logger Logger) *UserService { + return &UserService{repo: repo, logger: logger} +} +``` + +## Reference + +See skill: `golang-patterns` for comprehensive Go patterns including concurrency, error handling, and package organization. diff --git a/.cursor/rules/golang-security.md b/.cursor/rules/golang-security.md new file mode 100644 index 00000000..e2f88942 --- /dev/null +++ b/.cursor/rules/golang-security.md @@ -0,0 +1,33 @@ +--- +description: "Go security extending common rules" +globs: ["**/*.go", "**/go.mod", "**/go.sum"] +alwaysApply: false +--- +# Go Security + +> This file extends the common security rule with Go specific content. + +## Secret Management + +```go +apiKey := os.Getenv("OPENAI_API_KEY") +if apiKey == "" { + log.Fatal("OPENAI_API_KEY not configured") +} +``` + +## Security Scanning + +- Use **gosec** for static security analysis: + ```bash + gosec ./... + ``` + +## Context & Timeouts + +Always use `context.Context` for timeout control: + +```go +ctx, cancel := context.WithTimeout(ctx, 5*time.Second) +defer cancel() +``` diff --git a/.cursor/rules/golang-testing.md b/.cursor/rules/golang-testing.md new file mode 100644 index 00000000..530963a7 --- /dev/null +++ b/.cursor/rules/golang-testing.md @@ -0,0 +1,30 @@ +--- +description: "Go testing extending common rules" +globs: ["**/*.go", "**/go.mod", "**/go.sum"] +alwaysApply: false +--- +# Go Testing + +> This file extends the common testing rule with Go specific content. + +## Framework + +Use the standard `go test` with **table-driven tests**. + +## Race Detection + +Always run with the `-race` flag: + +```bash +go test -race ./... +``` + +## Coverage + +```bash +go test -cover ./... +``` + +## Reference + +See skill: `golang-testing` for detailed Go testing patterns and helpers. diff --git a/.cursor/rules/python-coding-style.md b/.cursor/rules/python-coding-style.md new file mode 100644 index 00000000..4537653b --- /dev/null +++ b/.cursor/rules/python-coding-style.md @@ -0,0 +1,42 @@ +--- +description: "Python coding style extending common rules" +globs: ["**/*.py", "**/*.pyi"] +alwaysApply: false +--- +# Python Coding Style + +> This file extends the common coding style rule with Python specific content. + +## Standards + +- Follow **PEP 8** conventions +- Use **type annotations** on all function signatures + +## Immutability + +Prefer immutable data structures: + +```python +from dataclasses import dataclass + +@dataclass(frozen=True) +class User: + name: str + email: str + +from typing import NamedTuple + +class Point(NamedTuple): + x: float + y: float +``` + +## Formatting + +- **black** for code formatting +- **isort** for import sorting +- **ruff** for linting + +## Reference + +See skill: `python-patterns` for comprehensive Python idioms and patterns. diff --git a/.cursor/rules/python-hooks.md b/.cursor/rules/python-hooks.md new file mode 100644 index 00000000..33165eae --- /dev/null +++ b/.cursor/rules/python-hooks.md @@ -0,0 +1,19 @@ +--- +description: "Python hooks extending common rules" +globs: ["**/*.py", "**/*.pyi"] +alwaysApply: false +--- +# Python Hooks + +> This file extends the common hooks rule with Python specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **black/ruff**: Auto-format `.py` files after edit +- **mypy/pyright**: Run type checking after editing `.py` files + +## Warnings + +- Warn about `print()` statements in edited files (use `logging` module instead) diff --git a/.cursor/rules/python-patterns.md b/.cursor/rules/python-patterns.md new file mode 100644 index 00000000..f6058bf0 --- /dev/null +++ b/.cursor/rules/python-patterns.md @@ -0,0 +1,39 @@ +--- +description: "Python patterns extending common rules" +globs: ["**/*.py", "**/*.pyi"] +alwaysApply: false +--- +# Python Patterns + +> This file extends the common patterns rule with Python specific content. + +## Protocol (Duck Typing) + +```python +from typing import Protocol + +class Repository(Protocol): + def find_by_id(self, id: str) -> dict | None: ... + def save(self, entity: dict) -> dict: ... +``` + +## Dataclasses as DTOs + +```python +from dataclasses import dataclass + +@dataclass +class CreateUserRequest: + name: str + email: str + age: int | None = None +``` + +## Context Managers & Generators + +- Use context managers (`with` statement) for resource management +- Use generators for lazy evaluation and memory-efficient iteration + +## Reference + +See skill: `python-patterns` for comprehensive patterns including decorators, concurrency, and package organization. diff --git a/.cursor/rules/python-security.md b/.cursor/rules/python-security.md new file mode 100644 index 00000000..5d76b184 --- /dev/null +++ b/.cursor/rules/python-security.md @@ -0,0 +1,30 @@ +--- +description: "Python security extending common rules" +globs: ["**/*.py", "**/*.pyi"] +alwaysApply: false +--- +# Python Security + +> This file extends the common security rule with Python specific content. + +## Secret Management + +```python +import os +from dotenv import load_dotenv + +load_dotenv() + +api_key = os.environ["OPENAI_API_KEY"] # Raises KeyError if missing +``` + +## Security Scanning + +- Use **bandit** for static security analysis: + ```bash + bandit -r src/ + ``` + +## Reference + +See skill: `django-security` for Django-specific security guidelines (if applicable). diff --git a/.cursor/rules/python-testing.md b/.cursor/rules/python-testing.md new file mode 100644 index 00000000..c72c612a --- /dev/null +++ b/.cursor/rules/python-testing.md @@ -0,0 +1,38 @@ +--- +description: "Python testing extending common rules" +globs: ["**/*.py", "**/*.pyi"] +alwaysApply: false +--- +# Python Testing + +> This file extends the common testing rule with Python specific content. + +## Framework + +Use **pytest** as the testing framework. + +## Coverage + +```bash +pytest --cov=src --cov-report=term-missing +``` + +## Test Organization + +Use `pytest.mark` for test categorization: + +```python +import pytest + +@pytest.mark.unit +def test_calculate_total(): + ... + +@pytest.mark.integration +def test_database_connection(): + ... +``` + +## Reference + +See skill: `python-testing` for detailed pytest patterns and fixtures. diff --git a/.cursor/rules/swift-coding-style.md b/.cursor/rules/swift-coding-style.md new file mode 100644 index 00000000..a1964049 --- /dev/null +++ b/.cursor/rules/swift-coding-style.md @@ -0,0 +1,47 @@ +--- +description: "Swift coding style extending common rules" +globs: ["**/*.swift", "**/Package.swift"] +alwaysApply: false +--- +# Swift Coding Style + +> This file extends the common coding style rule with Swift specific content. + +## Formatting + +- **SwiftFormat** for auto-formatting, **SwiftLint** for style enforcement +- `swift-format` is bundled with Xcode 16+ as an alternative + +## Immutability + +- Prefer `let` over `var` -- define everything as `let` and only change to `var` if the compiler requires it +- Use `struct` with value semantics by default; use `class` only when identity or reference semantics are needed + +## Naming + +Follow [Apple API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/): + +- Clarity at the point of use -- omit needless words +- Name methods and properties for their roles, not their types +- Use `static let` for constants over global constants + +## Error Handling + +Use typed throws (Swift 6+) and pattern matching: + +```swift +func load(id: String) throws(LoadError) -> Item { + guard let data = try? read(from: path) else { + throw .fileNotFound(id) + } + return try decode(data) +} +``` + +## Concurrency + +Enable Swift 6 strict concurrency checking. Prefer: + +- `Sendable` value types for data crossing isolation boundaries +- Actors for shared mutable state +- Structured concurrency (`async let`, `TaskGroup`) over unstructured `Task {}` diff --git a/.cursor/rules/swift-hooks.md b/.cursor/rules/swift-hooks.md new file mode 100644 index 00000000..f9f1b7fe --- /dev/null +++ b/.cursor/rules/swift-hooks.md @@ -0,0 +1,20 @@ +--- +description: "Swift hooks extending common rules" +globs: ["**/*.swift", "**/Package.swift"] +alwaysApply: false +--- +# Swift Hooks + +> This file extends the common hooks rule with Swift specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **SwiftFormat**: Auto-format `.swift` files after edit +- **SwiftLint**: Run lint checks after editing `.swift` files +- **swift build**: Type-check modified packages after edit + +## Warning + +Flag `print()` statements -- use `os.Logger` or structured logging instead for production code. diff --git a/.cursor/rules/swift-patterns.md b/.cursor/rules/swift-patterns.md new file mode 100644 index 00000000..d65947ca --- /dev/null +++ b/.cursor/rules/swift-patterns.md @@ -0,0 +1,66 @@ +--- +description: "Swift patterns extending common rules" +globs: ["**/*.swift", "**/Package.swift"] +alwaysApply: false +--- +# Swift Patterns + +> This file extends the common patterns rule with Swift specific content. + +## Protocol-Oriented Design + +Define small, focused protocols. Use protocol extensions for shared defaults: + +```swift +protocol Repository: Sendable { + associatedtype Item: Identifiable & Sendable + func find(by id: Item.ID) async throws -> Item? + func save(_ item: Item) async throws +} +``` + +## Value Types + +- Use structs for data transfer objects and models +- Use enums with associated values to model distinct states: + +```swift +enum LoadState: Sendable { + case idle + case loading + case loaded(T) + case failed(Error) +} +``` + +## Actor Pattern + +Use actors for shared mutable state instead of locks or dispatch queues: + +```swift +actor Cache { + private var storage: [Key: Value] = [:] + + func get(_ key: Key) -> Value? { storage[key] } + func set(_ key: Key, value: Value) { storage[key] = value } +} +``` + +## Dependency Injection + +Inject protocols with default parameters -- production uses defaults, tests inject mocks: + +```swift +struct UserService { + private let repository: any UserRepository + + init(repository: any UserRepository = DefaultUserRepository()) { + self.repository = repository + } +} +``` + +## References + +See skill: `swift-actor-persistence` for actor-based persistence patterns. +See skill: `swift-protocol-di-testing` for protocol-based DI and testing. diff --git a/.cursor/rules/swift-security.md b/.cursor/rules/swift-security.md new file mode 100644 index 00000000..b965f174 --- /dev/null +++ b/.cursor/rules/swift-security.md @@ -0,0 +1,33 @@ +--- +description: "Swift security extending common rules" +globs: ["**/*.swift", "**/Package.swift"] +alwaysApply: false +--- +# Swift Security + +> This file extends the common security rule with Swift specific content. + +## Secret Management + +- Use **Keychain Services** for sensitive data (tokens, passwords, keys) -- never `UserDefaults` +- Use environment variables or `.xcconfig` files for build-time secrets +- Never hardcode secrets in source -- decompilation tools extract them trivially + +```swift +let apiKey = ProcessInfo.processInfo.environment["API_KEY"] +guard let apiKey, !apiKey.isEmpty else { + fatalError("API_KEY not configured") +} +``` + +## Transport Security + +- App Transport Security (ATS) is enforced by default -- do not disable it +- Use certificate pinning for critical endpoints +- Validate all server certificates + +## Input Validation + +- Sanitize all user input before display to prevent injection +- Use `URL(string:)` with validation rather than force-unwrapping +- Validate data from external sources (APIs, deep links, pasteboard) before processing diff --git a/.cursor/rules/swift-testing.md b/.cursor/rules/swift-testing.md new file mode 100644 index 00000000..8b65b55b --- /dev/null +++ b/.cursor/rules/swift-testing.md @@ -0,0 +1,45 @@ +--- +description: "Swift testing extending common rules" +globs: ["**/*.swift", "**/Package.swift"] +alwaysApply: false +--- +# Swift Testing + +> This file extends the common testing rule with Swift specific content. + +## Framework + +Use **Swift Testing** (`import Testing`) for new tests. Use `@Test` and `#expect`: + +```swift +@Test("User creation validates email") +func userCreationValidatesEmail() throws { + #expect(throws: ValidationError.invalidEmail) { + try User(email: "not-an-email") + } +} +``` + +## Test Isolation + +Each test gets a fresh instance -- set up in `init`, tear down in `deinit`. No shared mutable state between tests. + +## Parameterized Tests + +```swift +@Test("Validates formats", arguments: ["json", "xml", "csv"]) +func validatesFormat(format: String) throws { + let parser = try Parser(format: format) + #expect(parser.isValid) +} +``` + +## Coverage + +```bash +swift test --enable-code-coverage +``` + +## Reference + +See skill: `swift-protocol-di-testing` for protocol-based dependency injection and mock patterns with Swift Testing. diff --git a/.cursor/rules/typescript-coding-style.md b/.cursor/rules/typescript-coding-style.md new file mode 100644 index 00000000..af5c4f84 --- /dev/null +++ b/.cursor/rules/typescript-coding-style.md @@ -0,0 +1,63 @@ +--- +description: "TypeScript coding style extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +# TypeScript/JavaScript Coding Style + +> This file extends the common coding style rule with TypeScript/JavaScript specific content. + +## Immutability + +Use spread operator for immutable updates: + +```typescript +// WRONG: Mutation +function updateUser(user, name) { + user.name = name // MUTATION! + return user +} + +// CORRECT: Immutability +function updateUser(user, name) { + return { + ...user, + name + } +} +``` + +## Error Handling + +Use async/await with try-catch: + +```typescript +try { + const result = await riskyOperation() + return result +} catch (error) { + console.error('Operation failed:', error) + throw new Error('Detailed user-friendly message') +} +``` + +## Input Validation + +Use Zod for schema-based validation: + +```typescript +import { z } from 'zod' + +const schema = z.object({ + email: z.string().email(), + age: z.number().int().min(0).max(150) +}) + +const validated = schema.parse(input) +``` + +## Console.log + +- No `console.log` statements in production code +- Use proper logging libraries instead +- See hooks for automatic detection diff --git a/.cursor/rules/typescript-hooks.md b/.cursor/rules/typescript-hooks.md new file mode 100644 index 00000000..9032d302 --- /dev/null +++ b/.cursor/rules/typescript-hooks.md @@ -0,0 +1,20 @@ +--- +description: "TypeScript hooks extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +# TypeScript/JavaScript Hooks + +> This file extends the common hooks rule with TypeScript/JavaScript specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **Prettier**: Auto-format JS/TS files after edit +- **TypeScript check**: Run `tsc` after editing `.ts`/`.tsx` files +- **console.log warning**: Warn about `console.log` in edited files + +## Stop Hooks + +- **console.log audit**: Check all modified files for `console.log` before session ends diff --git a/.cursor/rules/typescript-patterns.md b/.cursor/rules/typescript-patterns.md new file mode 100644 index 00000000..0c8a9b80 --- /dev/null +++ b/.cursor/rules/typescript-patterns.md @@ -0,0 +1,50 @@ +--- +description: "TypeScript patterns extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +# TypeScript/JavaScript Patterns + +> This file extends the common patterns rule with TypeScript/JavaScript specific content. + +## API Response Format + +```typescript +interface ApiResponse { + success: boolean + data?: T + error?: string + meta?: { + total: number + page: number + limit: number + } +} +``` + +## Custom Hooks Pattern + +```typescript +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay) + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} +``` + +## Repository Pattern + +```typescript +interface Repository { + findAll(filters?: Filters): Promise + findById(id: string): Promise + create(data: CreateDto): Promise + update(id: string, data: UpdateDto): Promise + delete(id: string): Promise +} +``` diff --git a/.cursor/rules/typescript-security.md b/.cursor/rules/typescript-security.md new file mode 100644 index 00000000..8aea61f5 --- /dev/null +++ b/.cursor/rules/typescript-security.md @@ -0,0 +1,26 @@ +--- +description: "TypeScript security extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +# TypeScript/JavaScript Security + +> This file extends the common security rule with TypeScript/JavaScript specific content. + +## Secret Management + +```typescript +// NEVER: Hardcoded secrets +const apiKey = "sk-proj-xxxxx" + +// ALWAYS: Environment variables +const apiKey = process.env.OPENAI_API_KEY + +if (!apiKey) { + throw new Error('OPENAI_API_KEY not configured') +} +``` + +## Agent Support + +- Use **security-reviewer** skill for comprehensive security audits diff --git a/.cursor/rules/typescript-testing.md b/.cursor/rules/typescript-testing.md new file mode 100644 index 00000000..894b9fe9 --- /dev/null +++ b/.cursor/rules/typescript-testing.md @@ -0,0 +1,16 @@ +--- +description: "TypeScript testing extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +# TypeScript/JavaScript Testing + +> This file extends the common testing rule with TypeScript/JavaScript specific content. + +## E2E Testing + +Use **Playwright** as the E2E testing framework for critical user flows. + +## Agent Support + +- **e2e-runner** - Playwright E2E testing specialist diff --git a/.opencode/index.ts b/.opencode/index.ts index 43240823..4b686715 100644 --- a/.opencode/index.ts +++ b/.opencode/index.ts @@ -4,8 +4,8 @@ * This package provides a complete OpenCode plugin with: * - 13 specialized agents (planner, architect, code-reviewer, etc.) * - 31 commands (/plan, /tdd, /code-review, etc.) - * - Plugin hooks (auto-format, TypeScript check, console.log warning, etc.) - * - Custom tools (run-tests, check-coverage, security-audit) + * - Plugin hooks (auto-format, TypeScript check, console.log warning, env injection, etc.) + * - Custom tools (run-tests, check-coverage, security-audit, format-code, lint-check, git-summary) * - 37 skills (coding-standards, security-review, tdd-workflow, etc.) * * Usage: @@ -39,7 +39,7 @@ export { ECCHooksPlugin, default } from "./plugins/index.js" export * from "./plugins/index.js" // Version export -export const VERSION = "1.4.1" +export const VERSION = "1.6.0" // Plugin metadata export const metadata = { @@ -59,13 +59,18 @@ export const metadata = { "session.idle", "session.deleted", "file.watcher.updated", - "permission.asked", + "permission.ask", "todo.updated", + "shell.env", + "experimental.session.compacting", ], customTools: [ "run-tests", "check-coverage", "security-audit", + "format-code", + "lint-check", + "git-summary", ], }, } diff --git a/.opencode/opencode.json b/.opencode/opencode.json index 0dbf6638..42cb5112 100644 --- a/.opencode/opencode.json +++ b/.opencode/opencode.json @@ -4,11 +4,19 @@ "small_model": "anthropic/claude-haiku-4-5", "default_agent": "build", "instructions": [ + "AGENTS.md", "CONTRIBUTING.md", ".opencode/instructions/INSTRUCTIONS.md", "skills/tdd-workflow/SKILL.md", "skills/security-review/SKILL.md", - "skills/coding-standards/SKILL.md" + "skills/coding-standards/SKILL.md", + "skills/frontend-patterns/SKILL.md", + "skills/backend-patterns/SKILL.md", + "skills/e2e-testing/SKILL.md", + "skills/verification-loop/SKILL.md", + "skills/api-design/SKILL.md", + "skills/strategic-compact/SKILL.md", + "skills/eval-harness/SKILL.md" ], "plugin": [ "./.opencode/plugins" diff --git a/.opencode/package.json b/.opencode/package.json index 75a24308..d8defaae 100644 --- a/.opencode/package.json +++ b/.opencode/package.json @@ -1,6 +1,6 @@ { "name": "ecc-universal", - "version": "1.4.1", + "version": "1.6.0", "description": "Everything Claude Code (ECC) plugin for OpenCode - agents, commands, hooks, and skills", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/.opencode/plugins/ecc-hooks.ts b/.opencode/plugins/ecc-hooks.ts index 50d23bfd..1f158d79 100644 --- a/.opencode/plugins/ecc-hooks.ts +++ b/.opencode/plugins/ecc-hooks.ts @@ -261,17 +261,6 @@ export const ECCHooksPlugin = async ({ } }, - /** - * Permission Asked Hook - * OpenCode-only feature - * - * Triggers: When permission is requested - * Action: Logs for audit trail - */ - "permission.asked": async (event: { tool: string; args: unknown }) => { - log("info", `[ECC] Permission requested for: ${event.tool}`) - }, - /** * Todo Updated Hook * OpenCode-only feature @@ -286,6 +275,131 @@ export const ECCHooksPlugin = async ({ log("info", `[ECC] Progress: ${completed}/${total} tasks completed`) } }, + + /** + * Shell Environment Hook + * OpenCode-specific: Inject environment variables into shell commands + * + * Triggers: Before shell command execution + * Action: Sets PROJECT_ROOT, PACKAGE_MANAGER, DETECTED_LANGUAGES, ECC_VERSION + */ + "shell.env": async () => { + const env: Record = { + ECC_VERSION: "1.6.0", + ECC_PLUGIN: "true", + PROJECT_ROOT: worktree || directory, + } + + // Detect package manager + const lockfiles: Record = { + "bun.lockb": "bun", + "pnpm-lock.yaml": "pnpm", + "yarn.lock": "yarn", + "package-lock.json": "npm", + } + for (const [lockfile, pm] of Object.entries(lockfiles)) { + try { + await $`test -f ${worktree}/${lockfile}` + env.PACKAGE_MANAGER = pm + break + } catch { + // Not found, try next + } + } + + // Detect languages + const langDetectors: Record = { + "tsconfig.json": "typescript", + "go.mod": "go", + "pyproject.toml": "python", + "Cargo.toml": "rust", + "Package.swift": "swift", + } + const detected: string[] = [] + for (const [file, lang] of Object.entries(langDetectors)) { + try { + await $`test -f ${worktree}/${file}` + detected.push(lang) + } catch { + // Not found + } + } + if (detected.length > 0) { + env.DETECTED_LANGUAGES = detected.join(",") + env.PRIMARY_LANGUAGE = detected[0] + } + + return env + }, + + /** + * Session Compacting Hook + * OpenCode-specific: Control context compaction behavior + * + * Triggers: Before context compaction + * Action: Push ECC context block and custom compaction prompt + */ + "experimental.session.compacting": async () => { + const contextBlock = [ + "# ECC Context (preserve across compaction)", + "", + "## Active Plugin: Everything Claude Code v1.6.0", + "- Hooks: file.edited, tool.execute.before/after, session.created/idle/deleted, shell.env, compacting, permission.ask", + "- Tools: run-tests, check-coverage, security-audit, format-code, lint-check, git-summary", + "- Agents: 13 specialized (planner, architect, tdd-guide, code-reviewer, security-reviewer, build-error-resolver, e2e-runner, refactor-cleaner, doc-updater, go-reviewer, go-build-resolver, database-reviewer, python-reviewer)", + "", + "## Key Principles", + "- TDD: write tests first, 80%+ coverage", + "- Immutability: never mutate, always return new copies", + "- Security: validate inputs, no hardcoded secrets", + "", + ] + + // Include recently edited files + if (editedFiles.size > 0) { + contextBlock.push("## Recently Edited Files") + for (const f of editedFiles) { + contextBlock.push(`- ${f}`) + } + contextBlock.push("") + } + + return { + context: contextBlock.join("\n"), + compaction_prompt: "Focus on preserving: 1) Current task status and progress, 2) Key decisions made, 3) Files created/modified, 4) Remaining work items, 5) Any security concerns flagged. Discard: verbose tool outputs, intermediate exploration, redundant file listings.", + } + }, + + /** + * Permission Auto-Approve Hook + * OpenCode-specific: Auto-approve safe operations + * + * Triggers: When permission is requested + * Action: Auto-approve reads, formatters, and test commands; log all for audit + */ + "permission.ask": async (event: { tool: string; args: unknown }) => { + log("info", `[ECC] Permission requested for: ${event.tool}`) + + const cmd = String((event.args as Record)?.command || event.args || "") + + // Auto-approve: read/search tools + if (["read", "glob", "grep", "search", "list"].includes(event.tool)) { + return { approved: true, reason: "Read-only operation" } + } + + // Auto-approve: formatters + if (event.tool === "bash" && /^(npx )?(prettier|biome|black|gofmt|rustfmt|swift-format)/.test(cmd)) { + return { approved: true, reason: "Formatter execution" } + } + + // Auto-approve: test execution + if (event.tool === "bash" && /^(npm test|npx vitest|npx jest|pytest|go test|cargo test)/.test(cmd)) { + return { approved: true, reason: "Test execution" } + } + + // Everything else: let user decide + return { approved: undefined } + }, } } diff --git a/.opencode/tools/format-code.ts b/.opencode/tools/format-code.ts new file mode 100644 index 00000000..080bd5b4 --- /dev/null +++ b/.opencode/tools/format-code.ts @@ -0,0 +1,66 @@ +/** + * ECC Custom Tool: Format Code + * + * Language-aware code formatter that auto-detects the project's formatter. + * Supports: Biome/Prettier (JS/TS), Black (Python), gofmt (Go), rustfmt (Rust) + */ + +import { tool } from "@opencode-ai/plugin" +import { z } from "zod" + +export default tool({ + name: "format-code", + description: "Format a file using the project's configured formatter. Auto-detects Biome, Prettier, Black, gofmt, or rustfmt.", + parameters: z.object({ + filePath: z.string().describe("Path to the file to format"), + formatter: z.string().optional().describe("Override formatter: biome, prettier, black, gofmt, rustfmt (default: auto-detect)"), + }), + execute: async ({ filePath, formatter }, { $ }) => { + const ext = filePath.split(".").pop()?.toLowerCase() || "" + + // Auto-detect formatter based on file extension and config files + let detected = formatter + if (!detected) { + if (["ts", "tsx", "js", "jsx", "json", "css", "scss"].includes(ext)) { + // Check for Biome first, then Prettier + try { + await $`test -f biome.json || test -f biome.jsonc` + detected = "biome" + } catch { + detected = "prettier" + } + } else if (["py", "pyi"].includes(ext)) { + detected = "black" + } else if (ext === "go") { + detected = "gofmt" + } else if (ext === "rs") { + detected = "rustfmt" + } + } + + if (!detected) { + return { formatted: false, message: `No formatter detected for .${ext} files` } + } + + const commands: Record = { + biome: `npx @biomejs/biome format --write ${filePath}`, + prettier: `npx prettier --write ${filePath}`, + black: `black ${filePath}`, + gofmt: `gofmt -w ${filePath}`, + rustfmt: `rustfmt ${filePath}`, + } + + const cmd = commands[detected] + if (!cmd) { + return { formatted: false, message: `Unknown formatter: ${detected}` } + } + + try { + const result = await $`${cmd}`.text() + return { formatted: true, formatter: detected, output: result } + } catch (error: unknown) { + const err = error as { stderr?: string } + return { formatted: false, formatter: detected, error: err.stderr || "Format failed" } + } + }, +}) diff --git a/.opencode/tools/git-summary.ts b/.opencode/tools/git-summary.ts new file mode 100644 index 00000000..23fcc5e3 --- /dev/null +++ b/.opencode/tools/git-summary.ts @@ -0,0 +1,56 @@ +/** + * ECC Custom Tool: Git Summary + * + * Provides a comprehensive git status including branch info, status, + * recent log, and diff against base branch. + */ + +import { tool } from "@opencode-ai/plugin" +import { z } from "zod" + +export default tool({ + name: "git-summary", + description: "Get comprehensive git summary: branch, status, recent log, and diff against base branch.", + parameters: z.object({ + depth: z.number().optional().describe("Number of recent commits to show (default: 5)"), + includeDiff: z.boolean().optional().describe("Include diff against base branch (default: true)"), + baseBranch: z.string().optional().describe("Base branch for comparison (default: main)"), + }), + execute: async ({ depth = 5, includeDiff = true, baseBranch = "main" }, { $ }) => { + const results: Record = {} + + try { + results.branch = (await $`git branch --show-current`.text()).trim() + } catch { + results.branch = "unknown" + } + + try { + results.status = (await $`git status --short`.text()).trim() + } catch { + results.status = "unable to get status" + } + + try { + results.log = (await $`git log --oneline -${depth}`.text()).trim() + } catch { + results.log = "unable to get log" + } + + if (includeDiff) { + try { + results.stagedDiff = (await $`git diff --cached --stat`.text()).trim() + } catch { + results.stagedDiff = "" + } + + try { + results.branchDiff = (await $`git diff ${baseBranch}...HEAD --stat`.text()).trim() + } catch { + results.branchDiff = `unable to diff against ${baseBranch}` + } + } + + return results + }, +}) diff --git a/.opencode/tools/index.ts b/.opencode/tools/index.ts index e779fde6..dabacc4e 100644 --- a/.opencode/tools/index.ts +++ b/.opencode/tools/index.ts @@ -8,3 +8,6 @@ export { default as runTests } from "./run-tests.js" export { default as checkCoverage } from "./check-coverage.js" export { default as securityAudit } from "./security-audit.js" +export { default as formatCode } from "./format-code.js" +export { default as lintCheck } from "./lint-check.js" +export { default as gitSummary } from "./git-summary.js" diff --git a/.opencode/tools/lint-check.ts b/.opencode/tools/lint-check.ts new file mode 100644 index 00000000..30d3f93a --- /dev/null +++ b/.opencode/tools/lint-check.ts @@ -0,0 +1,74 @@ +/** + * ECC Custom Tool: Lint Check + * + * Multi-language linter that auto-detects the project's linting tool. + * Supports: ESLint/Biome (JS/TS), Pylint/Ruff (Python), golangci-lint (Go) + */ + +import { tool } from "@opencode-ai/plugin" +import { z } from "zod" + +export default tool({ + name: "lint-check", + description: "Run linter on files or directories. Auto-detects ESLint, Biome, Ruff, Pylint, or golangci-lint.", + parameters: z.object({ + target: z.string().optional().describe("File or directory to lint (default: current directory)"), + fix: z.boolean().optional().describe("Auto-fix issues if supported (default: false)"), + linter: z.string().optional().describe("Override linter: eslint, biome, ruff, pylint, golangci-lint (default: auto-detect)"), + }), + execute: async ({ target = ".", fix = false, linter }, { $ }) => { + // Auto-detect linter + let detected = linter + if (!detected) { + try { + await $`test -f biome.json || test -f biome.jsonc` + detected = "biome" + } catch { + try { + await $`test -f .eslintrc.json || test -f .eslintrc.js || test -f .eslintrc.cjs || test -f eslint.config.js || test -f eslint.config.mjs` + detected = "eslint" + } catch { + try { + await $`test -f pyproject.toml && grep -q "ruff" pyproject.toml` + detected = "ruff" + } catch { + try { + await $`test -f .golangci.yml || test -f .golangci.yaml` + detected = "golangci-lint" + } catch { + // Fall back based on file extensions in target + detected = "eslint" + } + } + } + } + } + + const fixFlag = fix ? " --fix" : "" + const commands: Record = { + biome: `npx @biomejs/biome lint${fix ? " --write" : ""} ${target}`, + eslint: `npx eslint${fixFlag} ${target}`, + ruff: `ruff check${fixFlag} ${target}`, + pylint: `pylint ${target}`, + "golangci-lint": `golangci-lint run${fixFlag} ${target}`, + } + + const cmd = commands[detected] + if (!cmd) { + return { success: false, message: `Unknown linter: ${detected}` } + } + + try { + const result = await $`${cmd}`.text() + return { success: true, linter: detected, output: result, issues: 0 } + } catch (error: unknown) { + const err = error as { stdout?: string; stderr?: string } + return { + success: false, + linter: detected, + output: err.stdout || "", + errors: err.stderr || "", + } + } + }, +}) diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..ad250c43 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,136 @@ +# Everything Claude Code (ECC) — Agent Instructions + +This is a **production-ready AI coding plugin** providing 13 specialized agents, 50+ skills, 33 commands, and automated hook workflows for software development. + +## Core Principles + +1. **Agent-First** — Delegate to specialized agents for domain tasks +2. **Test-Driven** — Write tests before implementation, 80%+ coverage required +3. **Security-First** — Never compromise on security; validate all inputs +4. **Immutability** — Always create new objects, never mutate existing ones +5. **Plan Before Execute** — Plan complex features before writing code + +## Available Agents + +| Agent | Purpose | When to Use | +|-------|---------|-------------| +| planner | Implementation planning | Complex features, refactoring | +| architect | System design and scalability | Architectural decisions | +| tdd-guide | Test-driven development | New features, bug fixes | +| code-reviewer | Code quality and maintainability | After writing/modifying code | +| security-reviewer | Vulnerability detection | Before commits, sensitive code | +| build-error-resolver | Fix build/type errors | When build fails | +| e2e-runner | End-to-end Playwright testing | Critical user flows | +| refactor-cleaner | Dead code cleanup | Code maintenance | +| doc-updater | Documentation and codemaps | Updating docs | +| go-reviewer | Go code review | Go projects | +| go-build-resolver | Go build errors | Go build failures | +| database-reviewer | PostgreSQL/Supabase specialist | Schema design, query optimization | +| python-reviewer | Python code review | Python projects | + +## Agent Orchestration + +Use agents proactively without user prompt: +- Complex feature requests → **planner** +- Code just written/modified → **code-reviewer** +- Bug fix or new feature → **tdd-guide** +- Architectural decision → **architect** +- Security-sensitive code → **security-reviewer** + +Use parallel execution for independent operations — launch multiple agents simultaneously. + +## Security Guidelines + +**Before ANY commit:** +- No hardcoded secrets (API keys, passwords, tokens) +- All user inputs validated +- SQL injection prevention (parameterized queries) +- XSS prevention (sanitized HTML) +- CSRF protection enabled +- Authentication/authorization verified +- Rate limiting on all endpoints +- Error messages don't leak sensitive data + +**Secret management:** NEVER hardcode secrets. Use environment variables or a secret manager. Validate required secrets at startup. Rotate any exposed secrets immediately. + +**If security issue found:** STOP → use security-reviewer agent → fix CRITICAL issues → rotate exposed secrets → review codebase for similar issues. + +## Coding Style + +**Immutability (CRITICAL):** Always create new objects, never mutate. Return new copies with changes applied. + +**File organization:** Many small files over few large ones. 200-400 lines typical, 800 max. Organize by feature/domain, not by type. High cohesion, low coupling. + +**Error handling:** Handle errors at every level. Provide user-friendly messages in UI code. Log detailed context server-side. Never silently swallow errors. + +**Input validation:** Validate all user input at system boundaries. Use schema-based validation. Fail fast with clear messages. Never trust external data. + +**Code quality checklist:** +- Functions small (<50 lines), files focused (<800 lines) +- No deep nesting (>4 levels) +- Proper error handling, no hardcoded values +- Readable, well-named identifiers + +## Testing Requirements + +**Minimum coverage: 80%** + +Test types (all required): +1. **Unit tests** — Individual functions, utilities, components +2. **Integration tests** — API endpoints, database operations +3. **E2E tests** — Critical user flows + +**TDD workflow (mandatory):** +1. Write test first (RED) — test should FAIL +2. Write minimal implementation (GREEN) — test should PASS +3. Refactor (IMPROVE) — verify coverage 80%+ + +Troubleshoot failures: check test isolation → verify mocks → fix implementation (not tests, unless tests are wrong). + +## Development Workflow + +1. **Plan** — Use planner agent, identify dependencies and risks, break into phases +2. **TDD** — Use tdd-guide agent, write tests first, implement, refactor +3. **Review** — Use code-reviewer agent immediately, address CRITICAL/HIGH issues +4. **Commit** — Conventional commits format, comprehensive PR summaries + +## Git Workflow + +**Commit format:** `: ` — Types: feat, fix, refactor, docs, test, chore, perf, ci + +**PR workflow:** Analyze full commit history → draft comprehensive summary → include test plan → push with `-u` flag. + +## Architecture Patterns + +**API response format:** Consistent envelope with success indicator, data payload, error message, and pagination metadata. + +**Repository pattern:** Encapsulate data access behind standard interface (findAll, findById, create, update, delete). Business logic depends on abstract interface, not storage mechanism. + +**Skeleton projects:** Search for battle-tested templates, evaluate with parallel agents (security, extensibility, relevance), clone best match, iterate within proven structure. + +## Performance + +**Context management:** Avoid last 20% of context window for large refactoring and multi-file features. Lower-sensitivity tasks (single edits, docs, simple fixes) tolerate higher utilization. + +**Build troubleshooting:** Use build-error-resolver agent → analyze errors → fix incrementally → verify after each fix. + +## Project Structure + +``` +agents/ — 13 specialized subagents +skills/ — 50+ workflow skills and domain knowledge +commands/ — 33 slash commands +hooks/ — Trigger-based automations +rules/ — Always-follow guidelines (common + per-language) +scripts/ — Cross-platform Node.js utilities +mcp-configs/ — 14 MCP server configurations +tests/ — Test suite +``` + +## Success Metrics + +- All tests pass with 80%+ coverage +- No security vulnerabilities +- Code is readable and maintainable +- Performance is acceptable +- User requirements are met diff --git a/README.md b/README.md index eb01c6f9..70acfcff 100644 --- a/README.md +++ b/README.md @@ -774,31 +774,102 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ## Cursor IDE Support -ecc-universal includes pre-translated configurations for [Cursor IDE](https://cursor.com). The `.cursor/` directory contains rules, agents, skills, commands, and MCP configs adapted for Cursor's format. +ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, commands, and MCP configs adapted for Cursor's native format. ### Quick Start (Cursor) ```bash -# Install the package -npm install ecc-universal - # Install for your language(s) ./install.sh --target cursor typescript -./install.sh --target cursor python golang +./install.sh --target cursor python golang swift ``` -### What's Translated +### What's Included -| Component | Claude Code → Cursor | Parity | -|-----------|---------------------|--------| -| Rules | YAML frontmatter added, paths flattened | Full | -| Agents | Model IDs expanded, tools → readonly flag | Full | -| Skills | No changes needed (identical standard) | Identical | -| Commands | Path references updated, multi-* stubbed | Partial | -| MCP Config | Env interpolation syntax updated | Full | -| Hooks | No equivalent in Cursor | See alternatives | +| Component | Count | Details | +|-----------|-------|---------| +| Hook Events | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt, and 10 more | +| Hook Scripts | 16 | Thin Node.js scripts delegating to `scripts/hooks/` via shared adapter | +| Rules | 29 | 9 common (alwaysApply) + 20 language-specific (TypeScript, Python, Go, Swift) | +| Agents | Shared | Via AGENTS.md at root (read by Cursor natively) | +| Skills | Shared | Via AGENTS.md at root | +| Commands | Shared | `.cursor/commands/` if installed | +| MCP Config | Shared | `.cursor/mcp.json` if installed | -See [.cursor/README.md](.cursor/README.md) for details and [.cursor/MIGRATION.md](.cursor/MIGRATION.md) for the full migration guide. +### Hook Architecture (DRY Adapter Pattern) + +Cursor has **more hook events than Claude Code** (20 vs 8). The `.cursor/hooks/adapter.js` module transforms Cursor's stdin JSON to Claude Code's format, allowing existing `scripts/hooks/*.js` to be reused without duplication. + +``` +Cursor stdin JSON → adapter.js → transforms → scripts/hooks/*.js + (shared with Claude Code) +``` + +Key hooks: +- **beforeShellExecution** — Blocks dev servers outside tmux (exit 2), git push review +- **afterFileEdit** — Auto-format + TypeScript check + console.log warning +- **beforeSubmitPrompt** — Detects secrets (sk-, ghp_, AKIA patterns) in prompts +- **beforeTabFileRead** — Blocks Tab from reading .env, .key, .pem files (exit 2) +- **beforeMCPExecution / afterMCPExecution** — MCP audit logging + +### Rules Format + +Cursor rules use YAML frontmatter with `description`, `globs`, and `alwaysApply`: + +```yaml +--- +description: "TypeScript coding style extending common rules" +globs: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"] +alwaysApply: false +--- +``` + +--- + +## Codex CLI Support + +ECC provides **first-class Codex CLI support** with a reference configuration, Codex-specific AGENTS.md supplement, and 10 ported skills. + +### Quick Start (Codex) + +```bash +# Copy the reference config to your home directory +cp .codex/config.toml ~/.codex/config.toml + +# Run Codex in the repo — AGENTS.md is auto-detected +codex +``` + +### What's Included + +| Component | Count | Details | +|-----------|-------|---------| +| Config | 1 | `.codex/config.toml` — model, permissions, MCP servers, persistent instructions | +| AGENTS.md | 2 | Root (universal) + `.codex/AGENTS.md` (Codex-specific supplement) | +| Skills | 10 | `.agents/skills/` — SKILL.md + agents/openai.yaml per skill | +| MCP Servers | 4 | GitHub, Context7, Memory, Sequential Thinking (command-based) | +| Profiles | 2 | `strict` (read-only sandbox) and `yolo` (full auto-approve) | + +### Skills + +Skills at `.agents/skills/` are auto-loaded by Codex: + +| Skill | Description | +|-------|-------------| +| tdd-workflow | Test-driven development with 80%+ coverage | +| security-review | Comprehensive security checklist | +| coding-standards | Universal coding standards | +| frontend-patterns | React/Next.js patterns | +| backend-patterns | API design, database, caching | +| e2e-testing | Playwright E2E tests | +| eval-harness | Eval-driven development | +| strategic-compact | Context management | +| api-design | REST API design patterns | +| verification-loop | Build, test, lint, typecheck, security | + +### Key Limitation + +Codex CLI does **not yet support hooks** ([GitHub Issue #2109](https://github.com/openai/codex/issues/2109), 430+ upvotes). Security enforcement is instruction-based via `persistent_instructions` in config.toml and the sandbox permission system. --- @@ -823,12 +894,12 @@ The configuration is automatically detected from `.opencode/opencode.json`. | Feature | Claude Code | OpenCode | Status | |---------|-------------|----------|--------| | Agents | ✅ 13 agents | ✅ 12 agents | **Claude Code leads** | -| Commands | ✅ 32 commands | ✅ 24 commands | **Claude Code leads** | -| Skills | ✅ 48 skills | ✅ 16 skills | **Claude Code leads** | -| Hooks | ✅ 3 phases | ✅ 20+ events | **OpenCode has more!** | -| Rules | ✅ 8 rules | ✅ 8 rules | **Full parity** | -| MCP Servers | ✅ Full | ✅ Full | **Full parity** | -| Custom Tools | ✅ Via hooks | ✅ Native support | **OpenCode is better** | +| Commands | ✅ 33 commands | ✅ 24 commands | **Claude Code leads** | +| Skills | ✅ 50+ skills | ✅ 37 skills | **Claude Code leads** | +| Hooks | ✅ 8 event types | ✅ 11 events | **OpenCode has more!** | +| Rules | ✅ 29 rules | ✅ 13 instructions | **Claude Code leads** | +| MCP Servers | ✅ 14 servers | ✅ Full | **Full parity** | +| Custom Tools | ✅ Via hooks | ✅ 6 native tools | **OpenCode is better** | ### Hook Support via Plugins @@ -910,6 +981,34 @@ Then add to your `opencode.json`: --- +## Cross-Tool Feature Parity + +ECC is the **first plugin to maximize every major AI coding tool**. Here's how each harness compares: + +| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | +|---------|------------|------------|-----------|----------| +| **Agents** | 13 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | +| **Commands** | 33 | Shared | Instruction-based | 24 | +| **Skills** | 50+ | Shared | 10 (native format) | 37 | +| **Hook Events** | 8 types | 15 types | None yet | 11 types | +| **Hook Scripts** | 9 scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | +| **Rules** | 29 (common + lang) | 29 (YAML frontmatter) | Instruction-based | 13 instructions | +| **Custom Tools** | Via hooks | Via hooks | N/A | 6 native tools | +| **MCP Servers** | 14 | Shared (mcp.json) | 4 (command-based) | Full | +| **Config Format** | settings.json | hooks.json + rules/ | config.toml | opencode.json | +| **Context File** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md | +| **Secret Detection** | Hook-based | beforeSubmitPrompt hook | Sandbox-based | Hook-based | +| **Auto-Format** | PostToolUse hook | afterFileEdit hook | N/A | file.edited hook | +| **Version** | Plugin | Plugin | Reference config | 1.6.0 | + +**Key architectural decisions:** +- **AGENTS.md** at root is the universal cross-tool file (read by all 4 tools) +- **DRY adapter pattern** lets Cursor reuse Claude Code's hook scripts without duplication +- **Skills format** (SKILL.md with YAML frontmatter) works across Claude Code, Codex, and OpenCode +- Codex's lack of hooks is compensated by `persistent_instructions` and sandbox permissions + +--- + ## 📖 Background I've been using Claude Code since the experimental rollout. Won the Anthropic x Forum Ventures hackathon in Sep 2025 building [zenith.chat](https://zenith.chat) with [@DRodriguezFX](https://x.com/DRodriguezFX) - entirely using Claude Code. diff --git a/examples/sessions/2026-01-17-debugging-memory.tmp b/examples/sessions/2026-01-17-debugging-memory.tmp deleted file mode 100644 index 1d0fb6fd..00000000 --- a/examples/sessions/2026-01-17-debugging-memory.tmp +++ /dev/null @@ -1,54 +0,0 @@ -# Session: Memory Leak Investigation -**Date:** 2026-01-17 -**Started:** 09:00 -**Last Updated:** 12:00 - ---- - -## Current State - -Investigating memory leak in production. Heap growing unbounded over 24h period. - -### Completed -- [x] Set up heap snapshots in staging -- [x] Identified leak source: event listeners not being cleaned up -- [x] Fixed leak in WebSocket handler -- [x] Verified fix with 4h soak test - -### Root Cause -WebSocket `onMessage` handlers were being added on reconnect but not removed on disconnect. After ~1000 reconnects, memory grew from 200MB to 2GB. - -### The Fix -```javascript -// Before (leaking) -socket.on('connect', () => { - socket.on('message', handleMessage) -}) - -// After (fixed) -socket.on('connect', () => { - socket.off('message', handleMessage) // Remove old listener first - socket.on('message', handleMessage) -}) - -// Even better - use once or cleanup on disconnect -socket.on('disconnect', () => { - socket.removeAllListeners('message') -}) -``` - -### Debugging Technique Worth Saving -1. Take heap snapshot at T=0 -2. Force garbage collection: `global.gc()` -3. Run suspected operation N times -4. Take heap snapshot at T=1 -5. Compare snapshots - look for objects with count = N - -### Notes for Next Session -- Add memory monitoring alert at 1GB threshold -- Document this debugging pattern for team - -### Context to Load -``` -src/services/websocket.js -``` diff --git a/examples/sessions/2026-01-19-refactor-api.tmp b/examples/sessions/2026-01-19-refactor-api.tmp deleted file mode 100644 index df104942..00000000 --- a/examples/sessions/2026-01-19-refactor-api.tmp +++ /dev/null @@ -1,43 +0,0 @@ -# Session: API Refactor - Error Handling -**Date:** 2026-01-19 -**Started:** 10:00 -**Last Updated:** 13:30 - ---- - -## Current State - -Standardizing error handling across all API endpoints. Moving from ad-hoc try/catch to centralized error middleware. - -### Completed -- [x] Created AppError class with status codes -- [x] Built global error handler middleware -- [x] Migrated `/users` routes to new pattern -- [x] Migrated `/products` routes - -### Key Findings -- 47 endpoints with inconsistent error responses -- Some returning `{ error: message }`, others `{ message: message }` -- No consistent HTTP status codes - -### Error Response Standard -```javascript -{ - success: false, - error: { - code: 'VALIDATION_ERROR', - message: 'Email is required', - field: 'email' // optional, for validation errors - } -} -``` - -### Notes for Next Session -- Migrate remaining routes: `/orders`, `/payments`, `/admin` -- Add error logging to monitoring service - -### Context to Load -``` -src/middleware/errorHandler.js -src/utils/AppError.js -``` diff --git a/examples/sessions/2026-01-20-feature-auth.tmp b/examples/sessions/2026-01-20-feature-auth.tmp deleted file mode 100644 index 20a59506..00000000 --- a/examples/sessions/2026-01-20-feature-auth.tmp +++ /dev/null @@ -1,76 +0,0 @@ -# Session: Auth Feature Implementation -**Date:** 2026-01-20 -**Started:** 14:30 -**Last Updated:** 17:45 - ---- - -## Current State - -Working on JWT authentication flow for the API. Main goal is replacing session-based auth with stateless tokens. - -### Completed -- [x] Set up JWT signing with RS256 -- [x] Created `/auth/login` endpoint -- [x] Added refresh token rotation -- [x] Fixed token expiry bug (was using seconds, needed milliseconds) - -### In Progress -- [ ] Add rate limiting to auth endpoints -- [ ] Implement token blacklist for logout - -### Blockers Encountered -1. **jsonwebtoken version mismatch** - v9.x changed the `verify()` signature, had to update error handling -2. **Redis TTL for refresh tokens** - Was setting TTL in seconds but passing milliseconds - -### Key Decisions Made -- Using RS256 over HS256 for better security with distributed services -- Storing refresh tokens in Redis with 7-day TTL -- Access tokens expire in 15 minutes - -### Code Locations Modified -- `src/middleware/auth.js` - JWT verification middleware -- `src/routes/auth.js` - Login/logout/refresh endpoints -- `src/services/token.service.js` - Token generation and validation - -### Notes for Next Session -- Need to add CSRF protection for cookie-based token storage -- Consider adding fingerprinting for refresh token binding -- Review rate limit values with team - -### Context to Load -``` -src/middleware/ -src/routes/auth.js -src/services/token.service.js -``` - ---- - -## Session Log - -**14:30** - Started session, goal is JWT implementation - -**14:45** - Set up basic JWT signing. Using RS256 with key pair stored in env vars. - -**15:20** - Login endpoint working. Discovered jsonwebtoken v9 breaking change - `verify()` now throws different error types. Updated catch block: -```javascript -// Old (v8) -if (err.name === 'TokenExpiredError') { ... } - -// New (v9) -if (err instanceof jwt.TokenExpiredError) { ... } -``` - -**16:00** - Refresh token rotation working but tokens expiring immediately. Bug: was passing `Date.now()` (milliseconds) to `expiresIn` which expects seconds. Fixed: -```javascript -// Wrong -expiresIn: Date.now() + 900000 - -// Correct -expiresIn: '15m' -``` - -**17:30** - Auth flow complete. Login -> access token -> refresh -> new tokens. Ready for rate limiting tomorrow. - -**17:45** - Saving session state. diff --git a/install.sh b/install.sh index 0b468f7d..dbc2c959 100755 --- a/install.sh +++ b/install.sh @@ -162,6 +162,17 @@ if [[ "$TARGET" == "cursor" ]]; then cp -r "$CURSOR_SRC/commands/." "$DEST_DIR/commands/" fi + # --- Hooks --- + if [[ -f "$CURSOR_SRC/hooks.json" ]]; then + echo "Installing hooks config -> $DEST_DIR/hooks.json" + cp "$CURSOR_SRC/hooks.json" "$DEST_DIR/hooks.json" + fi + if [[ -d "$CURSOR_SRC/hooks" ]]; then + echo "Installing hook scripts -> $DEST_DIR/hooks/" + mkdir -p "$DEST_DIR/hooks" + cp -r "$CURSOR_SRC/hooks/." "$DEST_DIR/hooks/" + fi + # --- MCP Config --- if [[ -f "$CURSOR_SRC/mcp.json" ]]; then echo "Installing MCP config -> $DEST_DIR/mcp.json" diff --git a/package.json b/package.json index e58f63e7..5a9f1535 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecc-universal", - "version": "1.4.1", + "version": "1.6.0", "description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, commands, and rules evolved over 10+ months of intensive daily use by an Anthropic hackathon winner", "keywords": [ "claude-code",