CLAUDE.md · diff

git:20260729.1b1adf6 to git:20260805.8daa3f0

92 added, 150 removed. Audit A to A.

# LeadAce Plugin Development Repository
- Repository for LeadAce — an autonomous sales automation Claude Code plugin by SurpassOne Inc.
+ LeadAce — an autonomous sales automation Claude Code plugin by SurpassOne Inc.
- ## Repository Structure
+ ## Layout
```
.claude-plugin/marketplace.json # Marketplace definition (source: "./plugin/")
- plugin/ # Claude Code plugin
- backend/ # Web API Server + MCP Server
- frontend/ # Web frontend
- docs/ # Project-wide docs (deploy runbook, self-host, tasks)
- docker-compose.yml # Local development environment
- ```
-
- ## Plugin Structure
-
- ```
- plugin/
- ├── .claude-plugin/
- │ └── plugin.json # Plugin manifest (required)
- ├── .mcp.json # MCP server configuration (LeadAce backend)
- ├── skills/ # Skills (each subdirectory has SKILL.md, invoked as `/<name>`)
- ├── scripts/ # Local utility tools (fetch_url.py)
- └── references/ # Shared reference documents
- ```
-
- Project-wide design docs, runbooks, and task tracking live in the top-level `docs/` directory, not under `plugin/`. Anything that is not specifically about the plugin's runtime structure (Workers, Pages, Stripe, Supabase, session-level task tracking, architecture history, etc.) belongs there.
-
- Plugin / skill authoring conventions live in [.claude/rules/plugin-development.md](.claude/rules/plugin-development.md) (auto-loaded when files under `plugin/` or `backend/seed-content/` are touched). For fundamentals not specific to LeadAce, consult `/skill-development` and `/plugin-structure`.
-
- ## Development Policy
-
- The plugin prioritizes **stability, reliability, controllability, and versatility**.
- - Do not hard-code values that depend on specific businesses or use cases (target numbers, success rates, etc.) into skills or templates
- - Defer business-specific decisions to project configuration (stored as documents in the DB: business, sales_strategy, etc.); the plugin provides control mechanisms and visibility
- - Improve skills by increasing user control, not by enforcing specific behavior
- - MCP tool surface: liberal with read tools, conservative with write/action tools. A destructive tool needs a read counterpart so the agent can preview what it is about to touch (`list_drafts` → `discard_drafts`)
- - MCP tool descriptions ship as context on every Claude Code turn — keep them terse: what the tool does / returns and any non-obvious caveat, nothing more. Interpretation and how-to guidance belong in the skill, not the tool description
- - An MCP tool answers with a **text block, never JSON**: the handler reads the API's JSON and formats a string, so a description checked against the *service* can name fields the model never sees. Describe what the emitted string carries. Never use JSON-shape notation (`{ a, b }`, `x[]`) for a payload the handler does not `JSON.stringify` (CI: `.github/scripts/check-mcp-descriptions.mjs`), and never name a value the string does not carry — `discard_drafts` once advertised `deletedIds` while emitting only its `.length`. Naming a label that appears verbatim in the output (`legalName: …`) is prose, not a shape claim
-
- ### Design Principles
-
- - **Explore wide, output narrow**: when investigating, brainstorming, or thinking, cast a wide net across angles. When producing output (docs, design, code), cut to the minimum that carries the conclusion. "Just in case" and "might as well note this" filler doesn't get read, buries the important parts, and breeds inconsistency over time — pure cost, no upside.
- - **Keep specs simple**: Default to the simplest spec that solves the problem. Extra conditional branches accrue as technical debt more often than they pay back as value — add one only when it encodes a real, distinct case.
- - **Encapsulate spec boundaries**: Each module / layer / skill should expose its responsibility through a narrow contract. Callers must not need to reason about its internals to use it correctly.
- - **Think before coding**: state assumptions explicitly. If multiple interpretations exist, present them — don't pick silently. If unclear, stop and ask. If a simpler approach exists, push back.
- - **Surgical changes**: touch only what you must — every changed line should trace to the request. Don't "improve" adjacent code, don't refactor what isn't broken, match existing style. Mention unrelated dead code; don't delete it. Remove orphans your changes created.
-
- ### Separation of Responsibilities: LLM vs MCP Tools
-
- Clearly separate what the LLM should handle from what MCP tools handle.
-
- - MCP tools (deterministic logic): DB operations (prospect registration, outreach logging, status updates, prospect-priority overrides, document storage), data queries (prospect identifiers, outbound targets, evaluation stats, document retrieval), master document retrieval via `get_master_document` — operations where rules are clear and behavior should be consistent every time. The server handles validation, deduplication, and status management
- - Local tools: email sending (`gog` CLI), form submission (`playwright-cli`), SNS DMs (`claude-in-chrome`), web page fetching (`fetch_url.py`) — operations requiring local environment access
- - LLM (judgment & generation): context-dependent judgment and natural language — drafting email bodies, evaluating prospects, analyzing/improving strategy, merging/deduplicating candidate data
-
- Design tools so the plugin never has to reason about state consistency. Each MCP tool is a thin 1:1 wrapper over one backend endpoint (`src/mcp/` does only response formatting — no business logic or state orchestration; project name-or-id resolution happens server-side in the API), so this is really an API-design rule: a data-mutating endpoint performs the action *and* applies every consequent state update atomically. The plugin then calls one simple, self-contained tool — never a multi-tool sequence in a fixed order — to keep data consistent. Canonical example: `send_email_and_record`.
-
- ## Plan Tiers & Limits
-
- Subscription is managed via Stripe. The API enforces limits based on user plan.
-
- | | Free | Starter $29/mo | Pro $79/mo | Scale $199/mo |
- |---|---|---|---|---|
- | Projects | 1 | 1 | 5 | Unlimited |
- | Outreach actions | 5/day (100 lifetime cap) | 1,500/mo | 4,000/mo | Unlimited |
- | Sending identities | 1 | 2 | 5 | Unlimited |
- | Prospect registration | 500 (lifetime) | — | — | — |
-
- Monthly caps derive from sending identities × ~25/day per-identity safe send rate (×30 days), rounded up to the next 500. Plans differentiate on throughput (identities, outreach volume, projects) only — insights computed from a tenant's own data are fully visible on every plan, never plan-gated.
-
- - Free has two outreach caps: `5/day` AND `100 lifetime`. Whichever runs out first blocks send. Paid plans use a single monthly cap that resets at the Stripe `current_period_start`.
- - Daily window is UTC midnight-to-midnight (no per-tenant timezone).
- - Outreach action = `record_outreach` with `status: "sent"`. Failed attempts don't count.
- - Quota enforcement: `get_outbound_targets` returns `min(requested, remainingQuota, availableTargets)` where `remainingQuota` is the smallest remaining across all applicable windows. When 0, returns empty list with a constraint-specific message ("try again tomorrow" for daily, "upgrade" for lifetime/monthly). `record_outreach` and `/outreach/send-and-record` guard as a safety net.
- - Billing: Stripe Checkout for new subs, Stripe Customer Portal for changes/cancel. No billing UI in our app.
- - Self-host: code is open source. Users run their own Supabase + Cloudflare deploy (see [docs/self-host.md](docs/self-host.md)). Same plan-limits code runs; defaults to Free.
-
- ## Multi-Tenancy
-
- All data is isolated by tenant. Every tenant-scoped table carries a `tenant_id` column, queries always filter on it, and RLS enforces the isolation at the DB level. See [.claude/rules/backend-architecture.md](.claude/rules/backend-architecture.md) for the schema (tables, role, middleware) and conventions (where `createDb()` is allowed to bypass RLS).
-
- ## Compliance
-
- `gmail.readonly` is a Google Restricted scope: the **CASA AL1 security assessment must be renewed annually** (else readonly reply-reading is blocked). Runbook: [docs/casa.md](docs/casa.md).
-
- ## Development Rules
-
- - Language: English (code comments, documentation, and all plugin-read content — illustrative examples included). Non-English text appears only as **functional runtime data**: fixed match tokens (ja micro-reply tokens, sales-refusal detection strings), user-input trigger phrases, and the like — never as prose or illustration. Runtime *output* language is owned by `targetLanguage`, not by this rule
- - **Types express the spec**: design types so invalid states cannot be constructed. `any` is prohibited (see Backend TypeScript Rules). When behavior depends on a runtime check, encode it in the type (discriminated union, branded type, narrowed return) instead of leaving it implicit
- - **Don't reach for `null` / `undefined` reflexively**: each optional field multiplies the states callers must handle. Before adding one, ask: is the value truly absent sometimes, can a sensible default replace it, or should the type be split into variants where each variant has the field present? Use optionality only when absence is a real, distinct state
- - **DB columns: NOT NULL by default** — make a column nullable only when `null` is a genuinely distinct state. A nullable column tends to cascade into "what if the whole row is absent" assumptions across every reader, and that assumption is much harder to retract later than to avoid up front
- - **Stick to the orthodox path**: prefer the boring, obvious implementation over a clever one. Code should read top-to-bottom without the reader having to reconstruct hidden context
- - **`const` by default, `let` only when genuinely reassigned**: if a binding is never reassigned, it must be `const`. A `let` whose reassignment is dead (the new value is never read) is a code smell — remove the write and the binding becomes `const`. No dead/unused assignments
- - **State names match reality, no double-duty columns**: don't overload a status / state field to carry a feature requirement (e.g., flipping `prospect.status` to `contacted` only to keep get_outbound_targets from re-picking a draft). Express the feature requirement on a separate axis (a derived query like `NOT EXISTS`, a separate column, an additional enum value) so the original state keeps its real-world meaning
- - **Testing philosophy**: rely on the type system (TypeScript strict, Zod schemas, drizzle's typed builder) as the first line of defense — anything the types can already guarantee is NOT tested. On top of that, the backend keeps a **minimal, coarse-grained** unit-test layer (Vitest, co-located `*.test.ts`) that covers only **pure business logic types cannot express**: branching / state transitions, arithmetic, ordering / tie-breaks, parsing, dedup / normalization, threshold decisions — where getting it wrong has real consequences. Where such logic is trapped inside a DB-coupled service function, extract the pure core (to `domain/`, or as an exported pure helper) and test that. Do NOT test routes, drizzle queries, or DB I/O via mocks — those stay covered by the `e2e/regression-*.sh` curl harness. Don't chase exhaustive per-endpoint coverage. Full standard: [.claude/rules/backend-architecture.md](.claude/rules/backend-architecture.md) § Testing
- - **Comments: default to none — the *what* is the code itself.** If code is hard to follow without a comment, treat that as a spec/design problem and fix the structure instead: simpler spec, leaner implementation, separation of concerns, encapsulation, loose coupling, clearer interfaces — not more abstraction. Write a comment only when a *why* genuinely cannot be expressed in code (external constraint, non-obvious invariant, deliberate tradeoff), and keep it minimal
-
- ## Backend Development (backend/)
-
- ### Architecture
-
- 3-layer pattern: `routes/` (HTTP adapter) → `services/` (orchestration + DB I/O) → `domain/` (pure functions). No repository layer — drizzle is the typed query builder. See [.claude/rules/backend-architecture.md](.claude/rules/backend-architecture.md) for the full standard (dependency rules, transaction conventions, Hono/drizzle gotchas). Reference implementation: `backend/src/api/routes/responses.ts` + `backend/src/services/responses.ts` + `backend/src/domain/{prospect-status,rejection-feedback}.ts`.
-
- ### DB Schema Changes
-
- Never write migration SQL by hand. `backend/src/db/schema.ts` is the single source of truth.
-
- Never edit a migration SQL file after it has been applied (local, staging, or prod). Once a `drizzle/NNNN_*.sql` is applied anywhere, treat it as immutable — if behavior needs to change, generate a new migration. Editing applied files drifts the committed file from actual DB state and the hashes in `drizzle.__drizzle_migrations` no longer match. Recovery requires manual SQL surgery on prod.
-
- Local dev flow:
-
- ```bash
- # 1. Edit backend/src/db/schema.ts
- # 2. Auto-generate migration SQL from the diff
- cd backend && npm run db:generate
- # 3. Apply to local DB
- npm run db:migrate
- # 4. Commit schema.ts + drizzle/ together
+ plugin/ # Claude Code plugin (skills/, scripts/, references/, .mcp.json)
+ backend/ # API Worker + MCP Worker (Cloudflare)
+ frontend/ # SvelteKit web app (Cloudflare Pages)
+ docs/ # Project-wide design docs, runbooks, task tracking
```
- Production: `db:migrate` runs via the `migrate-db` job in `.github/workflows/deploy.yml` on every `main` push (idempotent — drizzle tracks applied migrations). The job uses `DATABASE_URL_SESSION_POOLER` (Session Pooler, port 5432); Transaction Pooler (6543) breaks DDL like `CREATE ROLE`.
-
- When a migration adds a new tenant-scoped table, append `GRANT SELECT, INSERT, UPDATE, DELETE ON <table> TO app_rls;` to the generated SQL. `0001_rls_policies.sql`'s `ALTER DEFAULT PRIVILEGES` only covers objects created by the role that ran it — a migration applied by another role (e.g., hand-run via the Supabase SQL editor) silently skips the grant and runtime hits "permission denied for table …". The belt-and-braces grant is idempotent.
-
- ### TypeScript Rules (backend/)
-
- - `any` is prohibited. Use proper types or fix the design.
- - After modifying backend TypeScript, run `cd backend && npm run typecheck` before committing.
- - Zod is v4. Use top-level `z.email()` / `z.url()` / `z.uuid()` etc. for string-format validation. `z.string().email()` / `.url()` are deprecated.
- - For partial-update upsert endpoints (`PUT /xxx`), do not pre-load the row to merge with the patch. Set `INSERT` values from `patch ?? DEFAULTS` and `onConflictDoUpdate.set` to only the columns the caller explicitly provided (conditional spread). The pre-load + merge approach is racy: two concurrent PUTs read the same `existing`, each merges its own patch, and the loser's untouched columns clobber the winner's. See `backend/src/api/routes/project-settings.ts` PUT handler for the canonical shape.
+ Area standards live in `.claude/rules/` and auto-load when you touch matching
+ files: `backend-architecture.md` (backend/), `frontend-architecture.md`
+ (frontend/), `plugin-development.md` (plugin/, backend/seed-content/),
+ `release.md` (plugin.json). Starting work in an area before touching its
+ files? Read its rule first.
- ### Local Dev
+ ## Commands
```bash
- make dev # or ./scripts/dev.sh — Supabase + migrate + seed +
- # API (:8787) + MCP (:8788) + frontend (:5273).
- # Ctrl-C stops the dev servers; `make stop` halts Supabase.
+ make dev # local stack: Supabase + migrate + seed + API (:8787) + MCP (:8788) + frontend (:5273)
+ cd backend && npm run typecheck # required before committing backend TS
+ cd backend && npm test # backend unit tests (Vitest)
+ cd frontend && npm run check # required before committing frontend
```
- `scripts/dev.sh` is the orchestrator; the Makefile is a thin wrapper. Supabase
- stays on the Supabase CLI (it is already Docker and is coupled to
- `supabase/config.toml`); the Workers run natively (wrangler `workerd`). The
- manual breakdown and first-time Google-OAuth setup live in `README.md` → For
- Developers and `docs/self-host.md` → Local development.
-
- ## Frontend Development (frontend/)
+ The three check commands are the pre-release checklist. Local E2E harness:
+ `e2e/` (see `e2e/README.md`; prerequisites in `.claude/skills/local-e2e/SKILL.md`).
- - SvelteKit 2 (Svelte 5, runes mode) + `@sveltejs/adapter-cloudflare`
- - Tailwind CSS v4 (CSS-based config, no tailwind.config.js)
- - SSR + client hydration — Svelte / Supabase's [officially recommended pattern](https://supabase.com/docs/guides/auth/server-side/sveltekit). Auth resolves server-side in `hooks.server.ts` and is serialized into the page, so first paint isn't blocked on a client-side `getSession()`.
- - Auth via `@supabase/ssr`: `createServerClient` in `hooks.server.ts` + `createBrowserClient` in root `+layout.ts` (synced from server-loaded data). Both share cookie storage. `/auth/callback` is a server-side `+server.ts` that calls `exchangeCodeForSession` and 303-redirects.
- - Auth gating lives in `hooks.server.ts` (route-id based: `(app)` is protected, `/login` redirects signed-in users), not in component `$effect`. Loaders and routes assume `event.locals.session` is set when their route required it.
- - Public env vars via `$env/static/public`: `PUBLIC_SUPABASE_URL`, `PUBLIC_SUPABASE_ANON_KEY`, `PUBLIC_API_URL`. New code uses these, not `VITE_*` (still works but bypasses SvelteKit's typed env pipeline).
+ ## Product Principles
- Architecture details (layers, `hooks.server.ts` shape, server-load vs client-load split, API client transport contract) live in [.claude/rules/frontend-architecture.md](.claude/rules/frontend-architecture.md).
+ - Priorities: stability, reliability, controllability, versatility. Don't
+ hard-code business-specific values into skills or templates — defer them to
+ project configuration (documents in the DB). Improve skills by increasing
+ user control, not by enforcing behavior.
+ - Plans differentiate on throughput only (identities, outreach volume,
+ projects); insights computed from a tenant's own data are never plan-gated.
+ Quota semantics that are easy to get wrong: Free has two caps (5/day AND
+ 100 lifetime, whichever runs out first); paid plans use one monthly cap
+ resetting at Stripe `current_period_start`; the daily window is UTC; an
+ outreach action = `record_outreach` with `status: "sent"` (failures don't
+ count). Enforcement: `backend/src/services/plan-limits.ts`.
+ - Self-host: code is open source; the same plan-limits code runs and defaults
+ to Free ([docs/self-host.md](docs/self-host.md)).
+ - Multi-tenancy: every tenant-scoped table carries `tenant_id`, every query
+ filters on it, and RLS enforces it at the DB level.
+ - Compliance: `gmail.readonly` is a Google Restricted scope — the CASA AL1
+ assessment must be renewed annually or reply-reading breaks. Runbook:
+ [docs/casa.md](docs/casa.md).
- ## Local E2E Testing
+ ## Design Principles
- Project-internal skill at [.claude/skills/local-e2e/SKILL.md](.claude/skills/local-e2e/SKILL.md) holds the prerequisite knowledge for running E2E tests against a **local** LeadAce stack (local Supabase + API/MCP Workers + frontend, real Google OAuth — same code path a self-host install runs). The harness lives in `e2e/` (see [e2e/README.md](e2e/README.md)): `smoke.sh` drives the `/leadace` onboarding chain headless, and the curl-only `regression-*.sh` scripts cover the dedup and send-and-record branches (including a real-Gmail happy path redirected to a test mailbox via `E2E_RECIPIENT_OVERRIDE`). Invoke it when the user asks to run a local E2E.
+ - **Explore wide, output narrow**: investigate broadly; ship the minimum that
+ carries the conclusion. "Just in case" filler is pure cost.
+ - **Keep specs simple**: default to the simplest spec that solves the problem.
+ Add a conditional branch only when it encodes a real, distinct case.
+ - **Encapsulate spec boundaries**: narrow contracts; callers must not reason
+ about internals.
+ - **Think before coding**: state assumptions; if multiple interpretations
+ exist, present them — don't pick silently; push back when a simpler
+ approach exists; ask when unclear.
+ - **Surgical changes**: every changed line traces to the request. Don't
+ improve adjacent code or refactor what isn't broken; mention unrelated dead
+ code rather than deleting it; remove orphans your change created.
+ - **LLM vs deterministic split**: operations with clear rules that must behave
+ identically every time (DB writes, dedup, status transitions, quota) live in
+ the backend behind MCP tools; context-dependent judgment and natural
+ language (drafting, evaluating, strategy) stay with the LLM. A data-mutating
+ endpoint applies the action *and* every consequent state update atomically,
+ so the plugin calls one self-contained tool — never a fixed multi-tool
+ sequence (canonical: `send_email_and_record`). MCP surface: liberal with
+ read tools, conservative with write tools; a destructive tool needs a read
+ counterpart (`list_drafts` → `discard_drafts`).
+ - **MCP tool descriptions**: terse — they ship as context on every turn. An
+ MCP tool answers with a text block, never JSON: describe what the emitted
+ string carries, never a JSON shape, and never name a value the string
+ doesn't carry (CI: `.github/scripts/check-mcp-descriptions.mjs`).
- ## Pre-Release Checklist (Required)
+ ## Coding Rules
- - Backend (TypeScript): `cd backend && npm run typecheck`
- - Backend (tests): `cd backend && npm test`
- - Frontend (Svelte): `cd frontend && npm run check`
+ - Language: English for code, comments, docs, and all plugin-read content.
+ Non-English appears only as functional runtime data (fixed match tokens,
+ user-input trigger phrases). Runtime output language is owned by
+ `targetLanguage`, not this rule.
+ - **Types express the spec**: `any` is prohibited. Encode runtime distinctions
+ in types (discriminated unions, branded types, narrowed returns) so invalid
+ states cannot be constructed.
+ - Optionality is a design decision: add `null`/`undefined` only when absence
+ is a real, distinct state; otherwise use a default or split the type into
+ variants. DB columns are NOT NULL by default.
+ - A status/state field keeps its real-world meaning — express a feature need
+ on a separate axis (derived query, extra column, new enum value), never by
+ overloading an existing state.
+ - Prefer the boring, obvious implementation over a clever one. `const` by
+ default; a `let` whose reassignment is never read is a smell.
+ - **Comments: default to none** — the *what* is the code itself. If code needs
+ a comment to be followed, fix the structure instead. Comment only a *why*
+ code cannot express: external constraint, non-obvious invariant, deliberate
+ tradeoff.
+ - **Testing**: the type system is the first line of defense; unit tests cover
+ only pure business logic types can't express (branching, arithmetic,
+ ordering, parsing, dedup, thresholds). No route tests, no drizzle/DB mocks —
+ DB-level behavior is covered by the `e2e/regression-*.sh` harness. Full
+ standard: backend rule § Testing.
- ## Branch flow & Release
+ ## Branch Flow & Release
- Work on `develop` (default branch); merge to `main` to ship. See [.claude/rules/release.md](.claude/rules/release.md) for the release procedure and version bump rules.
+ Work on `develop` (default branch); merge to `main` to ship (CI deploys
+ Workers + Pages + plugin marketplace). Release procedure and version bump
+ rules: [.claude/rules/release.md](.claude/rules/release.md).