mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
* refactor(server): make ParseUUID error-returning to prevent silent data loss (MUL-1410) util.ParseUUID previously swallowed errors and returned a zero pgtype.UUID on invalid input. When this zero UUID reached a write query (DELETE/UPDATE), the SQL matched zero rows and the handler returned 2xx success — producing silent data corruption. #1661 (DeleteIssue with identifier-style ID) was the visible symptom; PR #1680 patched that one site, this commit closes the class of bug. Changes: - util.ParseUUID now returns (pgtype.UUID, error). Add util.MustParseUUID for trusted round-trips that should panic on invalid input. - handler/handler.go: parseUUID wrapper now calls MustParseUUID — any unguarded user-input string reaching it surfaces as a recovered panic (chi middleware.Recoverer → 500) instead of silently corrupting data. Add parseUUIDOrBadRequest(w, s, fieldName) for handler entry points. - Convert every Queries.Delete*/Update* call site reachable from raw user input (autopilot, comment, project, skill, skill_file, label, pin, attachment, feedback, issue assignee, daemon runtime, workspace) to validate UUIDs explicitly with parseUUIDOrBadRequest, returning 400 on invalid input. Where a resolved entity.ID is already in scope, write queries now use it directly instead of re-parsing the URL string. - Update getWorkspaceMember + loadIssueForUser to handle invalid UUIDs gracefully (404/400 instead of panic). - Update util/middleware/cmd-level callers (subscriber_listeners, notification_listeners, activity_listeners, scope_authorizer, middleware/workspace) to use the error-returning API. - Add server/internal/util/pgx_test.go covering valid/invalid input and the MustParseUUID panic contract. - Add TestDeleteIssueByIdentifier + TestDeleteIssueRejectsInvalidUUID regression tests in handler_test.go (the original #1661 bug + the invalid-input case). - Document the handler UUID parsing convention in CLAUDE.md so the rule is enforceable in future PR review. * fix(server): address GPT-Boy review of #1748 P1 fixes from PR #1748 review: 1. Migrate remaining request-boundary UUIDs to parseUUIDOrBadRequest so malformed input returns 400 instead of panic/500. Was missing on: - issue.go: workspace_id in CreateIssue/ChildIssueProgress/ListIssues/ SearchIssues/BatchUpdateIssues/BatchDeleteIssues; project_id / parent_issue_id / lead_id / assignee_id / assignee_ids / creator_id filters; batch issue_ids and assignee/parent/project fields in BatchUpdateIssues (skip on bad input via util.ParseUUID, matching the existing per-row continue semantics). - project.go: project id + workspace_id in GetProject/UpdateProject/ DeleteProject; lead_id in CreateProject/UpdateProject; workspace_id in ListProjects + SearchProjects. - handler.go: resolveActor now uses util.ParseUUID for X-Agent-ID / X-Task-ID headers; invalid UUID falls back to "member" (matches pre-existing semantics) instead of panicking. - issue.go: validateAssigneePair returns 400 on invalid workspace_id instead of panicking. 2. Fix issue:deleted WS event payloads to emit uuidToString(issue.ID) instead of the raw URL string. After an identifier-path delete ("MUL-7"), the previous payload would have leaked the identifier to subscribers, leaving stale entries in frontend caches that key by UUID. Updated DeleteIssue (issue.go:1341) and BatchDeleteIssues (issue.go:1641). The slog "issue deleted" log line also now records the resolved UUID so logs match the WS payload. 3. Extend TestDeleteIssueByIdentifier to subscribe to the bus and assert issue:deleted.payload.issue_id is the resolved UUID, not the identifier. * fix(server): validate remaining reviewed UUID inputs * fix(server): validate remaining handler UUID inputs * fix(server): finish request boundary UUID audit * fix(server): validate remaining request body UUIDs * fix(server): validate runtime path UUIDs * fix(server): validate remaining audit UUID inputs --------- Co-authored-by: Eve <eve@multica.ai>
359 lines
21 KiB
Markdown
359 lines
21 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 + monorepo frontend (pnpm workspaces + Turborepo) with shared packages.**
|
|
|
|
- `server/` — Go backend (Chi router, sqlc for DB, gorilla/websocket for real-time)
|
|
- `apps/web/` — Next.js frontend (App Router)
|
|
- `apps/desktop/` — Electron desktop app (electron-vite)
|
|
- `packages/core/` — Headless business logic (zero react-dom, all-platform reuse)
|
|
- `packages/ui/` — Atomic UI components (zero business logic)
|
|
- `packages/views/` — Shared business pages/components (zero next/* imports, zero react-router imports)
|
|
- `packages/tsconfig/` — Shared TypeScript configuration
|
|
|
|
### Key Architectural Decisions
|
|
|
|
**Internal Packages pattern** — all shared packages export raw `.ts`/`.tsx` files (no pre-compilation). The consuming app's bundler compiles them directly. This gives zero-config HMR and instant go-to-definition.
|
|
|
|
**Dependency direction:** `views/ → core/ + ui/`. Core and UI are independent of each other. No package imports from `next/*`, `react-router-dom`, or app-specific code.
|
|
|
|
**Platform bridge:** `packages/core/platform/` provides `CoreProvider` — initializes API client, auth/workspace stores, WS connection, and QueryClient. Each app wraps its root with `<CoreProvider>` and provides its own `NavigationAdapter` for routing.
|
|
|
|
**pnpm catalog** — `pnpm-workspace.yaml` defines `catalog:` for version pinning. All shared deps use `catalog:` references to guarantee a single version across all packages. When adding new shared deps (including test deps), add to catalog first.
|
|
|
|
### State Management
|
|
|
|
The architecture relies on a strict split between server state and client state. Mixing them is the most common way to break it.
|
|
|
|
- **TanStack Query owns all server state.** Issues, users, workspaces, inbox — anything fetched from the API lives in the Query cache. WS events keep it fresh via invalidation; no polling, no `staleTime` workarounds.
|
|
- **Zustand owns all client state.** UI selections, filters, drafts, modal state, navigation history. Stores live in `packages/core/` (never in `packages/views/`) so both apps share them.
|
|
- **React Context** is reserved for cross-cutting platform plumbing — `WorkspaceIdProvider`, `NavigationProvider`. Don't reach for it for general state.
|
|
- **Auth and workspace stores are the only stores allowed to call `api.*` directly**, because they manage critical state that must exist before queries can run. They're created via factory + injected dependencies, registered by the platform layer.
|
|
|
|
**Hard rules — these are how the architecture stays coherent:**
|
|
|
|
- **Never duplicate server data into Zustand.** If it came from the API, it belongs in the Query cache. Copying it into a store creates two sources of truth and they will drift.
|
|
- **Workspace-scoped queries must key on `wsId`.** This is what makes workspace switching automatic — the cache key changes, the right data appears, no manual invalidation needed.
|
|
- **Mutations are optimistic by default.** Apply the change locally, send the request, roll back on failure, invalidate on settle. The user shouldn't wait for the server.
|
|
- **WS events invalidate queries — they never write to stores directly.** This keeps the cache as the single source of truth and avoids race conditions.
|
|
- **Persist what's worth preserving across restarts** (user preferences, drafts, tab layout). **Don't persist ephemeral UI state** (modal open/close, transient selections) or server data.
|
|
|
|
**Common Zustand footguns to avoid:**
|
|
|
|
- Selectors must return stable references. Returning a freshly built object or array on every call (e.g. `s => ({ a: s.a, b: s.b })` or `s => s.items.map(...)`) triggers infinite re-renders. Either select primitives separately or use shallow comparison.
|
|
- Hooks that need workspace context should accept `wsId` as a parameter, not call `useWorkspaceId()` internally — this lets them work outside the `WorkspaceIdProvider` (e.g. in a sidebar that renders before workspace is loaded).
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# One-command dev (auto-setup + start everything)
|
|
make dev # Auto-creates env, installs deps, starts DB, migrates, launches app
|
|
|
|
# Explicit setup & run (if you prefer separate steps)
|
|
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 (all commands go through Turborepo)
|
|
pnpm install
|
|
pnpm dev:web # Next.js dev server (port 3000)
|
|
pnpm dev:desktop # Electron dev (electron-vite, HMR)
|
|
pnpm build # Build all frontend apps
|
|
pnpm typecheck # TypeScript check (all packages + apps via turbo)
|
|
pnpm lint # ESLint
|
|
pnpm test # TS tests (Vitest, all packages + apps via turbo)
|
|
|
|
# Backend (Go)
|
|
make server # Run Go server only (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 TS test (works for any package with a test script)
|
|
pnpm --filter @multica/views exec vitest run auth/login-page.test.tsx
|
|
pnpm --filter @multica/core exec vitest run runtimes/version.test.ts
|
|
pnpm --filter @multica/web exec vitest run app/\(auth\)/login/page.test.tsx
|
|
|
|
# Run a single Go test
|
|
cd server && go test ./internal/handler/ -run TestName
|
|
|
|
# Run a single E2E test (requires backend + frontend running)
|
|
pnpm exec playwright test e2e/tests/specific-test.spec.ts
|
|
|
|
# Desktop build & package
|
|
pnpm --filter @multica/desktop build # Compile TS → JS (reads .env.production)
|
|
pnpm --filter @multica/desktop package # Package into .app/.dmg/.exe (current platform only)
|
|
|
|
# shadcn — config lives in packages/ui/components.json (Base UI variant, base-nova style)
|
|
pnpm ui:add badge # Adds component to packages/ui/components/ui/
|
|
|
|
# Infrastructure
|
|
make db-up # Start shared PostgreSQL (pgvector/pg17 image)
|
|
make db-down # Stop shared PostgreSQL
|
|
make db-reset # Drop + recreate current env's DB, then re-run migrations (local only; stop backend first)
|
|
```
|
|
|
|
### 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`.
|
|
|
|
`make dev` auto-detects worktrees and handles everything. For explicit control:
|
|
|
|
```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.
|
|
- Avoid broad refactors unless required by the task.
|
|
- New global (pre-workspace) routes MUST use a single word (`/login`, `/inbox`) or a `/{noun}/{verb}` pair (`/workspaces/new`). NEVER add hyphenated word-group root routes (`/new-workspace`, `/create-team`) — they collide with common user workspace names and force endless reserved-slug audits. Reserving the noun (`workspaces`) automatically protects the entire `/workspaces/*` subtree.
|
|
|
|
### Backend Handler UUID Parsing Convention
|
|
|
|
Every Go handler in `server/internal/handler/` follows these rules. The convention exists because `util.ParseUUID` used to silently return a zero UUID on invalid input, which caused #1661 — a `DELETE` returning 204 success while the SQL `DELETE` matched zero rows.
|
|
|
|
- **Resource path params that accept either a UUID or a human-readable identifier** (e.g. `chi.URLParam(r, "id")` for an issue, which accepts both `MUL-123` and a UUID) MUST be resolved through the dedicated loader (`loadIssueForUser` / `loadSkillForUser` / `loadAgentForUser` / `requireDaemonRuntimeAccess`). After resolution, all subsequent DB calls — especially `Queries.Delete*` / `Queries.Update*` — MUST use `entity.ID` from the resolved object. Never round-trip the raw URL string through `parseUUID` for a write query.
|
|
- **Pure-UUID inputs from request boundaries** (URL params that are always UUIDs, request body fields, query params, headers) MUST be validated with `parseUUIDOrBadRequest(w, s, fieldName)`. On invalid input it writes a 400 and returns `ok=false` — return immediately.
|
|
- **Trusted UUID round-trips** (sqlc-returned UUIDs being passed back into queries, test fixtures) use `parseUUID(s)` which calls `util.MustParseUUID` and panics on invalid input. A panic here means an unguarded user-input string slipped in — that is a real bug. `chi`'s `middleware.Recoverer` translates the panic into a 500 so the process keeps running.
|
|
- **`util.ParseUUID(s) (pgtype.UUID, error)`** is the only safe variant outside the handler package. Always check the error.
|
|
|
|
When adding a `Queries.Delete*` or `Queries.Update*` call, ask: "Where did this UUID come from?" If the answer is "raw user input that hasn't been validated," route it through `parseUUIDOrBadRequest` or a loader first.
|
|
|
|
### Package Boundary Rules
|
|
|
|
These are hard constraints. Violating them breaks the cross-platform architecture:
|
|
|
|
- `packages/core/` — zero react-dom, zero localStorage (use StorageAdapter), zero process.env, zero UI libraries. **All shared Zustand stores live here**, even view-related ones (filters, view modes) — stores are pure state, not UI.
|
|
- `packages/ui/` — zero `@multica/core` imports (pure UI, no business logic).
|
|
- `packages/views/` — zero `next/*` imports, zero `react-router-dom` imports, zero stores. Use `NavigationAdapter` for all routing.
|
|
- `apps/web/platform/` — the only place for Next.js APIs (`next/navigation`).
|
|
- `apps/desktop/src/renderer/src/platform/` — the only place for react-router-dom navigation wiring.
|
|
|
|
### The No-Duplication Rule
|
|
|
|
**If the same logic exists in both apps, it must be extracted to a shared package.**
|
|
|
|
This applies to everything: components, hooks, guards, providers, utility functions. The decision process:
|
|
|
|
1. Does this code depend on Next.js or Electron APIs? → Keep in the respective app.
|
|
2. Does it depend on `react-router-dom` or `next/navigation`? → Keep in app's `platform/` layer.
|
|
3. Everything else → belongs in `packages/core/` (headless logic) or `packages/views/` (UI components).
|
|
|
|
When the two apps need different behavior for the same concept (e.g., different loading UI), extract the shared logic into a component with props/slots for the differences. Don't duplicate the logic.
|
|
|
|
### Cross-Platform Development Rules
|
|
|
|
When adding a new page or feature:
|
|
|
|
1. **New page component** → add to `packages/views/<domain>/`. Never import from `next/*` or `react-router-dom`.
|
|
2. **Wire it in both apps** → add a route in `apps/web/app/` (Next.js page file) AND in the desktop router. **Exception**: pre-workspace transition flows (create workspace, accept invite) are NOT routes on desktop — they're `WindowOverlay` state. See *Desktop-specific Rules → Route categories*.
|
|
3. **Navigation** → use `useNavigation().push()` or `<AppLink>`. Never use framework-specific link/router APIs in shared code.
|
|
4. **Shared guards/providers** → use `DashboardGuard` from `packages/views/layout/`. Don't create separate guard logic per app.
|
|
5. **Platform-specific UI** → if a feature is web-only or desktop-only, keep it in the respective app. Use props slots (`extra`, `topSlot`) on shared layout components to inject platform-specific UI.
|
|
6. **New hooks that need workspace context** → accept `wsId` as parameter instead of reading from `useWorkspaceId()` Context, so they work both inside and outside `WorkspaceIdProvider`.
|
|
|
|
### CSS Architecture
|
|
|
|
Both apps share the same CSS foundation from `packages/ui/styles/`.
|
|
|
|
- **Design tokens** → use semantic tokens (`bg-background`, `text-muted-foreground`). Never use hardcoded Tailwind colors (`text-red-500`, `bg-gray-100`).
|
|
- **Shared styles** → `packages/ui/styles/`. Never duplicate scrollbar styling, keyframes, or base layer rules in app CSS.
|
|
- **`@source` directives** → both apps scan shared packages so Tailwind sees all class names.
|
|
|
|
## Desktop-specific Rules
|
|
|
|
These rules apply to `apps/desktop/` only. Web has different constraints (URL bar, SSR, no tabs) and doesn't share these concerns. Every rule in this section was added after a concrete bug — treat them as enforced, not suggestions.
|
|
|
|
### Route categories
|
|
|
|
Every path in the desktop app falls into exactly one category. Choosing the wrong one reproduces bugs we've already fixed.
|
|
|
|
- **Session routes** — workspace-scoped pages (`/:slug/issues`, `/:slug/settings`). Rendered by the per-tab memory router under `WorkspaceRouteLayout`. These are legitimate tab destinations.
|
|
- **Transition flows** — pre-workspace / one-shot actions (create workspace, accept invite). **NOT routes.** They live as `WindowOverlay` state, dispatched when the navigation adapter sees `push('/workspaces/new')` or `push('/invite/<id>')`. The shared view (`NewWorkspacePage`, `InvitePage`) is the content; the overlay wrapper supplies platform chrome.
|
|
- **Error / stale states** — "workspace not available", tabs pointing at a revoked workspace. **NOT pages.** `WorkspaceRouteLayout` auto-heals by dropping the stale tab group from the store; the user never lands on an explicit error screen. Web keeps `NoAccessPage` (shareable URL makes the error state meaningful); desktop has no URL bar so stale = heal silently.
|
|
|
|
**Adding a new pre-workspace flow on desktop**: register a new `WindowOverlay` type in `stores/window-overlay-store.ts`. Do NOT add it to `routes.tsx`. If a shared view needs the flow on both platforms, add the route on web (`apps/web/app/(auth)/...`) AND the overlay type on desktop — the shared view component is identical.
|
|
|
|
### Workspace context
|
|
|
|
`setCurrentWorkspace(slug, uuid)` from `@multica/core/platform` is the single source of truth for the active workspace. `WorkspaceRouteLayout` sets it on mount; unmount does NOT clear it. Code that leaves workspace context (leave/delete workspace, force-navigate to overlay) must call `setCurrentWorkspace(null, null)` explicitly.
|
|
|
|
### Workspace destructive operations
|
|
|
|
Leave / Delete workspace flows must follow this order, otherwise concurrent refetches race and the renderer hard-reloads:
|
|
|
|
1. Read destination from cached workspace list.
|
|
2. `setCurrentWorkspace(null, null)`.
|
|
3. `navigation.push(destination)`.
|
|
4. THEN `await mutation.mutateAsync(workspaceId)`.
|
|
|
|
### Tab isolation
|
|
|
|
Tabs are grouped per workspace in `stores/tab-store.ts`. The TabBar shows only the active workspace's tabs; cross-workspace tab leakage is impossible by construction (no flat global tabs array).
|
|
|
|
Cross-workspace `push(path)` is detected by the navigation adapter (`platform/navigation.tsx`) and translated into `switchWorkspace(slug, targetPath)` — NOT a navigation within the current tab's router. Don't bypass the adapter; always go through `useNavigation()` from shared code.
|
|
|
|
### Drag region (macOS)
|
|
|
|
Every full-window desktop view (anything outside the dashboard shell) must mount `<DragStrip />` from `@multica/views/platform` as the first flex child of the page root, otherwise users can't drag the window. Interactive UI inside the top 48px needs `WebkitAppRegion: "no-drag"` to stay clickable.
|
|
|
|
## UI/UX Rules
|
|
|
|
- Prefer shadcn components over custom implementations. Install via `pnpm ui:add <component>` from project root — adds to `packages/ui/components/ui/`. All components use Base UI primitives (`@base-ui/react`), not Radix.
|
|
- Use shadcn design tokens for styling. Avoid hardcoded color values.
|
|
- Do not introduce extra state (useState, context, reducers) unless explicitly required by the design.
|
|
- Pay close attention to **overflow** (truncate long text, scrollable containers), **alignment**, and **spacing** consistency.
|
|
- **If a component is identical between web and desktop, it belongs in a shared package.** Do not copy-paste between apps.
|
|
|
|
## Testing Rules
|
|
|
|
### Where to write tests
|
|
|
|
Tests follow the code, not the app. This is the most important testing principle in this monorepo:
|
|
|
|
| What you're testing | Where the test lives | Why |
|
|
|---|---|---|
|
|
| Shared business logic (stores, queries, hooks) | `packages/core/*.test.ts` | No DOM needed, pure logic |
|
|
| Shared UI components (pages, forms, modals) | `packages/views/*.test.tsx` | jsdom, no framework mocks |
|
|
| Platform-specific wiring (cookies, redirects, searchParams) | `apps/web/*.test.tsx` or `apps/desktop/` | Needs framework-specific mocks |
|
|
| End-to-end user flows | `e2e/*.spec.ts` | Real browser, real backend |
|
|
|
|
**Never test shared component behavior in an app's test file.** If a test requires mocking `next/navigation` or `react-router-dom` to test a component from `@multica/views`, the test is in the wrong place — move it to `packages/views/` and mock `@multica/core` instead.
|
|
|
|
### Test infrastructure
|
|
|
|
- `packages/core/` — Vitest, Node environment (no DOM)
|
|
- `packages/views/` — Vitest, jsdom environment, `@testing-library/react`
|
|
- `apps/web/` — Vitest, jsdom environment, framework-specific mocks
|
|
- `e2e/` — Playwright
|
|
- `server/` — Go standard `go test`
|
|
|
|
All test deps are in the pnpm catalog for unified versioning.
|
|
|
|
### Mocking conventions
|
|
|
|
- Mock `@multica/core` stores with `vi.hoisted()` + `Object.assign(selectorFn, { getState })` pattern (Zustand stores are both callable and have `.getState()`).
|
|
- Mock `@multica/core/api` for API calls.
|
|
- In `packages/views/` tests: never mock `next/*` or `react-router-dom` — those don't exist here.
|
|
- In `apps/web/` tests: mock framework-specific APIs only for platform-specific behavior.
|
|
|
|
### TDD workflow
|
|
|
|
1. Write failing test in the **correct package** first.
|
|
2. Write implementation.
|
|
3. Run `pnpm test` (Turborepo discovers all packages).
|
|
4. Green → done.
|
|
|
|
### Go tests
|
|
|
|
Standard `go test`. Tests should create their own fixture data in a test database.
|
|
|
|
### E2E tests
|
|
|
|
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();
|
|
await loginAsDefault(page);
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
await api.cleanup();
|
|
});
|
|
|
|
test("example", async ({ page }) => {
|
|
const issue = await api.createIssue("Test Issue");
|
|
await page.goto(`/issues/${issue.id}`);
|
|
});
|
|
```
|
|
|
|
## Commit Rules
|
|
|
|
- Use atomic commits grouped by logical intent.
|
|
- Conventional format: `feat(scope)`, `fix(scope)`, `refactor(scope)`, `docs`, `test(scope)`, `chore(scope)`.
|
|
|
|
## 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, all packages)
|
|
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
|
|
```
|
|
|
|
**Workflow:**
|
|
- Write code to satisfy the requirement
|
|
- Run `make check`
|
|
- If any step fails, read the error output, fix the code, and re-run
|
|
- 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.
|
|
|
|
## CLI Release
|
|
|
|
**Prerequisite:** A CLI release must accompany every Production deployment.
|
|
|
|
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.
|
|
|
|
## 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).
|