# AGENTS.md 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/*`. --- ## 1. Repo layout ``` apps/ web/ Next.js 15 (App Router, React 19) — tRPC, NextAuth, Drizzle, TipTap, Tailwind collab-server/ Hocuspocus (Yjs) realtime server, Redis-backed mcp-server/ Model Context Protocol server exposing tasks/objects to agents packages/ database/ Drizzle ORM schema, client, migrations, markdown-backlog importer shared/ Shared TS types and utils (zod schemas live here) ai/ AI SDK wrappers, prompts, provider config (OpenAI / Anthropic / Ollama) config/ CursorSync.md Contract for app ↔ Cursor sync (read before touching sync code) docs/ Glossary.md Vocabulary for plans/epics/tasks/tenants/sync templates/ Plan / Epic / Task markdown templates plans/ The markdown backlog itself (see §6) docker/ Local + Coolify compose files, Dockerfile, prod next.config ``` Workspace tooling: **pnpm 9** + **Turborepo** + **TypeScript 5.7**. Node `>=20`. --- ## 2. Commands Run from the repo root unless noted. Always use `pnpm`, never `npm` or `yarn`. | Task | Command | |------|---------| | Install | `pnpm install` | | Dev (all apps) | `pnpm dev` | | Dev one app | `pnpm --filter @tasks/web dev` (or `@tasks/collab-server`, `@tasks/mcp-server`) | | Build | `pnpm build` | | Lint | `pnpm lint` | | Type-check | `pnpm type-check` | | Format | `pnpm format` (Prettier) | | Drizzle: generate migration | `pnpm db:generate` | | Drizzle: apply migrations | `pnpm db:migrate` | | Drizzle: push (dev only) | `pnpm db:push` | | Drizzle Studio | `pnpm --filter @tasks/database db:studio` | | Import `plans/` into DB | `pnpm --filter @tasks/database watch:markdown-backlog` (needs `MARKDOWN_BACKLOG_WORKSPACE_ID`) | | Test | `pnpm test` (turbo runs vitest in every package that has a `test` script) | | Test one package | `pnpm --filter @tasks/ test` | | Test in watch mode | `pnpm --filter @tasks/ test:watch` | 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). --- ## 3. Environment & secrets - 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 `. - 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. - UI: **Tailwind + Radix primitives + `cva` + `clsx` + `tailwind-merge`**. Reuse components from `components/ui/`; don't reinvent buttons, dialogs, dropdowns. - 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-/ Plan-.md Epic-/ Epic-.md Task-.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`). - `status` ∈ `draft | ready | in_progress | blocked | done | cancelled`. `priority` ∈ `P0 | P1 | P2 | P3`. - One Task per file. Keep tasks **bead-scale**: a single Cursor agent session can complete or materially advance the work. - Don't reach across tenants — `tenant_id` scopes every record and every Cursor mapping. When in doubt, copy from `docs/templates/` and look at `plans/Plan-multitenant-cursor-sync/` for a worked example. --- ## 7. Things to avoid - Adding a new package manager, monorepo tool, or framework without a discussion. - Bypassing tRPC by writing raw `fetch` calls to internal API routes. - Querying the DB without a `workspace_id` filter on tenant-scoped tables. - Inlining secrets, IPs, or hostnames from `AGENT-DEPLOY.md` into source or docs. - Editing existing migrations in `packages/database/migrations/`. - Adding files outside the directories listed in §1 without a reason. - Generating large binary blobs, hashes, or lockfile diffs by hand.