Operating manual for AI coding agents (Cursor, Codex, Copilot, etc.) working in this repo. Humans should read it too. Keep it short and accurate — if you change conventions in code, update this file in the same change.
> Product: **Echodo** — a multitenant task app whose backlog is mirrored as markdown under `plans/` and synced to Cursor.
> Repo name on disk is `tasks`. Package scope is `@tasks/*`.
Before opening a PR or finishing a task, run **`pnpm lint && pnpm type-check && pnpm test`** at minimum. Run `pnpm build` if you touched build config, server entry points, or cross-package exports.
Tests live next to source as `*.test.ts` files. Vitest is configured per package (see `vitest.config.ts` in `packages/shared`, `packages/database`, `packages/ai`). When you add a new package that has logic worth verifying, copy one of those configs and add `test` / `test:watch` scripts to the package's `package.json`. CI gates merges on lint + type-check + test — see [.github/workflows/ci.yml](./.github/workflows/ci.yml).
- Local dev reads `.env` at the repo root. `.env.example` documents every variable; keep it in sync when you add a new one.
-`.env`, `*.secrets`, `*-credentials.*`, `id_rsa`, `*.key`, and `AGENT-DEPLOY.md` are gitignored. **Never** read `AGENT-DEPLOY.md` contents into a committed file, log, or PR description — it contains homelab IPs and credentials.
- Production deploys run on Coolify (CT 107) against shared Postgres/Redis on CT 102. Don't change deploy assumptions (Docker compose files, `next.config.docker.ts`) without flagging it explicitly.
-`NEXT_PUBLIC_*` is the only browser-exposed prefix. Never put secrets behind it.
---
## 4. Code conventions
### TypeScript
-`strict: true` is on. Don't add `// @ts-ignore` or `any` to silence errors — fix the type. If you truly need to escape, use `// @ts-expect-error <reason>`.
- ESM only (`"type": "module"` in packages). Use named exports.
- Validate every external input (HTTP body, env var, markdown frontmatter, MCP tool args) with **zod**. Shared schemas live in `packages/shared`.
- Prefer `import type { … }` for type-only imports.
### Web app (`apps/web`)
- Next.js **App Router**. Server Components by default; add `"use client"` only when you need state, refs, or browser APIs.
- Data flow is **tRPC** (`apps/web/server/`) called via `@trpc/react-query` in client components and direct procedure calls in server components / actions.
- Auth is **NextAuth v5** (`apps/web/lib/auth.ts`). Always check session in protected procedures and route handlers.
- Editor stack is **TipTap 3 + Yjs + Hocuspocus**. Document state is collaborative — don't mutate doc JSON directly when a Y.Doc is available.
- Routing groups: `(app)` is authenticated app, `(auth)` is sign-in/up. Workspace pages live under `app/(app)/[workspaceSlug]/`.
### Database (`packages/database`)
- Schema lives in `src/schema/*.ts` and is re-exported from `src/schema/index.ts`. Drizzle config points there.
- **Multitenancy is enforced by `workspace_id`** on every tenant-scoped table. Any new table that holds user data must include `workspace_id` (UUID, not null) and an index on it. Every query must filter by `workspace_id`.
- Generate migrations with `pnpm db:generate` after schema changes. Commit the generated SQL in `packages/database/migrations/`. Don't hand-edit existing migrations — write a new one.
-`db:push` is for local prototyping only. Production uses `db:migrate`.
### Collab server (`apps/collab-server`)
- Persistence is via Hocuspocus database extension into Postgres; presence/awareness via Redis. Keep auth checks in `src/index.ts` aligned with the web app's session model.
### MCP server (`apps/mcp-server`)
- Tools live in `src/tools/`, resources in `src/resources/`. Every tool must have a zod schema for arguments and must respect tenant scoping (`workspace_id`).
### Shared (`packages/shared`)
- Cross-app types and zod schemas only. No React, no Node-specific APIs, no DB imports.
### AI (`packages/ai`)
- Use the **Vercel AI SDK** (`ai`, `@ai-sdk/openai`, `@ai-sdk/anthropic`). Provider selection is driven by env (`OPENAI_BASE_URL` can route through local Ollama on CT 108).
- Keep prompts in `src/prompts/`; no inline prompt strings scattered in components.
### Style
- Prettier formats everything. Don't fight it.
- Comments explain **why**, not **what**. Don't add narration comments like `// import the module` or `// loop over items`. Don't leave breadcrumbs describing the change you just made.
- Prefer small, named functions over deeply nested callbacks.
- File names: kebab-case for routes/components, camelCase for hooks/utilities matching the symbol they export.
---
## 5. Workflow
1.**Read before writing.** Skim `docs/Glossary.md` and (when relevant) `config/CursorSync.md` and the touched package's `src/index.ts` before editing.
2. Make a plan if the task touches >2 files or crosses package boundaries. Update or add a `Task-*.md` under the right `plans/Plan-*/Epic-*/` if the work is non-trivial product change.
3. Implement, then run `pnpm lint && pnpm type-check`. Fix every error you introduced.
4. Don't commit unless the user asks. When you do commit, write a short, focused message — *why*, not a diff narration.
5. Don't push, force-push, or amend without explicit instruction.
---
## 6. The markdown backlog (`plans/`)
This tree is **the source of truth for product work**. The importer in `packages/database/src/markdown-backlog/` parses every file under `plans/` into `markdown_backlog_items`, and `cursor_sync_mappings` stores the Cursor-side ids.
Layout is strict (the parser depends on it):
```
plans/
Plan-<slug>/
Plan-<slug>.md
Epic-<slug>/
Epic-<slug>.md
Task-<slug>.md
```
Rules:
- Slugs are lowercase-hyphenated and stable. Renaming the display title is fine; renaming a slug breaks sync — only do it deliberately.
- Every Plan/Epic/Task file must start with the YAML frontmatter from `docs/templates/`. Required fields: `kind`, `slug`, `title`, `status`, `priority`, `tenant_id`, `updated_at`. Plan/Epic/Task add their own (`plan_slug`, `epic_slug`, `cursor_*_id`).