@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.
- package/README.md +161 -13
- package/build/cli.js +12 -0
- package/build/commands/ci.js +299 -28
- package/build/commands/dismiss.js +6 -0
- package/build/commands/doctor.js +3 -0
- package/build/commands/feedback.js +433 -0
- package/build/commands/init.js +231 -15
- package/build/commands/ref-check.js +84 -0
- package/build/commands/review.js +191 -51
- package/build/commands/setup-auth.js +3 -0
- 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 +92 -0
- package/build/core/adjudicate.js +194 -0
- package/build/core/auth.js +5 -1
- package/build/core/claude-code.js +12 -1
- package/build/core/config-refs.js +772 -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 +4 -0
- package/build/core/log.js +1 -0
- package/build/core/noise.js +5 -0
- package/build/core/opencode.js +22 -0
- package/build/core/prompts.js +311 -3
- package/build/core/render.js +268 -45
- package/build/core/responses.js +158 -0
- package/build/core/review.js +307 -15
- package/build/core/schema.js +223 -2
- package/build/core/scrub.js +4 -0
- 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 +2 -0
- package/build/core/util.js +1 -0
- package/build/core/verify.js +5 -0
- package/build/reporters/github.js +465 -31
- package/build/reporters/terminal.js +10 -0
- package/build/sources/github-pr.js +272 -0
- package/build/sources/local-git.js +3 -0
- package/build/sources/source.js +35 -0
- package/package.json +2 -1
- package/templates/agents/consistency.md +6 -1
- package/templates/agents/correctness.md +9 -1
- package/templates/agents/security.md +11 -1
- package/templates/atlantis.yml +123 -0
- package/templates/command.yml +4 -0
- package/templates/config.jsonc +50 -1
- 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 +99 -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):
|
|
@@ -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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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",
|
|
161
|
-
{ "name": "
|
|
162
|
-
{ "name": "
|
|
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
|
-
//
|
|
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": ["
|
|
174
|
-
// shared.md, coordinator.md, agents/*.md live beside this file — the
|
|
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 `/
|
|
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;
|