The Create Workspace button in the top-header dropdown was still calling
`window.alert("Create workspace (placeholder)")` instead of opening the
real CreateWorkspaceDialog. Wires it to the same dialog the sidebar
workspace switcher uses.
Also clarifies in .env.example and AGENTS.md that the canonical Postgres
database name is `echodo` everywhere (local, Coolify, drizzle ledger,
docker-compose POSTGRES_DB). A stale `tasks` database on CT 102 from the
pre-rename era was the root cause of the 2026-06 Authentik "Configuration"
SSO outage: Coolify correctly pointed at `echodo` (which was empty), while
the only initialized schema lived in the relic `tasks` DB.
Co-authored-by: Cursor <cursoragent@cursor.com>
10 KiB
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 istasksand the package scope is@tasks/*(historical, pre-rename). Everything else — the database, the deploy, the public domain — isechodo. If you see atasksdatabase anywhere, it's an artifact of the old name; the source of truth isechodo.
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/<name> test |
| Test in watch mode | pnpm --filter @tasks/<name> 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.
If you edit anything under apps/mcp-server/ or any library it imports, your changes won't be visible to MCP tool calls in the current Cursor chat — the MCP server is a long-lived stdio process per session and doesn't auto-reload. See the MCP server lifecycle section in config/CursorSync.md for how to find and kill stale servers.
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.
3. Environment & secrets
- Local dev reads
.envat the repo root..env.exampledocuments every variable; keep it in sync when you add a new one. .env,*.secrets,*-credentials.*,id_rsa,*.key, andAGENT-DEPLOY.mdare gitignored. Never readAGENT-DEPLOY.mdcontents 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. - The Postgres database name is
echodo, full stop — local dev, Coolify, the drizzle migration ledger, and the docker-composePOSTGRES_DBall need to agree. An oldertasksdatabase may still exist on CT 102 as a relic of the pre-rename era; treat it as a dev sandbox at best and never point Coolify at it. The 2026-06 "Authentik SSO returnsConfiguration" incident was caused by Coolify pointing atechodo(correct) while a staletasksdatabase had the only initialized schema. NEXT_PUBLIC_*is the only browser-exposed prefix. Never put secrets behind it.
4. Code conventions
TypeScript
strict: trueis on. Don't add// @ts-ignoreoranyto 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-queryin 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 fromcomponents/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 underapp/(app)/[workspaceSlug]/.
Database (packages/database)
- Schema lives in
src/schema/*.tsand is re-exported fromsrc/schema/index.ts. Drizzle config points there. - Multitenancy is enforced by
workspace_idon every tenant-scoped table. Any new table that holds user data must includeworkspace_id(UUID, not null) and an index on it. Every query must filter byworkspace_id. - Generate migrations with
pnpm db:generateafter schema changes. Commit the generated SQL inpackages/database/migrations/. Don't hand-edit existing migrations — write a new one. db:pushis for local prototyping only. Production usesdb:migrate.
Collab server (apps/collab-server)
- Persistence is via Hocuspocus database extension into Postgres; presence/awareness via Redis. Keep auth checks in
src/index.tsaligned with the web app's session model.
MCP server (apps/mcp-server)
- Tools live in
src/tools/, resources insrc/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_URLcan 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 moduleor// 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
- Read before writing. Skim
docs/Glossary.mdand (when relevant)config/CursorSync.mdand the touched package'ssrc/index.tsbefore editing. - Make a plan if the task touches >2 files or crosses package boundaries. Update or add a
Task-*.mdunder the rightplans/Plan-*/Epic-*/if the work is non-trivial product change. - Implement, then run
pnpm lint && pnpm type-check. Fix every error you introduced. - Don't commit unless the user asks. When you do commit, write a short, focused message — why, not a diff narration.
- 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). 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_idscopes 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
fetchcalls to internal API routes. - Querying the DB without a
workspace_idfilter on tenant-scoped tables. - Inlining secrets, IPs, or hostnames from
AGENT-DEPLOY.mdinto 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.