Multica is a task management platform where people and agents collaborate on issues. These instructions apply to all coding agents working in this repository.
- Before changing
apps/mobile/, also read apps/mobile/AGENTS.md, even if your tool does not load nested instructions automatically. Platform-specific sections below apply only to the named platform. - For naming, translations, or Chinese UI/docs copy, read conventions.mdx and conventions.zh.mdx.
- Maintain shared rules here and mobile-specific rules in the mobile file.
CLAUDE.mdfiles only import them. Update instructions in the same change that alters the referenced workflow or boundary; do not add incident timelines, dependency version lists, or duplicate rules.
| Location | Responsibility and constraints |
|---|---|
server/ |
Go backend; Chi, sqlc, WebSocket |
packages/core/ |
Headless logic, API client, Query hooks, shared Zustand stores. No UI libraries, react-dom, localStorage, or process.env; use StorageAdapter for persistence. |
packages/ui/ |
UI primitives and shared styles. No business logic or @multica/core imports. |
packages/views/ |
Shared web/desktop pages and business components. No store definitions, next/*, or react-router-dom; use NavigationAdapter, useNavigation(), and <AppLink>. |
apps/web/ |
Next.js routes/layouts and web-only UI. Framework APIs stay here; shared navigation adapters live in apps/web/platform/. |
apps/desktop/ |
Electron and desktop-only UI/state. Application navigation goes through apps/desktop/src/renderer/src/platform/. |
apps/mobile/ |
Independent Expo/React Native client: owns UI, state, hooks, providers, i18n, build, and release. Shares core types and pure utilities, including platform-independent schemas. |
apps/docs/ |
Fumadocs documentation site |
- Dependency direction is
views -> core + ui; core and ui remain independent. Shared packages export raw TypeScript compiled by consuming apps. - Extract logic used by both web and desktop into the appropriate shared package. Keep framework/Electron APIs in the app layer; inject platform-specific UI through props/slots.
- Wire shared features into both web routes and the desktop router or overlay. Reuse existing guards/providers such as
DashboardGuardinpackages/views/layout/. - Each workspace declares its directly imported external dependencies. Use
catalog:for shared dependencies; mobile pins Expo/React Native dependencies in its own manifest.
Use Makefile, workspace package.json files, and pnpm-workspace.yaml for current commands and versions. See CONTRIBUTING.md for setup and worktree operations.
- Use the checkout's managed environment:
make up,make status,make down.make downpreserves data;make destroyremoves the environment and its data. - Worktrees share PostgreSQL but have isolated databases/ports. Use the environment scripts and
.env.worktree; do not copy the main checkout's.envor manually create a database through an assumed PostgreSQL instance. - Regenerate sqlc with
make sqlcafter SQL changes. - Run the narrowest useful checks while iterating, then broaden when risk warrants it. Report what actually ran and any skipped checks.
Run these from the repository root:
| Scope | Checks |
|---|---|
| Frontend excluding mobile | pnpm typecheck, pnpm lint, pnpm test |
| Go backend | make test |
| End-to-end | pnpm exec playwright test |
| Combined web/backend verification | make check |
| Mobile | Commands in apps/mobile/AGENTS.md |
Root frontend commands and make check do not verify mobile. Docs-only changes can use link/reference checks and git diff --check; state that code tests were not run.
- TanStack Query owns API/server data. Zustand owns client state such as filters, drafts, modals, and tab layout; persist only durable preferences/drafts/layout, not server data or ephemeral UI state.
- Web/desktop shared stores live in
packages/core/. Desktop platform stores remain in desktop; mobile stores remain in mobile. Do not define stores inpackages/views/. - On web/desktop, workspace identity is route-driven; platform mirrors exist only for request headers, storage namespaces, and reconnects. React Context is for platform plumbing, not a second server-state store.
- Among stores, only auth/workspace stores may call
api.*directly; other server interactions belong in queries/mutations. - Workspace-scoped query keys include
wsId; account-level keys remain account-scoped. Hooks needing workspace context acceptwsIdunless guaranteed to run under its provider. - Zustand selectors return stable references; use shallow comparison for allocated objects/arrays.
- WebSocket events patch or invalidate Query caches, not server payloads in Zustand. Clearing client-owned pointers is allowed with one responder and a self-initiated guard when this client can cause the event.
- Optimistic field patches require a predictable result, rare failure, trivial rollback, and staying on the current screen. Snapshot before patching, roll back on failure, and invalidate uncertain projections on settle.
- Create/delete/leave and confirmation flows await the server before navigation or cleanup; do not optimistically delete entities. Exceptions: the existing workspace-leave race noted under Desktop Rules, and mobile inbox mark-read as documented in its instructions.
- Message sends use visible pending state and retry on failure.
Installed desktop clients may talk to newer backends. Preserve response compatibility at the API boundary.
- UI-consumed JSON passes through a zod schema and
parseWithFallback, not anas Tcast. Web/desktop usepackages/core/api/schema.ts; mobile uses its own request helpers. - Provide defaults for optional fields and fallbacks for unknown server enums. Prefer explicit boolean checks; avoid tying critical affordances to a single backend flag when other contract signals are available.
- When adding/changing an endpoint, update its schema and malformed-response tests.
- Do not add foreign keys, cascading deletes, or cascading updates. Validate relationships and clean up dependents in application code, using a transaction when the operation must be atomic.
- Every migration-created index, including indexes on new tables, uses
CREATE [UNIQUE] INDEX CONCURRENTLY. Each concurrent index build gets its own single-statement migration file; the runner executes files outside an explicit transaction. - Conditionally skipped migrations are still recorded in
schema_migrations. Later DDL touching conditional objects must be idempotent (IF EXISTS/IF NOT EXISTS); document recovery if the missing object would break runtime behavior.
In server/internal/handler/, distinguish UUID sources before using them in writes:
- UUID-or-human-readable resource params: resolve with loaders such as
loadIssueForUser,loadSkillForUser,loadAgentForUser, orrequireDaemonRuntimeAccess, then write using the resolvedentity.ID. - Pure UUID request input:
parseUUIDOrBadRequest(w, s, fieldName); return immediately whenok=false. - Trusted sqlc/test-fixture round-trips:
parseUUID(s), which panics on invalid input. - Outside handlers:
util.ParseUUID(s)and check the error.
Workspace-scoped queries filter by workspace_id; membership gates access and X-Workspace-ID selects the workspace. Assignees are polymorphic: interpret assignee_id together with assignee_type.
- Workspace session routes are tab destinations. Pre-workspace one-shot flows (create workspace, accept invite) use
WindowOverlayinapps/desktop/src/renderer/src/stores/window-overlay-store.ts, not new routes. Stale workspace tabs heal by dropping stale tab groups. - Workspace route layouts own
setCurrentWorkspace(slug, uuid)from@multica/core/platform; leaving workspace context callssetCurrentWorkspace(null, null). - Cross-workspace navigation uses the adapter's
switchWorkspace(slug, targetPath)flow; do not bypass it with direct router navigation. - Workspace delete awaits the server. Existing workspace leave clears/navigates first to avoid the
member:removedrace; this is known debt inpackages/views/settings/components/workspace-tab.tsx, not a pattern for new flows. - Full-window views outside the dashboard shell mount
<DragStrip />from@multica/views/platformas the first flex child. Interactive controls in the top 48px needWebkitAppRegion: "no-drag".
- Descriptions are optional and omitted by default. Do not restate titles, labels, values, statuses, or button actions. Add help only for a non-obvious choice, constraint, consequence, or next step; state each fact once beside the relevant control.
- Keep permissions, cost, destructive consequences, execution prerequisites, and error recovery visible when relevant. Put advanced usage and diagnostics in accessible, explicit help. Preserve labels and accessible names; do not move redundant prose wholesale into
sr-onlytext. - Review copy with its surrounding controls and all supported translations, including mobile's independent copy. Follow the UI copy rules in the existing conventions pages; a description prop is not a requirement to write a paragraph.
-
For Button and Dialog usage, read
packages/ui/docs/button.mdandpackages/ui/docs/dialog.md. These component contracts also power UI Lab documentation. -
Prefer existing shadcn/Base UI primitives. Add components with
pnpm ui:add <component>. -
For
pnpm ui:add @reui/<name>, decline overwrite prompts. KeepREUI_LICENSE_KEYin the environment, never in repo files. Adapt vendored primitives intopackages/ui/components/ui/and compositions intopackages/views/. -
Use shared semantic tokens in
packages/ui/styles/. Typography uses the role-named--text-*scale inpackages/ui/styles/tokens.css, not Tailwind's default size ramp. -
Selected states remain identifiable on hover. Handle overflow, long text, and scrolling deliberately; avoid unnecessary local state and dividers.
- Tests live beside their implementation: shared logic in core, shared components in views, platform wiring in apps, E2E in
e2e/, Go tests in server. Do not test shared behavior in app suites. - Give each behavior one canonical test layer: helper tests own parsing/state matrices; component tests cover wiring, accessibility, happy paths, and named regressions. Prefer a failing regression test before behavioral fixes.
- DOM-free
.test.tsfiles start with// @vitest-environment node; do not use it if it would silently switch the code under test to an SSR path. - Views tests must not mock
next/*orreact-router-dom. Mock stores with their Zustand callable shape plusgetState; mock API calls at@multica/core/api. - E2E setup/teardown uses
TestApiClient. - DB-backed Go tests use
server/internal/testutilfixtures (dbfx.Issue,dbfx.Task,dbfx.Insert) andtestutil.Call(h, req).Want(status).JSON(&out). Keep product assertions and case-specific diagnostics in the test, not fixture helpers. - Default tests must not resolve or execute user-installed agent CLIs; pass test-created fake or missing executable paths. New default agent commands go in
scripts/agent-cli-command-names.txt. - Only run real-agent smoke tests when explicitly authorized. Gate them behind
agentintegrationand checkMULTICA_RUN_REAL_AGENT_SMOKE=1before executable lookup/account access. Run the specific test:(cd server && MULTICA_RUN_REAL_AGENT_SMOKE=1 go test -tags=agentintegration ./pkg/agent -run '<test-name>' -count=1 -v).
- Keep changes scoped; reuse existing patterns. Code comments are English.
- Do not add internal compatibility shims, dual writes, fallback paths, or legacy adapters unless requested. This does not relax API response compatibility above.
- New global pre-workspace routes use a single word or
/{noun}/{verb}, not hyphenated root names. Updateserver/internal/handler/reserved_slugs.json, runpnpm generate:reserved-slugs, and commitpackages/core/paths/reserved-slugs.tswhen changing reserved slugs. - Use atomic conventional commits and the repository PR template. For releases, follow .github/RELEASING.md; default to a patch bump unless specified otherwise.