@expo/code-review-cli 0.14.2 → 0.16.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 +31 -23
- package/build/commands/ci.js +2 -2
- package/build/commands/doctor.js +3 -3
- package/build/commands/init.js +6 -6
- package/build/commands/verify-config.js +3 -5
- package/build/config/load.js +36 -9
- package/build/core/config-refs.js +12 -4
- package/build/core/opencode.js +46 -4
- package/package.json +1 -1
- package/templates/atlantis.yml +3 -1
- package/templates/command.yml +3 -1
- package/templates/config.jsonc +1 -1
- package/templates/workflow.yml +3 -1
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@ A config-driven, multi-agent AI code reviewer. Specialist agents review a diff i
|
|
|
4
4
|
parallel; a coordinator consolidates their findings into one structured review.
|
|
5
5
|
The same engine runs locally (advisory) and in CI (posts one PR comment). The CLI
|
|
6
6
|
is the **engine** — each repo supplies its own agents and settings under
|
|
7
|
-
`.expo-code-review/`, so behavior is configured per-repo, not baked in.
|
|
7
|
+
`.expo-agents/code-review/`, so behavior is configured per-repo, not baked in.
|
|
8
|
+
(Repos set up before 0.15 keep their `.expo-code-review/` directory: every command
|
|
9
|
+
looks for the new name first and falls back to the old one, and `ecr init` reuses
|
|
10
|
+
an existing legacy directory rather than starting a second one.)
|
|
8
11
|
|
|
9
12
|
> **Status: experimental.** Comment-only and non-blocking — it never blocks a merge
|
|
10
13
|
> and never auto-approves. See [`ROADMAP.md`](./ROADMAP.md).
|
|
@@ -94,7 +97,7 @@ here ([LLP 0001](./llp/0001-trust-model.principles.md)) is built around that:
|
|
|
94
97
|
## Usage
|
|
95
98
|
|
|
96
99
|
Run via `npx @expo/code-review-cli <command>` (or the `ecr` / `expo-code-review`
|
|
97
|
-
binary once installed). On a repo that already has `.expo-code-review/` set up,
|
|
100
|
+
binary once installed). On a repo that already has `.expo-agents/code-review/` set up,
|
|
98
101
|
getting model credentials for local runs is one command:
|
|
99
102
|
|
|
100
103
|
```bash
|
|
@@ -108,7 +111,7 @@ package.
|
|
|
108
111
|
### First-time setup
|
|
109
112
|
|
|
110
113
|
```bash
|
|
111
|
-
# 1. Scaffold .expo-code-review/ + a CI workflow (--no-workflow to skip)
|
|
114
|
+
# 1. Scaffold .expo-agents/code-review/ + a CI workflow (--no-workflow to skip)
|
|
112
115
|
npx @expo/code-review-cli init
|
|
113
116
|
# 2. Get model credentials — guided; prints the export lines for your shell config
|
|
114
117
|
npx @expo/code-review-cli setup-auth
|
|
@@ -142,7 +145,7 @@ ecr review --pr 123
|
|
|
142
145
|
ecr review --pr 123 --post
|
|
143
146
|
# Preview once, save the exact result, and post it later without another model run
|
|
144
147
|
ecr review --repo owner/repo --pr 123 --save-review --json
|
|
145
|
-
ecr post-review --artifact .expo-code-review/.runs/deferred/<artifact>.json --repo owner/repo --pr 123
|
|
148
|
+
ecr post-review --artifact .expo-agents/code-review/.runs/deferred/<artifact>.json --repo owner/repo --pr 123
|
|
146
149
|
```
|
|
147
150
|
|
|
148
151
|
Options (most to least common):
|
|
@@ -178,7 +181,7 @@ is a ready example to adapt.
|
|
|
178
181
|
|
|
179
182
|
| Command | What it does |
|
|
180
183
|
| --- | --- |
|
|
181
|
-
| `ecr init [--no-workflow] [--force]` | Scaffold `.expo-code-review/` (config, agents, prompts) + a CI workflow. |
|
|
184
|
+
| `ecr init [--no-workflow] [--force]` | Scaffold `.expo-agents/code-review/` (config, agents, prompts) + a CI workflow. |
|
|
182
185
|
| `ecr init --monorepo` | …and add a `routing.jsonc` routing manifest (one default scope). |
|
|
183
186
|
| `ecr init --scope <dir>` | Scaffold a per-team scope under `<dir>` and register it in the manifest. |
|
|
184
187
|
| `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. |
|
|
@@ -215,7 +218,7 @@ Markdown, `//` in JSONC):
|
|
|
215
218
|
|
|
216
219
|
A target is a file, a `dir/`, `glob:<pattern>`, `file#symbol`, or `doc.md#heading` —
|
|
217
220
|
never a line number (line numbers rot without any signal). The check is strict on
|
|
218
|
-
purpose: any backticked token in `.expo-code-review/` that names something that
|
|
221
|
+
purpose: any backticked token in `.expo-agents/code-review/` that names something that
|
|
219
222
|
exists in the repo **must** be a ref, because stale citations are exactly the ones
|
|
220
223
|
nobody annotated. Mark the rare false positive once with
|
|
221
224
|
`<!-- @ref-ignore knex.raw() -->`. Refs are always repo-root-relative, including in
|
|
@@ -291,18 +294,18 @@ and no locking.
|
|
|
291
294
|
|
|
292
295
|
```
|
|
293
296
|
your-monorepo/
|
|
294
|
-
.expo-code-review/
|
|
297
|
+
.expo-agents/code-review/
|
|
295
298
|
routing.jsonc # the manifest — infra-owned, ordered scope list + locked defaults
|
|
296
299
|
config.jsonc # the default/root scope; the ONLY place auth/tokenEnv lives
|
|
297
300
|
shared.md coordinator.md agents/
|
|
298
301
|
apps/
|
|
299
|
-
api/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # api team
|
|
300
|
-
web/.expo-code-review/{config.jsonc(NO auth),coordinator.md,agents/} # web team
|
|
302
|
+
api/.expo-agents/code-review/{config.jsonc(NO auth),coordinator.md,agents/} # api team
|
|
303
|
+
web/.expo-agents/code-review/{config.jsonc(NO auth),coordinator.md,agents/} # web team
|
|
301
304
|
.github/workflows/expo-code-review.yml # unchanged shape: one workflow, one `ecr ci`
|
|
302
305
|
```
|
|
303
306
|
|
|
304
307
|
```jsonc
|
|
305
|
-
// .expo-code-review/routing.jsonc
|
|
308
|
+
// .expo-agents/code-review/routing.jsonc
|
|
306
309
|
{
|
|
307
310
|
// Central guardrails every scope inherits and CANNOT override.
|
|
308
311
|
"defaults": {
|
|
@@ -322,7 +325,7 @@ your-monorepo/
|
|
|
322
325
|
```
|
|
323
326
|
|
|
324
327
|
```jsonc
|
|
325
|
-
// apps/api/.expo-code-review/config.jsonc (the api team owns this)
|
|
328
|
+
// apps/api/.expo-agents/code-review/config.jsonc (the api team owns this)
|
|
326
329
|
{
|
|
327
330
|
// NO "auth" block — locked centrally; a tokenEnv here is rejected by loader + CI guard.
|
|
328
331
|
"model": "anthropic/claude-sonnet-5",
|
|
@@ -378,8 +381,8 @@ Enforced in code and by an independent CI guard step, not by convention:
|
|
|
378
381
|
itself, so even a custom workflow that checks out the head still gets
|
|
379
382
|
base-commit configuration.
|
|
380
383
|
|
|
381
|
-
Enforce ownership with CODEOWNERS: `/.expo-code-review/routing.jsonc @your-infra`,
|
|
382
|
-
`/apps/api/.expo-code-review/ @your-api-team`. Full detail:
|
|
384
|
+
Enforce ownership with CODEOWNERS: `/.expo-agents/code-review/routing.jsonc @your-infra`,
|
|
385
|
+
`/apps/api/.expo-agents/code-review/ @your-api-team`. Full detail:
|
|
383
386
|
[LLP 0006](./llp/0006-config-schema-loading-routing.explainer.md) (routing) and
|
|
384
387
|
[LLP 0001](./llp/0001-trust-model.principles.md) (trust model).
|
|
385
388
|
|
|
@@ -396,7 +399,7 @@ Enforce ownership with CODEOWNERS: `/.expo-code-review/routing.jsonc @your-infra
|
|
|
396
399
|
- **Chunking** — small PRs run in a single pass; large PRs are split into chunks
|
|
397
400
|
bounded by changed lines, plus one combined **cross-cutting pass** that looks
|
|
398
401
|
for issues spanning multiple changed files across every agent's concern.
|
|
399
|
-
- **Agents** — every `.md` file in `.expo-code-review/agents/` is an agent. They
|
|
402
|
+
- **Agents** — every `.md` file in `.expo-agents/code-review/agents/` is an agent. They
|
|
400
403
|
run in parallel with read-only repo tools (`read`/`grep`/`glob`/`list`).
|
|
401
404
|
- **Coordinator** — a single pass that dedupes, re-judges severity, and produces
|
|
402
405
|
the final `{ decision, findings, summary }`.
|
|
@@ -426,7 +429,7 @@ Every run reports what it spent and how much was served from the prompt cache, i
|
|
|
426
429
|
three places: one `Token usage — …` line in the job log, a per-pass table + cache
|
|
427
430
|
hit rate in the GitHub Actions step summary (which also preserves each run's posted
|
|
428
431
|
comment, since the PR comment is updated in place), and one JSON line per run in
|
|
429
|
-
`.expo-code-review/.runs/reviews.jsonl` (uploaded as a CI artifact) with per-pass
|
|
432
|
+
`.expo-agents/code-review/.runs/reviews.jsonl` (uploaded as a CI artifact) with per-pass
|
|
430
433
|
tokens, raw per-agent findings, bounded reviewer traces, and coverage notes.
|
|
431
434
|
|
|
432
435
|
Each reviewer can also return a compact trace (up to three concrete checks and two
|
|
@@ -452,10 +455,10 @@ cacheable size (~1–4K tokens) show `cache read 0` — expected, not a bug.
|
|
|
452
455
|
</details>
|
|
453
456
|
|
|
454
457
|
<details>
|
|
455
|
-
<summary><b>Configuration — <code>.expo-code-review/</code></b></summary>
|
|
458
|
+
<summary><b>Configuration — <code>.expo-agents/code-review/</code></b></summary>
|
|
456
459
|
|
|
457
460
|
```
|
|
458
|
-
.expo-code-review/
|
|
461
|
+
.expo-agents/code-review/
|
|
459
462
|
config.jsonc # model, policy, noise, auth, break-glass, comment tag
|
|
460
463
|
shared.md # instructions prepended to every agent (optional)
|
|
461
464
|
coordinator.md # the consolidation prompt (required)
|
|
@@ -480,7 +483,7 @@ temperature: 0.1
|
|
|
480
483
|
```
|
|
481
484
|
|
|
482
485
|
For a real-world example, see eas-cli's
|
|
483
|
-
[`.expo-code-review/`](https://github.com/expo/eas-cli/tree/main/.expo-code-review)
|
|
486
|
+
[`.expo-agents/code-review/`](https://github.com/expo/eas-cli/tree/main/.expo-code-review)
|
|
484
487
|
— correctness/security/consistency agents, a stronger model for security, and
|
|
485
488
|
per-repo `noise.additionalIgnores`.
|
|
486
489
|
|
|
@@ -718,7 +721,7 @@ variables: `ATLANTIS_BOT_LOGIN` (the Atlantis bot's comment login, e.g.
|
|
|
718
721
|
<details>
|
|
719
722
|
<summary><b>Run logs</b></summary>
|
|
720
723
|
|
|
721
|
-
Each run appends a JSON line to `.expo-code-review/.runs/reviews.jsonl` with the
|
|
724
|
+
Each run appends a JSON line to `.expo-agents/code-review/.runs/reviews.jsonl` with the
|
|
722
725
|
inputs, decision, finding count, duration, per-agent cost, and aggregate token
|
|
723
726
|
usage (incl. prompt-cache read/write counts) — for auditing and measuring
|
|
724
727
|
cost/latency/cache reuse over time. It also records the same bounded `reviewTrace`
|
|
@@ -727,7 +730,7 @@ one-line summary to the terminal / CI job log at the end of each run, so cache r
|
|
|
727
730
|
is visible even in CI (where the run log is ephemeral).
|
|
728
731
|
|
|
729
732
|
`ecr review --save-review` additionally writes a versioned artifact under
|
|
730
|
-
`.expo-code-review/.runs/deferred/` with owner-only permissions. It contains the
|
|
733
|
+
`.expo-agents/code-review/.runs/deferred/` with owner-only permissions. It contains the
|
|
731
734
|
verified final review and bounded feedback metadata, but no credential. `ecr
|
|
732
735
|
post-review` schema-validates it and refuses to post if its explicit repo/PR, live
|
|
733
736
|
head commit, or local comment-policy fingerprint no longer matches.
|
|
@@ -760,20 +763,25 @@ fallback key; `ecr doctor` diagnoses setup.
|
|
|
760
763
|
} }
|
|
761
764
|
```
|
|
762
765
|
|
|
763
|
-
- **Meta / Muse Spark 1.
|
|
764
|
-
model-pinned agent frontmatter to `meta/muse-spark-1.
|
|
766
|
+
- **Meta / Muse Spark 1.3 (public Model API).** Set the default model and any
|
|
767
|
+
model-pinned agent frontmatter to `meta/muse-spark-1.3`. ECR supplies the fixed
|
|
765
768
|
public Responses endpoint and the `@ai-sdk/openai` adapter; the generic
|
|
766
769
|
Chat-Completions compatibility adapter is intentionally not used because it
|
|
767
770
|
loses Muse's reasoning continuity across tool turns. Standard and Contributor
|
|
768
771
|
model ids are supported.
|
|
769
772
|
|
|
770
773
|
```jsonc
|
|
771
|
-
"model": "meta/muse-spark-1.
|
|
774
|
+
"model": "meta/muse-spark-1.3",
|
|
772
775
|
"auth": { "providers": {
|
|
773
776
|
"meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
|
774
777
|
} }
|
|
775
778
|
```
|
|
776
779
|
|
|
780
|
+
Standard and Contributor 1.2/1.3 model ids have known capability metadata. Any
|
|
781
|
+
future model explicitly named as `meta/<model>` is still routed to the fixed Meta
|
|
782
|
+
endpoint with neutral metadata, so a renamed family such as `meta/muse-foo` does
|
|
783
|
+
not require an ECR release; Meta validates an unknown id on its first request.
|
|
784
|
+
|
|
777
785
|
Create the key in the [Meta AI developer portal](https://developer.meta.com/ai/).
|
|
778
786
|
Locally, export it as `META_API_KEY`. In each review-running workflow, replace
|
|
779
787
|
the Anthropic credential line with
|
package/build/commands/ci.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ref LLP 0007#ecr-ci-the-trusted-root-run — fails CLOSED: trusted-root materialization failure never falls back to reading the checkout
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { configDirFor, hasScopeConfig, loadAuthFromRoot, loadReviewConfig, loadScopeConfig, tokenEnvMismatch, } from "../config/load.js";
|
|
5
5
|
import { loadRoutingManifest, resolveScopes, scopedCommentTag, scopePassesBudgetMs, formatOwnerTable, } from "../config/routing.js";
|
|
6
6
|
import { repoRoot, resolveTrustedTool, run } from "../core/exec.js";
|
|
7
7
|
import { errorMessage, publicFailureReason } from "../core/util.js";
|
|
@@ -390,7 +390,7 @@ function resolveStackConfirm(stack, noStackAware) {
|
|
|
390
390
|
* `.expo-code-review/.runs/reviews.jsonl` from the workspace as an artifact.
|
|
391
391
|
*/
|
|
392
392
|
function workspaceRunsDir(cwd) {
|
|
393
|
-
return path.join(cwd,
|
|
393
|
+
return path.join(configDirFor(cwd), ".runs");
|
|
394
394
|
}
|
|
395
395
|
/**
|
|
396
396
|
* The pre-routing single-config path. Kept byte-for-byte equivalent so that with no
|
package/build/commands/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ref LLP 0007#doctor-and-setup-auth — a preflight that must never disagree with a real run
|
|
2
|
-
import { loadReviewConfig, loadScopeConfig, loadAuthFromRoot, hasConfig, resolveConfigDir, tokenEnvMismatch, } from "../config/load.js";
|
|
2
|
+
import { loadReviewConfig, loadScopeConfig, loadAuthFromRoot, hasConfig, CONFIG_DIRNAME, resolveConfigDir, tokenEnvMismatch, } from "../config/load.js";
|
|
3
3
|
import { loadRoutingManifest, resolveScopes, scopePassesBudgetMs, formatOwnerTable, } from "../config/routing.js";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { setupAuthCommand } from "./setup-auth.js";
|
|
@@ -198,7 +198,7 @@ export async function doctorCommand(argv = []) {
|
|
|
198
198
|
info("gh CLI not on PATH — only needed for `ecr ci` (posting PR comments)");
|
|
199
199
|
}
|
|
200
200
|
if (!hasConfig(root)) {
|
|
201
|
-
line(false, `no ${
|
|
201
|
+
line(false, `no ${CONFIG_DIRNAME}/config.jsonc (run \`ecr init\`)`);
|
|
202
202
|
}
|
|
203
203
|
else {
|
|
204
204
|
try {
|
|
@@ -394,7 +394,7 @@ async function listScopes(root) {
|
|
|
394
394
|
return;
|
|
395
395
|
}
|
|
396
396
|
if (!manifest) {
|
|
397
|
-
process.stdout.write(`No ${
|
|
397
|
+
process.stdout.write(`No ${CONFIG_DIRNAME}/routing.jsonc — run \`ecr init --monorepo\`.\n`);
|
|
398
398
|
process.exitCode = 0;
|
|
399
399
|
return;
|
|
400
400
|
}
|
package/build/commands/init.js
CHANGED
|
@@ -3,14 +3,14 @@ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { CONFIG_DIRNAME } from "../config/load.js";
|
|
6
|
+
import { CONFIG_DIRNAME, configDirFor } from "../config/load.js";
|
|
7
7
|
import { ROUTING_FILENAME } from "../config/routing.js";
|
|
8
8
|
import { RoutingScopeSchema } from "../config/schema.js";
|
|
9
9
|
import { FORBIDDEN_TOKEN_ENVS } from "../core/auth.js";
|
|
10
10
|
import { repoRoot } from "../core/exec.js";
|
|
11
11
|
import { errorMessage } from "../core/util.js";
|
|
12
12
|
const TEMPLATES_DIR = fileURLToPath(new URL("../../templates/", import.meta.url));
|
|
13
|
-
const USAGE = `ecr init — scaffold .expo-code-review/ in the current repo
|
|
13
|
+
const USAGE = `ecr init — scaffold .expo-agents/code-review/ in the current repo (a legacy .expo-code-review/ is reused)
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
16
|
ecr init [--no-workflow] [--force] Scaffold the root config (+ CI workflow)
|
|
@@ -19,8 +19,8 @@ Usage:
|
|
|
19
19
|
register it in the root routing.jsonc
|
|
20
20
|
|
|
21
21
|
Options:
|
|
22
|
-
--monorepo Also write
|
|
23
|
-
--scope <dir> Scaffold <dir>/.expo-code-review/ (no auth) + add a scope entry
|
|
22
|
+
--monorepo Also write routing.jsonc in the setup dir (routing manifest)
|
|
23
|
+
--scope <dir> Scaffold <dir>/.expo-agents/code-review/ (no auth) + add a scope entry
|
|
24
24
|
--no-workflow Skip writing the CI workflows (review, command, and dismiss
|
|
25
25
|
under .github/workflows/)
|
|
26
26
|
--token-env <name[,name…]>
|
|
@@ -76,7 +76,7 @@ async function scaffold(argv) {
|
|
|
76
76
|
throw new Error("--token-env customizes the CI workflows; drop it or remove --no-workflow");
|
|
77
77
|
}
|
|
78
78
|
const root = (await repoRoot()) ?? process.cwd();
|
|
79
|
-
const configDir =
|
|
79
|
+
const configDir = configDirFor(root); // an existing legacy .expo-code-review/ is kept, never doubled
|
|
80
80
|
// A non-default --token-env only takes effect through the review workflows,
|
|
81
81
|
// but existing workflow files are skipped (not rewritten) without --force —
|
|
82
82
|
// the flag would silently never reach CI while the next steps claim the
|
|
@@ -201,7 +201,7 @@ async function scaffoldScope(argv, scopeDirRaw) {
|
|
|
201
201
|
throw new Error(`scope dir "${scopeDir}" yields an invalid scope entry (${parsed.error.issues[0]?.message}); ` +
|
|
202
202
|
`rename the directory or add the scope to ${CONFIG_DIRNAME}/${ROUTING_FILENAME} manually`);
|
|
203
203
|
}
|
|
204
|
-
const configDir = path.join(root, scopeDir
|
|
204
|
+
const configDir = configDirFor(path.join(root, scopeDir));
|
|
205
205
|
await mkdir(configDir, { recursive: true });
|
|
206
206
|
const created = [];
|
|
207
207
|
const skipped = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ref LLP 0007#verify-config-the-config-guard — the CI trust guard that runs before the loaders are trusted, so it deliberately does not use them
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { CONFIG_DIRNAME, stripJsonComments, stripTrailingCommas } from "../config/load.js";
|
|
4
|
+
import { CONFIG_DIRNAME, configDirFor, isConfigDirPath, stripJsonComments, stripTrailingCommas, } from "../config/load.js";
|
|
5
5
|
import { ROUTING_FILENAME } from "../config/routing.js";
|
|
6
6
|
import { repoRoot } from "../core/exec.js";
|
|
7
7
|
import { errorMessage } from "../core/util.js";
|
|
@@ -52,9 +52,7 @@ async function discoverConfigFiles(root) {
|
|
|
52
52
|
}
|
|
53
53
|
await walk(path.join(dir, entry.name));
|
|
54
54
|
}
|
|
55
|
-
else if (entry.isFile() &&
|
|
56
|
-
path.basename(dir) === CONFIG_DIRNAME &&
|
|
57
|
-
CONFIG_FILENAMES.has(entry.name)) {
|
|
55
|
+
else if (entry.isFile() && isConfigDirPath(dir) && CONFIG_FILENAMES.has(entry.name)) {
|
|
58
56
|
found.push(path.join(dir, entry.name));
|
|
59
57
|
}
|
|
60
58
|
}
|
|
@@ -112,7 +110,7 @@ function extractFacts(file, parsed) {
|
|
|
112
110
|
*/
|
|
113
111
|
export async function verifyConfig(root, options = {}) {
|
|
114
112
|
const findings = [];
|
|
115
|
-
const rootConfigDir =
|
|
113
|
+
const rootConfigDir = configDirFor(root);
|
|
116
114
|
const rel = (file) => path.relative(root, file) || path.basename(file);
|
|
117
115
|
const files = await discoverConfigFiles(root);
|
|
118
116
|
const tokenEnvOccurrences = [];
|
package/build/config/load.js
CHANGED
|
@@ -7,7 +7,22 @@ import { existsSync } from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { ReviewConfigSchema, ScopeReviewConfigSchema } from "./schema.js";
|
|
9
9
|
import { toolMap } from "../core/tools.js";
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The setup directory. `.expo-agents/code-review/` is the home from 0.15.0 —
|
|
12
|
+
* one `.expo-agents/` directory holds every Expo agent tool's per-repo files
|
|
13
|
+
* (code-review, verify) instead of one dot directory per tool at the root.
|
|
14
|
+
* The pre-0.15 `.expo-code-review/` keeps working unchanged: every lookup
|
|
15
|
+
* takes whichever of the two exists (new first), and scaffolding writes into
|
|
16
|
+
* an existing legacy directory rather than starting a second one.
|
|
17
|
+
*/
|
|
18
|
+
export const CONFIG_DIRNAME = path.join(".expo-agents", "code-review");
|
|
19
|
+
export const LEGACY_CONFIG_DIRNAME = ".expo-code-review";
|
|
20
|
+
export const CONFIG_DIRNAMES = [CONFIG_DIRNAME, LEGACY_CONFIG_DIRNAME];
|
|
21
|
+
/** Does this directory path name a setup dir (either spelling)? */
|
|
22
|
+
export function isConfigDirPath(dir) {
|
|
23
|
+
const normalized = path.normalize(dir);
|
|
24
|
+
return CONFIG_DIRNAMES.some((name) => normalized === name || normalized.endsWith(path.sep + name));
|
|
25
|
+
}
|
|
11
26
|
/** Stack config for a scope load (where `stack` is schema-rejected and absent). */
|
|
12
27
|
const STACK_CONFIG_DEFAULTS = {
|
|
13
28
|
enabled: false,
|
|
@@ -40,21 +55,33 @@ const RESEARCH_CONFIG_DEFAULTS = {
|
|
|
40
55
|
};
|
|
41
56
|
/** Default OpenCode tool toggles for a reviewer: read the repo, never mutate it. */
|
|
42
57
|
const DEFAULT_AGENT_TOOLS = toolMap(["read", "grep", "glob", "list"]);
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The setup dir under `base`: `.expo-agents/code-review/` when it exists, else
|
|
60
|
+
* the legacy `.expo-code-review/` when THAT exists, else the new name (for
|
|
61
|
+
* messages and for scaffolding a fresh repo). Works for the repo root and for
|
|
62
|
+
* scope directories alike.
|
|
63
|
+
*/
|
|
64
|
+
export function configDirFor(base) {
|
|
65
|
+
for (const name of CONFIG_DIRNAMES) {
|
|
66
|
+
const candidate = path.join(base, name);
|
|
67
|
+
if (existsSync(candidate)) {
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return path.join(base, CONFIG_DIRNAME);
|
|
45
72
|
}
|
|
46
73
|
/**
|
|
47
74
|
* Resolve the config directory: an explicit override (absolute or repo-relative)
|
|
48
|
-
* → the ECR_CONFIG_DIR env var → the repo's default
|
|
49
|
-
* the escape hatch (graft 1); with neither override present the
|
|
50
|
-
* `configDirFor(repoRoot)
|
|
75
|
+
* → the ECR_CONFIG_DIR env var → the repo's default setup dir (new name, then
|
|
76
|
+
* legacy). This is the escape hatch (graft 1); with neither override present the
|
|
77
|
+
* result is exactly `configDirFor(repoRoot)`.
|
|
51
78
|
*/
|
|
52
79
|
export function resolveConfigDir(repoRoot, override) {
|
|
53
80
|
const chosen = override ?? process.env.ECR_CONFIG_DIR;
|
|
54
81
|
if (chosen) {
|
|
55
82
|
return path.isAbsolute(chosen) ? chosen : path.join(repoRoot, chosen);
|
|
56
83
|
}
|
|
57
|
-
return
|
|
84
|
+
return configDirFor(repoRoot);
|
|
58
85
|
}
|
|
59
86
|
export function hasConfig(repoRoot, options = {}) {
|
|
60
87
|
// Resolve the same way loadReviewConfig does (incl. the ECR_CONFIG_DIR escape
|
|
@@ -238,7 +265,7 @@ export function hasScopeConfig(root, scope) {
|
|
|
238
265
|
if (scope.config === ".") {
|
|
239
266
|
return true;
|
|
240
267
|
}
|
|
241
|
-
const dir = path.join(root, scope.config
|
|
268
|
+
const dir = configDirFor(path.join(root, scope.config));
|
|
242
269
|
return existsSync(path.join(dir, "config.jsonc")) || existsSync(path.join(dir, "config.json"));
|
|
243
270
|
}
|
|
244
271
|
/**
|
|
@@ -265,7 +292,7 @@ export async function loadScopeConfig(root, scope, manifest, rootConfig) {
|
|
|
265
292
|
if (resolvedScope !== resolvedRoot && !resolvedScope.startsWith(resolvedRoot + path.sep)) {
|
|
266
293
|
throw new Error(`scope "${scope.name}": config "${scope.config}" resolves outside the repo checkout`);
|
|
267
294
|
}
|
|
268
|
-
const dir = path.join(root, scope.config
|
|
295
|
+
const dir = configDirFor(path.join(root, scope.config));
|
|
269
296
|
const { config } = await loadConfigDir(dir, ScopeReviewConfigSchema);
|
|
270
297
|
base = config;
|
|
271
298
|
// Non-default scopes never carry their own marker (the scope schema rejects
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// @ref LLP 0012#unannotated-citations-are-broken-refs — a path-like backtick token that is not a ref fails the check
|
|
4
4
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { CONFIG_DIRNAME, stripJsonComments, stripTrailingCommas } from "../config/load.js";
|
|
6
|
+
import { CONFIG_DIRNAME, LEGACY_CONFIG_DIRNAME, configDirFor, stripJsonComments, stripTrailingCommas, } from "../config/load.js";
|
|
7
7
|
import { ROUTING_FILENAME } from "../config/routing.js";
|
|
8
8
|
import { git, pathInside } from "./exec.js";
|
|
9
9
|
import { matchesIgnore } from "./noise.js";
|
|
@@ -448,10 +448,18 @@ export async function discoverSetupDirs(root) {
|
|
|
448
448
|
continue;
|
|
449
449
|
}
|
|
450
450
|
const child = path.join(dir, entry.name);
|
|
451
|
-
if (entry.name ===
|
|
451
|
+
if (entry.name === LEGACY_CONFIG_DIRNAME) {
|
|
452
452
|
found.push(child);
|
|
453
453
|
continue; // a setup dir never nests another
|
|
454
454
|
}
|
|
455
|
+
if (entry.name === ".expo-agents") {
|
|
456
|
+
// The shared Expo agent-tools dir: only its code-review/ child is ours.
|
|
457
|
+
const setup = path.join(dir, CONFIG_DIRNAME);
|
|
458
|
+
if ((await pathKind(setup)) === "dir") {
|
|
459
|
+
found.push(setup);
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
455
463
|
// Other dot dirs are skipped wholesale: `.claude/worktrees/` holds checkouts of
|
|
456
464
|
// this same repo, whose setup dirs would otherwise be swept as if they were scopes.
|
|
457
465
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) {
|
|
@@ -542,7 +550,7 @@ async function checkRoutingManifest(file, index, rootAgentIds) {
|
|
|
542
550
|
}
|
|
543
551
|
const configDir = typeof entry.config === "string" ? entry.config : null;
|
|
544
552
|
if (configDir) {
|
|
545
|
-
const setupDir = path.resolve(index.root, configDir
|
|
553
|
+
const setupDir = configDirFor(path.resolve(index.root, configDir));
|
|
546
554
|
if (!pathInside(setupDir, index.root)) {
|
|
547
555
|
problems.push({
|
|
548
556
|
file: relative,
|
|
@@ -556,7 +564,7 @@ async function checkRoutingManifest(file, index, rootAgentIds) {
|
|
|
556
564
|
file: relative,
|
|
557
565
|
line: 1,
|
|
558
566
|
kind: "structural",
|
|
559
|
-
problem: `scope "${name}" has no ${configDir}/${CONFIG_DIRNAME}/ directory`,
|
|
567
|
+
problem: `scope "${name}" has no ${configDir}/${CONFIG_DIRNAME}/ (or ${LEGACY_CONFIG_DIRNAME}/) directory`,
|
|
560
568
|
});
|
|
561
569
|
}
|
|
562
570
|
}
|
package/build/core/opencode.js
CHANGED
|
@@ -10,9 +10,9 @@ import { errorMessage, sleep } from "./util.js";
|
|
|
10
10
|
/** Discriminant for the Claude Code CLI engine (see core/claude-code.ts). */
|
|
11
11
|
export const CLAUDE_CODE_ENGINE = "claude-code";
|
|
12
12
|
/** Fixed so repo config cannot redirect the Meta credential. */
|
|
13
|
-
// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint and model
|
|
13
|
+
// @ref LLP 0003#muse-spark-via-meta-model-api [implements] — fixed endpoint, provider routing, and known-model metadata
|
|
14
14
|
export const META_MODEL_API_BASE_URL = "https://api.meta.ai/v1";
|
|
15
|
-
/**
|
|
15
|
+
/** Known public Muse models. Other explicit `meta/...` ids use neutral metadata. */
|
|
16
16
|
export const META_MUSE_MODELS = {
|
|
17
17
|
"muse-spark-1.2": {
|
|
18
18
|
name: "Muse Spark 1.2",
|
|
@@ -44,6 +44,36 @@ export const META_MUSE_MODELS = {
|
|
|
44
44
|
include: ["reasoning.encrypted_content"],
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
|
+
"muse-spark-1.3": {
|
|
48
|
+
name: "Muse Spark 1.3",
|
|
49
|
+
reasoning: true,
|
|
50
|
+
limit: { context: 1_048_576, output: 1_048_576 },
|
|
51
|
+
modalities: {
|
|
52
|
+
input: ["text", "image", "pdf", "video"],
|
|
53
|
+
output: ["text"],
|
|
54
|
+
},
|
|
55
|
+
options: {
|
|
56
|
+
store: false,
|
|
57
|
+
reasoningEffort: "high",
|
|
58
|
+
reasoningSummary: "auto",
|
|
59
|
+
include: ["reasoning.encrypted_content"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"muse-spark-1.3-contributor": {
|
|
63
|
+
name: "Muse Spark 1.3 Contributor",
|
|
64
|
+
reasoning: true,
|
|
65
|
+
limit: { context: 1_048_576, output: 1_048_576 },
|
|
66
|
+
modalities: {
|
|
67
|
+
input: ["text", "image", "pdf", "video"],
|
|
68
|
+
output: ["text"],
|
|
69
|
+
},
|
|
70
|
+
options: {
|
|
71
|
+
store: false,
|
|
72
|
+
reasoningEffort: "high",
|
|
73
|
+
reasoningSummary: "auto",
|
|
74
|
+
include: ["reasoning.encrypted_content"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
47
77
|
};
|
|
48
78
|
/**
|
|
49
79
|
* Resolve which engine an agent's pass dispatches to, and (when claude) which
|
|
@@ -187,7 +217,19 @@ export function buildOpencodeConfig(config, research) {
|
|
|
187
217
|
entry.mode === "api-key" &&
|
|
188
218
|
entry.tokenEnv &&
|
|
189
219
|
!entry.upstream) {
|
|
190
|
-
//
|
|
220
|
+
// Meta's Model API uses the Responses adapter; openai-compatible uses Chat
|
|
221
|
+
// Completions. Register every explicitly referenced `meta/…` model so a new
|
|
222
|
+
// Meta family/name does not require an ECR release merely to select the right
|
|
223
|
+
// provider. Known Muse ids get their published capabilities; unknown ids stay
|
|
224
|
+
// neutral and are validated by Meta on their first request.
|
|
225
|
+
const models = {};
|
|
226
|
+
for (const id of referencedModels) {
|
|
227
|
+
const slash = id.indexOf("/");
|
|
228
|
+
if (slash > 0 && id.slice(0, slash) === "meta" && id.length > slash + 1) {
|
|
229
|
+
const model = id.slice(slash + 1);
|
|
230
|
+
models[model] = META_MUSE_MODELS[model] ?? {};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
191
233
|
provider.meta = {
|
|
192
234
|
npm: "@ai-sdk/openai",
|
|
193
235
|
name: "Meta Model API",
|
|
@@ -195,7 +237,7 @@ export function buildOpencodeConfig(config, research) {
|
|
|
195
237
|
baseURL: META_MODEL_API_BASE_URL,
|
|
196
238
|
apiKey: `{env:${entry.tokenEnv}}`,
|
|
197
239
|
},
|
|
198
|
-
models
|
|
240
|
+
models,
|
|
199
241
|
};
|
|
200
242
|
continue;
|
|
201
243
|
}
|
package/package.json
CHANGED
package/templates/atlantis.yml
CHANGED
|
@@ -128,6 +128,8 @@ jobs:
|
|
|
128
128
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
129
129
|
with:
|
|
130
130
|
name: review-run-log-pr${{ github.event.issue.number }}
|
|
131
|
-
path:
|
|
131
|
+
path: |
|
|
132
|
+
.expo-agents/code-review/.runs/reviews.jsonl
|
|
133
|
+
.expo-code-review/.runs/reviews.jsonl
|
|
132
134
|
if-no-files-found: ignore
|
|
133
135
|
retention-days: 14
|
package/templates/command.yml
CHANGED
|
@@ -183,6 +183,8 @@ jobs:
|
|
|
183
183
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
184
184
|
with:
|
|
185
185
|
name: review-run-log-pr${{ github.event.issue.number }}
|
|
186
|
-
path:
|
|
186
|
+
path: |
|
|
187
|
+
.expo-agents/code-review/.runs/reviews.jsonl
|
|
188
|
+
.expo-code-review/.runs/reviews.jsonl
|
|
187
189
|
if-no-files-found: ignore
|
|
188
190
|
retention-days: 14
|
package/templates/config.jsonc
CHANGED
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
// "auth": { "mode": "api-key", "provider": "openai", "tokenEnv": "OPENAI_API_KEY" }
|
|
88
88
|
//
|
|
89
89
|
// Muse Spark via the public Meta Model API Responses endpoint:
|
|
90
|
-
// set models to `meta/muse-spark-1.
|
|
90
|
+
// set models to `meta/muse-spark-1.3` and configure:
|
|
91
91
|
// "auth": { "providers": {
|
|
92
92
|
// "meta": { "mode": "api-key", "tokenEnv": "META_API_KEY" }
|
|
93
93
|
// } }
|
package/templates/workflow.yml
CHANGED
|
@@ -133,6 +133,8 @@ jobs:
|
|
|
133
133
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
134
134
|
with:
|
|
135
135
|
name: review-run-log-pr${{ github.event.pull_request.number }}
|
|
136
|
-
path:
|
|
136
|
+
path: |
|
|
137
|
+
.expo-agents/code-review/.runs/reviews.jsonl
|
|
138
|
+
.expo-code-review/.runs/reviews.jsonl
|
|
137
139
|
if-no-files-found: ignore
|
|
138
140
|
retention-days: 14
|