@expo/code-review-cli 0.7.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 +118 -13
  2. package/build/cli.js +7 -0
  3. package/build/commands/ci.js +299 -28
  4. package/build/commands/dismiss.js +6 -0
  5. package/build/commands/doctor.js +3 -0
  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 +3 -0
  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 +92 -0
  14. package/build/core/adjudicate.js +194 -0
  15. package/build/core/auth.js +5 -1
  16. package/build/core/claude-code.js +12 -1
  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 +4 -0
  21. package/build/core/log.js +1 -0
  22. package/build/core/noise.js +5 -0
  23. package/build/core/opencode.js +22 -0
  24. package/build/core/prompts.js +311 -3
  25. package/build/core/render.js +255 -45
  26. package/build/core/responses.js +158 -0
  27. package/build/core/review.js +290 -15
  28. package/build/core/schema.js +213 -2
  29. package/build/core/scrub.js +4 -0
  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 +2 -0
  35. package/build/core/util.js +1 -0
  36. package/build/core/verify.js +5 -0
  37. package/build/reporters/github.js +465 -31
  38. package/build/reporters/terminal.js +2 -0
  39. package/build/sources/github-pr.js +272 -0
  40. package/build/sources/local-git.js +3 -0
  41. package/build/sources/source.js +35 -0
  42. package/package.json +2 -1
  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 +50 -1
  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 +96 -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):
@@ -115,10 +115,13 @@ is a ready example to adapt.
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
 
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;