@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.
Files changed (54) hide show
  1. package/README.md +151 -25
  2. package/build/cli.js +7 -0
  3. package/build/commands/ci.js +307 -36
  4. package/build/commands/dismiss.js +6 -0
  5. package/build/commands/doctor.js +170 -33
  6. package/build/commands/feedback.js +433 -0
  7. package/build/commands/init.js +231 -15
  8. package/build/commands/review.js +191 -51
  9. package/build/commands/setup-auth.js +86 -11
  10. package/build/commands/verify-config.js +3 -0
  11. package/build/config/load.js +39 -0
  12. package/build/config/routing.js +7 -0
  13. package/build/config/schema.js +99 -3
  14. package/build/core/adjudicate.js +194 -0
  15. package/build/core/auth.js +127 -10
  16. package/build/core/claude-code.js +691 -0
  17. package/build/core/context-file.js +42 -0
  18. package/build/core/coordinator.js +2 -2
  19. package/build/core/diff.js +1 -0
  20. package/build/core/exec.js +282 -9
  21. package/build/core/log.js +1 -0
  22. package/build/core/noise.js +5 -0
  23. package/build/core/opencode.js +117 -15
  24. package/build/core/prompts.js +330 -5
  25. package/build/core/render.js +274 -45
  26. package/build/core/responses.js +158 -0
  27. package/build/core/review.js +447 -39
  28. package/build/core/schema.js +219 -3
  29. package/build/core/scrub.js +63 -1
  30. package/build/core/stack-confirm.js +137 -0
  31. package/build/core/stack.js +25 -0
  32. package/build/core/step-summary.js +1 -0
  33. package/build/core/suppress.js +2 -0
  34. package/build/core/throttle.js +12 -0
  35. package/build/core/util.js +18 -0
  36. package/build/core/verify.js +18 -1
  37. package/build/reporters/github.js +544 -44
  38. package/build/reporters/terminal.js +2 -0
  39. package/build/sources/github-pr.js +286 -7
  40. package/build/sources/local-git.js +6 -2
  41. package/build/sources/source.js +35 -0
  42. package/package.json +4 -3
  43. package/templates/agents/consistency.md +2 -0
  44. package/templates/agents/correctness.md +2 -0
  45. package/templates/agents/security.md +3 -0
  46. package/templates/atlantis.yml +123 -0
  47. package/templates/command.yml +4 -0
  48. package/templates/config.jsonc +71 -4
  49. package/templates/coordinator.md +34 -9
  50. package/templates/dismiss.yml +4 -0
  51. package/templates/routing.jsonc +3 -0
  52. package/templates/scope-config.jsonc +1 -0
  53. package/templates/shared.md +124 -1
  54. 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 4057
73
+ ecr review --pr 123
74
74
  # …and post it as the PR comment
75
- ecr review --pr 4057 --post
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
- server/
142
- www/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # www team
143
- website/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # website team
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", "paths": ["**/*"], "config": "." },
161
- { "name": "server-www", "paths": ["server/www/**"], "config": "server/www" },
162
- { "name": "server-website", "paths": ["server/website/**"], "config": "server/website" }
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
- // server/www/.expo-code-review/config.jsonc (the www team owns this)
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": ["server/www/**/__generated__/**"] }
174
- // shared.md, coordinator.md, agents/*.md live beside this file — the www team's roster.
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 `/server/www/.expo-code-review/ @your-www-team`
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 (API key)** — set `auth.provider` to `"anthropic"`, point
549
- `tokenEnv` at the env var holding a Console API key (e.g. `ANTHROPIC_API_KEY`),
550
- and use `anthropic/...` model ids; the key is sent as `x-api-key`. Note that
551
- Claude Pro/Max **subscription** tokens cannot be used here: Anthropic prohibits
552
- them in third-party tools, and OpenCode has no Anthropic OAuth support — only an
553
- API key works.
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. *(Per-agent
558
- provider mixing beyond the alias mechanism above is on the
559
- [roadmap](./ROADMAP.md).)*
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 server**, so a typo or an id
574
- the provider doesn't have is reported once, up front, with the close matches.
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;