AGENTS.md ยท diff

git:20261017.7d9d0c6 to git:20261020.8b93d98

1 added, 1 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`, `release-version`, or `test-performance` 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 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.
- The edge build runs with Cloudflare's Node compat on. It may import `node:fs`, `node:path`, `node:url` and `node:util`. Keep node server code, like `node:http` and the process signal handlers, out of anything it reaches.
+ The edge build runs with Cloudflare's Node compat on. It may import `node:buffer`, `node:fs`, `node:path`, `node:url` and `node:util`. 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.
- **Reuse a code before you add one.** Search all of `ErrorCodes.ts`, every package's range included, for a code that describes the same failure, and pass what differs as message arguments. Add a code only when no existing code describes the failure, or when the fix differs from what the look-alike asks for. Two codes for one failure means a reader who searches one code misses the other's answers.
- **Pick the class by what went wrong.** `SeedcordTypeError` when the caller passed a value of the wrong kind or a combination the API rejects, like a decorator on the wrong class or a color that doesn't parse. `SeedcordRangeError` when a number falls outside its limits. `SeedcordAggregateError`, through `throwSingleOrAggregate`, when one pass collects several separate problems and reports them together. `SeedcordError` for a failure at run time, like I/O, Discord, the environment, or state.
- **Write the message for whoever hit it.** A message can carry four parts. Give it every part the reader needs to fix the problem without opening the source:
- **What** went wrong, stated as the fact.
- **Where** it came from: the class, file, key, route, env var, or value, painted per the rule below.
- **Why** it happened, when the what alone doesn't explain it.
- **What to do**, the one step that fixes it. A vague step like "check your config" counts as missing.
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.
- **Check what a dependency exports before you write it yourself.** [Check 9](.agents/skills/code-quality/PREVENT-REINVENTION.md#9-does-a-dependency-already-in-the-tree-export-it) has the steps.
- **Write each rule once.** When two places have to compute the same answer, like a path, a cache key, a wire format, or a validity check, both call one function/method. A comment like "keep in sync with X" means the rule exists twice. Extract the function and delete the comment. When the two copies can't share code, like a value the build writes into generated source and the runtime reads back, add a test that fails once they disagree. Without one, nothing catches two copies drifting apart. Each side's tests still pass, and someone reading one copy has no way to find the other.
- **Use what the language and the runtime already ship.** Before writing any helper, go looking for a built-in that does the job. Memory lags behind both, so search MDN and the Node docs, and read the `lib` declarations TypeScript ships. A hand-rolled copy of something the platform provides handles fewer cases and has no test suite behind it.
- **Suspect your own helper first.** Code that formats, parses, compares, or walks plain data, etc, is usually something the platform already does. Search for it before you write it, and look again when you read one that already exists.
- **Check that every runtime the code reaches has it.** A package runs on the oldest Node in its `engines` field, and anything the edge build imports also runs in workerd. Confirm the built-in exists in both before you use it.
- **Keep one copy.** When several packages need the same built-in wrapped, put the wrapper in `@seedcord/utils`. A package that can't import utils calls the built-in directly.
- **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.