{"owner":"trycompai","repo":"crm","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# Strict rules — review before starting any work\n\n**Read the doc for the area you are touching before you touch it.** The table\nbelow is the whole index. These are plain paths, not imports: they are not in your\ncontext until you read them, and the rules in them are not optional.\n\n| Working on | Read first |\n| --- | --- |\n| Anything in `apps/api` — tRPC, auth, logging, sync, deletes, caching | `docs/api.md` |\n| `apps/agent` — the eve research agent, tools, tasks, dispatch | `docs/agent.md` |\n| `.env`, configuration, which variables exist and why | `docs/environment.md` |\n| UI in `apps/app` or `packages/ui` | `docs/design.md` (below) |\n| Deal amounts, totals, charts, exchange rates | `docs/currency.md` |\n| The record sheet's Agent tab | `docs/agent-panel.md` |\n| `/settings/connections`, integrations, the intake endpoint | `docs/connections.md` |\n| The tracking script, the collector, form submissions | `docs/tracking.md` |\n| Running it locally, Google Cloud, DB commands, secrets | `docs/setup.md` |\n| Anything that sends a telemetry event, or a new property on one | `docs/telemetry.md` |\n| `.github/workflows`, versions, changelog, how a change reaches `release` | `CONTRIBUTING.md` |\n\nAlso check `.agents/skills/` for a relevant skill before starting — better-auth,\nprisma, nestjs-trpc, eve, shadcn, nuqs and others have one. Tell the user which\nrules and skills you read.\n\n## Always true\n\n- **Never add code comments.** Not to new code, not to code you edit.\n- **No coauthoring commits.** No `Co-Authored-By` trailer, ever.\n- **Intelligence lives in `apps/agent`, never in the API.** No vendor client, no\n  enrichment, no scoring, no identity matching in Nest — it writes an `AgentTask`\n  row and lets the agent decide. See `docs/api.md`.\n- **One `.env`, at the repo root.** `.env.example` is its documentation: add every\n  new variable there with a note on what it does, and declare it in\n  `apps/api/src/config/env.validation.ts` if the API reads it. Never add a\n  per-package `.env`.\n- **Anything a self-hoster might not have is optional and must never throw.** A\n  missing key removes a capability. `apps/agent/agent/lib/capabilities.ts` is the\n  pattern.\n- **`/packages/ui` is the single source of truth for UI.** Shared shadcn\n  components only; a new variant is implemented there, not overridden at the call\n  site.\n- **eve's own docs ship in `apps/agent/node_modules/eve/docs`** and match the\n  installed version. Read the relevant guide before writing eve code rather than\n  working from memory — guessing typechecks, builds, and then behaves differently.\n\n## Report every issue. Use ASD-STE100\n\nDo not bury a known problem inside a paragraph. A problem inside prose is a\nproblem nobody reads. Report **every** issue, including ones you caused, in a\nlist at the end of your reply.\n\nWrite every message, every report and every issue in **ASD-STE100**\n(Simplified Technical English):\n\n- One idea per sentence. Maximum 20 words.\n- Active voice. Present tense. No conditionals.\n- One word for one meaning. Do not use synonyms for variety.\n- Say the effect, not only the cause.\n- No hedging: never \"may\", \"might\", \"possibly\", \"somewhat\".\n\nUse exactly this shape:\n\n```\n## Issues\n\n1. BROKEN — Slack is not connected. Agents that post to Slack fail.\n   Fix: connect Slack in Settings → Connections.\n2. RISK — A run longer than 5 minutes is cancelled. Work is lost.\n   Fix: not done. Needs a separate execution lease.\n3. NOT DONE — The manual run button shows on event-only agents.\n```\n\nRules for the list:\n\n- One line for the problem. One line for the fix.\n- Start each with **BROKEN**, **RISK**, **NOT DONE**, or **UNKNOWN**.\n- **BROKEN** is failing now. **RISK** fails later. **NOT DONE** is unbuilt.\n  **UNKNOWN** is not investigated.\n- If you introduced it, write **I caused this** on the fix line.\n- Zero issues? Write `## Issues` then `None.`\n\n**Don't** — bury it in prose:\n\n> The fix works well. One honest limit: abandoning a sweep unblocks the queue but\n> doesn't cancel the underlying hung promise, so it leaks until restart.\n\n**Do** — put it in the list:\n\n> 1. RISK — An abandoned sweep leaks its promise. Memory grows until restart.\n>    Fix: not done. Needs cancellation in `receive()`. I caused this.\n\n## A server page computes. A client component renders.\n\nA client component must never import a server package. `@crm/auth` and `@crm/db`\nare server packages: their barrels reach Prisma, which reaches `pg`, which\nreaches `dns`. The bundler follows that chain into the browser and the build\nfails with `Module not found: Can't resolve 'dns'`.\n\nThe import trace is the whole error. Read it from the bottom: the last line is\nthe page, the line above is the client component that leaked, and the top is the\nNode module that cannot exist in a browser.\n\n**Don't** — a client component reaching for a server package:\n\n```tsx\n\"use client\";\nimport { describeSlackScopes, SLACK_SCOPE_GROUPS } from \"@crm/auth\";\n\nexport function SlackScopeGroups({ scopes }: { scopes: string[] }) {\n  const groups = SLACK_SCOPE_GROUPS.map(...)\n}\n```\n\n**Do** — the page does the work and hands over plain data:\n\n```tsx\n// page.tsx — server\nimport { describeSlackScopes, SLACK_SCOPE_GROUPS } from \"@crm/auth\";\n\nconst groups = groupScopes(status.scopes);\nreturn <SlackScopeGroups groups={groups} />;\n```\n\n```tsx\n// slack-scope-groups.tsx — client\n\"use client\";\n\nexport type ScopeGroup = { id: string; label: string; scopes: ScopeLine[] };\n\nexport function SlackScopeGroups({ groups }: { groups: ScopeGroup[] }) { … }\n```\n\nRules that follow:\n\n- The client component owns its own prop types. It does not re-export a server\n  type to get them.\n- Anything interactive — an accordion, a dialog, a search field — is a client\n  component that receives finished data. It never derives it.\n- A `\"use client\"` file may import from `@crm/ui`, the tRPC client, and React.\n  Anything else needs checking.\n- The server page is where `await` and secrets live. The client file has neither.\n\n## Constants belong in one file per area, not beside their first use\n\nA number that someone will want to tune goes in a named config module for its\narea. It does not go at the top of whichever file happened to need it first.\nSomebody changing a timeout must not have to know which file to open.\n\n**Don't** — one constant per file, found only by grep:\n\n```ts\n// dispatch.ts\nconst DRAIN_TIMEOUT_MS = 4 * 60_000;\n// crm.ts\nconst STALE_QUEUE_MS = 5 * 60_000;\n// tasks.ts\nconst LEASE_MS = 10 * 60_000;\n```\n\n**Do** — one object, grouped by concern, imported where used:\n\n```ts\n// dispatch-config.ts\nexport const DISPATCH = {\n  sweep: { timeoutMs: 4 * MINUTE_MS, staleQueueMs: 5 * MINUTE_MS },\n  task: { leaseMs: 10 * MINUTE_MS },\n} as const;\n```\n\n`apps/agent/agent/lib/dispatch-config.ts` is the pattern. Rules:\n\n- Group by concern, not by file that uses it.\n- Derive units from one base (`MINUTE_MS`). Never write `4 * 60_000` twice.\n- `as const`, so the values are literal types.\n- No magic numbers inline. If it is tunable, it belongs in the config.\n- One convention across the codebase. Do not invent a local style for one file.\n\n## Parse at the boundary, never pass `Record<string, unknown>` around\n\nUntyped data — a Prisma `Json` column, a webhook body, an API response — is\nparsed into a **domain type at the moment it enters the process**, with Zod, in a\nmodule that owns that shape. Every consumer downstream receives the parsed type\nand nothing else. `Record<string, unknown>`, `unknown` casts and one-off\n`recordOf()` helpers are how a shape becomes unknowable and a typo becomes a\nruntime bug two files away.\n\n**Don't** — reach into raw JSON, re-deriving the shape at each call site:\n\n```ts\nfunction manifestActions(value: unknown) {\n  const actions = recordOf(value).actions;\n  return Array.isArray(actions) ? actions.map(recordOf) : [];\n}\n\nconst slack = manifestActions(version.manifest).find(\n  (action) => action.type === \"slack.message.post\",\n);\nconst id = (slack?.destination as Record<string, unknown>)?.id;\n```\n\nNothing here is checked. `destination` may be missing, `id` may be a number, and\nthe compiler cannot help. Rename a field and every one of these silently returns\n`undefined`.\n\n**Do** — one schema, parsed once, at the read:\n\n```ts\nexport const agentManifestAction = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(AGENT_ACTION_TYPES.SLACK_MESSAGE_POST),\n    provider: z.literal(\"slack\"),\n    summary: z.string(),\n    destination: z.object({\n      kind: z.enum([\"channel\", \"user\"]),\n      resolution: z.literal(\"chosen\"),\n      id: z.string().trim().min(1).max(120),\n      label: z.string().trim().min(1).max(120),\n    }),\n  }),\n]);\n\nexport type AgentManifest = z.infer<typeof agentManifest>;\n\nexport function parseAgentManifest(value: unknown): AgentManifest { … }\n```\n\n```ts\nconst manifest = parseAgentManifest(version.manifest);\nconst slack = manifest.actions.find(\n  (action) => action.type === \"slack.message.post\",\n);\nconst id = slack?.destination.id;\n```\n\n`packages/validation/src/agent-manifest.ts` is the pattern. A shape that crosses\na package boundary — a `Json` column two apps read, a payload one app writes and\nanother consumes — lives in `packages/validation/src`, one module per shape, and\nis imported by subpath (`@crm/validation/agent-manifest`). Rules that follow from\nit:\n\n- The schema describes what is **actually stored**, not the loosest thing that\n  parses. If a test fixture fails the schema, fix the fixture — a fixture that\n  omits required fields is testing data that cannot exist.\n- Parse failure is a real error with a real message. Do not swallow it into an\n  empty array, because \"unreadable manifest\" and \"no actions\" are different\n  problems and only one of them is the user's fault.\n- Derive types with `z.infer`. Never hand-write an interface beside a schema;\n  they drift.\n\n## Design\n\n@docs/design.md\n\n## Median Tasks\n\nMedian can use a project-local workspace binding. If this repository has\n`.median/config.json`, run `mdn` commands from inside this repository so the\ncorrect Median workspace profile is selected. The local config stores only a\nprofile name; API keys stay in your user config.\n\nTo bind this repository to a workspace:\n\n```\nmdn setup --local\n```\n\nBefore starting work, check your assigned tasks:\n\n```\nmdn tasks --agent <your-agent-name>\n```\n\nWhen picking up a task:\n\n```\nmdn status <TASK-CODE> in_progress --agent <your-agent-name>\n```\n\nWhen completing a task:\n\n```\nmdn status <TASK-CODE> ready --agent <your-agent-name>\n```\n\nTo create a new task:\n\n```\nmdn create --title \"Description\" --status todo --priority medium --agent <your-agent-name>\n```\n\n## Commit Messages & Pull Requests\n\nAlways include the Median task ID in commit messages and PR titles so tasks get marked automatically.\n\n```\ngit commit -m \"MDN-42 fix: resolve auth token expiry\"\n```\n\nFor pull requests, include the task ID in the title:\n\n```\nMDN-42 fix: resolve auth token expiry\n```\n"},"files":{"AGENTS.md":"# Strict rules — review before starting any work\n\n**Read the doc for the area you are touching before you touch it.** The table\nbelow is the whole index. These are plain paths, not imports: they are not in your\ncontext until you read them, and the rules in them are not optional.\n\n| Working on | Read first |\n| --- | --- |\n| Anything in `apps/api` — tRPC, auth, logging, sync, deletes, caching | `docs/api.md` |\n| `apps/agent` — the eve research agent, tools, tasks, dispatch | `docs/agent.md` |\n| `.env`, configuration, which variables exist and why | `docs/environment.md` |\n| UI in `apps/app` or `packages/ui` | `docs/design.md` (below) |\n| Deal amounts, totals, charts, exchange rates | `docs/currency.md` |\n| The record sheet's Agent tab | `docs/agent-panel.md` |\n| `/settings/connections`, integrations, the intake endpoint | `docs/connections.md` |\n| The tracking script, the collector, form submissions | `docs/tracking.md` |\n| Running it locally, Google Cloud, DB commands, secrets | `docs/setup.md` |\n| Anything that sends a telemetry event, or a new property on one | `docs/telemetry.md` |\n| `.github/workflows`, versions, changelog, how a change reaches `release` | `CONTRIBUTING.md` |\n\nAlso check `.agents/skills/` for a relevant skill before starting — better-auth,\nprisma, nestjs-trpc, eve, shadcn, nuqs and others have one. Tell the user which\nrules and skills you read.\n\n## Always true\n\n- **Never add code comments.** Not to new code, not to code you edit.\n- **No coauthoring commits.** No `Co-Authored-By` trailer, ever.\n- **Intelligence lives in `apps/agent`, never in the API.** No vendor client, no\n  enrichment, no scoring, no identity matching in Nest — it writes an `AgentTask`\n  row and lets the agent decide. See `docs/api.md`.\n- **One `.env`, at the repo root.** `.env.example` is its documentation: add every\n  new variable there with a note on what it does, and declare it in\n  `apps/api/src/config/env.validation.ts` if the API reads it. Never add a\n  per-package `.env`.\n- **Anything a self-hoster might not have is optional and must never throw.** A\n  missing key removes a capability. `apps/agent/agent/lib/capabilities.ts` is the\n  pattern.\n- **`/packages/ui` is the single source of truth for UI.** Shared shadcn\n  components only; a new variant is implemented there, not overridden at the call\n  site.\n- **eve's own docs ship in `apps/agent/node_modules/eve/docs`** and match the\n  installed version. Read the relevant guide before writing eve code rather than\n  working from memory — guessing typechecks, builds, and then behaves differently.\n\n## Report every issue. Use ASD-STE100\n\nDo not bury a known problem inside a paragraph. A problem inside prose is a\nproblem nobody reads. Report **every** issue, including ones you caused, in a\nlist at the end of your reply.\n\nWrite every message, every report and every issue in **ASD-STE100**\n(Simplified Technical English):\n\n- One idea per sentence. Maximum 20 words.\n- Active voice. Present tense. No conditionals.\n- One word for one meaning. Do not use synonyms for variety.\n- Say the effect, not only the cause.\n- No hedging: never \"may\", \"might\", \"possibly\", \"somewhat\".\n\nUse exactly this shape:\n\n```\n## Issues\n\n1. BROKEN — Slack is not connected. Agents that post to Slack fail.\n   Fix: connect Slack in Settings → Connections.\n2. RISK — A run longer than 5 minutes is cancelled. Work is lost.\n   Fix: not done. Needs a separate execution lease.\n3. NOT DONE — The manual run button shows on event-only agents.\n```\n\nRules for the list:\n\n- One line for the problem. One line for the fix.\n- Start each with **BROKEN**, **RISK**, **NOT DONE**, or **UNKNOWN**.\n- **BROKEN** is failing now. **RISK** fails later. **NOT DONE** is unbuilt.\n  **UNKNOWN** is not investigated.\n- If you introduced it, write **I caused this** on the fix line.\n- Zero issues? Write `## Issues` then `None.`\n\n**Don't** — bury it in prose:\n\n> The fix works well. One honest limit: abandoning a sweep unblocks the queue but\n> doesn't cancel the underlying hung promise, so it leaks until restart.\n\n**Do** — put it in the list:\n\n> 1. RISK — An abandoned sweep leaks its promise. Memory grows until restart.\n>    Fix: not done. Needs cancellation in `receive()`. I caused this.\n\n## A server page computes. A client component renders.\n\nA client component must never import a server package. `@crm/auth` and `@crm/db`\nare server packages: their barrels reach Prisma, which reaches `pg`, which\nreaches `dns`. The bundler follows that chain into the browser and the build\nfails with `Module not found: Can't resolve 'dns'`.\n\nThe import trace is the whole error. Read it from the bottom: the last line is\nthe page, the line above is the client component that leaked, and the top is the\nNode module that cannot exist in a browser.\n\n**Don't** — a client component reaching for a server package:\n\n```tsx\n\"use client\";\nimport { describeSlackScopes, SLACK_SCOPE_GROUPS } from \"@crm/auth\";\n\nexport function SlackScopeGroups({ scopes }: { scopes: string[] }) {\n  const groups = SLACK_SCOPE_GROUPS.map(...)\n}\n```\n\n**Do** — the page does the work and hands over plain data:\n\n```tsx\n// page.tsx — server\nimport { describeSlackScopes, SLACK_SCOPE_GROUPS } from \"@crm/auth\";\n\nconst groups = groupScopes(status.scopes);\nreturn <SlackScopeGroups groups={groups} />;\n```\n\n```tsx\n// slack-scope-groups.tsx — client\n\"use client\";\n\nexport type ScopeGroup = { id: string; label: string; scopes: ScopeLine[] };\n\nexport function SlackScopeGroups({ groups }: { groups: ScopeGroup[] }) { … }\n```\n\nRules that follow:\n\n- The client component owns its own prop types. It does not re-export a server\n  type to get them.\n- Anything interactive — an accordion, a dialog, a search field — is a client\n  component that receives finished data. It never derives it.\n- A `\"use client\"` file may import from `@crm/ui`, the tRPC client, and React.\n  Anything else needs checking.\n- The server page is where `await` and secrets live. The client file has neither.\n\n## Constants belong in one file per area, not beside their first use\n\nA number that someone will want to tune goes in a named config module for its\narea. It does not go at the top of whichever file happened to need it first.\nSomebody changing a timeout must not have to know which file to open.\n\n**Don't** — one constant per file, found only by grep:\n\n```ts\n// dispatch.ts\nconst DRAIN_TIMEOUT_MS = 4 * 60_000;\n// crm.ts\nconst STALE_QUEUE_MS = 5 * 60_000;\n// tasks.ts\nconst LEASE_MS = 10 * 60_000;\n```\n\n**Do** — one object, grouped by concern, imported where used:\n\n```ts\n// dispatch-config.ts\nexport const DISPATCH = {\n  sweep: { timeoutMs: 4 * MINUTE_MS, staleQueueMs: 5 * MINUTE_MS },\n  task: { leaseMs: 10 * MINUTE_MS },\n} as const;\n```\n\n`apps/agent/agent/lib/dispatch-config.ts` is the pattern. Rules:\n\n- Group by concern, not by file that uses it.\n- Derive units from one base (`MINUTE_MS`). Never write `4 * 60_000` twice.\n- `as const`, so the values are literal types.\n- No magic numbers inline. If it is tunable, it belongs in the config.\n- One convention across the codebase. Do not invent a local style for one file.\n\n## Parse at the boundary, never pass `Record<string, unknown>` around\n\nUntyped data — a Prisma `Json` column, a webhook body, an API response — is\nparsed into a **domain type at the moment it enters the process**, with Zod, in a\nmodule that owns that shape. Every consumer downstream receives the parsed type\nand nothing else. `Record<string, unknown>`, `unknown` casts and one-off\n`recordOf()` helpers are how a shape becomes unknowable and a typo becomes a\nruntime bug two files away.\n\n**Don't** — reach into raw JSON, re-deriving the shape at each call site:\n\n```ts\nfunction manifestActions(value: unknown) {\n  const actions = recordOf(value).actions;\n  return Array.isArray(actions) ? actions.map(recordOf) : [];\n}\n\nconst slack = manifestActions(version.manifest).find(\n  (action) => action.type === \"slack.message.post\",\n);\nconst id = (slack?.destination as Record<string, unknown>)?.id;\n```\n\nNothing here is checked. `destination` may be missing, `id` may be a number, and\nthe compiler cannot help. Rename a field and every one of these silently returns\n`undefined`.\n\n**Do** — one schema, parsed once, at the read:\n\n```ts\nexport const agentManifestAction = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(AGENT_ACTION_TYPES.SLACK_MESSAGE_POST),\n    provider: z.literal(\"slack\"),\n    summary: z.string(),\n    destination: z.object({\n      kind: z.enum([\"channel\", \"user\"]),\n      resolution: z.literal(\"chosen\"),\n      id: z.string().trim().min(1).max(120),\n      label: z.string().trim().min(1).max(120),\n    }),\n  }),\n]);\n\nexport type AgentManifest = z.infer<typeof agentManifest>;\n\nexport function parseAgentManifest(value: unknown): AgentManifest { … }\n```\n\n```ts\nconst manifest = parseAgentManifest(version.manifest);\nconst slack = manifest.actions.find(\n  (action) => action.type === \"slack.message.post\",\n);\nconst id = slack?.destination.id;\n```\n\n`packages/validation/src/agent-manifest.ts` is the pattern. A shape that crosses\na package boundary — a `Json` column two apps read, a payload one app writes and\nanother consumes — lives in `packages/validation/src`, one module per shape, and\nis imported by subpath (`@crm/validation/agent-manifest`). Rules that follow from\nit:\n\n- The schema describes what is **actually stored**, not the loosest thing that\n  parses. If a test fixture fails the schema, fix the fixture — a fixture that\n  omits required fields is testing data that cannot exist.\n- Parse failure is a real error with a real message. Do not swallow it into an\n  empty array, because \"unreadable manifest\" and \"no actions\" are different\n  problems and only one of them is the user's fault.\n- Derive types with `z.infer`. Never hand-write an interface beside a schema;\n  they drift.\n\n## Design\n\n@docs/design.md\n\n## Median Tasks\n\nMedian can use a project-local workspace binding. If this repository has\n`.median/config.json`, run `mdn` commands from inside this repository so the\ncorrect Median workspace profile is selected. The local config stores only a\nprofile name; API keys stay in your user config.\n\nTo bind this repository to a workspace:\n\n```\nmdn setup --local\n```\n\nBefore starting work, check your assigned tasks:\n\n```\nmdn tasks --agent <your-agent-name>\n```\n\nWhen picking up a task:\n\n```\nmdn status <TASK-CODE> in_progress --agent <your-agent-name>\n```\n\nWhen completing a task:\n\n```\nmdn status <TASK-CODE> ready --agent <your-agent-name>\n```\n\nTo create a new task:\n\n```\nmdn create --title \"Description\" --status todo --priority medium --agent <your-agent-name>\n```\n\n## Commit Messages & Pull Requests\n\nAlways include the Median task ID in commit messages and PR titles so tasks get marked automatically.\n\n```\ngit commit -m \"MDN-42 fix: resolve auth token expiry\"\n```\n\nFor pull requests, include the task ID in the title:\n\n```\nMDN-42 fix: resolve auth token expiry\n```\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# Strict rules — review before starting any work\n\n**Read the doc for the area you are touching before you touch it.** The table\nbelow is the whole index. These are plain paths, not imports: they are not in your\ncontext until you read them, and the rules in them are not optional.\n\n| Working on | Read first |\n| --- | --- |\n| Anything in `apps/api` — tRPC, auth, logging, sync, deletes, caching | `docs/api.md` |\n| `apps/agent` — the eve research agent, tools, tasks, dispatch | `docs/agent.md` |\n| `.env`, configuration, which variables exist and why | `docs/environment.md` |\n| UI in `apps/app` or `packages/ui` | `docs/design.md` (below) |\n| Deal amounts, totals, charts, exchange rates | `docs/currency.md` |\n| The record sheet's Agent tab | `docs/agent-panel.md` |\n| `/settings/connections`, integrations, the intake endpoint | `docs/connections.md` |\n| The tracking script, the collector, form submissions | `docs/tracking.md` |\n| Running it locally, Google Cloud, DB commands, secrets | `docs/setup.md` |\n| Anything that sends a telemetry event, or a new property on one | `docs/telemetry.md` |\n| `.github/workflows`, versions, changelog, how a change reaches `release` | `CONTRIBUTING.md` |\n\nAlso check `.agents/skills/` for a relevant skill before starting — better-auth,\nprisma, nestjs-trpc, eve, shadcn, nuqs and others have one. Tell the user which\nrules and skills you read.\n\n## Always true\n\n- **Never add code comments.** Not to new code, not to code you edit.\n- **No coauthoring commits.** No `Co-Authored-By` trailer, ever.\n- **Intelligence lives in `apps/agent`, never in the API.** No vendor client, no\n  enrichment, no scoring, no identity matching in Nest — it writes an `AgentTask`\n  row and lets the agent decide. See `docs/api.md`.\n- **One `.env`, at the repo root.** `.env.example` is its documentation: add every\n  new variable there with a note on what it does, and declare it in\n  `apps/api/src/config/env.validation.ts` if the API reads it. Never add a\n  per-package `.env`.\n- **Anything a self-hoster might not have is optional and must never throw.** A\n  missing key removes a capability. `apps/agent/agent/lib/capabilities.ts` is the\n  pattern.\n- **`/packages/ui` is the single source of truth for UI.** Shared shadcn\n  components only; a new variant is implemented there, not overridden at the call\n  site.\n- **eve's own docs ship in `apps/agent/node_modules/eve/docs`** and match the\n  installed version. Read the relevant guide before writing eve code rather than\n  working from memory — guessing typechecks, builds, and then behaves differently.\n\n## Report every issue. Use ASD-STE100\n\nDo not bury a known problem inside a paragraph. A problem inside prose is a\nproblem nobody reads. Report **every** issue, including ones you caused, in a\nlist at the end of your reply.\n\nWrite every message, every report and every issue in **ASD-STE100**\n(Simplified Technical English):\n\n- One idea per sentence. Maximum 20 words.\n- Active voice. Present tense. No conditionals.\n- One word for one meaning. Do not use synonyms for variety.\n- Say the effect, not only the cause.\n- No hedging: never \"may\", \"might\", \"possibly\", \"somewhat\".\n\nUse exactly this shape:\n\n```\n## Issues\n\n1. BROKEN — Slack is not connected. Agents that post to Slack fail.\n   Fix: connect Slack in Settings → Connections.\n2. RISK — A run longer than 5 minutes is cancelled. Work is lost.\n   Fix: not done. Needs a separate execution lease.\n3. NOT DONE — The manual run button shows on event-only agents.\n```\n\nRules for the list:\n\n- One line for the problem. One line for the fix.\n- Start each with **BROKEN**, **RISK**, **NOT DONE**, or **UNKNOWN**.\n- **BROKEN** is failing now. **RISK** fails later. **NOT DONE** is unbuilt.\n  **UNKNOWN** is not investigated.\n- If you introduced it, write **I caused this** on the fix line.\n- Zero issues? Write `## Issues` then `None.`\n\n**Don't** — bury it in prose:\n\n> The fix works well. One honest limit: abandoning a sweep unblocks the queue but\n> doesn't cancel the underlying hung promise, so it leaks until restart.\n\n**Do** — put it in the list:\n\n> 1. RISK — An abandoned sweep leaks its promise. Memory grows until restart.\n>    Fix: not done. Needs cancellation in `receive()`. I caused this.\n\n## A server page computes. A client component renders.\n\nA client component must never import a server package. `@crm/auth` and `@crm/db`\nare server packages: their barrels reach Prisma, which reaches `pg`, which\nreaches `dns`. The bundler follows that chain into the browser and the build\nfails with `Module not found: Can't resolve 'dns'`.\n\nThe import trace is the whole error. Read it from the bottom: the last line is\nthe page, the line above is the client component that leaked, and the top is the\nNode module that cannot exist in a browser.\n\n**Don't** — a client component reaching for a server package:\n\n```tsx\n\"use client\";\nimport { describeSlackScopes, SLACK_SCOPE_GROUPS } from \"@crm/auth\";\n\nexport function SlackScopeGroups({ scopes }: { scopes: string[] }) {\n  const groups = SLACK_SCOPE_GROUPS.map(...)\n}\n```\n\n**Do** — the page does the work and hands over plain data:\n\n```tsx\n// page.tsx — server\nimport { describeSlackScopes, SLACK_SCOPE_GROUPS } from \"@crm/auth\";\n\nconst groups = groupScopes(status.scopes);\nreturn <SlackScopeGroups groups={groups} />;\n```\n\n```tsx\n// slack-scope-groups.tsx — client\n\"use client\";\n\nexport type ScopeGroup = { id: string; label: string; scopes: ScopeLine[] };\n\nexport function SlackScopeGroups({ groups }: { groups: ScopeGroup[] }) { … }\n```\n\nRules that follow:\n\n- The client component owns its own prop types. It does not re-export a server\n  type to get them.\n- Anything interactive — an accordion, a dialog, a search field — is a client\n  component that receives finished data. It never derives it.\n- A `\"use client\"` file may import from `@crm/ui`, the tRPC client, and React.\n  Anything else needs checking.\n- The server page is where `await` and secrets live. The client file has neither.\n\n## Constants belong in one file per area, not beside their first use\n\nA number that someone will want to tune goes in a named config module for its\narea. It does not go at the top of whichever file happened to need it first.\nSomebody changing a timeout must not have to know which file to open.\n\n**Don't** — one constant per file, found only by grep:\n\n```ts\n// dispatch.ts\nconst DRAIN_TIMEOUT_MS = 4 * 60_000;\n// crm.ts\nconst STALE_QUEUE_MS = 5 * 60_000;\n// tasks.ts\nconst LEASE_MS = 10 * 60_000;\n```\n\n**Do** — one object, grouped by concern, imported where used:\n\n```ts\n// dispatch-config.ts\nexport const DISPATCH = {\n  sweep: { timeoutMs: 4 * MINUTE_MS, staleQueueMs: 5 * MINUTE_MS },\n  task: { leaseMs: 10 * MINUTE_MS },\n} as const;\n```\n\n`apps/agent/agent/lib/dispatch-config.ts` is the pattern. Rules:\n\n- Group by concern, not by file that uses it.\n- Derive units from one base (`MINUTE_MS`). Never write `4 * 60_000` twice.\n- `as const`, so the values are literal types.\n- No magic numbers inline. If it is tunable, it belongs in the config.\n- One convention across the codebase. Do not invent a local style for one file.\n\n## Parse at the boundary, never pass `Record<string, unknown>` around\n\nUntyped data — a Prisma `Json` column, a webhook body, an API response — is\nparsed into a **domain type at the moment it enters the process**, with Zod, in a\nmodule that owns that shape. Every consumer downstream receives the parsed type\nand nothing else. `Record<string, unknown>`, `unknown` casts and one-off\n`recordOf()` helpers are how a shape becomes unknowable and a typo becomes a\nruntime bug two files away.\n\n**Don't** — reach into raw JSON, re-deriving the shape at each call site:\n\n```ts\nfunction manifestActions(value: unknown) {\n  const actions = recordOf(value).actions;\n  return Array.isArray(actions) ? actions.map(recordOf) : [];\n}\n\nconst slack = manifestActions(version.manifest).find(\n  (action) => action.type === \"slack.message.post\",\n);\nconst id = (slack?.destination as Record<string, unknown>)?.id;\n```\n\nNothing here is checked. `destination` may be missing, `id` may be a number, and\nthe compiler cannot help. Rename a field and every one of these silently returns\n`undefined`.\n\n**Do** — one schema, parsed once, at the read:\n\n```ts\nexport const agentManifestAction = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(AGENT_ACTION_TYPES.SLACK_MESSAGE_POST),\n    provider: z.literal(\"slack\"),\n    summary: z.string(),\n    destination: z.object({\n      kind: z.enum([\"channel\", \"user\"]),\n      resolution: z.literal(\"chosen\"),\n      id: z.string().trim().min(1).max(120),\n      label: z.string().trim().min(1).max(120),\n    }),\n  }),\n]);\n\nexport type AgentManifest = z.infer<typeof agentManifest>;\n\nexport function parseAgentManifest(value: unknown): AgentManifest { … }\n```\n\n```ts\nconst manifest = parseAgentManifest(version.manifest);\nconst slack = manifest.actions.find(\n  (action) => action.type === \"slack.message.post\",\n);\nconst id = slack?.destination.id;\n```\n\n`packages/validation/src/agent-manifest.ts` is the pattern. A shape that crosses\na package boundary — a `Json` column two apps read, a payload one app writes and\nanother consumes — lives in `packages/validation/src`, one module per shape, and\nis imported by subpath (`@crm/validation/agent-manifest`). Rules that follow from\nit:\n\n- The schema describes what is **actually stored**, not the loosest thing that\n  parses. If a test fixture fails the schema, fix the fixture — a fixture that\n  omits required fields is testing data that cannot exist.\n- Parse failure is a real error with a real message. Do not swallow it into an\n  empty array, because \"unreadable manifest\" and \"no actions\" are different\n  problems and only one of them is the user's fault.\n- Derive types with `z.infer`. Never hand-write an interface beside a schema;\n  they drift.\n\n## Design\n\n@docs/design.md\n\n## Median Tasks\n\nMedian can use a project-local workspace binding. If this repository has\n`.median/config.json`, run `mdn` commands from inside this repository so the\ncorrect Median workspace profile is selected. The local config stores only a\nprofile name; API keys stay in your user config.\n\nTo bind this repository to a workspace:\n\n```\nmdn setup --local\n```\n\nBefore starting work, check your assigned tasks:\n\n```\nmdn tasks --agent <your-agent-name>\n```\n\nWhen picking up a task:\n\n```\nmdn status <TASK-CODE> in_progress --agent <your-agent-name>\n```\n\nWhen completing a task:\n\n```\nmdn status <TASK-CODE> ready --agent <your-agent-name>\n```\n\nTo create a new task:\n\n```\nmdn create --title \"Description\" --status todo --priority medium --agent <your-agent-name>\n```\n\n## Commit Messages & Pull Requests\n\nAlways include the Median task ID in commit messages and PR titles so tasks get marked automatically.\n\n```\ngit commit -m \"MDN-42 fix: resolve auth token expiry\"\n```\n\nFor pull requests, include the task ID in the title:\n\n```\nMDN-42 fix: resolve auth token expiry\n```\n","category":"root","tokens":2730}]}