@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 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.2 (public Model API).** Set the default model and any
764
- model-pinned agent frontmatter to `meta/muse-spark-1.2`. ECR supplies the fixed
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.2",
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
@@ -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 { CONFIG_DIRNAME, hasScopeConfig, loadAuthFromRoot, loadReviewConfig, loadScopeConfig, tokenEnvMismatch, } from "../config/load.js";
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, CONFIG_DIRNAME, ".runs");
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
@@ -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 ${".expo-code-review"}/config.jsonc (run \`ecr init\`)`);
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 ${".expo-code-review"}/routing.jsonc — run \`ecr init --monorepo\`.\n`);
397
+ process.stdout.write(`No ${CONFIG_DIRNAME}/routing.jsonc — run \`ecr init --monorepo\`.\n`);
398
398
  process.exitCode = 0;
399
399
  return;
400
400
  }
@@ -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 .expo-code-review/routing.jsonc (routing manifest)
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 = path.join(root, CONFIG_DIRNAME);
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, CONFIG_DIRNAME);
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 = path.join(root, CONFIG_DIRNAME);
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 = [];
@@ -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
- export const CONFIG_DIRNAME = ".expo-code-review";
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
- export function configDirFor(repoRoot) {
44
- return path.join(repoRoot, CONFIG_DIRNAME);
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 `.expo-code-review/`. This is
49
- * the escape hatch (graft 1); with neither override present the result is exactly
50
- * `configDirFor(repoRoot)`, so the default behavior is byte-identical.
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 path.join(repoRoot, CONFIG_DIRNAME);
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, CONFIG_DIRNAME);
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, CONFIG_DIRNAME);
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 === CONFIG_DIRNAME) {
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, CONFIG_DIRNAME);
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
  }
@@ -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 allowlist
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
- /** Supported public Muse models. Unknown ids remain visible to preflight. */
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
- // Muse needs the Responses adapter; openai-compatible uses Chat Completions.
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: Object.fromEntries(Object.entries(META_MUSE_MODELS).filter(([model]) => referencedModels.includes(`meta/${model}`))),
240
+ models,
199
241
  };
200
242
  continue;
201
243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/code-review-cli",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "description": "Generic, config-driven AI code reviewer engine. Repos supply their agents via .expo-code-review/.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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: .expo-code-review/.runs/reviews.jsonl
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
@@ -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: .expo-code-review/.runs/reviews.jsonl
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
@@ -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.2` and configure:
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
  // } }
@@ -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: .expo-code-review/.runs/reviews.jsonl
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