diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 5db948090..93b4b67e5 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -28,3 +28,20 @@ Do **not** run these yourself unless the user asks — the human runs the dev se ## Conventions - **Backend test file naming**: `*.test.ts` = unit (no DB), `*.itest.ts` = integration (real Postgres/Redis/DynamoDB via testcontainers, single-threaded). Don't mix. +- **Package manager**: only use `npm`. Never use `yarn`, `pnpm`, or other package managers. +- **Installing packages**: always pass the `-E` (exact version) flag. +- **Linting**: before committing, run `npm run lint:fix` (auto-fixes), then `npm run lint` and fix remaining errors. Scope to the workspace you touched: backend-only changes → `npm run -w backend lint:fix`; frontend-only → `npm run -w frontend lint:fix`; otherwise run the root command. +- **Branches & PRs**: managed via Graphite (`gt`); use the `graphite` skill. Before creating a new branch, ask the user what to name it. Naming: `feat/[/]` or `chore/[/]`. + +## Skills + +Skills live under `.claude/skills/` at the repo root and inside individual packages (e.g. [packages/frontend/.claude/skills/](../packages/frontend/.claude/skills/), [packages/backend/.claude/skills/](../packages/backend/.claude/skills/)). Prefer repo-committed skills over locally/globally-installed ones. + +There are two kinds of skills, distinguished by whether they are listed in the sibling `skills-lock.json` (root, `packages/frontend/`, `packages/backend/`): + +- **Upstream skills** — listed in `skills-lock.json`, installed via `skills add --skill --agent claude-code -y`. Do **not** edit their files (`SKILL.md`, etc.); upstream updates may be merged later and would clobber local changes. Per-repo overrides to their behavior go in the "Skill overrides" section below (or in the relevant `rules/*.md` for package-scoped ones). +- **Plumber-specific skills** — anything in `.claude/skills/` that is **not** in `skills-lock.json`. These are authored in-repo and are freely editable. Create one with `skills init ` from the directory whose `.claude/skills/` should host it (repo root for cross-cutting skills, package root for package-scoped ones). + +### Skill overrides + +- **caveman**: default to `lite` mode (overrides the skill's own default of `full`). diff --git a/.claude/rules/backend.md b/.claude/rules/backend.md index f7f7056d5..af5eb441d 100644 --- a/.claude/rules/backend.md +++ b/.claude/rules/backend.md @@ -47,3 +47,8 @@ An app may declare its own queue config (concurrency, rate limits, etc.) via the ## Frontend integration GraphQL schema and resolvers also live here ([packages/backend/src/graphql/](../../packages/backend/src/graphql/)). `graphql-shield` handles authz, `graphql-rate-limit` rate-limits. The frontend code is documented in [.claude/rules/frontend.md](frontend.md). + +## Conventions + +- **Tests**: try to add unit tests (`*.test.ts`) for any new code. +- **APIs**: prefer adding REST endpoints over new root GraphQL fields (queries/mutations) when exposing new functionality. diff --git a/.claude/skills/caveman/README.md b/.claude/skills/caveman/README.md new file mode 100644 index 000000000..d749b8343 --- /dev/null +++ b/.claude/skills/caveman/README.md @@ -0,0 +1,48 @@ +# caveman + +Talk like smart caveman. Same brain, fewer tokens. + +## What it does + +Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped. + +Six intensity levels: + +| Level | What change | +|-------|-------------| +| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | +| `full` | Default. Drop articles, fragments OK, short synonyms. | +| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | +| `wenyan-lite` | Classical Chinese register, light compression. | +| `wenyan-full` | Maximum 文言文. 80-90% character reduction. | +| `wenyan-ultra` | Extreme classical compression. | + +Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. + +## How to invoke + +``` +/caveman # full mode (default) +/caveman lite # lighter compression +/caveman ultra # extreme compression +/caveman wenyan # classical Chinese +stop caveman # back to normal prose +``` + +## Example output + +Question: "Why does my React component re-render?" + +Normal prose: +> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. + +Caveman (full): +> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. + +Caveman (ultra): +> Inline obj prop → new ref → re-render. `useMemo`. + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview, install, benchmarks diff --git a/.claude/skills/caveman/SKILL.md b/.claude/skills/caveman/SKILL.md new file mode 100644 index 000000000..073d6bb10 --- /dev/null +++ b/.claude/skills/caveman/SKILL.md @@ -0,0 +1,74 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, + wenyan-lite, wenyan-full, wenyan-ultra. + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. +--- + +Respond terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". + +Default: **full**. Switch: `/caveman lite|full|ultra`. + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +## Intensity + +| Level | What change | +|-------|------------| +| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | +| **full** | Drop articles, fragments OK, short synonyms. Classic caveman | +| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | +| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | +| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | +| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | + +Example — "Why React component re-render?" +- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." +- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." +- ultra: "Inline obj prop → new ref → re-render. `useMemo`." +- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" +- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。" +- wenyan-ultra: "新參照→重繪。useMemo Wrap。" + +Example — "Explain database connection pooling." +- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." +- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." +- ultra: "Pool = reuse DB conn. Skip handshake → fast under load." +- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。" +- wenyan-ultra: "池reuse conn。skip handshake → fast。" + +## Auto-Clarity + +Drop caveman when: +- Security warnings +- Irreversible action confirmations +- Multi-step sequences where fragment order or omitted conjunctions risk misread +- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions) +- User asks to clarify or repeats question + +Resume caveman after clear part done. + +Example — destructive op: +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> ```sql +> DROP TABLE users; +> ``` +> Caveman resume. Verify backup exist first. + +## Boundaries + +Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. \ No newline at end of file diff --git a/.claude/skills/graphite/SKILL.md b/.claude/skills/graphite/SKILL.md new file mode 100644 index 000000000..b1020e57d --- /dev/null +++ b/.claude/skills/graphite/SKILL.md @@ -0,0 +1,344 @@ +--- +name: graphite +description: Work with Graphite (gt) for stacked PRs - creating, navigating, and managing PR stacks. +allowed-tools: + - "Bash(gt *)" + - "Bash(git add *)" + - "Bash(git reset *)" + - "Bash(git diff *)" + - "Bash(git status *)" + - "Bash(git stash *)" + - "Bash(git checkout *)" + - "Bash(git rebase *)" + - "Bash(git branch *)" + - "Bash(gh pr *)" +--- + +# Graphite Skill + +Work with Graphite (`gt`) for creating, navigating, and managing stacked pull requests. + +## Quick Reference + +| I want to... | Command | +|--------------|---------| +| Create a new branch/PR | `gt create branch-name -m "message"` | +| Amend current branch | `gt modify -m "message"` | +| Navigate up the stack | `gt up` | +| Navigate down the stack | `gt down` | +| Jump to top of stack | `gt top` | +| Jump to bottom of stack | `gt bottom` | +| View stack structure | `gt ls` | +| Submit stack for review | `gt submit --no-interactive` | +| Rebase stack on trunk | `gt restack` | +| Change branch parent | `gt track --parent ` | +| Rename current branch | `gt rename ` | +| Move branch in stack | `gt move` | + +--- + +## What Makes a Good PR? + +In roughly descending order of importance: + +- **Atomic/hermetic** - independent of other changes; will pass CI and be safe to deploy on its own +- **Narrow semantic scope** - changes only to module X, or the same change across modules X, Y, Z +- **Small diff** - (heuristic) small total diff line count + +**Do NOT worry about creating TOO MANY pull requests.** It is **always** preferable to create more pull requests than fewer. + +**NO CHANGE IS TOO SMALL:** tiny PRs allow for the medium/larger-sized PRs to have more clarity. + +Always argue in favor of creating more PRs, as long as they independently pass build. + +--- + +## Branch Naming Conventions + +When naming PRs in a stack, follow this syntax: + +`terse-stack-feature-name/terse-description-of-change` + +For example, a 4 PR stack: + +``` +auth-bugfix/reorder-args +auth-bugfix/improve-logging +auth-bugfix/improve-documentation +auth-bugfix/handle-401-status-codes +``` + +--- + +## Creating a Stack + +### Basic Workflow + +1. Make changes to files +2. Stage changes: `git add ` +3. Create branch: `gt create branch-name -m "commit message"` +4. Repeat for each PR in the stack +5. Submit: `gt submit --no-interactive` + +### Handle Untracked Branches (common with worktrees) + +Before creating branches, check if the current branch is tracked: + +```bash +gt branch info +``` + +If you see "ERROR: Cannot perform this operation on untracked branch": + +**Option A (Recommended): Track temporarily, then re-parent** +1. Track current branch: `gt track -p main` +2. Create your stack normally with `gt create` +3. After creating ALL branches, re-parent your first new branch onto main: + ```bash + gt checkout + gt track -p main + gt restack + ``` + +**Option B: Stash changes and start from main** +1. `git stash` +2. `git checkout main && git pull` +3. Create new branch and unstash: `git checkout -b temp-working && git stash pop` +4. Proceed with `gt track -p main` and `gt create` + +--- + +## Navigating a Stack + +```bash +# Move up one branch (toward top of stack) +gt up + +# Move down one branch (toward trunk) +gt down + +# Jump to top of stack +gt top + +# Jump to bottom of stack (first branch above trunk) +gt bottom + +# View the full stack structure +gt ls +``` + +--- + +## Modifying a Stack + +### Amend Current Branch + +```bash +git add +gt modify -m "updated commit message" +``` + +### Reorder Branches + +Use `gt move` to reorder branches in the stack. This is simpler than trying to use `gt create --insert`. + +### Re-parent a Stack + +If you created a stack on top of a feature branch but want it based on main: + +```bash +# Go to first branch of your stack +gt checkout + +# Change its parent to main +gt track --parent main + +# Rebase the entire stack +gt restack +``` + +### Rename a Branch + +```bash +gt rename new-branch-name +``` + +--- + +## Resetting Commits to Unstaged Changes + +If changes are already committed but you want to re-stack them differently: + +```bash +# Reset the last commit, keeping changes unstaged +git reset HEAD^ + +# Reset multiple commits (e.g., last 2 commits) +git reset HEAD~2 + +# View the diff to understand what you're working with +git diff HEAD +``` + +--- + +## Before Submitting + +### Verify Stack is Rooted on Main + +Before running `gt submit`, verify the first PR is parented on `main`: + +```bash +gt ls +``` + +If the first branch has a parent other than `main`: +```bash +gt checkout +gt track -p main +gt restack +``` + +### Run Validation + +After creating each PR, run appropriate linting, building, and testing: + +1. Refer to the project's CLAUDE.md for specific commands +2. If validation fails, fix the issue, stage changes, and use `gt modify` + +--- + +## Submitting and Updating PRs + +### Submit the Stack + +```bash +gt submit --no-interactive +``` + +### Update PR Descriptions + +After submitting, use `gh pr edit` to set proper titles and descriptions. + +**IMPORTANT:** Never use Bash heredocs for PR descriptions - shell escaping breaks markdown tables, code blocks, etc. Instead: + +1. Use the `Write` tool to create `/tmp/pr-body.md` with the full markdown content +2. Use `gh pr edit` with `--body-file`: + +```bash +gh pr edit --title "stack-name: description" --body-file /tmp/pr-body.md +``` + +PR descriptions must include: +- **Stack Context**: What is the bigger goal of this stack? +- **What?** (optional for small changes): Super terse, focus on what not why +- **Why?**: What prompted the change? Why this solution? How does it fit into the stack? + +**Example** (for a PR in a 3-PR stack adding a warning feature): + +```markdown +## Stack Context + +This stack adds a warning on the merge button when users are bypassing GitHub rulesets. + +## Why? + +Users who can bypass rulesets (via org admin or team membership) currently see no indication +they're circumventing branch protection. This PR threads the bypass data from the server to +enable the frontend warning (PR 2) to display it. +``` + +--- + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| "Cannot perform this operation on untracked branch" | Run `gt track -p main` first | +| Stack parented on wrong branch | Use `gt track -p main` then `gt restack` | +| Need to reorder PRs | Use `gt move` | +| Conflicts during restack | Resolve conflicts, then `git rebase --continue` | +| Want to split a PR | Reset commits (`git reset HEAD^`), re-stage selectively, create new branches | +| Need to delete a branch (non-interactive) | `gt delete -f -q` | +| `gt restack` hitting unrelated conflicts | Use targeted `git rebase ` instead (see below) | +| Rebase interrupted mid-conflict | Check if files are resolved but unstaged, then `git add` + `git rebase --continue` | + +--- + +## Advanced: Surgical Rebasing in Complex Stacks + +In deeply nested stacks with many sibling branches, `gt restack` can be problematic: +- It restacks ALL branches that need it, not just your stack +- Can hit conflicts in completely unrelated branches +- Is all-or-nothing - hard to be surgical + +### When to Use `git rebase` Instead of `gt restack` + +Use direct `git rebase` when: +- You only want to update specific branches in your stack +- `gt restack` is hitting conflicts in unrelated branches +- You need to skip obsolete commits during the rebase + +### Targeted Rebase Workflow + +```bash +# 1. Checkout the branch you want to rebase +git checkout my-feature-branch + +# 2. Rebase onto the target (e.g., updated parent branch) +git rebase target-branch + +# 3. If you hit conflicts: +# - Resolve the conflict in the file +# - Stage it: git add +# - Continue: git rebase --continue + +# 4. If a commit is obsolete and should be skipped: +git rebase --skip + +# 5. After rebase, use gt modify to sync graphite's tracking +gt modify --no-edit +``` + +### Recovering from Interrupted Rebase (Context Reset) + +If a rebase was interrupted (e.g., Claude session ran out of context): + +1. **Check status:** + ```bash + git status + # Look for "interactive rebase in progress" and "Unmerged paths" + ``` + +2. **Read the "unmerged" files** - they may already be resolved (no conflict markers) + +3. **If already resolved, just stage and continue:** + ```bash + git add + git rebase --continue + ``` + +4. **If still has conflict markers**, resolve them first, then stage and continue + +### Deleting Branches from a Stack + +```bash +# Delete a branch (non-interactive, even if not merged) +gt delete branch-to-delete -f -q + +# Also delete all children (upstack) +gt delete branch-to-delete -f -q --upstack + +# Also delete all ancestors (downstack) +gt delete branch-to-delete -f -q --downstack +``` + +**Flags:** +- `-f` / `--force`: Delete even if not merged or closed +- `-q` / `--quiet`: Implies `--no-interactive`, minimizes output + +**After deleting intermediate branches**, children are automatically restacked onto the parent. If you need to manually update tracking: +```bash +gt checkout child-branch +gt track --parent new-parent-branch +``` diff --git a/package.json b/package.json index f56ce16bd..3337f9794 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "start": "npm run -w backend start", "start:worker": "npm run -w backend start:worker", "lint": "npm run --workspaces --if-present lint", + "lint:fix": "npm run --workspaces --if-present lint:fix", "test": "vitest", "test:ui": "vitest --ui", "migrate": "npm run -w backend db:migrate", diff --git a/packages/backend/package.json b/packages/backend/package.json index 56bad4e0b..59ae74aa0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -13,6 +13,7 @@ "test:unit": "vitest --config ./vitest.config.ts", "test:integration": "vitest --config ./vitest.config.integration.ts", "lint": "eslint . --ignore-path ../../.eslintignore && tsc --noEmit", + "lint:fix": "eslint . --fix --ignore-path ../../.eslintignore && tsc --noEmit", "db": "DOTENV_CONFIG_PATH=.env knex", "readme:db:migration:create": "HOW TO USE: npm run -w backend db:migration:create your_migration_name", "db:migration:create": "npm run db -- migrate:make", diff --git a/packages/frontend/.claude/skills/vercel-composition-patterns/AGENTS.md b/packages/frontend/.claude/skills/vercel-composition-patterns/AGENTS.md new file mode 100644 index 000000000..558bf9aa1 --- /dev/null +++ b/packages/frontend/.claude/skills/vercel-composition-patterns/AGENTS.md @@ -0,0 +1,946 @@ +# React Composition Patterns + +**Version 1.0.0** +Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React codebases using composition. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale. + +--- + +## Table of Contents + +1. [Component Architecture](#1-component-architecture) — **HIGH** + - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation) + - 1.2 [Use Compound Components](#12-use-compound-components) +2. [State Management](#2-state-management) — **MEDIUM** + - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui) + - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection) + - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components) +3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM** + - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants) + - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props) +4. [React 19 APIs](#4-react-19-apis) — **MEDIUM** + - 4.1 [React 19 API Changes](#41-react-19-api-changes) + +--- + +## 1. Component Architecture + +**Impact: HIGH** + +Fundamental patterns for structuring components to avoid prop +proliferation and enable flexible composition. + +### 1.1 Avoid Boolean Prop Proliferation + +**Impact: CRITICAL (prevents unmaintainable component variants)** + +Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize + +component behavior. Each boolean doubles possible states and creates + +unmaintainable conditional logic. Use composition instead. + +**Incorrect: boolean props create exponential complexity** + +```tsx +function Composer({ + onSubmit, + isThread, + channelId, + isDMThread, + dmId, + isEditing, + isForwarding, +}: Props) { + return ( +
+
+ + {isDMThread ? ( + + ) : isThread ? ( + + ) : null} + {isEditing ? ( + + ) : isForwarding ? ( + + ) : ( + + )} +