@expo/code-review-cli 0.1.0 → 0.2.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 CHANGED
@@ -1,26 +1,95 @@
1
- # expo-code-review
1
+ # @expo/code-review-cli
2
2
 
3
3
  A config-driven, multi-agent AI code reviewer. Specialist agents review a diff in
4
4
  parallel; a coordinator consolidates their findings into one structured review.
5
- Runs the same engine locally (advisory) and in CI (posts a PR comment).
5
+ The same engine runs locally (advisory) and in CI (posts one PR comment). The CLI
6
+ is the **engine** — each repo supplies its own agents and settings under
7
+ `.expo-code-review/`, so behavior is configured per-repo, not baked in.
8
+
9
+ > **Status: experimental.** Comment-only and non-blocking — it never blocks a merge
10
+ > and never auto-approves. See [`ROADMAP.md`](./ROADMAP.md).
11
+
12
+ Inspired in part by Cloudflare's [_How we built our AI code review bot_](https://blog.cloudflare.com/ai-code-review/).
13
+
14
+ ```mermaid
15
+ flowchart TD
16
+ SRC["Source<br/>local git · GitHub PR (gh)"] --> FILTER["Noise filter<br/>drop lockfiles · generated · binary"]
17
+ FILTER --> CHUNK["Chunk<br/>by changed lines (large diffs only)"]
18
+ CHUNK --> AGENTS["Agents (parallel)<br/>each .md in agents/ · read·grep·glob·list"]
19
+ CHUNK --> XCUT["Cross-cutting pass<br/>multi-file issues (large diffs)"]
20
+ AGENTS --> COORD["Coordinator<br/>dedupe · re-judge · decide"]
21
+ XCUT --> COORD
22
+ COORD --> VERIFY["Verify<br/>quote-ground · adversarially verify criticals"]
23
+ VERIFY --> REPORT["Reporter<br/>one PR comment (CI) · terminal (local)"]
24
+ ```
6
25
 
7
- > **Status: experimental.** Phase 1 is **comment-only and non-blocking** — it
8
- > never blocks a merge and never auto-approves. The package is incubated inside
9
- > `eas-cli` for fast iteration and is intended to graduate into its own repo; see
10
- > [`ROADMAP.md`](./ROADMAP.md).
26
+ ## Usage
11
27
 
12
- The CLI is the **engine**. Each repo supplies its own agents and settings under
13
- `.expo-code-review/`, so behavior is configured per-repo, not baked in.
28
+ Run via `npx @expo/code-review-cli <command>` (or the `ecr` / `expo-code-review`
29
+ binary once installed).
30
+
31
+ Reviewing a PR (`--pr`/`ci`) needs the GitHub CLI — `brew install gh && gh auth login`.
32
+ Everything else the reviewer needs (including the `opencode` runtime) ships with the
33
+ package.
34
+
35
+ ### First-time setup
36
+
37
+ Scaffold, add credentials, verify.
38
+
39
+ ```bash
40
+ # Scaffold .expo-code-review/ + a CI workflow (--no-workflow to skip)
41
+ npx @expo/code-review-cli init
42
+ ```
14
43
 
15
- ## How it works
44
+ Then give it model credentials. **Recommended: a Claude Pro/Max subscription** — the
45
+ scaffolded config uses OAuth by default, so just mint a token and export it under the
46
+ env var your `config.jsonc`'s `auth.tokenEnv` names:
16
47
 
48
+ ```bash
49
+ # Mint a Claude Pro/Max token (prints an sk-ant-oat… token)
50
+ claude setup-token
51
+ # Export it under the env var your config.jsonc's auth.tokenEnv names
52
+ export ANTHROPIC_OAUTH_API_KEY=sk-ant-oat...
53
+ # Check env, config, and credentials
54
+ npx @expo/code-review-cli doctor
17
55
  ```
18
- diff source ─▶ noise filter ─▶ chunk ─▶ agents (parallel) ─▶ coordinator ─▶ reporter
19
- (git / gh) drop lockfiles, by each agent reviews dedupe, one PR comment
20
- generated, changed every chunk + re-judge, (CI) or terminal
21
- binary files lines a cross-cutting pass decide output (local)
56
+
57
+ Prefer an Anthropic **API key**, or **OpenAI/GPT** or another provider? See
58
+ [Other providers & auth modes](#other-providers) at the bottom.
59
+
60
+ ### Reviewing (already configured)
61
+
62
+ ```bash
63
+ # Review working-tree changes; prints here, posts nothing
64
+ ecr review
65
+ # Review a GitHub PR by number (preview only)
66
+ ecr review --pr 4057
67
+ # …and post it as the PR comment
68
+ ecr review --pr 4057 --post
22
69
  ```
23
70
 
71
+ In CI it runs automatically from the scaffolded workflows — by label or a `/review`
72
+ comment (see **CI usage**). From Claude Code (or another agent), add a slash command
73
+ that runs it; eas-cli's
74
+ [`/expo-review`](https://github.com/expo/eas-cli/blob/main/.claude/commands/expo-review.md)
75
+ is a ready example to adapt.
76
+
77
+ ### Command reference
78
+
79
+ | Command | What it does |
80
+ | --- | --- |
81
+ | `ecr init [--no-workflow] [--force]` | Scaffold `.expo-code-review/` (config, agents, prompts) + a CI workflow. |
82
+ | `ecr review [options]` | Review local changes and print an advisory review (default command). |
83
+ | `ecr ci` | Review the current GitHub PR and post/update a comment. For GitHub Actions. |
84
+ | `ecr doctor` | Check environment, config, and model credentials. |
85
+
86
+ (When developing this repo itself, use `bun run src/cli.ts <command>`.)
87
+
88
+ ---
89
+
90
+ <details>
91
+ <summary><b>How it works</b></summary>
92
+
24
93
  - **Source** — local git (working tree, staged, or a ref range) or a GitHub PR
25
94
  (diff + metadata fetched over the `gh` API).
26
95
  - **Noise filter** — drops lockfiles, generated bundles/maps, snapshots, files
@@ -33,6 +102,8 @@ diff source ─▶ noise filter ─▶ chunk ─▶ agents (parallel) ─▶ coo
33
102
  run in parallel with read-only repo tools (`read`/`grep`/`glob`/`list`).
34
103
  - **Coordinator** — a single pass that dedupes, re-judges severity, and produces
35
104
  the final `{ decision, findings, summary }`.
105
+ - **Verify** — quote-grounds every finding against the real file and adversarially
106
+ verifies criticals, so a confident-but-wrong finding doesn't ship.
36
107
  - **Reporter** — posts/updates a single fingerprinted PR comment (CI), or prints
37
108
  a grouped summary (local). Findings below the configured severity floor are
38
109
  suppressed.
@@ -40,19 +111,10 @@ diff source ─▶ noise filter ─▶ chunk ─▶ agents (parallel) ─▶ coo
40
111
  Built on the [OpenCode](https://opencode.ai) SDK, which spawns the model provider
41
112
  and applies Anthropic prompt caching automatically.
42
113
 
43
- ## Commands
114
+ </details>
44
115
 
45
- Run via the workspace during incubation (`yarn workspace expo-code-review dev …`),
46
- or as the `ecr` / `expo-code-review` binary once built/installed.
47
-
48
- | Command | What it does |
49
- | --- | --- |
50
- | `ecr review [options]` | Review local changes and print an advisory review (default command). |
51
- | `ecr ci` | Review the current GitHub PR and post/update a comment. For GitHub Actions. |
52
- | `ecr init [--with-workflow] [--force]` | Scaffold `.expo-code-review/` (config, agents, prompts) in this repo. |
53
- | `ecr doctor` | Check environment, config, and model credentials. |
54
-
55
- ### `ecr review` options
116
+ <details>
117
+ <summary><b><code>ecr review</code> options</b></summary>
56
118
 
57
119
  ```
58
120
  --base <ref> Base ref to diff against (default: merge-base with default branch)
@@ -70,18 +132,14 @@ or as the `ecr` / `expo-code-review` binary once built/installed.
70
132
  -h, --help Show help
71
133
  ```
72
134
 
73
- Reviewing a PR without checking it out — preview, then optionally post:
74
-
75
- ```bash
76
- ecr review --pr 4057 # print the review here; posts nothing
77
- ecr review --pr 4057 --post # re-run and post it as the PR comment
78
- ```
79
-
80
135
  `--pr` uses the PR's diff (authoritative) but reads your checked-out files for
81
136
  surrounding context; for full fidelity, `gh pr checkout <n>` first and run a plain
82
137
  `ecr review`.
83
138
 
84
- ## Configuration — `.expo-code-review/`
139
+ </details>
140
+
141
+ <details>
142
+ <summary><b>Configuration — <code>.expo-code-review/</code></b></summary>
85
143
 
86
144
  ```
87
145
  .expo-code-review/
@@ -101,14 +159,19 @@ surrounding context; for full fidelity, `gh pr checkout <n>` first and run a pla
101
159
  ---
102
160
  description: One line the router uses to decide relevance.
103
161
  alwaysRun: true # run even when the router would skip this agent
104
- model: anthropic/claude-sonnet-5 # override the default model (e.g. haiku for the coordinator)
162
+ model: anthropic/claude-sonnet-5 # override the default model
105
163
  temperature: 0.1
106
164
  ---
107
165
 
108
166
  # Agent instructions in Markdown…
109
167
  ```
110
168
 
111
- ### `config.jsonc`
169
+ For a real-world example, see eas-cli's
170
+ [`.expo-code-review/`](https://github.com/expo/eas-cli/tree/main/.expo-code-review)
171
+ — correctness/security/consistency agents, Opus for security + the coordinator, and
172
+ per-repo `noise.additionalIgnores`.
173
+
174
+ `config.jsonc` (JSONC — comments + trailing commas supported):
112
175
 
113
176
  ```jsonc
114
177
  {
@@ -119,142 +182,115 @@ temperature: 0.1
119
182
  "breakGlass": { "marker": "/skip-review" }, // PR body marker that skips the review
120
183
  "commentTag": "expo-ai-code-reviewer", // hidden tag used to find/update the comment
121
184
  "auth": { "mode": "oauth", "provider": "anthropic",
122
- "tokenEnv": "DO_NOT_USE_EXPERIMENTAL_ANTHROPIC_API_KEY" }
185
+ "tokenEnv": "ANTHROPIC_OAUTH_API_KEY" }
123
186
  }
124
187
  ```
125
188
 
126
- JSONC (comments + trailing commas) is supported.
189
+ </details>
127
190
 
128
- ## Authentication
191
+ <details>
192
+ <summary><b>Model selection</b></summary>
129
193
 
130
- Model credentials come from OpenCode. Two modes, set in `config.auth`:
194
+ Precedence: **`REVIEWER_MODEL` env** (global override) per-file **frontmatter
195
+ `model:`** → **`config.jsonc` `model`** (the default). So a repo can run a mixed
196
+ setup, and a developer can override everything locally.
131
197
 
132
- - **`api-key`** the token in `tokenEnv` is copied into the provider's API-key
133
- env var (e.g. `ANTHROPIC_API_KEY`).
134
- - **`oauth`** a Claude Pro/Max token (from `claude setup-token`, an
135
- `sk-ant-oat…` token, *not* an x-api-key) is written into an isolated OpenCode
136
- `auth.json` as a bearer credential, so it uses the native subscription path.
137
-
138
- Set **`REVIEWER_MODEL`** to override the model for every agent and use your own
139
- OpenCode login instead of the repo's configured credentials — handy locally
140
- (e.g. `REVIEWER_MODEL=openai/gpt-5.4-mini-fast`). There is no shared fallback key;
141
- if a run fails for lack of credentials, authenticate a provider in OpenCode.
142
-
143
- Run `ecr doctor` to diagnose setup.
144
-
145
- ## Model selection
146
-
147
- Models are resolved with this precedence: **`REVIEWER_MODEL` env** (global override)
148
- → per-file **frontmatter `model:`** → **`config.jsonc` `model`** (the default). So a
149
- repo can run a mixed setup, and a developer can override everything locally.
150
-
151
- Rules of thumb for the reviewer's workload:
152
-
153
- - **Specialist agents** (correctness/security/consistency) do the real bug-finding
154
- and benefit from a reasoning-tier model — **Sonnet** is the quality/speed sweet
155
- spot (the default for correctness/consistency). **Opus** finds more but is slower
156
- and more rate-limited, which makes large-PR timeouts worse — so scope it to the
157
- one agent where the extra threat-model reasoning pays off most: **security runs on
158
- Opus** (set in `security.md` frontmatter), the rest on Sonnet. This keeps the
159
- latency/rate-limit cost to a single agent, and the timeout handling (subdivide +
160
- per-fetch deadline) keeps a slow Opus pass from hanging the run.
161
- - **The coordinator** only consolidates text (no repo tools), so a fast, cheap
162
- model — **Haiku** — fits well and keeps the serial tail short. Set it in
163
- `coordinator.md` frontmatter.
198
+ - **Specialist agents** (correctness/security/consistency) benefit from a
199
+ reasoning-tier model — **Sonnet** is the quality/speed sweet spot (default for
200
+ correctness/consistency). **Opus** finds more but is slower and more
201
+ rate-limited, so scope it to the highest-stakes agent: **security runs on Opus**
202
+ (set in `security.md` frontmatter), the rest on Sonnet.
203
+ - **The coordinator** makes the final call (dedupe / re-judge / decide) — worth a
204
+ strong model; set it in `coordinator.md` frontmatter.
164
205
  - If latency/timeouts dominate on big PRs, moving the specialists to a faster model
165
206
  is the most direct lever (a real recall tradeoff — measure it).
166
207
 
167
- Example mixed setup:
168
-
169
- ```jsonc
170
- // config.jsonc
171
- "model": "anthropic/claude-sonnet-5" // default: specialists + cross-file pass
172
- ```
173
- ```markdown
174
- <!-- security.md frontmatter --> → Opus for the highest-stakes agent
175
- ---
176
- model: anthropic/claude-opus-4-8
177
- ---
178
-
179
- <!-- coordinator.md frontmatter --> → Haiku for the text-only consolidation
180
- ---
181
- model: anthropic/claude-haiku-4-5-20251001
182
- ---
183
- ```
184
-
185
208
  There is no automatic cross-provider "equivalent" fallback — that would silently
186
209
  change which model reviewed your code. Use an explicit override instead.
187
210
 
188
- ## Reliability
211
+ </details>
189
212
 
190
- A review must never hang, silently produce nothing, or present an unreviewed
191
- change as "looks good":
213
+ <details>
214
+ <summary><b>Reliability</b> never hangs, never silently drops work</summary>
192
215
 
193
- - **Per-task time caps** — focused chunk passes get 15 min; the cross-cutting pass
194
- gets 25 min; the coordinator gets 10 min. A global passes budget (32 min) bounds
195
- all passes incl. the subdivision waves below, so everything fits inside the CI
196
- job's `timeout-minutes` (60), since the coordinator + verification run afterward.
216
+ - **Per-task time caps** — chunk passes 15 min; cross-cutting 25 min; coordinator
217
+ 10 min. A global passes budget (32 min) bounds all passes incl. the subdivision
218
+ waves, fitting inside the CI job's `timeout-minutes` (60).
197
219
  - **Tool-call cap** — a pass that makes too many `read`/`grep` calls without
198
- finishing is *wandering*, not converging (the usual cause of a non-convergent
199
- timeout). Hitting the cap trips the same soft landing as the time cap.
220
+ finishing is *wandering*, not converging; hitting the cap trips the soft landing.
200
221
  - **Soft landing on timeout** — at either cap, the run is interrupted and the agent
201
222
  is asked to return the findings it already has, rather than discarding its work.
202
- - **Subdivide-on-timeout — the reviewer never silently drops work.** If a pass
203
- times out with nothing to show, its chunk is split in half and the halves are
204
- re-reviewed (recursively, down to a single file). A chunk that won't converge at
205
- 13 files almost always converges at 6. If even a single file won't converge, a
206
- fast **no-tools fallback** reviews just its inlined diff (a lighter review, but
207
- never nothing). Only if *that* can't finish inside the budget is a coverage gap
208
- reported — and it is always reported, never silent.
223
+ - **Subdivide-on-timeout**a pass that times out with nothing to show has its
224
+ chunk split in half and the halves re-reviewed (recursively, down to a single
225
+ file), then a fast **no-tools fallback** over the inlined diff. Only a genuinely
226
+ un-reducible pass reports a coverage gap and it is always reported, never silent.
209
227
  - **Parse failures are retried** (same session, then once in a bounded fresh
210
- session); that is separate from the timeout path above.
211
- - **A failed run never reads as "Approve"** — if every pass fails, the review says
212
- it could not complete (treat as unreviewed); if some passes fail, the decision
213
- is never a clean approve, and the coordinator is told coverage was reduced.
214
- - **The coordinator can't sink the run** — if the consolidation step fails, findings
215
- are merged deterministically and still posted, rather than thrown away.
216
- - **Coverage notes**passes that timed out or failed are listed so a real
217
- coverage gap is never silent (routine noise filtering is *not* flagged it's
218
- expected and stays in the run log).
219
- - **CI always gets a terminal state** — on any failure the PR gets a comment saying
220
- the reviewer didn't run, not a stuck reaction and silence.
221
-
222
- ## CI usage
223
-
224
- `ecr init --with-workflow` scaffolds a `pull_request` workflow. In this repo the
225
- reviewer runs via two workflows, split along a clean line: **comments = one-shot
226
- actions, labels = persistent configuration.**
227
-
228
- - **`expo-code-review-command.yml`** — one-shot `/review` comments (maintainers):
229
- - `/review` run once now; the router picks the agents
230
- - `/review all` — run once with every agent
231
- - `/review correctness security` run once with just those agents
232
-
233
- These never change configuration.
234
- - **`expo-code-review.yml`**continuous review, configured by **labels**:
235
- - `ai-review` auto-review every push; the router picks the agents
236
- - `ai-review:all`auto-review with every agent
237
- - `ai-review:<agent>` auto-review with only those agents (e.g.
238
- `ai-review:security`); combine several to widen the set
239
- - `ai-review:skip` — never auto-review this PR (opt-out)
240
- - **`expo-code-review-dismiss.yml`** dismiss/restore a finding on a PR (maintainers):
241
- - `/dismiss <id> [<id> …] [-- reason]` hide finding(s); they move to a collapsed
242
- "Dismissed" section and stay dismissed across re-reviews
243
- - `/undismiss <id> …` — restore them
244
-
245
- Each finding shows a short `` `id:…` `` in the comment. Dismissal is a **display
246
- filter only** — the reviewer still analyzes the code every run, and a `critical`
247
- or `secrets` finding can never be hidden this way. (Also: an inline
248
- `expo-code-review-ignore` comment on/above a line suppresses that line's findings,
249
- same critical/secrets carve-out.)
250
-
251
- These workflows are comment-only (they never fail the PR's checks). For security,
252
- they build/run only the trusted base ref (never the PR head) — see the comment at
253
- the top of each file.
254
-
255
- ## Run logs
228
+ session) separate from the timeout path.
229
+ - **A failed run never reads as "Approve"** — all passes fail "could not
230
+ complete"; some fail never a clean approve, and coverage-reduced.
231
+ - **The coordinator can't sink the run** if consolidation fails, findings are
232
+ merged deterministically and still posted.
233
+ - **Coverage notes** passes that timed out/failed are listed (routine noise
234
+ filtering is *not* flagged it's expected and stays in the run log).
235
+ - **CI always gets a terminal state** on any failure the PR gets a "didn't run"
236
+ comment, not a stuck reaction and silence.
237
+
238
+ </details>
239
+
240
+ <details>
241
+ <summary><b>CI usage</b></summary>
242
+
243
+ `ecr init --with-workflow` scaffolds a `pull_request` workflow. Split along a clean
244
+ line: **comments = one-shot actions, labels = persistent configuration.**
245
+
246
+ - **command workflow** — one-shot `/review` comments (maintainers): `/review`
247
+ (router picks agents), `/review all`, `/review correctness security`. Never
248
+ changes configuration.
249
+ - **auto workflow**continuous review, configured by **labels**: `ai-review`
250
+ (router), `ai-review:all`, `ai-review:<agent>` (e.g. `ai-review:security`;
251
+ combine to widen), `ai-review:skip` (opt-out).
252
+ - **dismiss workflow** `/dismiss <id> [… -- reason]` / `/undismiss <id>`
253
+ (maintainers). Each finding shows a short `` `id:…` ``. Dismissal is a **display
254
+ filter only**the reviewer still analyzes everything, and a `critical`/`secrets`
255
+ finding can never be hidden. (An inline `expo-code-review-ignore` comment on/above
256
+ a line does the same, with the same critical/secrets carve-out.)
257
+
258
+ These workflows are comment-only (they never fail the PR's checks). The engine runs
259
+ as the published package via `npx`, so no PR-controlled code is built.
260
+
261
+ </details>
262
+
263
+ <details>
264
+ <summary><b>Run logs</b></summary>
256
265
 
257
266
  Each run appends a JSON line to `.expo-code-review/.runs/reviews.jsonl` with the
258
267
  inputs, decision, finding count, duration, per-agent cost, and aggregate token
259
- usage (including prompt-cache read/write counts) — for auditing and measuring
268
+ usage (incl. prompt-cache read/write counts) — for auditing and measuring
260
269
  cost/latency/cache reuse over time.
270
+
271
+ </details>
272
+
273
+ <a id="other-providers"></a>
274
+ <details>
275
+ <summary><b>Other providers & auth modes</b></summary>
276
+
277
+ The recommended setup is a Claude Pro/Max subscription (OAuth) — see Usage above.
278
+ Alternatives, all set in `config.auth` (credentials come from OpenCode):
279
+
280
+ - **Anthropic API key** — set `auth.mode` to `"api-key"` and point `tokenEnv` at the
281
+ env var holding the key (e.g. `ANTHROPIC_API_KEY`); it's sent as `x-api-key`. Omit
282
+ the `auth` block entirely to fall back to OpenCode's own login / `ANTHROPIC_API_KEY`.
283
+ - **OAuth (Pro/Max)** — `tokenEnv` holds an `sk-ant-oat…` token from
284
+ `claude setup-token` (*not* an x-api-key); it's written to an isolated OpenCode
285
+ `auth.json` as a bearer credential, using the native subscription path.
286
+ - **OpenAI / GPT, or another provider** — the current path is the `REVIEWER_MODEL`
287
+ env override: `opencode auth login` once (pick the provider), then run with
288
+ e.g. `REVIEWER_MODEL=openai/gpt-5.4-mini-fast`. It overrides every agent's model
289
+ and uses your OpenCode login, so no `auth` block is needed. *(First-class
290
+ per-provider config — Anthropic/OpenAI/others in `config.jsonc`, and mixing them
291
+ per agent — is on the [roadmap](./ROADMAP.md).)*
292
+
293
+ There is no shared fallback key; if a run fails for lack of credentials, authenticate
294
+ a provider in OpenCode. `ecr doctor` diagnoses setup.
295
+
296
+ </details>
@@ -9,12 +9,12 @@ const TEMPLATES_DIR = fileURLToPath(new URL('../../templates/', import.meta.url)
9
9
  const USAGE = `ecr init — scaffold .expo-code-review/ in the current repo
10
10
 
11
11
  Usage:
12
- ecr init [--with-workflow] [--force]
12
+ ecr init [--no-workflow] [--force]
13
13
 
14
14
  Options:
15
- --with-workflow Also write .github/workflows/expo-code-review.yml
16
- --force Overwrite existing files
17
- -h, --help Show this help
15
+ --no-workflow Skip writing the CI workflow (.github/workflows/expo-code-review.yml)
16
+ --force Overwrite existing files
17
+ -h, --help Show this help
18
18
  `;
19
19
  export async function initCommand(argv) {
20
20
  if (argv.includes('-h') || argv.includes('--help')) {
@@ -32,7 +32,10 @@ export async function initCommand(argv) {
32
32
  /** Scaffold .expo-code-review/ (and optionally the CI workflow) into the repo. */
33
33
  async function scaffold(argv) {
34
34
  const force = argv.includes('--force');
35
- const withWorkflow = argv.includes('--with-workflow');
35
+ // The CI workflow is scaffolded by default (most repos adopting this want it);
36
+ // `--no-workflow` opts out. `--with-workflow` is still accepted as a no-op for
37
+ // back-compat.
38
+ const withWorkflow = !argv.includes('--no-workflow');
36
39
  const root = (await repoRoot()) ?? process.cwd();
37
40
  const configDir = path.join(root, CONFIG_DIRNAME);
38
41
  // Create only the config dir; let copyInto create prompts/ so it reports
@@ -70,8 +73,8 @@ async function scaffold(argv) {
70
73
  ' 2. Configure a model provider in OpenCode (or set REVIEWER_MODEL).',
71
74
  ' 3. Run `ecr doctor`, then `ecr review`.',
72
75
  withWorkflow
73
- ? ' 4. Add the model-key secret referenced by the workflow.'
74
- : ' 4. Run `ecr init --with-workflow` to add the CI workflow.',
76
+ ? ' 4. Add the model-key secret referenced by the workflow, then add an `ai-review` label to a PR.'
77
+ : ' 4. (No CI workflow written — re-run without `--no-workflow` to add it.)',
75
78
  '',
76
79
  ].join('\n'));
77
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/code-review-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Generic, config-driven AI code reviewer engine. Repos supply their agents via .expo-code-review/.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "typecheck": "tsc --noEmit",
29
29
  "dev": "bun run src/cli.ts",
30
30
  "test:unit": "bun test",
31
+ "release": "bash scripts/release.sh",
31
32
  "prepublishOnly": "rimraf build && tsc -p tsconfig.build.json"
32
33
  },
33
34
  "dependencies": {
@@ -31,14 +31,16 @@
31
31
  // HTML marker used to find + update the single PR comment. Keep it stable.
32
32
  "commentTag": "expo-ai-code-reviewer",
33
33
 
34
- // How model credentials are provided.
35
- // "api-key": tokenEnv holds a provider API key (sent as x-api-key). If you
36
- // omit `auth`, OpenCode's own login / ANTHROPIC_API_KEY is used.
34
+ // How model credentials are provided. Default: a Claude Pro/Max subscription.
37
35
  // "oauth": tokenEnv holds a Claude Pro/Max OAuth token (from
38
36
  // `claude setup-token`); it's injected as a bearer credential.
37
+ // "api-key": tokenEnv holds a provider API key (sent as x-api-key). If you
38
+ // omit `auth`, OpenCode's own login / ANTHROPIC_API_KEY is used.
39
+ // For OpenAI/GPT or another provider, omit `auth` and set REVIEWER_MODEL after an
40
+ // `opencode auth login` for that provider.
39
41
  "auth": {
40
- "mode": "api-key",
42
+ "mode": "oauth",
41
43
  "provider": "anthropic",
42
- "tokenEnv": "ANTHROPIC_API_KEY"
44
+ "tokenEnv": "ANTHROPIC_OAUTH_API_KEY"
43
45
  }
44
46
  }
@@ -2,12 +2,13 @@ name: AI code review
2
2
 
3
3
  on:
4
4
  pull_request:
5
- types: [opened, synchronize, reopened]
5
+ types: [opened, synchronize, reopened, labeled, unlabeled]
6
6
 
7
- # Comment-only: needs to read the repo and write PR comments.
7
+ # Comment-only: read the repo, write PR comments (issue comments API).
8
8
  permissions:
9
9
  contents: read
10
10
  pull-requests: write
11
+ issues: write
11
12
 
12
13
  concurrency:
13
14
  group: ai-code-review-${{ github.event.pull_request.number }}
@@ -16,28 +17,53 @@ concurrency:
16
17
  jobs:
17
18
  review:
18
19
  runs-on: ubuntu-latest
20
+ # Opt-in per PR: only run when the `ai-review` label is present. Remove this
21
+ # line to review every PR automatically.
22
+ if: contains(join(github.event.pull_request.labels.*.name, ','), 'ai-review')
23
+ # Backstop so a stalled review fails fast instead of hanging.
24
+ timeout-minutes: 60
19
25
  # A reviewer failure must never fail the PR's checks.
20
26
  continue-on-error: true
21
27
  steps:
22
- - uses: actions/checkout@v4
28
+ - uses: actions/checkout@v5
23
29
  with:
24
- fetch-depth: 0
30
+ # Shallow is enough — the reviewer gets the diff from the API (`gh`).
31
+ fetch-depth: 1
25
32
 
26
- - uses: actions/setup-node@v4
33
+ # SECURITY: this workflow checks out the PR's code, including
34
+ # .expo-code-review/config.jsonc, whose auth.tokenEnv names the env var the CLI
35
+ # forwards as the model credential. Refuse to run unless it's the expected value
36
+ # (below / repo var ECR_EXPECTED_TOKEN_ENV) so a PR can't repoint it at another
37
+ # secret in the runner. Keep this in sync with auth.tokenEnv in config.jsonc.
38
+ - name: Guard config.jsonc tokenEnv
39
+ env:
40
+ EXPECTED: ${{ vars.ECR_EXPECTED_TOKEN_ENV || 'ANTHROPIC_OAUTH_API_KEY' }}
41
+ run: |
42
+ values=$(grep -oE '"tokenEnv"[[:space:]]*:[[:space:]]*"[A-Za-z0-9_]+"' .expo-code-review/config.jsonc | sed -E 's/.*"([A-Za-z0-9_]+)"$/\1/')
43
+ count=$(printf '%s\n' "$values" | grep -c .)
44
+ if [ "$count" != "1" ] || [ "$values" != "$EXPECTED" ]; then
45
+ echo "::error::.expo-code-review/config.jsonc auth.tokenEnv must be \"$EXPECTED\" (found: \"${values:-none}\"). Refusing to run so a PR can't redirect which secret is forwarded to the model provider."
46
+ exit 1
47
+ fi
48
+
49
+ - uses: actions/setup-node@v5
27
50
  with:
28
51
  node-version: 24
52
+ # The reviewer runs via npx and never installs with a package manager, so
53
+ # disable setup-node's auto package-manager cache (its post step would try
54
+ # to save an empty cache and error).
55
+ package-manager-cache: false
29
56
 
30
57
  - name: Run AI review
31
- # Requires the `expo-code-review` package to be published to npm. Until
32
- # then, vendor the CLI or run it from a checkout instead of via npx.
33
- # npx installs the CLI and its bundled `opencode` binary and puts them on
34
- # PATH for this process.
35
- run: npx --yes expo-code-review@latest ci
58
+ # npx installs @expo/code-review-cli and its bundled `opencode` binary and
59
+ # puts them on PATH for this process. Pin @latest to a version to freeze it.
60
+ run: npx --yes -p "@expo/code-review-cli@latest" ecr ci
36
61
  continue-on-error: true
37
62
  env:
38
63
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39
- # OpenCode reads ANTHROPIC_API_KEY. Source it from a dedicated,
40
- # code-review-scoped secret. Swap for your provider's key/var.
41
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY_FOR_CODE_REVIEW }}
42
- # Optional: override the model for every agent.
64
+ # Claude Pro/Max OAuth token (from `claude setup-token`) — the env var
65
+ # named by auth.tokenEnv in config.jsonc. Store it as a repo secret.
66
+ # (For an API key instead, set auth.mode "api-key" and pass that key here.)
67
+ ANTHROPIC_OAUTH_API_KEY: ${{ secrets.ANTHROPIC_OAUTH_API_KEY }}
68
+ # Optional: override the model for every agent (uses your OpenCode login).
43
69
  REVIEWER_MODEL: ${{ vars.REVIEWER_MODEL }}