AGENTS.md · git:20260906.9d0de93 · 2026-09-06 · sha256 de93e63a6090cd83
AGENTS.md git:20260906.9d0de93A
Immutable. This exact content is served forever at /api/v1/blob/de93e63a6090cd83.
# Repository guidance
## Code hygiene
- **Extract and rewire in one change.** Every dead module this repo has had to
delete was born the same way: a refactor lifted helpers into a new file and
never switched the original over, so the monolith kept its inline copy and
the new file had zero importers from its first commit. If a commit adds
`foo-codec.ts`, the same commit deletes the code it replaced and leaves
`foo.ts` importing it. The follow-up PR that "wires it up" does not arrive.
- **A module with no production importer is not delivered.** This repo's
dominant failure mode is a thoroughly tested service that nothing mounts.
Before believing a capability exists, find the production caller, not the
test. Before opening a PR, confirm every file it adds is reachable from
`src/index.ts`, a route, a CLI entry, or a hook — a passing suite proves
nothing about whether the code runs.
- **Look for the helper before writing it.** `dev/http.ts` owns request and
response helpers (`diagnostic`, `requestError`, `isRequestDiagnostic`,
`responseDiagnostic`, `responseJson`, `singleHeader`, `isJsonRequest`,
`readBody`, `readJsonBody`, `rawPathname`, `decodedOpaqueSegment`);
`core/strict-json.ts`, `core/errors.ts`, `core/paths.ts`, and
`core/freeze.ts` own their equivalents. A route module that defines its own
`readBody` has forked a security-relevant bound that will be fixed in one
copy and not the other.
- **Never copy a helper to dodge an import cycle.** Move it to a leaf module
both sides import — `config/conventional-entry.ts` is the pattern. A comment
explaining why the copy exists documents the debt; it does not discharge it.
- **One class per name.** Two identical `class FooError` declarations in two
modules are not interchangeable: `instanceof` against the wrong one silently
returns `false`, so the `catch` that was supposed to handle it falls through.
Error classes live with the code that throws them, exported once.
- **Delete on sight.** Unreferenced code is not free — it is read during
review, matched by search, and copied by the next author who finds it before
the live version. Removing it is a `patch` changeset, not a project.
- Neither `pnpm lint` nor `pnpm typecheck` reports an unreferenced module, so
check by hand when a change adds or moves files:
```sh
# any tracked file that mentions the module, other than itself
git grep -l '<module-stem>' -- ':!repos'
```
One hit means the module only mentions itself and nothing imports it. Watch
for false positives from prose in `docs/**` and from strings that merely
contain the name: `Symbol('epoch-staging')` in `dev/epoch-store.ts` was the
only match for a 343-line dead file, which is why it read as reachable.
- Gate before pushing: `pnpm build && pnpm typecheck && pnpm lint && pnpm
test:unit`. Every Rstest pool and `pnpm typecheck` refuse to start over a
`dist` older than its sources (`scripts/dist-freshness.mjs`), so the build
comes first whatever the change touched.
## Workbench platform scope
- The developer Workbench is a desktop-only application.
- Design, implementation, and browser acceptance should target desktop viewports and desktop interaction patterns.
- Do not add mobile-specific layouts, responsive behavior, or mobile acceptance requirements unless the user explicitly requests them.
- A mobile-only layout defect is not a release blocker for this repository.
## Public examples
- Treat `examples/*` as user-facing products, not test fixtures.
- Use only public `agent-bundle` package exports and `workspace:*` dependencies.
- Validate examples at a 1440×900 desktop viewport; mobile support is not required.
- Never accept or capture a Workbench route while its loading state is still visible.
- Browser acceptance must cover populated state plus the documented stale-diagnostic and repair flow.
## Generated plugin output
- Generated plugin output is self-contained. The compiler profile in
`packages/agent-bundle/src/build/rslib.ts` (`composeEntryLibConfig`) bundles
every dependency of a generated executable — `output.autoExternal: false`,
`bundle: true`, `splitChunks: false`, no `externals`. Rslib's `node` target
leaves only Node built-ins (and `pnpapi`) external. The compiler service
(`src/build/compiler.ts`, `external-policy.ts`,
`dependency-audit-plugin.ts`) records every `ExternalModule` of every
host-pack, `dist`, and MCP App view compilation and fails the build
(`AB6005`) on anything but a Node built-in, `pnpapi`, or an emitted sibling
— whatever spelling Rspack emitted. The package build's `dist` bundles are
judged by the same rule and then walked like host-pack modules
(`src/build/package-build.ts` reuses `validateJavaScriptModules`), so a
generated executable in a host pack or in `dist` imports nothing but Node
built-ins from outside its tree. MCP App views
(`src/build/mcp-apps.ts`) inline every script and style into one HTML file.
The framework never adds `externals` to a plugin build; the `externals`
handling in `rslib.ts` (`reservedExternalsViolation`,
`guardReservedExternals`) only rejects reserved specifiers in the resolved
externals, which come from the author's `tools` hatch and Rslib's built-in
list, never from the profile.
- No refactor, toolchain upgrade, or "leaner install" change may enable
`autoExternal` or externalize a dependency on the author's behalf. A package
a consumer must install is the author's explicit decision, and an import
kept external through the `tools` hatch is not a way to make it anywhere:
`AB6005` fails such an import in a host pack and in `dist` alike. What
legitimately puts a package under `dependencies` is one of three `AB7014`
evidence sources: `runtimeDependencies` on a prebuilt payload
(`definePrebuilt`), a packed declaration reference, or a consumer-side
install script that names or runs it. The framework's own runtime modules
load no package at run time (`generated-module-evidence.test.ts`), so there
is no framework process-dependency record to read. `AB7015` additionally
requires a specifier a consumer's npm can install.
- Proof is compiler evidence, the persisted record, and packed-process
tests: each compilation's own external and module records are judged before
emission, `build` writes them beside the emitted files as
`agent-bundle.compile-evidence.json` (`src/build/compile-evidence.ts`) and
`validate --artifact` re-checks the record against the file table
(`AB6039`), the prepack gate judges declared dependencies from evidence,
and the packed pool (`pnpm test:packed`) installs the packed tarball into a
clean consumer, builds, removes the project source, and spawns the
generated entry as a real process (`packed-deleted-source`). The
emitted-module walk (`src/build/validate-artifact-modules.ts`) remains
only for what the compiler cannot see: an expression `import()` in a
compiled module (Rslib's profile leaves `import(<expression>)`,
`require(<expression>)`, `require.resolve(…)`, `createRequire(…)(…)`, and
`import.meta.resolve(…)` verbatim — no module, no external, no warning),
JavaScript the framework did not compile (`install.mjs`, copied scripts),
and every module of a build with a `tools` hatch (`coverage.rewritable`).
A compiled module the record proves is lexed, not parsed, and each literal
import it still carries is held to the record: a Node built-in or a
recorded external passes, anything else is an import the build was told
to ignore (`rspackIgnore`/`webpackIgnore` — Rspack leaves it verbatim with
no module, external, or warning) and fails `AB6005`. A matching digest
proves the bytes are the compiler's, never that every import in them was
resolved. The walk is not a second self-containment check and must not
grow one: it resolves nothing the record already judged.
## Documentation site
- `website/` is the public Rspress docsite
(<https://scriptedalchemy.github.io/agent-bundle/>), deployed from `main`
by `.github/workflows/docs.yml`. It is user-facing product, held to the
same accuracy bar as the code it describes.
- A PR that adds or changes user-facing behavior — a CLI command or flag,
config key, public export or entry point, hook event or result rule, host
target or artifact, diagnostic code, environment variable, or example —
updates the matching page under `website/docs/en/**` **and** its
`website/docs/zh/**` translation in the same PR. Ask the same question
when writing the changeset: if the summary is user-facing, the docs almost
certainly need the same change. Internal refactors, tests, and CI do not.
- Reference prose must match the source. State what the code does, not what
the design intended; when unsure, read the adapter or validator before
writing the sentence. Where the generated pages and hand-written pages
disagree, the generated pages are right — fix the hand-written one.
- Never hand-edit generated pages: `website/docs/{en,zh}/api/**` comes from
TypeDoc, and the hosts, events, and diagnostics reference pages are
rendered at build time from `packages/agent-bundle/src/adapters/capabilities/*.json`
and `docs/diagnostics.md`. Change the source, and the site follows.
- `pnpm docs:site:build` is the gate: typecheck, build, and Rspress's
dead-link, dead-anchor, dead-image, and language-parity checks. Parity
fails the build if one locale gains a page the other lacks. Run it before
pushing anything under `website/`, and after any change to public
exports, after `pnpm build`, since TypeDoc compiles the declarations under
`packages/agent-bundle/dist`.
- The site is desktop-first, like the Workbench. Wide tables scroll; code
samples wrap at roughly 90 columns so they render without horizontal
overflow at the default content width.
## Changesets
- Every PR that changes a publishable package (`packages/agent-bundle`,
`packages/rsc-runtime`, `packages/rsc-markdown-stream`,
`packages/create-agent-bundle` — anything except `tests/**`) must include
exactly one changeset: `pnpm changeset` or a
hand-written `.changeset/<slug>.md`. Private packages (`packages/workbench`,
`examples/*`, `website`) are ignored and never named in a changeset.
- Pre-1.0 semver: `minor` = breaking, `patch` = everything else (features
included). No `major` before 1.0.
- Summary: user-facing, imperative, names the command/export/config key,
mentions diagnostic codes, ends with `(#PR)`. Not an implementation note.
- The `Changeset present` CI check fails without one; the `skip-changeset`
label is the escape hatch for genuinely no-op changes only.
- The "Version Packages" PR is machine-owned. Never edit `CHANGELOG.md` or a
publishable `package.json` `version` by hand. Details: `.changeset/README.md`.
## Pull requests
- Every PR gets a deslop pass before review: read the full diff against
`origin/main` and remove what a human author would not have written —
comments that restate the code or break the file's style, defensive checks
and `try`/`catch` on trusted paths, `as any` / `as unknown as` casts that
only silence the type checker, nesting that early returns would flatten,
helpers that `dev/http.ts` or `core/*` already own, placeholder prose in
the PR body. Behavior stays unchanged unless the pass finds a clear bug.
Record it in the PR body ("Deslop: model, N edits").
- Every PR gets a self-review before merge: spawn a local reviewer subagent
(`change-risk-reviewer` if available, else `generalPurpose`; a different
model from the author's, e.g. `gpt-5.6-sol-high` or a Claude model — never
a Grok model for review) with the repo
path and the PR number or branch, asking for concrete merge risks only —
bugs, breaking changes, missed tests, doc or changeset gaps — against the
current diff vs `origin/main`. Fix or explicitly dismiss every finding in
the PR description under a "Self-review" section (reviewer model, findings,
disposition), run the reviewer once more after fixes, then merge on green
CI.
- CI green first. Address every review thread, whoever opened it — fix it in
the same PR or reply with a precise reason — and re-check for new threads
after each push until none remain. Only then merge.
- PRs are squash-merged. Review threads left on an already-merged PR must
still be answered, in a follow-up PR.
- `main` requires no status checks (owner decision, 2026-09-06): the local
gate is the merge gate. Before `gh pr merge --squash`, run on a branch that
contains current `origin/main`: `pnpm build && pnpm typecheck && pnpm lint
&& pnpm test:unit`, every integration/packed test file the diff touches,
and `pnpm docs:site:build` when `website/` or public exports changed;
paste the commands and their results in the PR body. Do not wait for CI on
the PR. CI still runs on `main` after the merge: whoever merged watches
that run and fixes or reverts a red `main` before starting anything else.
A later push to `main` cancels the superseded `CI` and `Package preview`
run (`concurrency` in `ci.yml`, `package-preview.yml`), so watch the tip
commit's run; `docs.yml` and `release.yml` never cancel a `main` run,
since a deploy or publish must not die mid-flight.
`gh pr update-branch` only when GitHub reports the branch as conflicting;
never `--admin`, never force-push `main`.
## Vendored repos
- `repos/` is **read-only reference material**. Do not edit, format, or import from `repos/**`.
- Application code imports the published npm package (`effect`), never a path under `repos/`.
- Before writing Effect code, read `repos/effect/LLMS.md` and the linked
`agent-patterns/effect-{stream,scope,concurrency,errors}.md`.
- Editor search, file watching, and auto-import exclude `repos/**`
(`.vscode/settings.json`). Subtree updates ride the same named chore as
the Effect RC re-pin — see `docs/effect-conventions.md`.