AGENTS.md was a minimal 17-line summary while CLAUDE.md had comprehensive project documentation. Updated AGENTS.md to include all sections from CLAUDE.md (architecture, state management, backend structure, UI/UX rules, worktree support, E2E patterns, verification loop) while preserving AGENTS.md-unique details (naming conventions, test directory, PR guidelines). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
Repository Guidelines
This file provides guidance to AI agents 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 dependenciese2e/— Playwright end-to-end testsscripts/and rootMakefile— local setup and verification
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/)
├── features/ # Business logic, organized by domain
├── shared/ # Cross-feature utilities (api client, types, logger)
├── test/ # Shared test utilities and setup
├── public/ # Static assets
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/.
features/ — Domain modules, each with its own components, hooks, stores, and config:
| Feature | Purpose | Exports |
|---|---|---|
features/auth/ |
Authentication state | useAuthStore, AuthInitializer |
features/workspace/ |
Workspace, members, agents | useWorkspaceStore, useActorName |
features/issues/ |
Issue state, components, config | useIssueStore, icons, pickers, status/priority config |
features/inbox/ |
Inbox notification state | useInboxStore |
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:
shared/api/—ApiClient(REST) andWSClient(WebSocket) for backend communication, plus theapisingleton.shared/types/— Domain types (Issue, Agent, Workspace, etc.) and WebSocket event types.shared/logger.ts— Logger utility.
State Management
- Zustand for global client state — one store per feature domain (
features/auth/store.ts,features/workspace/store.ts,features/issues/store.ts,features/inbox/store.ts). - React Context only for connection lifecycle (
WSProviderinfeatures/realtime/). - Local
useStatefor component-scoped UI state (forms, modals, filters). - Do not use React Context for data that can be a zustand store.
Store conventions:
- One store per feature domain. Import via
useAuthStore(selector)oruseWorkspaceStore(selector). - Stores must not call
useRouteror any React hooks — keep navigation in components. - Cross-store reads use
useOtherStore.getState()inside actions (not hooks). - Dependency direction:
workspace→auth,realtime→auth,issues→workspace. Never reverse.
Import Aliases
Use @/ alias (maps to apps/web/):
import { api } from "@/shared/api";
import type { Issue } from "@/shared/types";
import { useAuthStore } from "@/features/auth";
import { useWorkspaceStore } from "@/features/workspace";
import { useIssueStore } from "@/features/issues";
import { useInboxStore } from "@/features/inbox";
import { useWSEvent } from "@/features/realtime";
import { StatusIcon } from "@/features/issues/components";
Within a feature, use relative imports. Between features or to shared, use @/.
Data Flow
Browser → ApiClient (shared/api) → REST API (Chi handlers) → sqlc queries → PostgreSQL
Browser ← WSClient (shared/api) ← WebSocket ← Hub.Broadcast() ← Handlers/TaskService
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 holdsQueries,DB,Hub, andTaskService. - 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 setsX-User-IDandX-User-Emailheaders. 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/): UnifiedBackendinterface for executing prompts via Claude Code or Codex. Each backend spawns its CLI and streams results viaSession.Messages+Session.Resultchannels. - 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 themulticaCLI — 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_LEVELenv var controls level (debug, info, warn, error). - Database: PostgreSQL with pgvector extension (
pgvector/pgvector:pg17). sqlc generates Go code from SQL inpkg/db/queries/→pkg/db/generated/. Migrations inmigrations/. - 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
# 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.24 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.
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.
- TypeScript in
apps/webuses 2-space indentation, double quotes, and semicolons. - Prefer PascalCase for React components, camelCase for hooks and helpers, and colocated test files such as
page.test.tsx. - Go code follows standard Go conventions (gofmt, go vet). Use domain-oriented filenames like
issue.goorcmd_issue.go. - Do not hand-edit generated code in
server/pkg/db/generated/. - 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. Prefer zustand stores for shared state over React Context.
- 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 with Testing Library. Shared test setup lives in
apps/web/test/. Mock external/third-party dependencies only. - Go: Standard
go test. Tests should create their own fixture data in a test database. - End-to-end tests live in
e2e/*.spec.ts;make checkwill start missing services automatically, while direct Playwright runs expect the app to already be running. - Add or update tests whenever you change handlers, CLI commands, daemon behavior, or SQL-backed flows.
Commit & Pull Request Rules
- Use atomic commits grouped by logical intent.
- Conventional format with scopes:
feat(web): ...,feat(cli): ...fix(web): ...,fix(cli): ...refactor(daemon): ...test(cli): ...docs: ...chore(scope): ...
- Keep PRs focused and include a short description, linked issue or PR number when relevant, screenshots for UI work, and notes for migrations, env changes, or CLI surface changes.
- Before opening a PR, run
make checkor the relevant frontend/backend subset.
Minimum Pre-Push Checks
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:
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:
make check
This runs all checks in sequence:
- TypeScript typecheck (
pnpm typecheck) - TypeScript unit tests (
pnpm test) - Go tests (
go test ./...) - 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:
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
});