@expo/code-review-cli 0.6.0 → 0.8.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 +151 -25
- package/build/cli.js +7 -0
- package/build/commands/ci.js +307 -36
- package/build/commands/dismiss.js +6 -0
- package/build/commands/doctor.js +170 -33
- package/build/commands/feedback.js +433 -0
- package/build/commands/init.js +231 -15
- package/build/commands/review.js +191 -51
- package/build/commands/setup-auth.js +86 -11
- package/build/commands/verify-config.js +3 -0
- package/build/config/load.js +39 -0
- package/build/config/routing.js +7 -0
- package/build/config/schema.js +99 -3
- package/build/core/adjudicate.js +194 -0
- package/build/core/auth.js +127 -10
- package/build/core/claude-code.js +691 -0
- package/build/core/context-file.js +42 -0
- package/build/core/coordinator.js +2 -2
- package/build/core/diff.js +1 -0
- package/build/core/exec.js +282 -9
- package/build/core/log.js +1 -0
- package/build/core/noise.js +5 -0
- package/build/core/opencode.js +117 -15
- package/build/core/prompts.js +330 -5
- package/build/core/render.js +274 -45
- package/build/core/responses.js +158 -0
- package/build/core/review.js +447 -39
- package/build/core/schema.js +219 -3
- package/build/core/scrub.js +63 -1
- package/build/core/stack-confirm.js +137 -0
- package/build/core/stack.js +25 -0
- package/build/core/step-summary.js +1 -0
- package/build/core/suppress.js +2 -0
- package/build/core/throttle.js +12 -0
- package/build/core/util.js +18 -0
- package/build/core/verify.js +18 -1
- package/build/reporters/github.js +544 -44
- package/build/reporters/terminal.js +2 -0
- package/build/sources/github-pr.js +286 -7
- package/build/sources/local-git.js +6 -2
- package/build/sources/source.js +35 -0
- package/package.json +4 -3
- package/templates/agents/consistency.md +2 -0
- package/templates/agents/correctness.md +2 -0
- package/templates/agents/security.md +3 -0
- package/templates/atlantis.yml +123 -0
- package/templates/command.yml +4 -0
- package/templates/config.jsonc +71 -4
- package/templates/coordinator.md +34 -9
- package/templates/dismiss.yml +4 -0
- package/templates/routing.jsonc +3 -0
- package/templates/scope-config.jsonc +1 -0
- package/templates/shared.md +124 -1
- package/templates/workflow.yml +5 -0
package/README.md
CHANGED
|
@@ -70,9 +70,9 @@ excludes) — see [the mixed setup](#other-providers) below. Prefer
|
|
|
70
70
|
# Review working-tree changes; prints here, posts nothing
|
|
71
71
|
ecr review
|
|
72
72
|
# Review a GitHub PR by number (preview only)
|
|
73
|
-
ecr review --pr
|
|
73
|
+
ecr review --pr 123
|
|
74
74
|
# …and post it as the PR comment
|
|
75
|
-
ecr review --pr
|
|
75
|
+
ecr review --pr 123 --post
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
Options (most to least common):
|
|
@@ -110,15 +110,18 @@ is a ready example to adapt.
|
|
|
110
110
|
| `ecr init [--no-workflow] [--force]` | Scaffold `.expo-code-review/` (config, agents, prompts) + a CI workflow. |
|
|
111
111
|
| `ecr init --monorepo` | …and add a `routing.jsonc` routing manifest (one default scope). |
|
|
112
112
|
| `ecr init --scope <dir>` | Scaffold a per-team scope under `<dir>` and register it in the manifest. |
|
|
113
|
-
| `ecr setup-auth [--yes]` | Walk through getting model credentials for local runs (ChatGPT sign-in and/or API keys), printing the `export` lines for your shell config. |
|
|
113
|
+
| `ecr setup-auth [--yes]` | Walk through getting model credentials for local runs (ChatGPT/Claude sign-in and/or API keys), printing the `export` lines for your shell config. |
|
|
114
114
|
| `ecr review [options]` | Review local changes and print an advisory review (default command). |
|
|
115
115
|
| `ecr review --scope <name>` | Review only one routing scope over just that scope's changed files. |
|
|
116
116
|
| `ecr ci` | Review the current GitHub PR and post/update a comment. For GitHub Actions. |
|
|
117
117
|
| `ecr doctor [--list-scopes]` | Check environment, config, credentials, and (with a manifest) scopes. |
|
|
118
|
+
| `ecr feedback [--repo <owner/repo>]` | Report which findings PR authors pushed back on, across history. See below. |
|
|
118
119
|
|
|
119
120
|
Extra flags for monorepos: `review`/`ci` `--config-dir <dir>` (load config from an
|
|
120
121
|
alternate dir; also `ECR_CONFIG_DIR`), `ci --scopes a,b` (limit the fan-out to
|
|
121
|
-
named scopes), `ci --comment single|per-scope` (override the manifest).
|
|
122
|
+
named scopes), `ci --comment single|per-scope` (override the manifest). Both
|
|
123
|
+
`review` and `ci` also take `--context-file <path>` (inject a file's text as
|
|
124
|
+
untrusted external context; see below).
|
|
122
125
|
|
|
123
126
|
(When developing this repo itself, use `bun run src/cli.ts <command>`.)
|
|
124
127
|
|
|
@@ -138,9 +141,9 @@ your-monorepo/
|
|
|
138
141
|
routing.jsonc # the manifest — infra-owned, ordered scope list + locked defaults
|
|
139
142
|
config.jsonc # the default/root scope; the ONLY place auth/tokenEnv lives
|
|
140
143
|
shared.md coordinator.md agents/
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
apps/
|
|
145
|
+
api/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # api team
|
|
146
|
+
web/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # web team
|
|
144
147
|
.github/workflows/expo-code-review.yml # unchanged shape: one workflow, one `ecr ci`
|
|
145
148
|
```
|
|
146
149
|
|
|
@@ -157,21 +160,21 @@ your-monorepo/
|
|
|
157
160
|
"comment": "single", // "single" = one aggregated comment (default) | "per-scope"
|
|
158
161
|
// Ordered; the LAST matching scope wins per changed file (CODEOWNERS discipline).
|
|
159
162
|
"scopes": [
|
|
160
|
-
{ "name": "default",
|
|
161
|
-
{ "name": "
|
|
162
|
-
{ "name": "
|
|
163
|
+
{ "name": "default", "paths": ["**/*"], "config": "." },
|
|
164
|
+
{ "name": "apps-api", "paths": ["apps/api/**"], "config": "apps/api" },
|
|
165
|
+
{ "name": "apps-web", "paths": ["apps/web/**"], "config": "apps/web" }
|
|
163
166
|
]
|
|
164
167
|
}
|
|
165
168
|
```
|
|
166
169
|
|
|
167
170
|
```jsonc
|
|
168
|
-
//
|
|
171
|
+
// apps/api/.expo-code-review/config.jsonc (the api team owns this)
|
|
169
172
|
{
|
|
170
173
|
// NO "auth" block — locked centrally; a tokenEnv here is rejected by loader + CI guard.
|
|
171
174
|
"model": "openai/gpt-5.5",
|
|
172
175
|
"policy": { "includeSuggestions": false },
|
|
173
|
-
"noise": { "additionalIgnores": ["
|
|
174
|
-
// shared.md, coordinator.md, agents/*.md live beside this file — the
|
|
176
|
+
"noise": { "additionalIgnores": ["apps/api/**/__generated__/**"] }
|
|
177
|
+
// shared.md, coordinator.md, agents/*.md live beside this file — the api team's roster.
|
|
175
178
|
}
|
|
176
179
|
```
|
|
177
180
|
|
|
@@ -245,7 +248,7 @@ your-monorepo/
|
|
|
245
248
|
security warning and will be removed on a minor boundary.
|
|
246
249
|
|
|
247
250
|
Ownership is enforced with CODEOWNERS: `/.expo-code-review/routing.jsonc @your-infra`
|
|
248
|
-
(the single authoritative router) and `/
|
|
251
|
+
(the single authoritative router) and `/apps/api/.expo-code-review/ @your-api-team`
|
|
249
252
|
(each team owns only its own scope dir). Rerouting globs is gated behind infra review.
|
|
250
253
|
|
|
251
254
|
---
|
|
@@ -486,6 +489,108 @@ as the published package via `npx`, so no PR-controlled code is built.
|
|
|
486
489
|
|
|
487
490
|
</details>
|
|
488
491
|
|
|
492
|
+
<details>
|
|
493
|
+
<summary><b>Author feedback (replies to findings)</b></summary>
|
|
494
|
+
|
|
495
|
+
A PR author's reply to a finding is matched to it deterministically — by quoting
|
|
496
|
+
the finding's title back, or by citing its short `` `id:…` `` token (only the id can
|
|
497
|
+
clear a finding, see below) — and recorded
|
|
498
|
+
in the comment's embedded state, no model involved in the matching itself. A
|
|
499
|
+
matched finding shows `💬 @login replied` (linked to the comment) and a visible
|
|
500
|
+
count above the fold; the reply's own text is never stored or rendered, only the
|
|
501
|
+
login, the comment link, and (optionally) an enum-valued verdict. Controlled by
|
|
502
|
+
the root-only `feedback` block in `config.jsonc`:
|
|
503
|
+
|
|
504
|
+
```jsonc
|
|
505
|
+
"feedback": {
|
|
506
|
+
"mode": "annotate", // "off" | "annotate" | "adjudicate"
|
|
507
|
+
"match": "both", // "quote" | "id" | "both"
|
|
508
|
+
"dismiss": "never", // "never" | "maintainers" | "adjudicated"
|
|
509
|
+
"protectedCategories": ["secrets", "security"],
|
|
510
|
+
"maxAdjudications": 10 // cap on model calls per run, when mode is "adjudicate"
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
- **`annotate`** (the default) matches and shows "author replied" with zero effect
|
|
515
|
+
on the decision — safe and useful even if you never touch this block.
|
|
516
|
+
- **Clearing a finding always needs the `` `id:…` `` token**, in the replier's own words
|
|
517
|
+
(an id inside a `>` quote does not count). Quoting the title is enough to *annotate*,
|
|
518
|
+
never to clear: GitHub's "Quote reply" copies the PR author's text verbatim, so a
|
|
519
|
+
maintainer clicking it would otherwise dismiss a finding on words the author wrote —
|
|
520
|
+
by accident, or because they were led to.
|
|
521
|
+
- **`adjudicate`** additionally has a model re-check the reply against the actual
|
|
522
|
+
source (distrust-by-default, like the finding verifier) and record a verdict.
|
|
523
|
+
Whether that verdict can actually clear a finding is a separate, still-off-by-
|
|
524
|
+
default choice — `mode` and `dismiss` are independent axes: `dismiss:
|
|
525
|
+
"maintainers"` lets a maintainer's own reply dismiss with no model involved
|
|
526
|
+
(it works under plain `annotate` too); `dismiss: "adjudicated"` additionally
|
|
527
|
+
accepts an author reply the model confirmed (which does need `mode:
|
|
528
|
+
"adjudicate"` for verdicts to exist). Either way the reply has to cite the finding's
|
|
529
|
+
`` `id:…` ``. A `critical` finding, or one categorized `secrets`/
|
|
530
|
+
`security`, can never be cleared this way, whatever the config — that floor is
|
|
531
|
+
enforced in code, not the prompt.
|
|
532
|
+
- **`/undismiss <id>` wins over a reply.** Running it on a finding a reply cleared
|
|
533
|
+
puts the finding back in the active list and keeps it there: another reply from
|
|
534
|
+
the PR author can't clear it again. The restore is recorded against the FINDING
|
|
535
|
+
in the comment state, not against the reply, so editing or deleting the reply
|
|
536
|
+
doesn't drop it either. Only a maintainer lifts that — either `/dismiss <id>` on
|
|
537
|
+
the same finding, or a maintainer's own reply to it.
|
|
538
|
+
|
|
539
|
+
`ecr feedback` mines this substrate retroactively, with no model call and no
|
|
540
|
+
re-review: it crawls a repo's PRs, reads each one's existing reviewer comment
|
|
541
|
+
(which already embeds its findings), matches non-bot replies against it, and
|
|
542
|
+
reports totals, a reply-rate, breakdowns by category/severity/agent, and — the
|
|
543
|
+
highest-value part — "repeat offenders": findings whose title recurred across 2+
|
|
544
|
+
PRs and drew a reply every single time.
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
ecr feedback --repo your-org/your-repo --limit 100 --since 2026-06-01
|
|
548
|
+
ecr feedback --as my-review-bot # if CI posts under a PAT/app identity
|
|
549
|
+
ecr feedback --json # for scripting
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
The crawl matches the reviewer's comments by author. CI posts them as
|
|
553
|
+
`github-actions[bot]` (the default), so a locally-run crawl uses that identity —
|
|
554
|
+
pass `--as <login>` when your workflow posts under something else.
|
|
555
|
+
|
|
556
|
+
`ecr feedback` always reads `.expo-code-review/config.jsonc` from the LOCAL
|
|
557
|
+
checkout, even with `--repo`. If `--repo` points at a different repo, it warns
|
|
558
|
+
that `commentTag` may not match, so a zero-findings result there is not read as
|
|
559
|
+
zero pushback. It also warns when every scanned PR had no bot comment at all,
|
|
560
|
+
instead of leaving that as an easy-to-miss "0 with a bot comment" in the totals.
|
|
561
|
+
|
|
562
|
+
See [LLP 0011](./llp/0011-author-feedback.explainer.md) for why matching is
|
|
563
|
+
deterministic, why reply text is never echoed into the comment, and why the
|
|
564
|
+
defaults are asymmetric.
|
|
565
|
+
|
|
566
|
+
</details>
|
|
567
|
+
|
|
568
|
+
<details>
|
|
569
|
+
<summary><b>External context (--context-file)</b></summary>
|
|
570
|
+
|
|
571
|
+
`ecr review --context-file <path>` and `ecr ci --context-file <path>` inject the
|
|
572
|
+
file's UTF-8 text into the reviewer prompts as an explicitly UNTRUSTED external
|
|
573
|
+
block (the reviewer is told to use it but never follow instructions in it). The
|
|
574
|
+
main use is a CI-provided terraform plan. The text is sanitized like any untrusted
|
|
575
|
+
prose and capped at 24k chars (head 16k + tail 8k, so a big plan keeps both its
|
|
576
|
+
resource list and its `Plan: N to add…` summary); the read itself is bounded at
|
|
577
|
+
1 MiB.
|
|
578
|
+
|
|
579
|
+
Read errors differ by command on purpose. `ecr review` fails loud (exits 2) on a
|
|
580
|
+
missing or oversized file, since you typed the path. `ecr ci` warns and continues
|
|
581
|
+
with no context, because a CI run must never fail the PR's checks.
|
|
582
|
+
|
|
583
|
+
### Atlantis terraform plans
|
|
584
|
+
|
|
585
|
+
`templates/atlantis.yml` is an opt-in workflow (not scaffolded by `ecr init`) that
|
|
586
|
+
runs the reviewer when Atlantis posts a `terraform plan` comment on a PR and feeds
|
|
587
|
+
the plan into the review as `--context-file`. The plan comment body is treated as
|
|
588
|
+
untrusted data throughout. Copy it into `.github/workflows/` and set two repo
|
|
589
|
+
variables: `ATLANTIS_BOT_LOGIN` (the Atlantis bot's comment login, e.g.
|
|
590
|
+
`atlantis-app[bot]`) and optionally `ATLANTIS_PLAN_MARKER`.
|
|
591
|
+
|
|
592
|
+
</details>
|
|
593
|
+
|
|
489
594
|
<details>
|
|
490
595
|
<summary><b>Run logs</b></summary>
|
|
491
596
|
|
|
@@ -545,18 +650,36 @@ set in `config.auth` (credentials come from OpenCode):
|
|
|
545
650
|
One caveat: OpenCode can't price alias models (they're config-declared), so
|
|
546
651
|
pro passes report `$0` in the run log's cost column — token counts are
|
|
547
652
|
correct, and the OpenAI project dashboard is the source of truth for spend.
|
|
548
|
-
- **Anthropic / Claude
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
653
|
+
- **Anthropic / Claude** — use `anthropic/...` model ids and every anthropic pass
|
|
654
|
+
runs through the **Claude Code CLI** (`claude -p --output-format json`), inferred
|
|
655
|
+
from the model. The credential is (in order) a `tokenEnv` you name, an ambient
|
|
656
|
+
`CLAUDE_CODE_OAUTH_TOKEN`, or your local `claude` login — an `auth` entry is
|
|
657
|
+
entirely optional. Run `claude setup-token` for a Max/Team subscription token
|
|
658
|
+
(forwarded as `CLAUDE_CODE_OAUTH_TOKEN`) or point `tokenEnv` at an Anthropic
|
|
659
|
+
Console API key (`sk-ant-api…`, forwarded as `ANTHROPIC_API_KEY`); the CLI reads
|
|
660
|
+
either. `ecr setup-auth` walks you through it. Each pass is trust-isolated and
|
|
661
|
+
read-only: it runs with `--safe-mode` (no `CLAUDE.md`/hooks/MCP/plugins),
|
|
662
|
+
`--strict-mcp-config`, `--permission-mode dontAsk`, and only the
|
|
663
|
+
`Read`/`Grep`/`Glob` tools — never `Bash`/`Edit`/`Write`/`WebFetch`/`WebSearch`.
|
|
664
|
+
The child env is an allowlist that omits ambient `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`
|
|
665
|
+
(only the configured credential is re-injected).
|
|
666
|
+
```jsonc
|
|
667
|
+
// Optional — no anthropic entry at all falls back to your `claude` login.
|
|
668
|
+
"auth": { "providers": {
|
|
669
|
+
"anthropic": { "tokenEnv": "CLAUDE_CODE_OAUTH_TOKEN" }
|
|
670
|
+
} }
|
|
671
|
+
```
|
|
554
672
|
- **Another provider** — the current path is the `REVIEWER_MODEL`
|
|
555
673
|
env override: `opencode auth login` once (pick the provider), then run with
|
|
556
674
|
e.g. `REVIEWER_MODEL=google/gemini-3-pro`. It overrides every agent's model
|
|
557
|
-
and uses your OpenCode login, so no `auth` block is needed.
|
|
558
|
-
|
|
559
|
-
|
|
675
|
+
and uses your OpenCode login, so no `auth` block is needed.
|
|
676
|
+
|
|
677
|
+
Engines are inferred **per agent** from that agent's resolved model alone: an
|
|
678
|
+
`anthropic/…` agent runs through the Claude Code CLI while other agents run through
|
|
679
|
+
OpenCode — in the SAME run. So an anthropic model may coexist with an `openai` (or
|
|
680
|
+
any other) OpenCode provider, and each agent's `model` selects its engine.
|
|
681
|
+
`REVIEWER_MODEL` still overrides every agent's model (and therefore every agent's
|
|
682
|
+
engine), converging the whole run onto one engine.
|
|
560
683
|
|
|
561
684
|
There is no shared fallback key; if a run fails for lack of credentials, authenticate
|
|
562
685
|
a provider in OpenCode. `ecr doctor` diagnoses setup.
|
|
@@ -570,8 +693,11 @@ rediscovering one fixable thing, then reports N coverage gaps. So before any pas
|
|
|
570
693
|
model, with nothing pointing at the credential. A truncated value, surrounding
|
|
571
694
|
whitespace, or a token that can't work for the configured `auth.mode` is rejected
|
|
572
695
|
by name.
|
|
573
|
-
- **Configured model ids are checked against the running
|
|
574
|
-
the provider doesn't have is reported once, up front,
|
|
696
|
+
- **Configured model ids for OpenCode-routed providers are checked against the running
|
|
697
|
+
server**, so a typo or an id the provider doesn't have is reported once, up front,
|
|
698
|
+
with the close matches. `anthropic/…` (Claude Code) model ids aren't checked up
|
|
699
|
+
front — Claude validates them per-request, so a typo there surfaces as a per-pass
|
|
700
|
+
error instead.
|
|
575
701
|
- **`ecr doctor` reports the `opencode` version actually in use** and warns when a
|
|
576
702
|
different one is first on your `PATH` — runs use the version this package pins.
|
|
577
703
|
|
package/build/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// @ref LLP 0007#command-dispatch-and-conventions — argv dispatch only; no execution logic lives here
|
|
2
3
|
import { ciCommand } from "./commands/ci.js";
|
|
3
4
|
import { dismissCommand } from "./commands/dismiss.js";
|
|
4
5
|
import { doctorCommand } from "./commands/doctor.js";
|
|
6
|
+
import { feedbackCommand } from "./commands/feedback.js";
|
|
5
7
|
import { initCommand } from "./commands/init.js";
|
|
6
8
|
import { reviewCommand } from "./commands/review.js";
|
|
7
9
|
import { setupAuthCommand } from "./commands/setup-auth.js";
|
|
@@ -13,6 +15,7 @@ Usage:
|
|
|
13
15
|
ecr ci Review the current PR and post a comment (GitHub Actions).
|
|
14
16
|
ecr dismiss --pr <n> <id...> Hide a finding on a PR (see \`ecr dismiss --help\`).
|
|
15
17
|
ecr undismiss --pr <n> <id...> Restore a dismissed finding.
|
|
18
|
+
ecr feedback [--repo <owner/repo>] [--limit <n>] Report what humans pushed back on (see \`ecr feedback --help\`).
|
|
16
19
|
ecr init [--monorepo] [--scope <dir>] Scaffold .expo-code-review/ in this repo.
|
|
17
20
|
ecr setup-auth [--yes] Walk through getting model credentials for local runs.
|
|
18
21
|
ecr doctor [--list-scopes] Check environment, config, credentials, and scopes.
|
|
@@ -28,6 +31,7 @@ async function main() {
|
|
|
28
31
|
process.stdout.write(USAGE);
|
|
29
32
|
return;
|
|
30
33
|
}
|
|
34
|
+
// @ref LLP 0007#command-dispatch-and-conventions [implements] — default-to-review also swallows leading-flag typos; new global flags must not collide with review flags
|
|
31
35
|
// No subcommand (or a leading flag) defaults to `review`.
|
|
32
36
|
if (!sub || sub.startsWith("-")) {
|
|
33
37
|
await reviewCommand(process.argv.slice(2));
|
|
@@ -46,6 +50,9 @@ async function main() {
|
|
|
46
50
|
case "undismiss":
|
|
47
51
|
await dismissCommand(rest, "remove");
|
|
48
52
|
break;
|
|
53
|
+
case "feedback":
|
|
54
|
+
await feedbackCommand(rest);
|
|
55
|
+
break;
|
|
49
56
|
case "init":
|
|
50
57
|
await initCommand(rest);
|
|
51
58
|
break;
|