AGENTS.md ยท diff
git:20261007.971bc2b to git:20261008.501288b
4 added, 2 removed. Audit B to B.
# seedcord agent guidelines
Rules for anyone working in this repo with an AI coding tool. `CLAUDE.md` symlinks to this file.
Two things live elsewhere. [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) covers setup, PR mechanics, and where AI-generated code stands. Each package's `README.md` describes that package.
seedcord is pre-1.0. Breaking changes go in minor versions. Pick the cleanest design and break things to get it right. Add a changeset for every change to a published package.
---
## Load these skills first
Before any work, load the four skills in [`.agents/skills`](.agents/skills): `code-quality` and `tdd` with every file in their folders, plus `code-commenting-guidelines` and `writing-voice`. Load `guide-voice`, `changeset-guidelines`, `envapt`, or `release-version` when the task touches what each covers. [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) lists when.
---
## Ground every claim
The expensive failure here is a confident sentence about how two modules connect that nobody checked.
1. **Cite or flag every load-bearing claim.** Give a `file:line` you opened this session, or write the word "assumption". A labelled guess costs one line of reading. An unlabelled one costs an afternoon.
2. **Open both ends.** Read the module that implements a thing and the module that consumes it. Do this before you write an interface or the words "X must be Y".
3. **Verify before you repeat.** A claim from a subagent, a summary, or your own memory is unverified until you reopen the source. Findings from another agent are input. They are never evidence.
This applies to work you delegate. Reopen a citation yourself before relaying it.
---
## Repo map
Six workspace globs, declared in `pnpm-workspace.yaml`.
<!-- prettier-ignore-start -->
| glob | what belongs there |
| --- | --- |
| `packages/*` | framework leaves, plus small libraries that do not depend on seedcord |
| `plugins/*` | one plugin per backing service |
| `cli/*` | command-line tools |
| `tooling/*` | shared configs, the lint rules, the docs pipeline, and the UI kit |
| `apps/*` | the Next.js sites |
| `mocks/*` | one runnable bot per transport |
<!-- prettier-ignore-end -->
The root [`README.md`](README.md) lists every published package. `private` in a package.json says whether it publishes. The `exports` map is its surface.
Never wire a cross-package source path. Never point `paths` or `include` at another package's `src`.
A re-export from another package goes in `src/index.ts`. One file then says which of a package's names come from elsewhere.
`./internal` entries are framework wiring. A symbol reachable only through one gets no TSDoc. [`packages/AGENTS.md`](packages/AGENTS.md#changesets) says when a change to one still needs a changeset.
---
## Two transports
Everything ships against one of two transports.
`@seedcord/gateway` runs a stateful `Seedcord` class that holds a websocket connection through the discord.js client. It carries message, member, voice, and reaction events.
- `@seedcord/http` answers Discord's interactions endpoint. Node runs through a `Seedcord` class. `@seedcord/http/edge` builds a handler through `createSeedcord` for Web-standard runtimes. Discord posts only interactions here.
+ `@seedcord/http` answers Discord's interactions endpoint through a `Seedcord` class. Node gets the default build. Cloudflare Workers get a separate edge `Seedcord` under the same name through the `workerd` export condition. Discord posts only interactions here.
Both transports re-export all of `@seedcord/core`. A bot installs one seedcord package. Code they share belongs in `core`. A plugin extends `@seedcord/core/plugin`, which both transport barrels re-export.
- Keep Node-only code out of anything the edge build reaches. Answer a question for both transports unless the task names one.
+ The edge build runs with Cloudflare's Node compat on. It may import `node:fs`, `node:path` and `node:url`. Keep node server code, like `node:http` and the process signal handlers, out of anything it reaches.
+
+ When someone asks how seedcord does something, answer for gateway and for http. Answer for one only when they ask about that one.
---
## Other important rules
- **Never throw a raw `Error`.** Framework code throws `SeedcordError`, `SeedcordTypeError`, `SeedcordRangeError`, or `SeedcordAggregateError` from `@seedcord/errors`, each with a registered code. Translate a third-party throw before it reaches a consumer if it makes sense to do so.
- **Write the message for whoever hit it.** Say what went wrong, and give them enough to act. Sometimes that means naming the likely cause, sometimes the env var to check, sometimes the one line that fixes it. Read it back as the developer who just got it at 2am, and cut anything they cannot use. Two throw sites that mean different things get different codes or better message arguments, because one message covering both tells the reader nothing.
- **Routes, class names, ids, paths, and package names get a `paint` tone** from `@seedcord/errors`. A package name gets `bold`. `paint` also carries `bold`, `italic` and `underline` for the parts that use no color. Multi-line output goes through `logger.utils`.
- **Derive types from their source.** A constructor shape is `TypedConstructor<typeof X>`. A member union comes from `keyof`, an indexed access, or `TypedExtract`. A hand-written copy drifts.
- **Only implement what was asked.** Surface additions as a question first. Wait for the third use before extracting an abstraction.
- **No dead code.** Before adding `export`, confirm something outside the file names the symbol. A symbol that exists only for a test belongs in the test folder.
- **Grep every consuming surface before deleting anything.** That includes markdown and generated output. Show no callers or name the replacement.
- **Commit subjects are one lowercase line with no scope and no body.** Write `feat:`, never `feat(http):`. A breaking change marks the bare type, `feat!:`. A PR title follows the same rule, because a squash merge turns it into the commit.
- **Move files with `git mv`.** Add dependencies with `pnpm add` so the lockfile updates.
---
## Commands and gates
Run from the repo root. `pnpm -C <package> <script>` targets one package.
```sh
pnpm -C <pkg> lint:fix # always lint:fix, never plain lint
pnpm -C <pkg> tc
pnpm -C <pkg> test # after lint and tc pass
```
Rebuild a shared package before you check its dependents:
```sh
pnpm -C packages/<name> build
pnpm -C packages/<dependent> tc
```
Two whole-workspace gates exist and they differ:
- `pnpm prePush` runs the checks through `turbo --affected`, against `next`.
- `pnpm prePush:all` runs every check across every package. A green `prePush` covers less than a green `prePush:all`, so run `prePush:all` before a release and at the end of a long branch.
Both start with `build`, which builds everything except the three sites in `apps/`, and `codegen:check`, then `check:catalog`, the script and markdown lint, `tc`, `lint`, `fmt:check`, and `test`. The root `package.json` has the exact chain.
`pnpm knip` (dead code) and `pnpm react-doctor` (React patterns) are real gates. **No hook and no CI job runs them.** Run them by hand when the change warrants it.
Zero lint errors, zero lint warnings, zero from `tc`, every test passing. Fix the cause. Never comment out a test, weaken an assertion, or add a broad `eslint-disable` to get past a failure. Disable a rule inline with a reason, `// eslint-disable-next-line <rule> -- <why>`, never file-wide.
Watch warnings. `lint-staged` passes `--max-warnings=0`, so a warn-severity rule blocks your commit. Plain `pnpm lint` lets it through.
Regenerate docs with `pnpm docs:extract` after changing the public surface.
---
## Tests
Tests live in `<package>/tests/` mirroring `src/`, never beside the source. Vitest runs them.
Reproduce a bug with a failing test before fixing it. A test that passes before the fix proves nothing. This holds however the bug surfaced.
Tests reach behavior through the public members of a class or a module. A test that reads a private method, or mocks a collaborator you own, breaks on the next refactor and tells you nothing about the contract.
The import path is a separate question. Every test imports through its own package's `#src/*` alias, which resolves to the same module the barrel re-exports. A consuming package imports by package name, so a missing barrel export already fails that package's `tc`.
A test pins behavior. `tsc`, ESLint, and the build already reject a missing export, a renamed symbol, a wrong type, and a malformed manifest, so a test asserting any of those gives you a second place to edit when the rule changes.
No tautological tests. If a test can only fail when someone edits the value it checks, it checks nothing the code does. That covers a constant compared to its own literal, a mock checked for returning what the test stubbed, and a rule about authored content, like a page description's length.
Nothing gets a test for staying absent. A deleted export leaves no caller that compiles, and that is the proof.
A fixture cast is fine with a short comment naming what makes it safe. `any` stays banned in tests.
---
## Apps
The three Next.js apps have their own rules in [`apps/AGENTS.md`](apps/AGENTS.md). Read it before touching `apps/`.
---
## Ask before acting outside the repo
Anything that leaves this machine needs an explicit request first: opening a PR, filing or commenting on an issue, publishing a package, and deploying. Prepare the work, show it, and wait.