crm Agent Skills & AI Rules

GitHub

Comp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.

11,591 stars TypeScript TypeScript 28 files ยท ~94,154 tokens
RAW Rules
362 Discovered Rules โ€ข ~361,816 Estimated Tokens
Quick:
Loading skill content...

AI Agent Protocol & Instructions

AGENTS.md root-instruction ~2,634 tokens
Raw
# Strict rules โ€” review before starting any work

**Read the doc for the area you are touching before you touch it.** The table
below is the whole index. These are plain paths, not imports: they are not in your
context until you read them, and the rules in them are not optional.

| Working on | Read first |
| --- | --- |
| Anything in `apps/api` โ€” tRPC, auth, logging, sync, deletes, caching | `docs/api.md` |
| `apps/agent` โ€” the eve research agent, tools, tasks, dispatch | `docs/agent.md` |
| `.env`, configuration, which variables exist and why | `docs/environment.md` |
| UI in `apps/app` or `packages/ui` | `docs/design.md` (below) |
| Deal amounts, totals, charts, exchange rates | `docs/currency.md` |
| The record sheet's Agent tab | `docs/agent-panel.md` |
| `/settings/connections`, integrations, the intake endpoint | `docs/connections.md` |
| The tracking script, the collector, form submissions | `docs/tracking.md` |
| Running it locally, Google Cloud, DB commands, secrets | `docs/setup.md` |
| Anything that sends a telemetry event, or a new property on one | `docs/telemetry.md` |
| `.github/workflows`, versions, changelog, how a change reaches `release` | `CONTRIBUTING.md` |

Also check `.agents/skills/` for a relevant skill before starting โ€” better-auth,
prisma, nestjs-trpc, eve, shadcn, nuqs and others have one. Tell the user which
rules and skills you read.

## Always true

- **Never add code comments.** Not to new code, not to code you edit.
- **No coauthoring commits.** No `Co-Authored-By` trailer, ever.
- **Intelligence lives in `apps/agent`, never in the API.** No vendor client, no
  enrichment, no scoring, no identity matching in Nest โ€” it writes an `AgentTask`
  row and lets the agent decide. See `docs/api.md`.
- **One `.env`, at the repo root.** `.env.example` is its documentation: add every
  new variable there with a note on what it does, and declare it in
  `apps/api/src/config/env.validation.ts` if the API reads it. Never add a
  per-package `.env`.
- **Anything a self-hoster might not have is optional and must never throw.** A
  missing key removes a capability. `apps/agent/agent/lib/capabilities.ts` is the
  pattern.
- **`/packages/ui` is the single source of truth for UI.** Shared shadcn
  components only; a new variant is implemented there, not overridden at the call
  site.
- **eve's own docs ship in `apps/agent/node_modules/eve/docs`** and match the
  installed version. Read the relevant guide before writing eve code rather than
  working from memory โ€” guessing typechecks, builds, and then behaves differently.

## Report every issue. Use ASD-STE100

Do not bury a known problem inside a paragraph. A problem inside prose is a
problem nobody reads. Report **every** issue, including ones you caused, in a
list at the end of your reply.

Write every message, every report and every issue in **ASD-STE100**
(Simplified Technical English):

- One idea per sentence. Maximum 20 words.
- Active voice. Present tense. No conditionals.
- One word for one meaning. Do not use synonyms for variety.
- Say the effect, not only the cause.
- No hedging: never "may", "might", "possibly", "somewhat".

Use exactly this shape:

```
## Issues

1. BROKEN โ€” Slack is not connected. Agents that post to Slack fail.
   Fix: connect Slack in Settings โ†’ Connections.
2. RISK โ€” A run longer than 5 minutes is cancelled. Work is lost.
   Fix: not done. Needs a separate execution lease.
3. NOT DONE โ€” The manual run button shows on event-only agents.
```

Rules for the list:

- One line for the problem. One line for the fix.
- Start each with **BROKEN**, **RISK**, **NOT DONE**, or **UNKNOWN**.
- **BROKEN** is failing now. **RISK** fails later. **NOT DONE** is unbuilt.
  **UNKNOWN** is not investigated.
- If you introduced it, write **I caused this** on the fix line.
- Zero issues? Write `## Issues` then `None.`

**Don't** โ€” bury it in prose:

> The fix works well. One honest limit: abandoning a sweep unblocks the queue but
> doesn't cancel the underlying hung promise, so it leaks until restart.

**Do** โ€” put it in the list:

> 1. RISK โ€” An abandoned sweep leaks its promise. Memory grows until restart.
>    Fix: not done. Needs cancellation in `receive()`. I caused this.

## A server page computes. A client component renders.

A client component must never import a server package. `@crm/auth` and `@crm/db`
are server packages: their barrels reach Prisma, which reaches `pg`, which
reaches `dns`. The bundler follows that chain into the browser and the build
fails with `Module not found: Can't resolve 'dns'`.

The import trace is the whole error. Read it from the bottom: the last line is
the page, the line above is the client component that leaked, and the top is the
Node module that cannot exist in a browser.

**Don't** โ€” a client component reaching for a server package:

```tsx
"use client";
import {
  describeSlackScopes,
  SLACK_SCOPE_GROUPS,
} from "@crm/auth/slack-scopes";

export function SlackScopeGroups({ scopes }: { scopes: string[] }) {
  const groups = SLACK_SCOPE_GROUPS.map(...)
}
```

**Do** โ€” the page does the work and hands over plain data:

```tsx
// page.tsx โ€” server
import {
  describeSlackScopes,
  SLACK_SCOPE_GROUPS,
} from "@crm/auth/slack-scopes";

const groups = groupScopes(status.scopes);
return <SlackScopeGroups groups={groups} />;
```

```tsx
// slack-scope-groups.tsx โ€” client
"use client";

export type ScopeGroup = { id: string; label: string; scopes: ScopeLine[] };

export function SlackScopeGroups({ groups }: { groups: ScopeGroup[] }) { โ€ฆ }
```

Rules that follow:

- The client component owns its own prop types. It does not re-export a server
  type to get them.
- Anything interactive โ€” an accordion, a dialog, a search field โ€” is a client
  component that receives finished data. It never derives it.
- A `"use client"` file may import from `@crm/ui`, the tRPC client, and React.
  Anything else needs checking.
- The server page is where `await` and secrets live. The client file has neither.

## Constants belong in one file per area, not beside their first use

A number that someone will want to tune goes in a named config module for its
area. It does not go at the top of whichever file happened to need it first.
Somebody changing a timeout must not have to know which file to open.

**Don't** โ€” one constant per file, found only by grep:

```ts
// dispatch.ts
const DRAIN_TIMEOUT_MS = 4 * 60_000;
// crm.ts
const STALE_QUEUE_MS = 5 * 60_000;
// tasks.ts
const LEASE_MS = 10 * 60_000;
```

**Do** โ€” one object, grouped by concern, imported where used:

```ts
// dispatch-config.ts
export const DISPATCH = {
  sweep: { timeoutMs: 4 * MINUTE_MS, staleQueueMs: 5 * MINUTE_MS },
  task: { leaseMs: 10 * MINUTE_MS },
} as const;
```

`apps/agent/agent/lib/dispatch-config.ts` is the pattern. Rules:

- Group by concern, not by file that uses it.
- Derive units from one base (`MINUTE_MS`). Never write `4 * 60_000` twice.
- `as const`, so the values are literal types.
- No magic numbers inline. If it is tunable, it belongs in the config.
- One convention across the codebase. Do not invent a local style for one file.

## Parse at the boundary, never pass `Record<string, unknown>` around

Untyped data โ€” a Prisma `Json` column, a webhook body, an API response โ€” is
parsed into a **domain type at the moment it enters the process**, with Zod, in a
module that owns that shape. Every consumer downstream receives the parsed type
and nothing else. `Record<string, unknown>`, `unknown` casts and one-off
`recordOf()` helpers are how a shape becomes unknowable and a typo becomes a
runtime bug two files away.

**Don't** โ€” reach into raw JSON, re-deriving the shape at each call site:

```ts
function manifestActions(value: unknown) {
  const actions = recordOf(value).actions;
  return Array.isArray(actions) ? actions.map(recordOf) : [];
}

const slack = manifestActions(version.manifest).find(
  (action) => action.type === "slack.message.post",
);
const id = (slack?.destination as Record<string, unknown>)?.id;
```

Nothing here is checked. `destination` may be missing, `id` may be a number, and
the compiler cannot help. Rename a field and every one of these silently returns
`undefined`.

**Do** โ€” one schema, parsed once, at the read:

```ts
export const agentManifestAction = z.discriminatedUnion("type", [
  z.object({
    type: z.literal(AGENT_ACTION_TYPES.SLACK_MESSAGE_POST),
    provider: z.literal("slack"),
    summary: z.string(),
    destination: z.object({
      kind: z.enum(["channel", "user"]),
      resolution: z.literal("chosen"),
      id: z.string().trim().min(1).max(120),
      label: z.string().trim().min(1).max(120),
    }),
  }),
]);

export type AgentManifest = z.infer<typeof agentManifest>;

export function parseAgentManifest(value: unknown): AgentManifest { โ€ฆ }
```

```ts
const manifest = parseAgentManifest(version.manifest);
const slack = manifest.actions.find(
  (action) => action.type === "slack.message.post",
);
const id = slack?.destination.id;
```

`packages/validation/src/agent-manifest.ts` is the pattern. A shape that crosses
a package boundary โ€” a `Json` column two apps read, a payload one app writes and
another consumes โ€” lives in `packages/validation/src`, one module per shape, and
is imported by subpath (`@crm/validation/agent-manifest`). Rules that follow from
it:

- The schema describes what is **actually stored**, not the loosest thing that
  parses. If a test fixture fails the schema, fix the fixture โ€” a fixture that
  omits required fields is testing data that cannot exist.
- Parse failure is a real error with a real message. Do not swallow it into an
  empty array, because "unreadable manifest" and "no actions" are different
  problems and only one of them is the user's fault.
- Derive types with `z.infer`. Never hand-write an interface beside a schema;
  they drift.

## Design

@docs/design.md

## Commit Messages & Pull Requests

**Branch from `main`, never from `release`.** `release` is the default branch so
that a plain clone runs the last tagged release, and the only things that merge
into it are release pull requests. A branch cut from `release` carries release
merge commits that do not belong in a feature pull request.

**You do not write the pull request title.** `.github/scripts/pr-title.sh` reads
the diff and writes one. Retitle it yourself and the automation stops.
`CONTRIBUTING.md` is the whole account of how a change reaches `release`; read it
before shipping.