ubiquitous-invention/AGENTS.md

162 lines
10 KiB
Markdown
Raw Permalink Normal View History

# 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` and the package scope is `@tasks/*` (historical, pre-rename). Everything else — the database, the deploy, the public domain — is `echodo`. If you see a `tasks` database anywhere, it's an artifact of the old name; the source of truth is `echodo`.
---
## 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(ci): bootstrap vitest + GitHub Actions and lock in 3-gate quality bar First Path-B task. Path A landed daily-driver features without a test runner; Path B is "harden so the next batch of changes can't silently regress what just shipped." Step 1 is making `pnpm test` real and gating CI on it. Test runner: * Install vitest + @vitest/coverage-v8 at the workspace root. * Add vitest.config.ts (environment: "node", no JSDOM) + test / test:watch scripts to packages/shared, packages/database, packages/ai. Wire `test` into turbo.json with dependsOn: ^build for future-proofing; add `pnpm test` to root package.json. Three real tests (no snapshot theater — verified by mutation): * packages/shared/src/utils/id.test.ts: asserts generateId() matches the RFC 4122 v4 regex and produces 1000 distinct values. Mutating generateId() to a constant fails both assertions. * packages/shared/src/types/objects.test.ts: pins objectTypes and objectStatuses arrays. These back the zod enum on objects.create and the "open tasks" count on the workspace-home dashboard; a silent reorder/rename would otherwise corrupt the dashboard math. * packages/database/src/markdown-backlog/parse.test.ts: covers parseBacklogMarkdown across three shapes (well-formed Task, no-frontmatter Plan with path inference, malformed YAML that must NOT throw — the importer runs in a file watcher). Plus hashFileContents determinism. * packages/ai/src/actions/index.test.ts: five tests across the prompt builders (summarize/expand/rewrite × 3 tones / translate / generateFromPrompt). Pure functions; no model mocking needed. CI: * .github/workflows/ci.yml runs on pull_request and push to main. Node 20, pnpm 9 pinned explicitly (per AGENTS.md). Uses setup-node's built-in pnpm cache. Steps: install --frozen-lockfile, lint, type-check, test. Concurrency group cancels superseded runs on non-main branches. Docs: * AGENTS.md: drop the "no test runner configured" disclaimer. Document pnpm test / test:watch. Update the PR-readiness rule from `pnpm lint && pnpm type-check` to `pnpm lint && pnpm type-check && pnpm test`. All 14 tests pass; lint + type-check still green across all 6 packages. The CI workflow's first run is gated on the operator pushing this branch — that's the only acceptance criterion left unverified in this commit. Closes plans/Plan-multitenant-saas-hardening/Epic-test-foundation/ Task-bootstrap-vitest-and-ci.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 10:23:55 -04:00
| 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` |
test(ci): bootstrap vitest + GitHub Actions and lock in 3-gate quality bar First Path-B task. Path A landed daily-driver features without a test runner; Path B is "harden so the next batch of changes can't silently regress what just shipped." Step 1 is making `pnpm test` real and gating CI on it. Test runner: * Install vitest + @vitest/coverage-v8 at the workspace root. * Add vitest.config.ts (environment: "node", no JSDOM) + test / test:watch scripts to packages/shared, packages/database, packages/ai. Wire `test` into turbo.json with dependsOn: ^build for future-proofing; add `pnpm test` to root package.json. Three real tests (no snapshot theater — verified by mutation): * packages/shared/src/utils/id.test.ts: asserts generateId() matches the RFC 4122 v4 regex and produces 1000 distinct values. Mutating generateId() to a constant fails both assertions. * packages/shared/src/types/objects.test.ts: pins objectTypes and objectStatuses arrays. These back the zod enum on objects.create and the "open tasks" count on the workspace-home dashboard; a silent reorder/rename would otherwise corrupt the dashboard math. * packages/database/src/markdown-backlog/parse.test.ts: covers parseBacklogMarkdown across three shapes (well-formed Task, no-frontmatter Plan with path inference, malformed YAML that must NOT throw — the importer runs in a file watcher). Plus hashFileContents determinism. * packages/ai/src/actions/index.test.ts: five tests across the prompt builders (summarize/expand/rewrite × 3 tones / translate / generateFromPrompt). Pure functions; no model mocking needed. CI: * .github/workflows/ci.yml runs on pull_request and push to main. Node 20, pnpm 9 pinned explicitly (per AGENTS.md). Uses setup-node's built-in pnpm cache. Steps: install --frozen-lockfile, lint, type-check, test. Concurrency group cancels superseded runs on non-main branches. Docs: * AGENTS.md: drop the "no test runner configured" disclaimer. Document pnpm test / test:watch. Update the PR-readiness rule from `pnpm lint && pnpm type-check` to `pnpm lint && pnpm type-check && pnpm test`. All 14 tests pass; lint + type-check still green across all 6 packages. The CI workflow's first run is gated on the operator pushing this branch — that's the only acceptance criterion left unverified in this commit. Closes plans/Plan-multitenant-saas-hardening/Epic-test-foundation/ Task-bootstrap-vitest-and-ci.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 10:23:55 -04:00
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](./config/CursorSync.md) for how to find and kill stale servers.
test(ci): bootstrap vitest + GitHub Actions and lock in 3-gate quality bar First Path-B task. Path A landed daily-driver features without a test runner; Path B is "harden so the next batch of changes can't silently regress what just shipped." Step 1 is making `pnpm test` real and gating CI on it. Test runner: * Install vitest + @vitest/coverage-v8 at the workspace root. * Add vitest.config.ts (environment: "node", no JSDOM) + test / test:watch scripts to packages/shared, packages/database, packages/ai. Wire `test` into turbo.json with dependsOn: ^build for future-proofing; add `pnpm test` to root package.json. Three real tests (no snapshot theater — verified by mutation): * packages/shared/src/utils/id.test.ts: asserts generateId() matches the RFC 4122 v4 regex and produces 1000 distinct values. Mutating generateId() to a constant fails both assertions. * packages/shared/src/types/objects.test.ts: pins objectTypes and objectStatuses arrays. These back the zod enum on objects.create and the "open tasks" count on the workspace-home dashboard; a silent reorder/rename would otherwise corrupt the dashboard math. * packages/database/src/markdown-backlog/parse.test.ts: covers parseBacklogMarkdown across three shapes (well-formed Task, no-frontmatter Plan with path inference, malformed YAML that must NOT throw — the importer runs in a file watcher). Plus hashFileContents determinism. * packages/ai/src/actions/index.test.ts: five tests across the prompt builders (summarize/expand/rewrite × 3 tones / translate / generateFromPrompt). Pure functions; no model mocking needed. CI: * .github/workflows/ci.yml runs on pull_request and push to main. Node 20, pnpm 9 pinned explicitly (per AGENTS.md). Uses setup-node's built-in pnpm cache. Steps: install --frozen-lockfile, lint, type-check, test. Concurrency group cancels superseded runs on non-main branches. Docs: * AGENTS.md: drop the "no test runner configured" disclaimer. Document pnpm test / test:watch. Update the PR-readiness rule from `pnpm lint && pnpm type-check` to `pnpm lint && pnpm type-check && pnpm test`. All 14 tests pass; lint + type-check still green across all 6 packages. The CI workflow's first run is gated on the operator pushing this branch — that's the only acceptance criterion left unverified in this commit. Closes plans/Plan-multitenant-saas-hardening/Epic-test-foundation/ Task-bootstrap-vitest-and-ci.md. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 10:23:55 -04:00
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.
- **The Postgres database name is `echodo`**, full stop — local dev, Coolify, the drizzle migration ledger, and the docker-compose `POSTGRES_DB` all need to agree. An older `tasks` database 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 returns `Configuration`" incident was caused by Coolify pointing at `echodo` (correct) while a stale `tasks` database had the only initialized schema.
- `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.
- 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-<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_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.