@expo/code-review-cli 0.7.0 → 0.9.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 (56) hide show
  1. package/README.md +161 -13
  2. package/build/cli.js +12 -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/ref-check.js +84 -0
  9. package/build/commands/review.js +191 -51
  10. package/build/commands/setup-auth.js +3 -0
  11. package/build/commands/verify-config.js +3 -0
  12. package/build/config/load.js +39 -0
  13. package/build/config/routing.js +7 -0
  14. package/build/config/schema.js +92 -0
  15. package/build/core/adjudicate.js +194 -0
  16. package/build/core/auth.js +5 -1
  17. package/build/core/claude-code.js +12 -1
  18. package/build/core/config-refs.js +772 -0
  19. package/build/core/context-file.js +42 -0
  20. package/build/core/coordinator.js +2 -2
  21. package/build/core/diff.js +1 -0
  22. package/build/core/exec.js +4 -0
  23. package/build/core/log.js +1 -0
  24. package/build/core/noise.js +5 -0
  25. package/build/core/opencode.js +22 -0
  26. package/build/core/prompts.js +311 -3
  27. package/build/core/render.js +268 -45
  28. package/build/core/responses.js +158 -0
  29. package/build/core/review.js +307 -15
  30. package/build/core/schema.js +223 -2
  31. package/build/core/scrub.js +4 -0
  32. package/build/core/stack-confirm.js +137 -0
  33. package/build/core/stack.js +25 -0
  34. package/build/core/step-summary.js +1 -0
  35. package/build/core/suppress.js +2 -0
  36. package/build/core/throttle.js +2 -0
  37. package/build/core/util.js +1 -0
  38. package/build/core/verify.js +5 -0
  39. package/build/reporters/github.js +465 -31
  40. package/build/reporters/terminal.js +10 -0
  41. package/build/sources/github-pr.js +272 -0
  42. package/build/sources/local-git.js +3 -0
  43. package/build/sources/source.js +35 -0
  44. package/package.json +2 -1
  45. package/templates/agents/consistency.md +6 -1
  46. package/templates/agents/correctness.md +9 -1
  47. package/templates/agents/security.md +11 -1
  48. package/templates/atlantis.yml +123 -0
  49. package/templates/command.yml +4 -0
  50. package/templates/config.jsonc +50 -1
  51. package/templates/coordinator.md +34 -9
  52. package/templates/dismiss.yml +4 -0
  53. package/templates/routing.jsonc +3 -0
  54. package/templates/scope-config.jsonc +1 -0
  55. package/templates/shared.md +99 -1
  56. 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,15 +115,61 @@ 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. |
119
+ | `ecr ref-check [--json]` | Fail when the review setup cites code that moved or vanished. See below. |
118
120
 
119
121
  Extra flags for monorepos: `review`/`ci` `--config-dir <dir>` (load config from an
120
122
  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).
123
+ named scopes), `ci --comment single|per-scope` (override the manifest). Both
124
+ `review` and `ci` also take `--context-file <path>` (inject a file's text as
125
+ untrusted external context; see below).
122
126
 
123
127
  (When developing this repo itself, use `bun run src/cli.ts <command>`.)
124
128
 
125
129
  ---
126
130
 
131
+ ## Keeping prompts true (`ecr ref-check`)
132
+
133
+ Good reviewer prompts cite real code: "the only session entry point is
134
+ `server/src/session.ts`", "every webhook router must call `sanitizeSecrets`". Then the
135
+ code moves and the prompt keeps citing a path that no longer exists — the reviewer
136
+ reasons from a fiction, on every PR, with nothing to warn you.
137
+
138
+ `ecr ref-check` makes those citations checkable. Pin each one with a ref, in a comment
139
+ of its own (`<!-- … -->` in Markdown, `//` in JSONC):
140
+
141
+ ```md
142
+ <!-- @ref server/src/session.ts#createSession — the only place a session is minted -->
143
+ <!-- @ref server/src/entities/oauth/ — every provider lives here -->
144
+ <!-- @ref glob:**/*WebhookRouter.ts — the routers this rule is about -->
145
+ ```
146
+
147
+ A target is a file, a `dir/`, `glob:<pattern>`, `file#symbol`, or `doc.md#heading` —
148
+ never a line number, since a line number rots without any signal. Symbol anchors are
149
+ checked by whole-word match, so code moving inside its file is fine and a rename is not.
150
+
151
+ The check is strict on purpose: a backticked token in `.expo-code-review/` that looks
152
+ like a repo path **must** be a ref, because the stale citations are exactly the ones
153
+ nobody thought to annotate. A token with no extension (`eas-build-worker/terraform`,
154
+ `general-central/{module,production}`, `finops`) counts when it names something that
155
+ exists — so those get pinned too, while `anthropic/claude-opus-5`, shaped the same way,
156
+ stays prose. For a token that only looks like a path, say so once:
157
+ `<!-- @ref-ignore knex.raw() -->`. It also checks what your config already declares —
158
+ `enforceAgents` ids, scope `config` directories, scope path globs.
159
+
160
+ Refs are repo-root-relative, including in a scope's own setup dir. A scope prompt that
161
+ cites `general-central/module` for `infrastructure/general-central/module` gets told the
162
+ root-relative form to use.
163
+
164
+ Two run points:
165
+
166
+ - `ecr ref-check` exits 1 on any problem. Run it in CI or a pre-commit hook.
167
+ - `ecr review` / `ecr ci` run it too, and never fail a PR's checks with it. The comment
168
+ carries a **Review setup** note instead: refs that no longer resolve, plus cited code
169
+ *this PR* changes, where the ref still resolves but the guidance may not.
170
+
171
+ ---
172
+
127
173
  ## Monorepos (routing manifest)
128
174
 
129
175
  A monorepo can route different subtrees to different reviewer rosters from a single
@@ -138,9 +184,9 @@ your-monorepo/
138
184
  routing.jsonc # the manifest — infra-owned, ordered scope list + locked defaults
139
185
  config.jsonc # the default/root scope; the ONLY place auth/tokenEnv lives
140
186
  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
187
+ apps/
188
+ api/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # api team
189
+ web/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # web team
144
190
  .github/workflows/expo-code-review.yml # unchanged shape: one workflow, one `ecr ci`
145
191
  ```
146
192
 
@@ -157,21 +203,21 @@ your-monorepo/
157
203
  "comment": "single", // "single" = one aggregated comment (default) | "per-scope"
158
204
  // Ordered; the LAST matching scope wins per changed file (CODEOWNERS discipline).
159
205
  "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" }
206
+ { "name": "default", "paths": ["**/*"], "config": "." },
207
+ { "name": "apps-api", "paths": ["apps/api/**"], "config": "apps/api" },
208
+ { "name": "apps-web", "paths": ["apps/web/**"], "config": "apps/web" }
163
209
  ]
164
210
  }
165
211
  ```
166
212
 
167
213
  ```jsonc
168
- // server/www/.expo-code-review/config.jsonc (the www team owns this)
214
+ // apps/api/.expo-code-review/config.jsonc (the api team owns this)
169
215
  {
170
216
  // NO "auth" block — locked centrally; a tokenEnv here is rejected by loader + CI guard.
171
217
  "model": "openai/gpt-5.5",
172
218
  "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.
219
+ "noise": { "additionalIgnores": ["apps/api/**/__generated__/**"] }
220
+ // shared.md, coordinator.md, agents/*.md live beside this file — the api team's roster.
175
221
  }
176
222
  ```
177
223
 
@@ -245,7 +291,7 @@ your-monorepo/
245
291
  security warning and will be removed on a minor boundary.
246
292
 
247
293
  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`
294
+ (the single authoritative router) and `/apps/api/.expo-code-review/ @your-api-team`
249
295
  (each team owns only its own scope dir). Rerouting globs is gated behind infra review.
250
296
 
251
297
  ---
@@ -486,6 +532,108 @@ as the published package via `npx`, so no PR-controlled code is built.
486
532
 
487
533
  </details>
488
534
 
535
+ <details>
536
+ <summary><b>Author feedback (replies to findings)</b></summary>
537
+
538
+ A PR author's reply to a finding is matched to it deterministically — by quoting
539
+ the finding's title back, or by citing its short `` `id:…` `` token (only the id can
540
+ clear a finding, see below) — and recorded
541
+ in the comment's embedded state, no model involved in the matching itself. A
542
+ matched finding shows `💬 @login replied` (linked to the comment) and a visible
543
+ count above the fold; the reply's own text is never stored or rendered, only the
544
+ login, the comment link, and (optionally) an enum-valued verdict. Controlled by
545
+ the root-only `feedback` block in `config.jsonc`:
546
+
547
+ ```jsonc
548
+ "feedback": {
549
+ "mode": "annotate", // "off" | "annotate" | "adjudicate"
550
+ "match": "both", // "quote" | "id" | "both"
551
+ "dismiss": "never", // "never" | "maintainers" | "adjudicated"
552
+ "protectedCategories": ["secrets", "security"],
553
+ "maxAdjudications": 10 // cap on model calls per run, when mode is "adjudicate"
554
+ }
555
+ ```
556
+
557
+ - **`annotate`** (the default) matches and shows "author replied" with zero effect
558
+ on the decision — safe and useful even if you never touch this block.
559
+ - **Clearing a finding always needs the `` `id:…` `` token**, in the replier's own words
560
+ (an id inside a `>` quote does not count). Quoting the title is enough to *annotate*,
561
+ never to clear: GitHub's "Quote reply" copies the PR author's text verbatim, so a
562
+ maintainer clicking it would otherwise dismiss a finding on words the author wrote —
563
+ by accident, or because they were led to.
564
+ - **`adjudicate`** additionally has a model re-check the reply against the actual
565
+ source (distrust-by-default, like the finding verifier) and record a verdict.
566
+ Whether that verdict can actually clear a finding is a separate, still-off-by-
567
+ default choice — `mode` and `dismiss` are independent axes: `dismiss:
568
+ "maintainers"` lets a maintainer's own reply dismiss with no model involved
569
+ (it works under plain `annotate` too); `dismiss: "adjudicated"` additionally
570
+ accepts an author reply the model confirmed (which does need `mode:
571
+ "adjudicate"` for verdicts to exist). Either way the reply has to cite the finding's
572
+ `` `id:…` ``. A `critical` finding, or one categorized `secrets`/
573
+ `security`, can never be cleared this way, whatever the config — that floor is
574
+ enforced in code, not the prompt.
575
+ - **`/undismiss <id>` wins over a reply.** Running it on a finding a reply cleared
576
+ puts the finding back in the active list and keeps it there: another reply from
577
+ the PR author can't clear it again. The restore is recorded against the FINDING
578
+ in the comment state, not against the reply, so editing or deleting the reply
579
+ doesn't drop it either. Only a maintainer lifts that — either `/dismiss <id>` on
580
+ the same finding, or a maintainer's own reply to it.
581
+
582
+ `ecr feedback` mines this substrate retroactively, with no model call and no
583
+ re-review: it crawls a repo's PRs, reads each one's existing reviewer comment
584
+ (which already embeds its findings), matches non-bot replies against it, and
585
+ reports totals, a reply-rate, breakdowns by category/severity/agent, and — the
586
+ highest-value part — "repeat offenders": findings whose title recurred across 2+
587
+ PRs and drew a reply every single time.
588
+
589
+ ```bash
590
+ ecr feedback --repo your-org/your-repo --limit 100 --since 2026-06-01
591
+ ecr feedback --as my-review-bot # if CI posts under a PAT/app identity
592
+ ecr feedback --json # for scripting
593
+ ```
594
+
595
+ The crawl matches the reviewer's comments by author. CI posts them as
596
+ `github-actions[bot]` (the default), so a locally-run crawl uses that identity —
597
+ pass `--as <login>` when your workflow posts under something else.
598
+
599
+ `ecr feedback` always reads `.expo-code-review/config.jsonc` from the LOCAL
600
+ checkout, even with `--repo`. If `--repo` points at a different repo, it warns
601
+ that `commentTag` may not match, so a zero-findings result there is not read as
602
+ zero pushback. It also warns when every scanned PR had no bot comment at all,
603
+ instead of leaving that as an easy-to-miss "0 with a bot comment" in the totals.
604
+
605
+ See [LLP 0011](./llp/0011-author-feedback.explainer.md) for why matching is
606
+ deterministic, why reply text is never echoed into the comment, and why the
607
+ defaults are asymmetric.
608
+
609
+ </details>
610
+
611
+ <details>
612
+ <summary><b>External context (--context-file)</b></summary>
613
+
614
+ `ecr review --context-file <path>` and `ecr ci --context-file <path>` inject the
615
+ file's UTF-8 text into the reviewer prompts as an explicitly UNTRUSTED external
616
+ block (the reviewer is told to use it but never follow instructions in it). The
617
+ main use is a CI-provided terraform plan. The text is sanitized like any untrusted
618
+ prose and capped at 24k chars (head 16k + tail 8k, so a big plan keeps both its
619
+ resource list and its `Plan: N to add…` summary); the read itself is bounded at
620
+ 1 MiB.
621
+
622
+ Read errors differ by command on purpose. `ecr review` fails loud (exits 2) on a
623
+ missing or oversized file, since you typed the path. `ecr ci` warns and continues
624
+ with no context, because a CI run must never fail the PR's checks.
625
+
626
+ ### Atlantis terraform plans
627
+
628
+ `templates/atlantis.yml` is an opt-in workflow (not scaffolded by `ecr init`) that
629
+ runs the reviewer when Atlantis posts a `terraform plan` comment on a PR and feeds
630
+ the plan into the review as `--context-file`. The plan comment body is treated as
631
+ untrusted data throughout. Copy it into `.github/workflows/` and set two repo
632
+ variables: `ATLANTIS_BOT_LOGIN` (the Atlantis bot's comment login, e.g.
633
+ `atlantis-app[bot]`) and optionally `ATLANTIS_PLAN_MARKER`.
634
+
635
+ </details>
636
+
489
637
  <details>
490
638
  <summary><b>Run logs</b></summary>
491
639
 
package/build/cli.js CHANGED
@@ -1,8 +1,11 @@
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";
8
+ import { refCheckCommand } from "./commands/ref-check.js";
6
9
  import { reviewCommand } from "./commands/review.js";
7
10
  import { setupAuthCommand } from "./commands/setup-auth.js";
8
11
  import { verifyConfigCommand } from "./commands/verify-config.js";
@@ -13,10 +16,12 @@ Usage:
13
16
  ecr ci Review the current PR and post a comment (GitHub Actions).
14
17
  ecr dismiss --pr <n> <id...> Hide a finding on a PR (see \`ecr dismiss --help\`).
15
18
  ecr undismiss --pr <n> <id...> Restore a dismissed finding.
19
+ ecr feedback [--repo <owner/repo>] [--limit <n>] Report what humans pushed back on (see \`ecr feedback --help\`).
16
20
  ecr init [--monorepo] [--scope <dir>] Scaffold .expo-code-review/ in this repo.
17
21
  ecr setup-auth [--yes] Walk through getting model credentials for local runs.
18
22
  ecr doctor [--list-scopes] Check environment, config, credentials, and scopes.
19
23
  ecr verify-config [--expected <env>] [--json] Refuse to run if a config could redirect the credential (CI guard).
24
+ ecr ref-check [--root <dir>] [--json] Fail if the review setup cites code that moved or vanished.
20
25
 
21
26
  Agents live in each repo under .expo-code-review/. This CLI is the engine.
22
27
 
@@ -28,6 +33,7 @@ async function main() {
28
33
  process.stdout.write(USAGE);
29
34
  return;
30
35
  }
36
+ // @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
37
  // No subcommand (or a leading flag) defaults to `review`.
32
38
  if (!sub || sub.startsWith("-")) {
33
39
  await reviewCommand(process.argv.slice(2));
@@ -46,6 +52,9 @@ async function main() {
46
52
  case "undismiss":
47
53
  await dismissCommand(rest, "remove");
48
54
  break;
55
+ case "feedback":
56
+ await feedbackCommand(rest);
57
+ break;
49
58
  case "init":
50
59
  await initCommand(rest);
51
60
  break;
@@ -58,6 +67,9 @@ async function main() {
58
67
  case "verify-config":
59
68
  await verifyConfigCommand(rest);
60
69
  break;
70
+ case "ref-check":
71
+ await refCheckCommand(rest);
72
+ break;
61
73
  default:
62
74
  process.stderr.write(`Unknown command: ${sub}\n\n${USAGE}`);
63
75
  process.exitCode = 2;