- Add core/ layer documentation (queries, mutations, WS updaters) - Rewrite State Management section: TQ for server state, Zustand for client-only - Update features table: reflect gutted stores (issues, inbox, workspace) - Add @core/* import alias examples - Update Data Flow diagram to include TQ layer - Restore @core/* path alias in tsconfig + vitest (lost during merge) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
302 lines
15 KiB
Markdown
302 lines
15 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Context
|
|
|
|
Multica is an AI-native task management platform — like Linear, but with AI agents as first-class citizens.
|
|
|
|
- Agents can be assigned issues, create issues, comment, and change status
|
|
- Supports local (daemon) and cloud agent runtimes
|
|
- Built for 2-10 person AI-native teams
|
|
|
|
## Architecture
|
|
|
|
**Go backend + standalone Next.js frontend.**
|
|
|
|
- `server/` — Go backend (Chi router, sqlc for DB, gorilla/websocket for real-time)
|
|
- `apps/web/` — Next.js 16 frontend (App Router) — self-contained, no shared package dependencies
|
|
|
|
### Web App Structure (`apps/web/`)
|
|
|
|
The frontend uses a **feature-based architecture** with four layers:
|
|
|
|
```
|
|
apps/web/
|
|
├── app/ # Routing layer (thin shells — import from features/)
|
|
├── core/ # Headless business logic (TanStack Query, zero JSX, zero react-dom)
|
|
├── features/ # UI business components, organized by domain
|
|
├── shared/ # Cross-feature utilities (api client, types, logger)
|
|
```
|
|
|
|
**`app/`** — Next.js App Router pages. Route files should be thin: import and re-export from `features/`. Layout components and route-specific glue (redirects, auth guards) live here. Shared layout components (e.g. `app-sidebar`) stay in `app/(dashboard)/_components/`.
|
|
|
|
**`core/`** — Headless business logic. Query key factories, `queryOptions`, mutation hooks, WS cache updaters. **No JSX, no react-dom.** Designed for future extraction to `packages/core/` in a monorepo.
|
|
|
|
| Module | Purpose | Key exports |
|
|
|---|---|---|
|
|
| `core/issues/` | Issue queries, mutations, WS updaters | `issueListOptions`, `useUpdateIssue`, `onIssueUpdated` |
|
|
| `core/inbox/` | Inbox queries, mutations, WS updaters | `inboxListOptions`, `useMarkInboxRead` |
|
|
| `core/workspace/` | Member/agent/skill queries, workspace mutations | `memberListOptions`, `agentListOptions` |
|
|
| `core/runtimes/` | Runtime queries | `runtimeListOptions` |
|
|
| `core/query-client.ts` | QueryClient factory | `createQueryClient` |
|
|
| `core/provider.tsx` | QueryClientProvider wrapper | `QueryProvider` |
|
|
| `core/hooks.ts` | Shared hooks | `useWorkspaceId` |
|
|
|
|
**`features/`** — Domain modules with UI components, client-only stores, and config:
|
|
|
|
| Feature | Purpose | Exports |
|
|
|---|---|---|
|
|
| `features/auth/` | Authentication state | `useAuthStore`, `AuthInitializer` |
|
|
| `features/workspace/` | Workspace identity + UI | `useWorkspaceStore` (client-only: workspace/workspaces), `useActorName` |
|
|
| `features/issues/` | Issue UI components + client state | `useIssueStore` (client-only: activeIssueId), icons, pickers, config |
|
|
| `features/realtime/` | WebSocket connection + sync | `WSProvider`, `useWSEvent`, `useRealtimeSync` |
|
|
| `features/modals/` | Modal registry and state | Modal store and components |
|
|
| `features/skills/` | Skill management | Skill components |
|
|
|
|
**`shared/`** — Code used across multiple features (will migrate to `core/` in Phase 5):
|
|
- `shared/api/` — `ApiClient` (REST) and `WSClient` (WebSocket) for backend communication, plus the `api` singleton.
|
|
- `shared/types/` — Domain types (Issue, Agent, Workspace, etc.) and WebSocket event types.
|
|
- `shared/logger.ts` — Logger utility.
|
|
|
|
### State Management
|
|
|
|
- **TanStack Query** for all server state — issues, inbox, members, agents, skills, runtimes. Query definitions live in `core/<domain>/queries.ts`, mutations in `core/<domain>/mutations.ts`.
|
|
- **Zustand** for client-only state — UI selections (`activeIssueId`), view filters, modal state, workspace identity, navigation. No API calls in Zustand stores.
|
|
- **React Context** only for connection lifecycle (`WSProvider` in `features/realtime/`).
|
|
- **Local `useState`** for component-scoped UI state (forms, modals, filters).
|
|
|
|
**TanStack Query conventions:**
|
|
- `staleTime: Infinity` — WS events handle cache freshness, no polling or refetch-on-focus.
|
|
- WS events trigger `queryClient.invalidateQueries()` (preferred) or `queryClient.setQueryData()` for granular updates.
|
|
- All workspace-scoped query keys include `wsId` — workspace switch automatically uses new cache.
|
|
- Mutations use `onMutate` for optimistic updates + `onError` for rollback + `onSettled` for invalidation.
|
|
- Components access QueryClient via `useQueryClient()` hook. Non-React contexts (e.g. Tiptap plugin callbacks) receive QueryClient via closure from the parent React component — never use module-level singletons.
|
|
|
|
**Zustand store conventions:**
|
|
- Stores hold only client state (UI selections, persisted preferences). Zero `api.*` calls in stores.
|
|
- Import via `useAuthStore(selector)` or `useWorkspaceStore(selector)`.
|
|
- Stores must not call `useRouter` or any React hooks — keep navigation in components.
|
|
- `useWorkspaceStore` manages workspace identity (`workspace`, `workspaces`, `api.setWorkspaceId`, localStorage). Server data (members, agents, skills) is in TanStack Query, not the store.
|
|
|
|
### Import Aliases
|
|
|
|
Use `@/` alias (maps to `apps/web/`) and `@core/` alias (maps to `apps/web/core/`):
|
|
```typescript
|
|
// Core (headless business logic)
|
|
import { issueListOptions, issueKeys } from "@core/issues/queries";
|
|
import { useUpdateIssue, useCreateIssue } from "@core/issues/mutations";
|
|
import { memberListOptions, agentListOptions } from "@core/workspace/queries";
|
|
import { useWorkspaceId } from "@core/hooks";
|
|
|
|
// Shared (api client, types)
|
|
import { api } from "@/shared/api";
|
|
import type { Issue } from "@/shared/types";
|
|
|
|
// Features (UI components, client stores)
|
|
import { useAuthStore } from "@/features/auth";
|
|
import { useWorkspaceStore } from "@/features/workspace";
|
|
import { useWSEvent } from "@/features/realtime";
|
|
import { StatusIcon } from "@/features/issues/components";
|
|
```
|
|
|
|
Within a feature, use relative imports. Between features or to shared, use `@/`. For core modules, use `@core/`.
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
Browser → useQuery (core/) → ApiClient (shared/api) → REST API (Chi handlers) → sqlc queries → PostgreSQL
|
|
Browser ← useQuery cache ← invalidateQueries ← WS event handlers ← WSClient ← Hub.Broadcast()
|
|
```
|
|
|
|
Mutations: `useMutation (core/)` → optimistic cache update → API call → onSettled invalidation.
|
|
WS events: `use-realtime-sync.ts` → `queryClient.invalidateQueries()` for most events, `setQueryData()` for granular issue/inbox updates.
|
|
|
|
### Backend Structure (`server/`)
|
|
|
|
- **Entry points** (`cmd/`): `server` (HTTP API), `multica` (CLI — daemon, agent management, config), `migrate`
|
|
- **Handlers** (`internal/handler/`): One file per domain (issue, comment, agent, auth, daemon, etc.). Each handler holds `Queries`, `DB`, `Hub`, and `TaskService`.
|
|
- **Real-time** (`internal/realtime/`): Hub manages WebSocket clients. Server broadcasts events; inbound WS message routing is still TODO.
|
|
- **Auth** (`internal/auth/` + `internal/middleware/`): JWT (HS256). Middleware sets `X-User-ID` and `X-User-Email` headers. Login creates user on-the-fly if not found.
|
|
- **Task lifecycle** (`internal/service/task.go`): Orchestrates agent work — enqueue → claim → start → complete/fail. Syncs issue status automatically and broadcasts WS events at each transition.
|
|
- **Agent SDK** (`pkg/agent/`): Unified `Backend` interface for executing prompts via Claude Code or Codex. Each backend spawns its CLI and streams results via `Session.Messages` + `Session.Result` channels.
|
|
- **Daemon** (`internal/daemon/`): Local agent runtime — auto-detects available CLIs (claude, codex), registers runtimes, polls for tasks, routes by provider.
|
|
- **CLI** (`internal/cli/`): Shared helpers for the `multica` CLI — API client, config management, output formatting.
|
|
- **Events** (`internal/events/`): Internal event bus for decoupled communication between handlers and services.
|
|
- **Logging** (`internal/logger/`): Structured logging via slog. `LOG_LEVEL` env var controls level (debug, info, warn, error).
|
|
- **Database**: PostgreSQL with pgvector extension (`pgvector/pgvector:pg17`). sqlc generates Go code from SQL in `pkg/db/queries/` → `pkg/db/generated/`. Migrations in `migrations/`.
|
|
- **Routes** (`cmd/server/router.go`): Public routes (auth, health, ws) + protected routes (require JWT) + daemon routes (unauthenticated, separate auth model).
|
|
|
|
### Multi-tenancy
|
|
|
|
All queries filter by `workspace_id`. Membership checks gate access. `X-Workspace-ID` header routes requests to the correct workspace.
|
|
|
|
### Agent Assignees
|
|
|
|
Assignees are polymorphic — can be a member or an agent. `assignee_type` + `assignee_id` on issues. Agents render with distinct styling (purple background, robot icon).
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# One-click setup & run
|
|
make setup # First-time: ensure shared DB, create app DB, migrate
|
|
make start # Start backend + frontend together
|
|
make stop # Stop app processes for the current checkout
|
|
make db-down # Stop the shared PostgreSQL container
|
|
|
|
# Frontend
|
|
pnpm install
|
|
pnpm dev:web # Next.js dev server (port 3000)
|
|
pnpm build # Build frontend
|
|
pnpm typecheck # TypeScript check
|
|
pnpm lint # ESLint via Next.js
|
|
pnpm test # TS tests (Vitest)
|
|
|
|
# Backend (Go)
|
|
make dev # Run Go server (port 8080)
|
|
make daemon # Run local daemon
|
|
make build # Build server + CLI binaries to server/bin/
|
|
make cli ARGS="..." # Run multica CLI (e.g. make cli ARGS="config")
|
|
make test # Go tests
|
|
make sqlc # Regenerate sqlc code after editing SQL in server/pkg/db/queries/
|
|
make migrate-up # Run database migrations
|
|
make migrate-down # Rollback migrations
|
|
|
|
# Run a single Go test
|
|
cd server && go test ./internal/handler/ -run TestName
|
|
|
|
# Run a single TS test
|
|
pnpm --filter @multica/web exec vitest run src/path/to/file.test.ts
|
|
|
|
# Run a single E2E test (requires backend + frontend running)
|
|
pnpm exec playwright test e2e/tests/specific-test.spec.ts
|
|
|
|
# Infrastructure
|
|
make db-up # Start shared PostgreSQL (pgvector/pg17 image)
|
|
make db-down # Stop shared PostgreSQL
|
|
```
|
|
|
|
### CI Requirements
|
|
|
|
CI runs on Node 22 and Go 1.26.1 with a `pgvector/pgvector:pg17` PostgreSQL service. See `.github/workflows/ci.yml`.
|
|
|
|
### Worktree Support
|
|
|
|
All checkouts share one PostgreSQL container. Isolation is at the database level — each worktree gets its own DB name and unique ports via `.env.worktree`. Main checkouts use `.env`.
|
|
|
|
```bash
|
|
make worktree-env # Generate .env.worktree with unique DB/ports
|
|
make setup-worktree # Setup using .env.worktree
|
|
make start-worktree # Start using .env.worktree
|
|
```
|
|
|
|
## Coding Rules
|
|
|
|
- TypeScript strict mode is enabled; keep types explicit.
|
|
- Go code follows standard Go conventions (gofmt, go vet).
|
|
- Keep comments in code **English only**.
|
|
- Prefer existing patterns/components over introducing parallel abstractions.
|
|
- Unless the user explicitly asks for backwards compatibility, do **not** add compatibility layers, fallback paths, dual-write logic, legacy adapters, or temporary shims.
|
|
- If a flow or API is being replaced and the product is not yet live, prefer removing the old path instead of preserving both old and new behavior.
|
|
- Treat compatibility code as a maintenance cost, not a default safety mechanism. Avoid "just in case" branches that make the codebase harder to reason about.
|
|
- Avoid broad refactors unless required by the task.
|
|
|
|
## UI/UX Rules
|
|
|
|
- Prefer shadcn components over custom implementations. Install missing components via `npx shadcn add`.
|
|
- **Feature-specific components** → `features/<domain>/components/` — issue icons, pickers, and other domain-bound UI live inside their feature module.
|
|
- Use shadcn design tokens for styling (e.g. `bg-primary`, `text-muted-foreground`, `text-destructive`). Avoid hardcoded color values (e.g. `text-red-500`, `bg-gray-100`).
|
|
- Do not introduce extra state (useState, context, reducers) unless explicitly required by the design. Server data goes through TanStack Query (`core/`), client-only shared state through Zustand, React Context only for connection lifecycle.
|
|
- Pay close attention to **overflow** (truncate long text, scrollable containers), **alignment**, and **spacing** consistency.
|
|
- When unsure about interaction or state design, ask — the user will provide direction.
|
|
|
|
## Testing Rules
|
|
|
|
- **TypeScript**: Vitest. Mock external/third-party dependencies only.
|
|
- **Go**: Standard `go test`. Tests should create their own fixture data in a test database.
|
|
|
|
## Commit Rules
|
|
|
|
- Use atomic commits grouped by logical intent.
|
|
- Conventional format:
|
|
- `feat(scope): ...`
|
|
- `fix(scope): ...`
|
|
- `refactor(scope): ...`
|
|
- `docs: ...`
|
|
- `test(scope): ...`
|
|
- `chore(scope): ...`
|
|
|
|
## CLI Release
|
|
|
|
**Prerequisite:** A CLI release must accompany every Production deployment. When deploying to Production, always release a new CLI version as part of the process.
|
|
|
|
1. Create a tag on the `main` branch: `git tag v0.x.x`
|
|
2. Push the tag: `git push origin v0.x.x`
|
|
3. GitHub Actions automatically triggers `release.yml`: runs Go tests → GoReleaser builds multi-platform binaries → publishes to GitHub Releases + Homebrew tap
|
|
|
|
By default, bump the patch version each release (e.g. `v0.1.12` → `v0.1.13`), unless the user specifies a specific version.
|
|
|
|
## Minimum Pre-Push Checks
|
|
|
|
```bash
|
|
make check # Runs all checks: typecheck, unit tests, Go tests, E2E
|
|
```
|
|
|
|
Run verification only when the user explicitly asks for it.
|
|
|
|
For targeted checks when requested:
|
|
```bash
|
|
pnpm typecheck # TypeScript type errors only
|
|
pnpm test # TS unit tests only (Vitest)
|
|
make test # Go tests only
|
|
pnpm exec playwright test # E2E only (requires backend + frontend running)
|
|
```
|
|
|
|
## AI Agent Verification Loop
|
|
|
|
After writing or modifying code, always run the full verification pipeline:
|
|
|
|
```bash
|
|
make check
|
|
```
|
|
|
|
This runs all checks in sequence:
|
|
1. TypeScript typecheck (`pnpm typecheck`)
|
|
2. TypeScript unit tests (`pnpm test`)
|
|
3. Go tests (`go test ./...`)
|
|
4. E2E tests (auto-starts backend + frontend if needed, runs Playwright)
|
|
|
|
**Workflow:**
|
|
- Write code to satisfy the requirement
|
|
- Run `make check`
|
|
- If any step fails, read the error output, fix the code, and re-run `make check`
|
|
- Repeat until all checks pass
|
|
- Only then consider the task complete
|
|
|
|
**Quick iteration:** If you know only TypeScript or Go is affected, run individual checks first for faster feedback, then finish with a full `make check` before marking work complete.
|
|
|
|
## E2E Test Patterns
|
|
|
|
E2E tests should be self-contained. Use the `TestApiClient` fixture for data setup/teardown:
|
|
|
|
```typescript
|
|
import { loginAsDefault, createTestApi } from "./helpers";
|
|
import type { TestApiClient } from "./fixtures";
|
|
|
|
let api: TestApiClient;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
api = await createTestApi(); // logged-in API client
|
|
await loginAsDefault(page); // browser session
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await api.cleanup(); // delete any data created during the test
|
|
});
|
|
|
|
test("example", async ({ page }) => {
|
|
const issue = await api.createIssue("Test Issue"); // create via API
|
|
await page.goto(`/issues/${issue.id}`); // test via UI
|
|
// api.cleanup() in afterEach removes the issue
|
|
});
|
|
```
|