@expo/code-review-cli 0.12.6 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +328 -495
- package/build/commands/ci.js +79 -22
- package/build/commands/setup-auth.js +7 -1
- package/build/core/auth.js +1 -0
- package/build/core/opencode.js +52 -0
- package/build/core/prior-review.js +47 -0
- package/build/core/prompts.js +62 -2
- package/build/core/render.js +5 -2
- package/build/core/review-cache.js +17 -0
- package/build/core/review.js +2 -2
- package/package.json +1 -1
- package/templates/atlantis.yml +2 -0
- package/templates/command.yml +2 -0
- package/templates/config.jsonc +7 -0
- package/templates/workflow.yml +2 -0
package/README.md
CHANGED
|
@@ -11,6 +11,10 @@ is the **engine** — each repo supplies its own agents and settings under
|
|
|
11
11
|
|
|
12
12
|
Inspired in part by Cloudflare's [_How we built our AI code review bot_](https://blog.cloudflare.com/ai-code-review/).
|
|
13
13
|
|
|
14
|
+
This README is the scannable overview. Every subsystem has a detailed design doc
|
|
15
|
+
under [`llp/`](./llp/) — start with
|
|
16
|
+
[LLP 0000](./llp/0000-expo-code-review-cli.explainer.md), the system map.
|
|
17
|
+
|
|
14
18
|
```mermaid
|
|
15
19
|
flowchart TD
|
|
16
20
|
SRC["Source<br/>local git · GitHub PR (gh)"] --> FILTER["Noise filter<br/>drop lockfiles · generated · binary"]
|
|
@@ -23,6 +27,70 @@ flowchart TD
|
|
|
23
27
|
VERIFY --> REPORT["Reporter<br/>one PR comment (CI) · terminal (local)"]
|
|
24
28
|
```
|
|
25
29
|
|
|
30
|
+
## Why not just `claude -p "review this diff"`?
|
|
31
|
+
|
|
32
|
+
A one-shot prompt in a workflow works — until the failure modes show up. The
|
|
33
|
+
engine exists for those:
|
|
34
|
+
|
|
35
|
+
- **Everything the reviewer reads is attacker-controlled.** The diff, the PR
|
|
36
|
+
description, and the repo files are all input an outside contributor wrote —
|
|
37
|
+
and a bare CLI run obeys instruction files from that same checkout
|
|
38
|
+
(`CLAUDE.md`, `.mcp.json`, plugins) while holding the model credential and
|
|
39
|
+
comment token. Here, configuration comes only from the trusted base commit,
|
|
40
|
+
those files are scrubbed before the engine starts, and every pass is read-only
|
|
41
|
+
([details below](#the-security-gap-specifically)).
|
|
42
|
+
- **Confident-but-wrong findings post as-is.** Raw model output is the review.
|
|
43
|
+
Here, every finding is quote-grounded against the real file and criticals are
|
|
44
|
+
adversarially verified before anything is posted.
|
|
45
|
+
- **Failures read as silence or approval.** A one-shot call that stalls, gets
|
|
46
|
+
rate-limited, or returns garbage either fails the job or posts nothing. Here,
|
|
47
|
+
each failure class has its own bounded retry, a failed run never renders as a
|
|
48
|
+
clean result, and CI always posts a terminal comment.
|
|
49
|
+
- **Big diffs blow the context window.** Large PRs are chunked with a separate
|
|
50
|
+
cross-cutting pass for multi-file issues; specialist agents run in parallel; the
|
|
51
|
+
prompt layout is cache-stable, and every run reports tokens, cost, and cache
|
|
52
|
+
hit rate.
|
|
53
|
+
- **Noise wastes the model's attention.** Lockfiles, generated bundles, and
|
|
54
|
+
binaries are filtered before the model sees them — recorded, never silently
|
|
55
|
+
dropped.
|
|
56
|
+
- **Comment spam.** One fingerprinted comment updated in place, with `/dismiss`,
|
|
57
|
+
severity floors, inline ignores, and author-reply tracking — not a new wall of
|
|
58
|
+
text per push.
|
|
59
|
+
- **Review prompts rot.** `ecr ref-check` fails when a prompt cites code that
|
|
60
|
+
moved or vanished, and every run warns loudly if a provider substituted a
|
|
61
|
+
different model than configured.
|
|
62
|
+
|
|
63
|
+
### The security gap, specifically
|
|
64
|
+
|
|
65
|
+
A review bot is a process holding a model credential and a GitHub write token
|
|
66
|
+
that reads attacker-controlled input for a living. A bare `claude -p` workflow
|
|
67
|
+
typically checks out the PR head and hands the model broad tools — so a PR that
|
|
68
|
+
says the right words can run code, read secrets, or post as you. The trust model
|
|
69
|
+
here ([LLP 0001](./llp/0001-trust-model.principles.md)) is built around that:
|
|
70
|
+
|
|
71
|
+
- **No PR-controlled code is ever built or executed.** The engine runs as the
|
|
72
|
+
published npm package via `npx`; the scaffolded workflows check out only the
|
|
73
|
+
base commit, with `persist-credentials: false`.
|
|
74
|
+
- **The model has no write tools and no ambient web.** `Read`/`Grep`/`Glob` only
|
|
75
|
+
— never `Bash`, `Edit`, or `WebFetch`. An injected instruction has nothing to
|
|
76
|
+
act with; the worst it can do is produce a wrong finding, which then has to
|
|
77
|
+
survive verification.
|
|
78
|
+
- **Review policy is not PR-editable.** Config, prompts, rosters, and the auth
|
|
79
|
+
mapping load from the PR's immutable base commit; `tokenEnv` is honored in
|
|
80
|
+
exactly one root-owned place, enforced at the schema level and again by an
|
|
81
|
+
independent CI guard step.
|
|
82
|
+
- **Credentials are compartmentalized.** Child environments are allowlists that
|
|
83
|
+
omit ambient keys; the research MCP never sees a model credential, the model
|
|
84
|
+
process never sees the search key, and outbound research queries fail closed
|
|
85
|
+
on credential-shaped input.
|
|
86
|
+
- **Untrusted text can't impersonate the reviewer.** PR prose travels inside
|
|
87
|
+
sanitized boundary markers, and every `<!--` in it is escaped so a forged
|
|
88
|
+
state marker can't hijack the dismissal list. `critical`/`secrets` findings
|
|
89
|
+
can't be dismissed or cleared by replies — enforced in code, not the prompt.
|
|
90
|
+
- **The blast radius is capped by design.** The review is advisory and
|
|
91
|
+
comment-only: even a fully fooled model can't approve, merge, or block
|
|
92
|
+
anything.
|
|
93
|
+
|
|
26
94
|
## Usage
|
|
27
95
|
|
|
28
96
|
Run via `npx @expo/code-review-cli <command>` (or the `ecr` / `expo-code-review`
|
|
@@ -52,15 +120,16 @@ npx @expo/code-review-cli doctor
|
|
|
52
120
|
The scaffolded default is **Anthropic via the Claude Code CLI**: locally your
|
|
53
121
|
`claude` login is enough, and for CI it helps you mint a token with
|
|
54
122
|
`claude setup-token`. It also handles the alternatives (an OpenAI **API key**,
|
|
55
|
-
a **ChatGPT/Codex subscription** sign-in
|
|
56
|
-
credential is missing.
|
|
123
|
+
a **ChatGPT/Codex subscription** sign-in, or a Meta Model API key for **Muse
|
|
124
|
+
Spark**). `doctor` offers to run it whenever a credential is missing.
|
|
57
125
|
|
|
58
126
|
In CI, store the credential as the repo secret the scaffolded workflow forwards
|
|
59
127
|
(`CLAUDE_CODE_REVIEW_SHARED_API_TOKEN` by default — an `sk-ant-oat…` token from
|
|
60
128
|
`claude setup-token`, or an `sk-ant-api…` Console key; the CLI reads either).
|
|
61
129
|
|
|
62
|
-
Prefer **OpenAI** (API key, or a ChatGPT/Codex subscription, or
|
|
63
|
-
another provider? See [Other providers & auth modes](#other-providers)
|
|
130
|
+
Prefer **Muse Spark**, **OpenAI** (API key, or a ChatGPT/Codex subscription, or
|
|
131
|
+
both mixed), or another provider? See [Other providers & auth modes](#other-providers)
|
|
132
|
+
below.
|
|
64
133
|
|
|
65
134
|
### Reviewing (already configured)
|
|
66
135
|
|
|
@@ -133,219 +202,92 @@ untrusted external context; see below).
|
|
|
133
202
|
|
|
134
203
|
## Keeping prompts true (`ecr ref-check`)
|
|
135
204
|
|
|
136
|
-
Good reviewer prompts cite real code
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`ecr ref-check` makes those citations checkable. Pin each one with a ref, in a comment
|
|
142
|
-
of its own (`<!-- … -->` in Markdown, `//` in JSONC):
|
|
205
|
+
Good reviewer prompts cite real code ("every webhook router must call
|
|
206
|
+
`sanitizeSecrets`"). Then the code moves and the prompt keeps citing a path that no
|
|
207
|
+
longer exists — the reviewer reasons from a fiction on every PR. `ecr ref-check`
|
|
208
|
+
makes those citations checkable: pin each one with a `@ref` comment (`<!-- … -->` in
|
|
209
|
+
Markdown, `//` in JSONC):
|
|
143
210
|
|
|
144
211
|
```md
|
|
145
212
|
<!-- @ref server/src/session.ts#createSession — the only place a session is minted -->
|
|
146
|
-
<!-- @ref server/src/entities/oauth/ — every provider lives here -->
|
|
147
213
|
<!-- @ref glob:**/*WebhookRouter.ts — the routers this rule is about -->
|
|
148
214
|
```
|
|
149
215
|
|
|
150
216
|
A target is a file, a `dir/`, `glob:<pattern>`, `file#symbol`, or `doc.md#heading` —
|
|
151
|
-
never a line number
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
exists — so those get pinned too, while `anthropic/claude-opus-5`, shaped the same way,
|
|
159
|
-
stays prose. For a token that only looks like a path, say so once:
|
|
160
|
-
`<!-- @ref-ignore knex.raw() -->`. It also checks what your config already declares —
|
|
161
|
-
`enforceAgents` ids, scope `config` directories, scope path globs.
|
|
162
|
-
|
|
163
|
-
Refs are repo-root-relative, including in a scope's own setup dir. A scope prompt that
|
|
164
|
-
cites `general-central/module` for `infrastructure/general-central/module` gets told the
|
|
165
|
-
root-relative form to use.
|
|
217
|
+
never a line number (line numbers rot without any signal). The check is strict on
|
|
218
|
+
purpose: any backticked token in `.expo-code-review/` that names something that
|
|
219
|
+
exists in the repo **must** be a ref, because stale citations are exactly the ones
|
|
220
|
+
nobody annotated. Mark the rare false positive once with
|
|
221
|
+
`<!-- @ref-ignore knex.raw() -->`. Refs are always repo-root-relative, including in
|
|
222
|
+
a scope's own setup dir, and config declarations (`enforceAgents` ids, scope
|
|
223
|
+
directories and globs) are checked too.
|
|
166
224
|
|
|
167
225
|
Two run points:
|
|
168
226
|
|
|
169
227
|
- `ecr ref-check` exits 1 on any problem. Run it in CI or a pre-commit hook.
|
|
170
|
-
- `ecr review` / `ecr ci` run it too
|
|
171
|
-
|
|
172
|
-
|
|
228
|
+
- `ecr review` / `ecr ci` run it too but never fail a PR's checks with it — broken
|
|
229
|
+
refs (and cited code *this PR* changes) surface as a **Review setup** note in the
|
|
230
|
+
comment.
|
|
231
|
+
|
|
232
|
+
Full detail: [LLP 0012](./llp/0012-config-ref-integrity.explainer.md).
|
|
173
233
|
|
|
174
234
|
---
|
|
175
235
|
|
|
176
|
-
##
|
|
236
|
+
## Platform research (bundled MCP)
|
|
177
237
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
238
|
+
The package bundles `review-research-mcp`, a local MCP exposed only to the reviewer
|
|
239
|
+
and cross-file passes (never the coordinator, verifier, or no-tools passes). When a
|
|
240
|
+
judgment depends on an externally owned API contract, an agent can search a fixed
|
|
241
|
+
catalog of official documentation providers — Apple/Android platform APIs, Expo,
|
|
242
|
+
React Native, OkHttp, Kotlin coroutines, Gradle/AGP, Swift evolution, and more — or
|
|
243
|
+
fetch one exact supported documentation URL. Queries are short exact symbols
|
|
244
|
+
(`CameraView barcodeScannerSettings` is useful; a source snippet or a question is
|
|
245
|
+
not), and an empty result stays empty rather than becoming a loose guess. There is
|
|
246
|
+
no offline index: every passage is fetched live during that review.
|
|
184
247
|
|
|
185
|
-
Enable it only in the root config
|
|
248
|
+
Enable it only in the root config (CI loads it from the PR's trusted base) and add a
|
|
249
|
+
`BRAVE_SEARCH_API_KEY` Actions secret (Expo and OkHttp use their own official search
|
|
250
|
+
and consume no Brave quota):
|
|
186
251
|
|
|
187
252
|
```jsonc
|
|
188
253
|
{
|
|
189
|
-
"research": {
|
|
190
|
-
"enabled": true,
|
|
191
|
-
"maxQueries": 8,
|
|
192
|
-
"resultsPerQuery": 2,
|
|
193
|
-
"timeoutMs": 30000
|
|
194
|
-
}
|
|
254
|
+
"research": { "enabled": true, "maxQueries": 8, "resultsPerQuery": 2, "timeoutMs": 30000 }
|
|
195
255
|
}
|
|
196
256
|
```
|
|
197
257
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
documentation providers. Search queries are normalized before
|
|
223
|
-
logging or networking: quoted literals, URLs, email addresses, paths, prose stop
|
|
224
|
-
words, overlong/high-entropy tokens, and unsupported punctuation are removed;
|
|
225
|
-
credential-shaped or secret-labeled input fails closed. The remaining query must be at
|
|
226
|
-
most eight short tokens and either contain an API-like symbol or be a short multi-word
|
|
227
|
-
lowercase concept phrase. Direct URLs must use plain
|
|
228
|
-
HTTPS with no credentials, port, query string, or fragment; suspicious/high-entropy
|
|
229
|
-
path segments fail closed. The fixed provider host/path allowlist and redirect,
|
|
230
|
-
response-size, content-type, and timeout checks still apply after that first gate.
|
|
231
|
-
These deterministic checks greatly reduce accidental exfiltration; they are not a
|
|
232
|
-
proof that every low-entropy string is harmless, so reviewer prompts also forbid
|
|
233
|
-
sending repository text and the review-wide MCP budget defaults to eight calls.
|
|
234
|
-
|
|
235
|
-
`maxQueries` bounds MCP calls, not network requests. One search selects up to four
|
|
236
|
-
providers, and each issues its own discovery request plus a page fetch per candidate,
|
|
237
|
-
so eight calls can mean roughly thirty discovery requests and over a hundred page
|
|
238
|
-
downloads. Every call therefore reports its own ledger — discovery requests, page
|
|
239
|
-
fetches, redirect hops, total HTTP requests, and elapsed time — and the review log and
|
|
240
|
-
Actions summary report the totals. `timeoutMs` is the MCP's own end-to-end deadline for
|
|
241
|
-
one call, enforced by the server across discovery, redirects, retrieval, and
|
|
242
|
-
extraction; a call that hits it returns what it already has rather than failing. It has
|
|
243
|
-
to live there because OpenCode's `timeout` bounds only tool discovery and Claude
|
|
244
|
-
provides no per-call timeout at all.
|
|
245
|
-
|
|
246
|
-
For non-Expo providers, discovery sends a fixed, provider-owned `site:` scope plus
|
|
247
|
-
the bounded query to Brave's fixed Web Search endpoint. Search snippets and titles
|
|
248
|
-
are never treated as evidence. ECR independently rejects off-allowlist result URLs,
|
|
249
|
-
manually validates every redirect, fetches a few official pages, verifies content
|
|
250
|
-
types and response sizes, extracts visible documentation text, and returns locally
|
|
251
|
-
ranked bounded passages. Sparse search-engine coverage therefore produces an honest
|
|
252
|
-
empty result rather than a loose guess.
|
|
253
|
-
|
|
254
|
-
Research is root-only in routed monorepos because it starts a host process; scope
|
|
255
|
-
configs cannot alter its network behavior or limits. Result-cache reuse remains
|
|
256
|
-
disabled while research is enabled because web results and documentation can change
|
|
257
|
-
without a config change.
|
|
258
|
-
|
|
259
|
-
The fixed provider catalog covers Apple/Android APIs plus SDWebImage, Media3, Glide, OkHttp,
|
|
260
|
-
Kotlin coroutines, Gradle/AGP, Swift concurrency/evolution, platform release/API
|
|
261
|
-
availability, Expo, React Native, Reanimated, Gesture Handler, Screens, and Worklets.
|
|
262
|
-
Queries are short exact symbols plus at most one useful member or behavior term. For
|
|
263
|
-
example, `CameraView barcodeScannerSettings` is useful; a source snippet, import path,
|
|
264
|
-
or natural-language question is not. The MCP publishes the same guidance in its tool
|
|
265
|
-
metadata. An empty result stays empty; it is not replaced with a loose semantic guess.
|
|
266
|
-
The tool metadata also includes an explicit provider map, so the reviewing model can
|
|
267
|
-
distinguish core platform APIs from release notes, dependency-owned documentation,
|
|
268
|
-
build-tool references, and issue-tracker context before choosing a corpus.
|
|
269
|
-
Reviewer instructions require grounding whenever a judgment depends on an externally
|
|
270
|
-
owned API contract, whether the evidence confirms a finding or dismisses a candidate
|
|
271
|
-
as safe; model memory alone is not treated as sufficient for those decisions.
|
|
272
|
-
Native source keeps its platform context: Apple or Android documents the OS contract,
|
|
273
|
-
while an explicit dependency provider documents library-owned behavior. Providers are
|
|
274
|
-
additive when both contracts matter. A path under `packages/expo-*` does not by itself
|
|
275
|
-
route Swift or Kotlin code to Expo's JavaScript documentation.
|
|
276
|
-
|
|
277
|
-
Direct clients can also call `fetch_platform_doc` with an exact documentation URL.
|
|
278
|
-
The tool infers the narrowest matching provider (or accepts an explicit provider
|
|
279
|
-
hint), then applies the same fixed HTTPS host/path allowlist, manual redirect checks,
|
|
280
|
-
10-second timeout, 5 MB response limit, content-type validation, extraction, and
|
|
281
|
-
passage bounds as search-discovered pages. It returns normalized extracted text, never
|
|
282
|
-
raw HTML or DocC JSON. An optional `query` selects context only within that one page;
|
|
283
|
-
it never broadens discovery. Context expands progressively:
|
|
284
|
-
|
|
285
|
-
- `focused` returns the best passage plus adjacent passages.
|
|
286
|
-
- `section` (the default) returns a contiguous window of at most 12,000 characters
|
|
287
|
-
around the best passage.
|
|
288
|
-
- `document` returns at most 20,000 characters of extracted page text and should be
|
|
289
|
-
used only when the contract is spread across the page.
|
|
290
|
-
|
|
291
|
-
The response reports returned and original character counts, whether it was truncated,
|
|
292
|
-
the anchor passage id, and bounded available passage ids. Search results also carry
|
|
293
|
-
neighboring passage ids so an agent can recognize when more local context exists. For example,
|
|
294
|
-
`https://developer.apple.com/documentation/swiftui/view/menustyle(_:)` is resolved to
|
|
295
|
-
Apple's DocC JSON and returned with the canonical page URL and API availability.
|
|
296
|
-
|
|
297
|
-
Every research-enabled review audits each sanitized outbound query and each returned result's
|
|
298
|
-
title, provider, provenance class, and canonical URL. GitHub Actions receives the
|
|
299
|
-
same audit trail in the step summary, while `.runs/reviews.jsonl` keeps the queries
|
|
300
|
-
plus bounded returned passages for short-lived operational inspection. Reviewers
|
|
301
|
-
are instructed to attach `sources` only when documentation materially supports a
|
|
302
|
-
finding. ECR accepts only exact URLs returned during that review, restores canonical
|
|
303
|
-
titles, carries citations through coordination, and renders them below the finding.
|
|
304
|
-
A citation to a URL this review never retrieved is dropped outright. Relatedness is a
|
|
305
|
-
separate, weaker guarantee: a cited finding is escalated to the verifier with the
|
|
306
|
-
audited passage inline, which judges whether that passage actually supports the claim
|
|
307
|
-
and strips the citation when it does not.
|
|
308
|
-
|
|
309
|
-
Reviewers also emit a bounded `researchDecisions` record only when documentation
|
|
310
|
-
materially confirms a finding candidate or proves one safe. ECR grounds those records
|
|
311
|
-
against the exact MCP audit and discards ungrounded claims. After verification and
|
|
312
|
-
suppression, the log and Actions summary report final findings with citations,
|
|
313
|
-
supported and dismissed candidates, and unique audited results materially used versus
|
|
314
|
-
unused. Counts use canonical URLs rather than passage count, so repeated hits do not
|
|
315
|
-
inflate usefulness.
|
|
316
|
-
|
|
317
|
-
For a query routed to the `expo` provider, `serve` POSTs the already-sanitized query
|
|
318
|
-
directly to Expo's public Algolia search endpoint and returns canonical
|
|
319
|
-
`docs.expo.dev` hits. The endpoint, application id, and browser-visible search-only
|
|
320
|
-
key are fixed in the package; redirects are rejected; response size, timeout, hit
|
|
321
|
-
count, and returned URL host are bounded.
|
|
322
|
-
|
|
323
|
-
OkHttp's newly migrated documentation is still sparse in Brave, so its provider
|
|
324
|
-
downloads the fixed official `lysine.dev` static search index, validates and indexes
|
|
325
|
-
it in memory once per MCP process, and rejects any entry outside the existing OkHttp
|
|
326
|
-
allowlist. Brave remains a fallback if that official index is unavailable.
|
|
327
|
-
|
|
328
|
-
There is no offline index. Every passage a review sees is fetched live from the
|
|
329
|
-
provider allowlist during that review, so evidence is never served from a local
|
|
330
|
-
artifact whose contents ECR cannot vouch for. The crawler, its seed catalog, and
|
|
331
|
-
`research.indexPath` were removed once live discovery replaced them; a config still
|
|
332
|
-
naming `indexPath` fails to parse rather than silently ignoring it.
|
|
333
|
-
Installation-specific provider configuration is intentionally
|
|
334
|
-
deferred: when added, it should follow the trusted root-config model used for agents
|
|
335
|
-
without permitting PR-controlled URLs, commands, or executable parsers. Expo skills
|
|
336
|
-
are complementary, not another search corpus: their pinned procedural guidance can
|
|
337
|
-
later be supplied to review agents as separately labeled trusted context, while
|
|
338
|
-
documentation search continues to return citable API evidence. Dynamic skills or
|
|
339
|
-
instructions retrieved from documentation must never become executable reviewer
|
|
340
|
-
instructions.
|
|
258
|
+
The boundary, in brief:
|
|
259
|
+
|
|
260
|
+
- **Bounded egress, not a confidentiality boundary.** Queries are normalized and
|
|
261
|
+
sanitized (credential-shaped or secret-labeled input fails closed); direct URLs
|
|
262
|
+
must be plain HTTPS on a fixed provider host/path allowlist; redirects, response
|
|
263
|
+
sizes, content types, and per-call deadlines are enforced server-side. The model
|
|
264
|
+
still chooses the query terms, so enable research only where repository-derived
|
|
265
|
+
terms may be shared with Brave and the documentation providers.
|
|
266
|
+
- **The server never sees a model credential.** ECR starts the MCP from its own
|
|
267
|
+
installed package through a wrapper that rebuilds the child environment from an
|
|
268
|
+
explicit allowlist; the Brave key never enters the model process either.
|
|
269
|
+
- **Audited and citable.** Every outbound query and returned result is audited (job
|
|
270
|
+
log, Actions step summary, `.runs/reviews.jsonl`). A finding can cite only URLs
|
|
271
|
+
actually retrieved during that review, and the verifier strips citations whose
|
|
272
|
+
passage does not support the claim.
|
|
273
|
+
- **`maxQueries` bounds MCP calls, not HTTP requests** — one search can fan out to
|
|
274
|
+
many discovery and page fetches, so each call reports its own request ledger and
|
|
275
|
+
the run reports totals.
|
|
276
|
+
- **Root-only in routed monorepos** (it starts a host process); scope configs
|
|
277
|
+
cannot alter it. Result-cache reuse is disabled while research is enabled,
|
|
278
|
+
because web content can change without a config change.
|
|
279
|
+
|
|
280
|
+
Full detail — providers, query grammar, `fetch_platform_doc` modes, provenance and
|
|
281
|
+
citation grounding: [LLP 0013](./llp/0013-platform-research.explainer.md).
|
|
341
282
|
|
|
342
283
|
## Monorepos (routing manifest)
|
|
343
284
|
|
|
344
285
|
A monorepo can route different subtrees to different reviewer rosters from a single
|
|
345
286
|
infra-owned manifest. There is still **one workflow, one `ecr ci` process** per PR:
|
|
346
|
-
it reads the changed files once, assigns each to exactly one scope
|
|
347
|
-
|
|
348
|
-
|
|
287
|
+
it reads the changed files once, assigns each to exactly one scope (scopes are
|
|
288
|
+
ordered, the **last** match wins — CODEOWNERS discipline), reviews each active scope
|
|
289
|
+
over only its files, and renders one comment — a single writer, so no comment race
|
|
290
|
+
and no locking.
|
|
349
291
|
|
|
350
292
|
```
|
|
351
293
|
your-monorepo/
|
|
@@ -390,79 +332,56 @@ your-monorepo/
|
|
|
390
332
|
}
|
|
391
333
|
```
|
|
392
334
|
|
|
393
|
-
- **
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
- **Scoped flags** — `ecr ci --scopes a,b
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
the override swaps the root config/manifest against the *real* scope tree, it
|
|
409
|
-
does not relocate the scopes themselves.**
|
|
410
|
-
- **Passes budget** — `defaults`-level `budget` bounds total review time:
|
|
411
|
-
`totalPassesMinutes` (default 55) is split across active scopes (which run
|
|
412
|
-
sequentially in one `ecr ci`), clamped up to `minScopeMinutes` (default 5) so a
|
|
413
|
-
single scope still gets a workable window. When enough scopes are active that the
|
|
414
|
-
floor would overshoot the total, `ecr ci` keeps the floor but warns, and `ecr
|
|
415
|
-
doctor` flags the worst case (`scopes × floor` vs total) — raise the workflow
|
|
416
|
-
`timeout-minutes` or trim scopes.
|
|
417
|
-
- **Adoption is incremental** — with no `routing.jsonc`, behavior is exactly as
|
|
418
|
-
before (single config). Add the manifest with just a default scope → still one
|
|
419
|
-
comment, identical behavior. Land per-team scope dirs one at a time; everything
|
|
420
|
-
else keeps hitting the default scope.
|
|
335
|
+
- **Keep a `**/*` catch-all scope** so no changed file goes unreviewed (`ecr
|
|
336
|
+
doctor` flags a coverage gap otherwise); broad scopes first, specific ones after.
|
|
337
|
+
- **Comment modes** — `single` posts one aggregated comment; `per-scope` posts one
|
|
338
|
+
namespaced comment per scope. A scope with zero matched files gets its stale
|
|
339
|
+
comment deleted.
|
|
340
|
+
- **Passes budget** — a `defaults`-level `budget` splits `totalPassesMinutes`
|
|
341
|
+
(default 55) across the active scopes, floored at `minScopeMinutes` (default 5)
|
|
342
|
+
per scope; `ecr ci` and `ecr doctor` warn when the floor can overshoot the total.
|
|
343
|
+
- **Scoped flags** — `ecr ci --scopes a,b`, `ecr ci --comment single|per-scope`,
|
|
344
|
+
`ecr review --scope <name>`, `--config-dir <dir>` / `ECR_CONFIG_DIR` (alternate
|
|
345
|
+
ROOT config+manifest dir — scope `config` paths stay repo-root-relative), and
|
|
346
|
+
`ecr doctor --list-scopes`.
|
|
347
|
+
- **Adoption is incremental** — no `routing.jsonc` means exactly the old
|
|
348
|
+
single-config behavior; a manifest with just a default scope is identical; land
|
|
349
|
+
per-team scope dirs one at a time.
|
|
421
350
|
|
|
422
351
|
### Security
|
|
423
352
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
a PR is reviewed with the root config until it merges.
|
|
447
|
-
- **The model runtime never sees PR-owned ambient config.** The head worktree the
|
|
448
|
-
agents read from is scrubbed of runtime configuration before the OpenCode
|
|
449
|
-
server starts: `opencode.json{,c}`, `.opencode/` (plugins), `AGENTS.md`,
|
|
450
|
-
`CLAUDE.md`, `.claude/`, `.mcp.json`, `.cursor*`, and `.env*` at every depth.
|
|
451
|
-
A PR can't install a plugin, MCP server, instruction file, or `.env` into the
|
|
452
|
-
process that holds the model credential and the comment token. (Changes to
|
|
453
|
-
those files are still reviewed — their diffs are inlined in the prompt — but a
|
|
454
|
-
finding citing one can't be re-read during verification; that's the tradeoff.)
|
|
353
|
+
Enforced in code and by an independent CI guard step, not by convention:
|
|
354
|
+
|
|
355
|
+
- **auth and research are locked to the root.** `tokenEnv` is honored only in the
|
|
356
|
+
root `config.jsonc` / `routing.jsonc` `defaults.auth`; a scope config declaring
|
|
357
|
+
`auth`/`breakGlass`/`research` fails to parse, and the CI guard sweeps every
|
|
358
|
+
config file repo-wide and refuses to run unless `tokenEnv` appears exactly once,
|
|
359
|
+
root-owned, equal to `ECR_EXPECTED_TOKEN_ENV`. Routing globs choose *which
|
|
360
|
+
roster* reviews a file, never *which secret* is sent.
|
|
361
|
+
- **enforceAgents can't be weakened.** Enforced agents (e.g. `security`) are
|
|
362
|
+
injected into every scope from the ROOT roster with `alwaysRun`; a scope defining
|
|
363
|
+
a same-id agent gets the root one.
|
|
364
|
+
- **Configuration comes from the PR's trusted base commit.** In `ecr ci`, all
|
|
365
|
+
review configuration loads from the PR's immutable base; the head is untrusted
|
|
366
|
+
content, materialized separately only to read and verify against. A PR editing
|
|
367
|
+
rosters, prompts, or routing is reviewed under the **previous** config, and a
|
|
368
|
+
missing base fails closed — never a fallback to the checkout. (Temporary escape
|
|
369
|
+
hatch: `ecr ci --unsafe-config-from-head`, with a loud warning; to be removed.)
|
|
370
|
+
- **The model runtime never sees PR-owned ambient config.** The head worktree is
|
|
371
|
+
scrubbed of `opencode.json{,c}`, `.opencode/`, `AGENTS.md`, `CLAUDE.md`,
|
|
372
|
+
`.claude/`, `.mcp.json`, `.cursor*`, and `.env*` before the engine starts, so a
|
|
373
|
+
PR can't install a plugin, MCP server, instruction file, or `.env` into the
|
|
374
|
+
process holding the model credential and comment token.
|
|
455
375
|
- **The scaffolded workflows check out only the base commit** with
|
|
456
|
-
`persist-credentials: false`;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
`ecr ci --unsafe-config-from-head` restores the old behavior with a loud
|
|
461
|
-
security warning and will be removed on a minor boundary.
|
|
376
|
+
`persist-credentials: false`; git fetches authenticate through `gh`, so the
|
|
377
|
+
token never lands in `.git/config` or argv. The CLI enforces the trust model
|
|
378
|
+
itself, so even a custom workflow that checks out the head still gets
|
|
379
|
+
base-commit configuration.
|
|
462
380
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
(
|
|
381
|
+
Enforce ownership with CODEOWNERS: `/.expo-code-review/routing.jsonc @your-infra`,
|
|
382
|
+
`/apps/api/.expo-code-review/ @your-api-team`. Full detail:
|
|
383
|
+
[LLP 0006](./llp/0006-config-schema-loading-routing.explainer.md) (routing) and
|
|
384
|
+
[LLP 0001](./llp/0001-trust-model.principles.md) (trust model).
|
|
466
385
|
|
|
467
386
|
---
|
|
468
387
|
|
|
@@ -493,62 +412,42 @@ Ownership is enforced with CODEOWNERS: `/.expo-code-review/routing.jsonc @your-i
|
|
|
493
412
|
feedback always run fresh.
|
|
494
413
|
|
|
495
414
|
Built on the [OpenCode](https://opencode.ai) SDK, which spawns the model provider
|
|
496
|
-
and applies the provider's prompt caching automatically.
|
|
415
|
+
and applies the provider's prompt caching automatically. Full detail:
|
|
416
|
+
[LLP 0002](./llp/0002-review-engine-pipeline.explainer.md) (pipeline) and
|
|
417
|
+
[LLP 0005](./llp/0005-verification-fingerprints-rendering.explainer.md)
|
|
418
|
+
(verification and rendering).
|
|
497
419
|
|
|
498
420
|
</details>
|
|
499
421
|
|
|
500
422
|
<details>
|
|
501
423
|
<summary><b>Tokens, cost & prompt caching</b></summary>
|
|
502
424
|
|
|
503
|
-
Every run reports what it spent and how much
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
`
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
in that prefix invalidates everything after it. The reviewer is laid out so the
|
|
530
|
-
prefix is stable — the system prompt (`shared.md` + the agent's own `.md`) is
|
|
531
|
-
byte-identical for every chunk an agent reviews, while the volatile parts (the
|
|
532
|
-
diff, file lists, PR metadata) travel in the user message *after* the prefix and
|
|
533
|
-
never touch it. OpenAI caches automatically (no write premium; cached input is
|
|
534
|
-
billed at a steep discount and shows up as `cache read`); Anthropic charges a
|
|
535
|
-
small premium to **write** the cache (~1.25× input) and ~0.1× input to **read**
|
|
536
|
-
it. Entries live minutes, refreshed on use — comfortably covering a run's
|
|
537
|
-
concurrent calls.
|
|
538
|
-
|
|
539
|
-
**Reading the numbers.** Hit rate = `cache read / (cache read + input)` — the
|
|
540
|
-
share of prompt tokens served from cache instead of being reprocessed at full
|
|
541
|
-
price. Multi-chunk reviews should show a high rate; single-chunk reviews mostly
|
|
542
|
-
show writes (there is nothing to re-read within the run).
|
|
543
|
-
|
|
544
|
-
**Keeping hits high:**
|
|
545
|
-
|
|
546
|
-
- Keep `shared.md` and `agents/*.md` stable. Any edit writes a new prefix — one
|
|
547
|
-
extra cache write per agent on the next run, then it is warm again. Never put
|
|
548
|
-
varying text (dates, PR numbers) into prompt files.
|
|
549
|
-
- Very short prompts may show `cache read 0`: prompts below the model's minimum
|
|
550
|
-
cacheable size (~1–4K tokens depending on the model) are silently not cached.
|
|
551
|
-
That is expected, not a bug.
|
|
425
|
+
Every run reports what it spent and how much was served from the prompt cache, in
|
|
426
|
+
three places: one `Token usage — …` line in the job log, a per-pass table + cache
|
|
427
|
+
hit rate in the GitHub Actions step summary (which also preserves each run's posted
|
|
428
|
+
comment, since the PR comment is updated in place), and one JSON line per run in
|
|
429
|
+
`.expo-code-review/.runs/reviews.jsonl` (uploaded as a CI artifact) with per-pass
|
|
430
|
+
tokens, raw per-agent findings, bounded reviewer traces, and coverage notes.
|
|
431
|
+
|
|
432
|
+
Each reviewer can also return a compact trace (up to three concrete checks and two
|
|
433
|
+
unresolved questions). It is stored only inside the hidden base64 comment marker as
|
|
434
|
+
`review.reviewTrace` — never rendered — declared
|
|
435
|
+
`trust: "unverified-model-diagnostics"`, and capped at 6 KB so it can't crowd
|
|
436
|
+
visible findings out of GitHub's comment-size limit.
|
|
437
|
+
|
|
438
|
+
**How the caching works.** Provider prompt caching is a *prefix match*: any byte
|
|
439
|
+
change in the prefix invalidates everything after it. The reviewer keeps the prefix
|
|
440
|
+
stable — the system prompt (`shared.md` + the agent's own `.md`) is byte-identical
|
|
441
|
+
for every chunk an agent reviews, while the volatile parts (diff, file lists, PR
|
|
442
|
+
metadata) travel after it. OpenAI caches automatically at a steep read discount;
|
|
443
|
+
Anthropic charges ~1.25× input to **write** and ~0.1× to **read**. Hit rate =
|
|
444
|
+
`cache read / (cache read + input)`; multi-chunk reviews should show a high rate,
|
|
445
|
+
single-chunk reviews mostly show writes.
|
|
446
|
+
|
|
447
|
+
To keep hits high: keep `shared.md` and `agents/*.md` stable (an edit costs one
|
|
448
|
+
cache write per agent on the next run, then it's warm again), and never put varying
|
|
449
|
+
text (dates, PR numbers) in prompt files. Prompts below the model's minimum
|
|
450
|
+
cacheable size (~1–4K tokens) show `cache read 0` — expected, not a bug.
|
|
552
451
|
|
|
553
452
|
</details>
|
|
554
453
|
|
|
@@ -637,60 +536,32 @@ change which model reviewed your code. Use an explicit override instead.
|
|
|
637
536
|
<details>
|
|
638
537
|
<summary><b>Reliability</b> — never hangs, never silently drops work</summary>
|
|
639
538
|
|
|
640
|
-
- **
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
- **
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
visible fact about a run, never a mystery slowdown.
|
|
667
|
-
- **Soft landing on timeout** — at either cap, the run is interrupted and the agent
|
|
668
|
-
is asked to return the findings it already has, rather than discarding its work.
|
|
669
|
-
Tools are disabled for that request, so the salvage step can't resume investigating
|
|
670
|
-
instead of answering.
|
|
671
|
-
- **Subdivide-on-timeout** — a reviewer pass that times out with nothing to show has
|
|
672
|
-
its chunk split in half and the halves re-reviewed (recursively, down to a single
|
|
673
|
-
file), then a fast **no-tools fallback** over the inlined diff (the cross-file pass
|
|
674
|
-
skips straight to the fallback, which still sees the whole diff). Only a genuinely
|
|
675
|
-
un-reducible pass reports a coverage gap — and it is always reported, never silent.
|
|
676
|
-
- **Parse failures are retried** (same session, then once in a bounded fresh
|
|
677
|
-
session) — separate from the timeout path.
|
|
678
|
-
- **Transient API errors are retried** (bounded backoff on 429/5xx/network) —
|
|
679
|
-
distinct from both the timeout path (abandon) and the parse path; a one-off blip
|
|
680
|
-
no longer drops an entire pass.
|
|
681
|
-
- **Auth failures surface once, and fail fast** — `ecr` checks the configured
|
|
682
|
-
provider's credential at startup and stops with one clear message if it's missing
|
|
683
|
-
(rather than failing every pass); a credential rejected mid-run (401/403)
|
|
684
|
-
collapses into a single actionable coverage note pointing at
|
|
685
|
-
`auth.tokenEnv`/`REVIEWER_MODEL`.
|
|
686
|
-
- **A failed run never reads as "Approve"** — all passes fail → "could not
|
|
687
|
-
complete"; some fail → never a clean approve, and coverage-reduced.
|
|
688
|
-
- **The coordinator can't sink the run** — if consolidation fails, findings are
|
|
689
|
-
merged deterministically and still posted.
|
|
690
|
-
- **Coverage notes** — passes that timed out/failed are listed (routine noise
|
|
691
|
-
filtering is *not* flagged — it's expected and stays in the run log).
|
|
692
|
-
- **CI always gets a terminal state** — on any failure the PR gets a "didn't run"
|
|
693
|
-
comment, not a stuck reaction and silence.
|
|
539
|
+
- **Time caps everywhere** — chunk passes 15 min, coordinator 10 min, and a global
|
|
540
|
+
55-min passes budget that fits inside the CI job's `timeout-minutes`. The
|
|
541
|
+
cross-file pass is elastic (it gets whatever budget is left) because halving its
|
|
542
|
+
file set would delete exactly the coverage it exists for.
|
|
543
|
+
- **Wandering and wedged passes are cut short** — a tool-call cap catches a pass
|
|
544
|
+
that reads without converging; a 4-minute stall detector abandons a wedged model
|
|
545
|
+
request and retries once from a clean session, inside the same budget.
|
|
546
|
+
- **Rate limits are waited out, not fought** — provider 429s (observed in the
|
|
547
|
+
OpenCode server log) turn a stall into 90s waits instead of re-sends; explicit
|
|
548
|
+
429s retry on a slow schedule; subscription (oauth) runs default to lower
|
|
549
|
+
concurrency. Rate-limit events are reported in the job and run logs, so
|
|
550
|
+
throttling is never a mystery slowdown.
|
|
551
|
+
- **Soft landing, then subdivision** — at a cap, the agent is asked to return the
|
|
552
|
+
findings it already has (tools disabled, so it can't keep investigating). A pass
|
|
553
|
+
with nothing to show has its chunk split and re-reviewed recursively, down to a
|
|
554
|
+
no-tools fallback over the inlined diff. Only a genuinely un-reducible pass
|
|
555
|
+
reports a coverage gap — always reported, never silent.
|
|
556
|
+
- **Bounded retries per failure class** — parse failures, transient API errors
|
|
557
|
+
(429/5xx/network), and timeouts each have their own separate retry path; a
|
|
558
|
+
one-off blip never drops a whole pass.
|
|
559
|
+
- **Failure can't read as success** — auth problems fail fast with one actionable
|
|
560
|
+
message; all-passes-failed reads "could not complete"; a partial run is never a
|
|
561
|
+
clean pass; a failed coordinator falls back to deterministic merging; and CI
|
|
562
|
+
always posts a terminal comment, never silence.
|
|
563
|
+
|
|
564
|
+
Full detail: [LLP 0002](./llp/0002-review-engine-pipeline.explainer.md).
|
|
694
565
|
|
|
695
566
|
</details>
|
|
696
567
|
|
|
@@ -718,19 +589,19 @@ line: **comments = one-shot actions, labels = persistent configuration.**
|
|
|
718
589
|
These workflows are comment-only (they never fail the PR's checks). The engine runs
|
|
719
590
|
as the published package via `npx`, so no PR-controlled code is built.
|
|
720
591
|
|
|
592
|
+
Full detail: [LLP 0009](./llp/0009-adoption-templates-and-ci-workflows.guide.md)
|
|
593
|
+
(workflows) and [LLP 0007](./llp/0007-cli-commands-and-ci.explainer.md) (commands).
|
|
594
|
+
|
|
721
595
|
</details>
|
|
722
596
|
|
|
723
597
|
<details>
|
|
724
598
|
<summary><b>Author feedback (replies to findings)</b></summary>
|
|
725
599
|
|
|
726
600
|
A PR author's reply to a finding is matched to it deterministically — by quoting
|
|
727
|
-
the finding's title back, or by citing its short `` `id:…` `` token
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
count above the fold; the reply's own text is never stored or rendered, only the
|
|
732
|
-
login, the comment link, and (optionally) an enum-valued verdict. Controlled by
|
|
733
|
-
the root-only `feedback` block in `config.jsonc`:
|
|
601
|
+
the finding's title back, or by citing its short `` `id:…` `` token — with no model
|
|
602
|
+
involved in the matching. A matched finding shows `💬 @login replied` (linked to
|
|
603
|
+
the comment); the reply's own text is never stored or rendered. Controlled by the
|
|
604
|
+
root-only `feedback` block in `config.jsonc`:
|
|
734
605
|
|
|
735
606
|
```jsonc
|
|
736
607
|
"feedback": {
|
|
@@ -743,36 +614,26 @@ the root-only `feedback` block in `config.jsonc`:
|
|
|
743
614
|
```
|
|
744
615
|
|
|
745
616
|
- **`annotate`** (the default) matches and shows "author replied" with zero effect
|
|
746
|
-
on the decision — safe
|
|
747
|
-
- **Clearing a finding always needs the `` `id:…` `` token
|
|
748
|
-
(an id inside a `>` quote does not count). Quoting the title
|
|
749
|
-
never
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
`` `id:…` ``. A `critical` finding, or one categorized `secrets`/
|
|
761
|
-
`security`, can never be cleared this way, whatever the config — that floor is
|
|
762
|
-
enforced in code, not the prompt.
|
|
763
|
-
- **`/undismiss <id>` wins over a reply.** Running it on a finding a reply cleared
|
|
764
|
-
puts the finding back in the active list and keeps it there: another reply from
|
|
765
|
-
the PR author can't clear it again. The restore is recorded against the FINDING
|
|
766
|
-
in the comment state, not against the reply, so editing or deleting the reply
|
|
767
|
-
doesn't drop it either. Only a maintainer lifts that — either `/dismiss <id>` on
|
|
768
|
-
the same finding, or a maintainer's own reply to it.
|
|
617
|
+
on the decision — safe even if you never touch this block.
|
|
618
|
+
- **Clearing a finding always needs the `` `id:…` `` token** in the replier's own
|
|
619
|
+
words (an id inside a `>` quote does not count). Quoting the title *annotates*,
|
|
620
|
+
never clears — otherwise GitHub's "Quote reply" could dismiss a finding on words
|
|
621
|
+
the PR author wrote.
|
|
622
|
+
- **`mode` and `dismiss` are independent axes.** `adjudicate` has a model re-check
|
|
623
|
+
the reply against the actual source and record a verdict; `dismiss:
|
|
624
|
+
"maintainers"` lets a maintainer's own reply dismiss with no model involved;
|
|
625
|
+
`dismiss: "adjudicated"` additionally accepts an author reply the model
|
|
626
|
+
confirmed. A `critical`, `secrets`, or `security` finding can never be cleared
|
|
627
|
+
this way, whatever the config — that floor is enforced in code, not the prompt.
|
|
628
|
+
- **`/undismiss <id>` wins over a reply** — it restores the finding and pins the
|
|
629
|
+
restore to the FINDING, so another author reply (or editing/deleting the old
|
|
630
|
+
one) can't clear it again; only a maintainer lifts that.
|
|
769
631
|
|
|
770
632
|
`ecr feedback` mines this substrate retroactively, with no model call and no
|
|
771
|
-
re-review: it crawls a repo's PRs,
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
PRs and drew a reply every single time.
|
|
633
|
+
re-review: it crawls a repo's PRs, matches non-bot replies against each existing
|
|
634
|
+
reviewer comment, and reports totals, a reply-rate, breakdowns by
|
|
635
|
+
category/severity/agent, and "repeat offenders" — findings whose title recurred
|
|
636
|
+
across 2+ PRs and drew a reply every time.
|
|
776
637
|
|
|
777
638
|
```bash
|
|
778
639
|
ecr feedback --repo your-org/your-repo --limit 100 --since 2026-06-01
|
|
@@ -780,19 +641,13 @@ ecr feedback --as my-review-bot # if CI posts under a PAT/app identity
|
|
|
780
641
|
ecr feedback --json # for scripting
|
|
781
642
|
```
|
|
782
643
|
|
|
783
|
-
The crawl matches the reviewer's comments by author
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
`ecr feedback` always reads `.expo-code-review/config.jsonc` from the LOCAL
|
|
788
|
-
checkout, even with `--repo`. If `--repo` points at a different repo, it warns
|
|
789
|
-
that `commentTag` may not match, so a zero-findings result there is not read as
|
|
790
|
-
zero pushback. It also warns when every scanned PR had no bot comment at all,
|
|
791
|
-
instead of leaving that as an easy-to-miss "0 with a bot comment" in the totals.
|
|
644
|
+
The crawl matches the reviewer's comments by author (`github-actions[bot]` by
|
|
645
|
+
default — pass `--as <login>` when your workflow posts under something else), and
|
|
646
|
+
always reads config from the LOCAL checkout, warning when `commentTag` may not
|
|
647
|
+
match a different `--repo`.
|
|
792
648
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
defaults are asymmetric.
|
|
649
|
+
Full detail — why matching is deterministic, why reply text is never echoed, why
|
|
650
|
+
the defaults are asymmetric: [LLP 0011](./llp/0011-author-feedback.explainer.md).
|
|
796
651
|
|
|
797
652
|
</details>
|
|
798
653
|
|
|
@@ -845,39 +700,54 @@ head commit, or local comment-policy fingerprint no longer matches.
|
|
|
845
700
|
<details>
|
|
846
701
|
<summary><b>Other providers & auth modes</b></summary>
|
|
847
702
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
`--strict-mcp-config`, `--permission-mode dontAsk`, and only the
|
|
864
|
-
`Read`/`Grep`/`Glob` tools — never `Bash`/`Edit`/`Write`/`WebFetch`/`WebSearch`.
|
|
865
|
-
The child env is an allowlist that omits ambient `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`
|
|
866
|
-
(only the configured credential is re-injected).
|
|
703
|
+
Everything is set in `config.auth`. Engines are inferred **per agent** from that
|
|
704
|
+
agent's resolved model: an `anthropic/…` agent runs through the Claude Code CLI,
|
|
705
|
+
any other provider runs through OpenCode — in the SAME run. `REVIEWER_MODEL`
|
|
706
|
+
overrides every agent's model (and therefore engine) at once. There is no shared
|
|
707
|
+
fallback key; `ecr doctor` diagnoses setup.
|
|
708
|
+
|
|
709
|
+
- **Anthropic / Claude (the scaffolded default).** The credential is (in order) a
|
|
710
|
+
`tokenEnv` you name, an ambient `CLAUDE_CODE_OAUTH_TOKEN`, or your local
|
|
711
|
+
`claude` login — an `auth` entry is entirely optional. `claude setup-token`
|
|
712
|
+
mints a Max/Team subscription token for CI, or point `tokenEnv` at a Console API
|
|
713
|
+
key (`sk-ant-api…`); the CLI reads either. Each pass is trust-isolated and
|
|
714
|
+
read-only: `--safe-mode` (no `CLAUDE.md`/hooks/MCP/plugins),
|
|
715
|
+
`--strict-mcp-config`, only `Read`/`Grep`/`Glob`, and an allowlisted child env
|
|
716
|
+
that omits ambient Anthropic credentials.
|
|
717
|
+
|
|
867
718
|
```jsonc
|
|
868
|
-
// The scaffolded default. No anthropic entry at all falls back to
|
|
869
|
-
// `claude` login.
|
|
719
|
+
// The scaffolded default. No anthropic entry at all falls back to `claude` login.
|
|
870
720
|
"auth": { "providers": {
|
|
871
721
|
"anthropic": { "tokenEnv": "CLAUDE_CODE_REVIEW_SHARED_API_TOKEN" }
|
|
872
722
|
} }
|
|
873
723
|
```
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
724
|
+
|
|
725
|
+
- **Meta / Muse Spark 1.2 (public Model API).** Set the default model and any
|
|
726
|
+
model-pinned agent frontmatter to `meta/muse-spark-1.2`. ECR supplies the fixed
|
|
727
|
+
public Responses endpoint and the `@ai-sdk/openai` adapter; the generic
|
|
728
|
+
Chat-Completions compatibility adapter is intentionally not used because it
|
|
729
|
+
loses Muse's reasoning continuity across tool turns. Standard and Contributor
|
|
730
|
+
model ids are supported.
|
|
731
|
+
|
|
732
|
+
```jsonc
|
|
733
|
+
"model": "meta/muse-spark-1.2",
|
|
734
|
+
"auth": { "providers": {
|
|
735
|
+
"meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
|
736
|
+
} }
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Create the key in the [Meta AI developer portal](https://developer.meta.com/ai/).
|
|
740
|
+
Locally, export it as `META_API_KEY`. In each review-running workflow, replace
|
|
741
|
+
the Anthropic credential line with
|
|
742
|
+
`META_API_KEY: ${{ secrets.META_API_KEY }}`, set the repo variable
|
|
743
|
+
`ECR_EXPECTED_TOKEN_ENV=META_API_KEY`, and remove the Claude CLI install step
|
|
744
|
+
if no configured agent uses an `anthropic/…` model. Muse runs with high reasoning.
|
|
745
|
+
|
|
746
|
+
- **OpenAI: ChatGPT/Codex subscription (OAuth) + usage-based API key** — the
|
|
747
|
+
recommended mix if you review with OpenAI. Default models run on the
|
|
748
|
+
subscription (zero marginal cost); a metered key covers subscription-excluded
|
|
749
|
+
pro models via a synthesized `openai-api` alias (agents reference
|
|
750
|
+
`openai-api/gpt-5.5-pro` in frontmatter):
|
|
881
751
|
|
|
882
752
|
```jsonc
|
|
883
753
|
"auth": { "providers": {
|
|
@@ -886,63 +756,26 @@ credentials through OpenCode.
|
|
|
886
756
|
} }
|
|
887
757
|
```
|
|
888
758
|
|
|
889
|
-
`
|
|
890
|
-
(`
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
- **Auditability**: every pass logs which provider/model answered it (job log,
|
|
911
|
-
step summary, run log), so the subscription/API split is visible per run.
|
|
912
|
-
One caveat: OpenCode can't price alias models (they're config-declared), so
|
|
913
|
-
pro passes report `$0` in the run log's cost column — token counts are
|
|
914
|
-
correct, and the OpenAI project dashboard is the source of truth for spend.
|
|
915
|
-
- **Another provider** — the current path is the `REVIEWER_MODEL`
|
|
916
|
-
env override: `opencode auth login` once (pick the provider), then run with
|
|
917
|
-
e.g. `REVIEWER_MODEL=google/gemini-3-pro`. It overrides every agent's model
|
|
918
|
-
and uses your OpenCode login, so no `auth` block is needed.
|
|
919
|
-
|
|
920
|
-
Engines are inferred **per agent** from that agent's resolved model alone: an
|
|
921
|
-
`anthropic/…` agent runs through the Claude Code CLI while other agents run through
|
|
922
|
-
OpenCode — in the SAME run. So an anthropic model may coexist with an `openai` (or
|
|
923
|
-
any other) OpenCode provider, and each agent's `model` selects its engine.
|
|
924
|
-
`REVIEWER_MODEL` still overrides every agent's model (and therefore every agent's
|
|
925
|
-
engine), converging the whole run onto one engine.
|
|
926
|
-
|
|
927
|
-
There is no shared fallback key; if a run fails for lack of credentials, log in
|
|
928
|
-
with `claude` (the default) or authenticate a provider in OpenCode. `ecr doctor`
|
|
929
|
-
diagnoses setup.
|
|
930
|
-
|
|
931
|
-
**Setup errors fail fast, with the fix in the message.** A bad credential or model id
|
|
932
|
-
would otherwise fail every pass identically — a run that spends its whole budget
|
|
933
|
-
rediscovering one fixable thing, then reports N coverage gaps. So before any pass runs:
|
|
934
|
-
|
|
935
|
-
- **The credential's shape is checked.** OpenCode refuses a malformed credential by
|
|
936
|
-
dropping the provider entirely, which then surfaces as "model not found" for every
|
|
937
|
-
model, with nothing pointing at the credential. A truncated value, surrounding
|
|
938
|
-
whitespace, or a token that can't work for the configured `auth.mode` is rejected
|
|
939
|
-
by name.
|
|
940
|
-
- **Configured model ids for OpenCode-routed providers are checked against the running
|
|
941
|
-
server**, so a typo or an id the provider doesn't have is reported once, up front,
|
|
942
|
-
with the close matches. `anthropic/…` (Claude Code) model ids aren't checked up
|
|
943
|
-
front — Claude validates them per-request, so a typo there surfaces as a per-pass
|
|
944
|
-
error instead.
|
|
945
|
-
- **`ecr doctor` reports the `opencode` version actually in use** and warns when a
|
|
946
|
-
different one is first on your `PATH` — runs use the version this package pins.
|
|
759
|
+
The oauth `tokenEnv` holds the ACCESS token from an `opencode auth login`
|
|
760
|
+
ChatGPT sign-in (`ecr setup-auth` extracts it) — never the single-use refresh
|
|
761
|
+
token. Access tokens expire (~10 days observed), so CI secrets need periodic
|
|
762
|
+
re-minting; `doctor` and the run preflight warn before expiry. The API key
|
|
763
|
+
needs only *Responses → Request* and *Chat completions → Request*, in a
|
|
764
|
+
dedicated budget-capped project. In CI, set `ECR_EXPECTED_TOKEN_ENV` to both
|
|
765
|
+
env names, comma-separated. Every pass logs which provider/model answered it,
|
|
766
|
+
so the subscription/API split is visible per run (alias-model passes report
|
|
767
|
+
`$0` cost — OpenCode can't price config-declared aliases; the OpenAI dashboard
|
|
768
|
+
is the source of truth for spend).
|
|
769
|
+
|
|
770
|
+
- **Another provider** — `opencode auth login` once, then run with e.g.
|
|
771
|
+
`REVIEWER_MODEL=google/gemini-3-pro`. No `auth` block needed.
|
|
772
|
+
|
|
773
|
+
Setup errors fail fast, with the fix in the message, instead of failing every pass
|
|
774
|
+
identically: the credential's shape is checked by name before any pass runs,
|
|
775
|
+
configured OpenCode model ids are validated against the running server (with close
|
|
776
|
+
matches suggested; `anthropic/…` ids are validated per-request by Claude instead),
|
|
777
|
+
and `ecr doctor` reports the `opencode` version actually in use.
|
|
778
|
+
|
|
779
|
+
Full detail: [LLP 0003](./llp/0003-model-runtimes-and-credentials.explainer.md).
|
|
947
780
|
|
|
948
781
|
</details>
|