From 3c6e23bc5ac055a069b1d7b178393eec916b0571 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Mon, 13 Apr 2026 15:07:11 -0700 Subject: [PATCH 1/3] Rewrite CLAUDE.md, update @rafters/ledger imports, remove orchestrator - CLAUDE.md: Rafters Studio Platform identity, plain Hono RPC (no OpenAPI), passkeys + OAuth2, hono/client, TanStack Router, updated test examples - apps/web: @ezmode-games/drizzle-ledger -> @rafters/ledger@^0.2.0 - Remove retired orchestrator agent (PR #221 audit, never worked) Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/agents/orchestrator.md | 108 --------------------------------- CLAUDE.md | 76 +++++++++++++---------- apps/web/package.json | 2 +- apps/web/src/api/auth.ts | 2 +- package.json | 2 +- 5 files changed, 47 insertions(+), 143 deletions(-) delete mode 100644 .claude/agents/orchestrator.md diff --git a/.claude/agents/orchestrator.md b/.claude/agents/orchestrator.md deleted file mode 100644 index 7f35bbd..0000000 --- a/.claude/agents/orchestrator.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: orchestrator -description: Haiku state machine for per-issue build loops. Spawns backend and frontend agents, routes housekeeping tasks back to the correct agent by file path, tracks what is done and what is pending. Platform spawns this with an issue spec and it runs until blocked or done. -model: claude-haiku-4-5-20251001 ---- - -# Platform Issue Orchestrator - -You are a state machine. You manage one issue at a time from start to PR-ready. You do not write code. You coordinate agents, carry state between them, and enforce gates. - -## How You Are Invoked - -Platform spawns you with: - -- Issue number and full spec -- Current phase (if resuming mid-loop) -- Any pending tasks from simplify or PR review (if in fix loop) - -## State Machine - -``` -not-started - -> backend spawn backend agent - -> frontend spawn frontend agent (with API shape from backend) - -> housekeeping report to platform, await simplify/review results - -> fix-backend spawn backend agent with specific fix tasks - -> fix-frontend spawn frontend agent with specific fix tasks - -> pr-ready confirm files committed, report to platform - -> done -``` - -## Phase: not-started - -1. Parse the issue spec. Separate it into: - - **Backend requirements**: API routes, D1 queries, Zod schemas, SSE endpoints, migrations - - **Frontend requirements**: React components, TanStack Query hooks, route files - - **Shared contracts**: Request/response shapes, search param schemas - -2. Confirm a feature branch exists for this issue. If not, ask platform to create one before proceeding. - -3. Spawn the `backend` agent with backend requirements + file locations from the spec. - -4. When backend returns: verify tests pass. If tests fail, spawn backend again with the failure output. Do not proceed to frontend until tests pass. - -5. Extract the **API shape summary** from backend output: endpoint paths, request schemas, response schemas, TypeScript type export locations. This is the contract you pass to frontend. - -## Phase: frontend - -Spawn the `frontend` agent with: - -- Frontend requirements from the issue spec -- Full API shape summary from backend -- Branch/worktree path - -When frontend returns: verify tests pass (`pnpm test:spec`, `pnpm test:a11y`). If any fail, spawn frontend again with the failure output. Do not proceed until all pass. - -## Phase: housekeeping - -When both agents are done with passing tests, report to platform: - -``` -Backend done: [file list] -Frontend done: [file list] -Tests passing: unit [N], spec [N], a11y [N] -API shape: [summary] -Ready for: simplify and PR review -``` - -Do not run simplify or PR review yourself. Platform runs those. - -## Phase: fix-backend / fix-frontend - -When platform provides tasks from simplify or PR review, determine which agent owns each file: - -**Backend owns:** - -- `apps/web/src/api/routes/**` -- `apps/web/src/db/schema/**` -- `tests/routes/**` - -**Frontend owns:** - -- `apps/*/src/routes/**` -- `apps/*/src/components/**` -- `apps/*/src/lib/**` -- `tests/components/**` -- `tests/flows/**` - -Group tasks by owner. Spawn each agent once with all its tasks. Do not split one agent's work across multiple invocations. - -When agents return fixes: verify tests still pass. Report results to platform. Repeat until platform confirms simplify and PR review are clean. - -## Phase: pr-ready - -When platform confirms housekeeping is clean: - -1. Confirm all files are committed on the feature branch. -2. Report to platform: file list, branch name, test counts. -3. Platform creates the PR and posts to the legion bullpen. - -## Rules - -- Never write code yourself. -- Never run migrations, push to remote, or merge. -- Never skip a gate. If tests fail, stay in the current phase. -- If you are unsure which agent owns a file, ask platform. -- If backend and frontend have no shared types, you may spawn them in parallel. Most issues are sequential -- the API shape must exist before frontend can consume it. -- Keep state summaries short. Platform needs signal, not logs. diff --git a/CLAUDE.md b/CLAUDE.md index f6a9cd6..214d05c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,30 +1,32 @@ -# ezmode.games Platform +# Rafters Studio Platform + +Infrastructure for all Rafters surfaces. Auth, database, API, middleware. + +## Apps + +| App | Path | What | +| -------- | ------------ | ------------------------------------------------- | +| **web** | `apps/web/` | Cloudflare Worker -- Hono API, better-auth, D1 | +| **ctrl** | `apps/ctrl/` | Internal operations SPA -- Vite + TanStack Router | ## Stack - **Runtime**: Cloudflare Workers -- **API**: Hono + `@hono/zod-openapi` + `stoker` -- **API Docs**: Scalar (`@scalar/hono-api-reference`) -- **Auth**: better-auth (native D1, OAuth2 for v2, API key for v1) +- **API**: Hono + `@hono/zod-validator`. Plain RPC, `AppType` exported for `hono/client`. +- **Auth**: better-auth (native D1, passkeys + OAuth2). No email/password. Ever. - **Database**: Cloudflare D1, Drizzle as query builder only - **Schemas**: Zod is source of truth. `drizzle-zod` bridges DB to API schemas. -- **Audit/Soft-delete/GDPR**: `@ezmode-games/drizzle-ledger` -- **Client**: `openapi-typescript` + `openapi-fetch` + `openapi-react-query` +- **Audit/Soft-delete/GDPR**: `@rafters/ledger` +- **Client**: `hono/client` with `AppType` for end-to-end type safety - **State**: TanStack Query (cache/fetch), TanStack Forms (validation/submission) -- **UI**: Rafters for all surfaces, with ezmode-specific composite library +- **Router**: TanStack Router (ctrl SPA, file-based, type-safe) +- **UI**: Rafters design system for all surfaces - **Lint/Format**: OXC (oxlint + oxc-format) - **TypeScript**: v6 (Go parser) - **Package Manager**: pnpm (never npm/yarn) - **IDs**: UUIDv7 for all identifiers -## API Versioning - -Two API versions served from the same Hono app: - -- **v1**: Nexus-compatible endpoints. API key auth. Exists so MO2 and Wabbajack work without changes -- users swap the base URL and API key, existing mod managers just work. Thin translation layer over shared business logic. -- **v2**: Our real API, internal and external. OAuth2 auth. OpenAPI spec published for third-party developers. Internal-only endpoints are unpublished and require session + additional headers. - -Both versions have their own OpenAPI spec and Scalar docs page. +No OpenAPI. The API is internal-only, ctrl is the only consumer. Removed in PR #92. ## Migrations @@ -45,6 +47,17 @@ NEVER: Drizzle schema files exist for type inference and query building. Wrangler handles all migration state. Breaking this rule corrupts wrangler's migration tracking. +## Schema Pattern + +Every table gets two files: + +1. **Drizzle schema**: `apps/web/src/db/schema/*.ts` -- table definitions for query building +2. **Zod companion**: `apps/web/src/db/schema/*.zod.ts` -- insert/select schemas via `drizzle-zod` + +No barrel files. Import directly from the specific schema file. + +`wrangler types` generates the Env type. Never manually type it. + --- # Testing @@ -58,25 +71,20 @@ Tests are tools, not theater. Every test file has one job based on its extension All tests live in `tests/` mirroring the source tree: ``` -src/ +apps/web/src/ + api/routes/ + notifications.ts +apps/ctrl/src/ routes/ - mods/ - mods.handlers.ts - mods.routes.ts - components/ - mod-card/ - mod-card.tsx + _ctrl/dashboard.tsx tests/ - routes/ - mods/ - mods.handlers.test.ts # unit - mods.routes.test.ts # unit - components/ - mod-card/ - mod-card.spec.ts # component behavior + api/routes/ + notifications.test.ts # unit + ctrl/ + dashboard.spec.ts # component behavior flows/ - publish-mod.e2e.ts # end-to-end + auth-login.e2e.ts # end-to-end ``` Tests are NEVER colocated with source files. The `tests/` folder is the single location. @@ -147,8 +155,8 @@ Tests are NEVER colocated with source files. The `tests/` folder is the single l **What belongs here**: -- Complete user journeys (sign up, upload mod, get paid) -- Auth flows (OAuth2 login, API key validation, session expiry) +- Complete user journeys (login, manage properties, review content) +- Auth flows (passkey login, OAuth2, session expiry) - Cross-page navigation and deep linking - Real API calls through the full Workers stack - Visual regression on critical pages @@ -214,3 +222,7 @@ pnpm test:all # everything - No arbitrary Tailwind values (`-[400px]`). Use design tokens. - No CSS positioning (`absolute`, `relative`, `fixed`) unless no alternative. Use flexbox/grid. - Prefer container queries (`@container`, `@md:`) over media queries for component-level responsiveness. +- No barrel files on the edge. Import directly from the specific file. +- `wrangler types` generates Env. Never manually type it. +- better-auth generates its own schema/migrations. Do not hand-write auth tables. +- Every Drizzle schema file gets a `.zod.ts` companion. diff --git a/apps/web/package.json b/apps/web/package.json index cdc9ec7..9859614 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,11 +13,11 @@ "@astrojs/mdx": "^5.0.2", "@astrojs/react": "^5.0.1", "@better-auth/passkey": "^1.5.5", - "@ezmode-games/drizzle-ledger": "^0.0.1", "@hono/zod-validator": "^0.7.6", "@polar-sh/better-auth": "^1.8.3", "@polar-sh/checkout": "^0.2.0", "@polar-sh/sdk": "^0.46.6", + "@rafters/ledger": "^0.2.0", "@tailwindcss/vite": "^4.2.2", "@tanstack/react-form": "^1.28.5", "@tanstack/react-query": "^5.91.0", diff --git a/apps/web/src/api/auth.ts b/apps/web/src/api/auth.ts index ba58a5c..07fb3ed 100644 --- a/apps/web/src/api/auth.ts +++ b/apps/web/src/api/auth.ts @@ -6,7 +6,7 @@ import { organization } from "better-auth/plugins/organization"; import { passkey } from "@better-auth/passkey"; import { checkout, polar, webhooks } from "@polar-sh/better-auth"; import { Polar } from "@polar-sh/sdk"; -import { ledgerPlugin } from "@ezmode-games/drizzle-ledger/better-auth"; +import { ledgerPlugin } from "@rafters/ledger/better-auth"; import { uuidv7 } from "uuidv7"; import { createDb } from "../db/client"; import { auditLog } from "../db/schema/audit"; diff --git a/package.json b/package.json index f802cf6..c9ed25b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "packageManager": "pnpm@10.32.1", "pnpm": { "onlyBuiltDependencies": [ - "@ezmode-games/drizzle-ledger", + "@rafters/ledger", "esbuild", "sharp", "workerd" From ae059544b38abc78778b0272e2e0c3ac66e58671 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Mon, 27 Apr 2026 14:29:24 -0700 Subject: [PATCH 2/3] Update pnpm-lock.yaml for @rafters/ledger@0.2.0 Lockfile was stale after the @ezmode-games/drizzle-ledger -> @rafters/ledger swap in 3c6e23b, breaking pnpm install --frozen-lockfile in CI. Co-Authored-By: Claude Opus 4.7 (1M context) --- pnpm-lock.yaml | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5d13e2..59ad078 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,9 +96,6 @@ importers: '@better-auth/passkey': specifier: ^1.5.5 version: 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.13)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-auth@1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))))(better-call@1.3.2(zod@4.3.6))(nanostores@1.2.0) - '@ezmode-games/drizzle-ledger': - specifier: ^0.0.1 - version: 0.0.1(better-auth@1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))))(drizzle-orm@0.45.1(kysely@0.28.13)) '@hono/zod-validator': specifier: ^0.7.6 version: 0.7.6(hono@4.12.8)(zod@4.3.6) @@ -111,6 +108,15 @@ importers: '@polar-sh/sdk': specifier: ^0.46.6 version: 0.46.6 + '@rafters/color-utils': + specifier: link:/Volumes/store/projects/rafters-studio/rafters/packages/color-utils + version: link:../../../rafters/packages/color-utils + '@rafters/ledger': + specifier: ^0.2.0 + version: 0.2.0(better-auth@1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))))(drizzle-orm@0.45.1(kysely@0.28.13)) + '@rafters/shared': + specifier: link:/Volumes/store/projects/rafters-studio/rafters/packages/shared + version: link:../../../rafters/packages/shared '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2)) @@ -1139,16 +1145,6 @@ packages: cpu: [x64] os: [win32] - '@ezmode-games/drizzle-ledger@0.0.1': - resolution: {integrity: sha512-a2+8KB0EEQWGoJUxoAgZzNf7cEIy8XU5nSJh7VEnCaLT1o0iLfR9sh9egxb4qtCsyfPJ5WdgfkfDtyY5+8L4VQ==} - engines: {node: '>=18'} - peerDependencies: - better-auth: '*' - drizzle-orm: '>=0.30.0' - peerDependenciesMeta: - better-auth: - optional: true - '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2211,6 +2207,18 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rafters/ledger@0.2.0': + resolution: {integrity: sha512-inYp61hrP8cokRO8xop4FOmA9QEM7Hlxo5ZXrdzEFtkxMgCnA+cC8WD/hxsREXFhplA7eR0r0FFkrEQ36vB7TA==} + engines: {node: '>=22'} + peerDependencies: + better-auth: '*' + drizzle-orm: '>=0.30.0' + peerDependenciesMeta: + better-auth: + optional: true + drizzle-orm: + optional: true + '@reduxjs/toolkit@2.11.2': resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} peerDependencies: @@ -5903,13 +5911,6 @@ snapshots: '@esbuild/win32-x64@0.27.4': optional: true - '@ezmode-games/drizzle-ledger@0.0.1(better-auth@1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))))(drizzle-orm@0.45.1(kysely@0.28.13))': - dependencies: - drizzle-orm: 0.45.1(kysely@0.28.13) - uuidv7: 1.1.0 - optionalDependencies: - better-auth: 1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))) - '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -6951,6 +6952,13 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@rafters/ledger@0.2.0(better-auth@1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))))(drizzle-orm@0.45.1(kysely@0.28.13))': + dependencies: + uuidv7: 1.1.0 + optionalDependencies: + better-auth: 1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))) + drizzle-orm: 0.45.1(kysely@0.28.13) + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.1.0 From c77d2e64f722891fbd3cde65062b851ddc48cbd6 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Mon, 27 Apr 2026 14:42:20 -0700 Subject: [PATCH 3/3] Regenerate pnpm-lock.yaml without local link: entries The prior lockfile commit (ae05954) accidentally included link: entries pointing to absolute /Volumes/store paths from local WIP, which broke pnpm install --frozen-lockfile in CI on Linux runners. Regenerated against the committed package.json so the lockfile only references registry-resolvable specifiers. Co-Authored-By: Claude Opus 4.7 (1M context) --- pnpm-lock.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59ad078..6eba144 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,15 +108,9 @@ importers: '@polar-sh/sdk': specifier: ^0.46.6 version: 0.46.6 - '@rafters/color-utils': - specifier: link:/Volumes/store/projects/rafters-studio/rafters/packages/color-utils - version: link:../../../rafters/packages/color-utils '@rafters/ledger': specifier: ^0.2.0 version: 0.2.0(better-auth@1.5.5(drizzle-kit@0.31.10)(drizzle-orm@0.45.1(kysely@0.28.13))(mongodb@7.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))))(drizzle-orm@0.45.1(kysely@0.28.13)) - '@rafters/shared': - specifier: link:/Volumes/store/projects/rafters-studio/rafters/packages/shared - version: link:../../../rafters/packages/shared '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@7.3.1(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.2))