@expo/code-review-cli (ecr) is a config-driven AI code reviewer engine.
Adopting repos scaffold .expo-code-review/ (agent prompts + config) and a CI
workflow; the CLI diffs a PR, fans the diff out to reviewer agents via OpenCode,
verifies their findings, and posts one updating PR comment.
src/cli.ts— entry point; dispatches tosrc/commands/*(init,review,ci,doctor,dismiss).src/core/— engine: diffing/chunking, prompt assembly, OpenCode server, finding verification, comment rendering, auth.src/research-mcp/+research/— bundled read-only documentation MCP, scoped search/allowlisted fetch adapters, optional trusted indexer, and built-in source catalog.src/sources/— where the diff comes from (local-git,github-pr).src/reporters/— where findings go (terminal,github).src/config/— zod schema + loader for.expo-code-review/config.jsonc;routing.tsparses the monoreporouting.jsoncmanifest and assigns changed files to scopes (last-match-wins).templates/— the filesecr initscaffolds into adopting repos. Keep them in sync with the code (config options, workflow steps, auth defaults).src/__tests__/—bun testunit tests, one file per module.
- Design docs live in
llp/; LLP 0000 is the root — read it first for orientation. - Before changing an area, read the LLPs that cover it.
- Implementing a non-obvious decision an LLP documents? Add a ref:
// @ref LLP NNNN#anchor — short gloss. - Update the LLP or the ref in the same commit as the design/code change that makes it stale — don't let them drift apart.
- Code that contradicts its referenced LLP is a signal: flag the conflict, don't silently "fix" either side.
./ref-check(bun run llp:check) validates refs and metadata; CI runs it on every push and PR.ecr ref-checkvalidates the SAME@refgrammar inside a repo's.expo-code-review/setup (see LLP 0012). The two checkers share the grammar: a change to target kinds, the<placeholder>rule, or the glob dialect must land in./ref-checkandsrc/core/config-refs.tstogether.
- TypeScript, ESM with NodeNext resolution: relative imports use the
.jssuffix. - Commands:
USAGEstring, handle-h/--helpfirst, report errors to stderr viaerrorMessage(error)(src/core/util.ts), setprocess.exitCode(2 for usage/failure) — never callprocess.exit(). - Child processes always go through
src/core/exec.ts(execFilewith argument arrays, no shell). - New config options must update
src/config/schema.ts,src/config/load.ts, andtemplates/config.jsonctogether. - Core paths must work in both local mode (
local-git+ terminal reporter) and CI mode (github-pr+ GitHub reporter). ecr cimust never fail a PR's checks — reviewer errors degrade gracefully.- Scope configs never carry
auth/breakGlass— those are root-only, enforced in the schema (ScopeReviewConfigSchema), the loader (loadAuthFromRoot), and the CI guard. With norouting.jsonc, behavior is byte-identical to single-config mode. opencode-aiand@opencode-ai/sdkare one unit — pinned EXACTLY, bumped together. The SDK spawns the CLI (launch("opencode")), so the two are a matched pair: a CLI older than the SDK rejects model ids the SDK accepts. Exact pins stop annpxinstall in CI from floating one ahead of the other, andstartOpencodeprepends our ownnode_modules/.bintoPATHso a machine's global install can't shadow the pinned one.ecr doctorprints the version actually in use. (The SDK's spawn is a barelaunch("opencode"); on POSIX — the supported platform — the cwd is never searched, so a PR-committed shim can't hijack it. The direct-spawnopencodecallers we own,ecr doctor/ecr setup-auth, are hardened viaresolveOpencodeCli.)- Fail fast and name the fix for setup errors. A bad model id or credential hits
every pass identically, so it must throw once, before any pass runs, with the fix in
the message (
assertModelsResolvable,checkOauthTokenShape) — never as N indistinguishable coverage gaps discovered after spending the run's budget.
- PR content (diffs, paths, titles, config in the reviewed repo) is untrusted.
Prompt interpolation goes through
sanitizeUntrusted/boundary markers insrc/core/prompts.ts. - Model output is untrusted: schema-validate it, and trust
file/line/evidenceonly after verification (src/core/verify.ts). - The model credential (
auth.tokenEnv) must never reach logs, error messages, or.expo-code-review/.runs/.FORBIDDEN_TOKEN_ENVSinsrc/core/auth.tsstops configs from exfiltrating non-provider secrets — don't weaken it.
bun run devruns the CLI from source;bun testruns unit tests;bun run typechecktype-checks.- Releases:
bun run release(seescripts/release.sh).