Retrofit of https://github.com/ccheever/llp. Ten LLP docs capture the
design rationale (trust model, engine pipeline, runtimes, config,
verification, sources/reporters, CLI, templates); 161 @ref comments link
code to the sections that explain it. ref-check (copied from upstream,
dependency-free) validates refs and metadata on every push and PR.
Every rationale claim is tagged [observed] with its evidence.
ecr ci now loads all review configuration (config.jsonc, routing, prompts,
models, auth mapping) from the PR's immutable base commit, materialized via
the GitHub API — never from the PR head — failing closed with one terminal
comment when the base can't be materialized. The PR head is materialized
separately (pinned to its immutable OID, mandatory in CI) purely as source
content, and is scrubbed of ambient runtime config (opencode.json{,c},
.opencode/ plugins, AGENTS.md, CLAUDE.md, .claude/, .mcp.json, .cursor*,
.env*) before the OpenCode server starts, closing a fork-reachable code
execution path through the /review command workflow.
- git fetches append `-c credential.helper=!gh auth git-credential` so
base-SHA checkouts with persist-credentials:false still fetch private repos
(token never in argv or .git/config)
- run log + patch workspace anchor at the workspace (runsDir), never the
temporary trusted root, so the artifact upload path keeps resolving
- a scope config that is new in a PR reviews with the root config and
activates after merge (hasScopeConfig fallback)
- scaffolded workflows check out the base SHA with persist-credentials:false;
the repo's own self-review workflow runs the engine from base source
- escape hatch: ecr ci --unsafe-config-from-head (loud warning, never
scaffolded, removal scheduled)
* Detect provider rate limits and wait them out; subscription-aware concurrency
A shared subscription credential can serve several PRs' reviews at once, so
throttling is a normal condition to ride out, not a failure — and it must be
distinguishable from a wedged request.
- watch the OpenCode server's own log (isolated per run) for 429/rate-limit
ERROR lines — explicit throttle evidence, the only kind that exists outside
the provider; a silently parked request emits nothing anywhere
- a stall WITH recent evidence waits in 90s beats (never consuming the one
wedged retry, bounded by the pass deadline) instead of re-sending the pass's
whole context into a limited account; without evidence, the wedged-retry
behavior is unchanged
- explicit 429 errors retry on a patient 15s/45s/90s schedule instead of 2s/8s
- chunk.concurrency unset now resolves by auth mode: 3 on a subscription
(oauth) credential, 6 on API keys — one account handles six parallel streams
poorly (the parked-request stall signature on eas-cli#4084 hit both agents'
chunk 2 simultaneously); an explicit value always wins
- rate-limit events surface at run end and in the run log (rateLimitEvents),
so throttle frequency is measurable across PRs
* Support ChatGPT ACCESS tokens (rotation-safe); honest verdict when nothing ran
The refresh-token-as-static-secret strategy died in production (euxy#8: every
pass "Token refresh failed: 401" — refresh tokens are SINGLE-USE; the shared
copy was spent by its first CI use and the sign-in with it).
- oauthAuthJsonEntry detects the token kind: a JWT (ACCESS token) is used
as-is with its exp claim as the expiry — no rotation involvement, safe as a
shared/static secret; an opaque value keeps the refresh-token shape for
sole-consumer setups
- an expired access token fails fast naming the re-mint; near-expiry warns
(doctor + preflight) — the credential can't silently lapse mid-run
- setup-auth hands out the ACCESS token (default env CODEX_OAUTH_ACCESS_TOKEN)
and never surfaces the refresh token, which stays in OpenCode's own store
as its sole legitimate consumer
- an all-passes-failed run no longer renders as "Approve with comments":
couldNotComplete overrides the decision label with "No review — every pass
failed" (single and per-scope comments)
- README: retract the false "refresh-token reuse verified, static CI secret
works" claim; document the access-token strategy
- ROADMAP: token-rotator cron job added as the most important next item
- this repo's config/workflow: tokenEnv renamed to CODEX_OAUTH_ACCESS_TOKEN
* Mixed Codex-subscription + API-key auth; GPT by default; report each pass's model
- auth accepts a per-provider map (auth.providers) alongside the legacy single
object; upstream aliases synthesize OpenCode provider blocks so one upstream
is reachable with two credentials at once (subscription for default models,
metered key for the pro tier the subscription excludes)
- openai oauth stores the REFRESH token (expires 0; OpenCode's codex plugin
mints access tokens; reuse across runs verified)
- tokenEnv guard (verify-config) + runtime lock use exact SET semantics via
comma-separated ECR_EXPECTED_TOKEN_ENV
- default model is openai/gpt-5.5; anthropic subscription OAuth removed from
docs (prohibited upstream and unsupported by OpenCode; API key still works)
- an empty REVIEWER_MODEL no longer wipes configured model ids
- every pass reports the provider/model that ACTUALLY answered (job log, step
summary table, run log agentModels) and warns loudly on substitution —
reviews silently ran on opencode's free gateway model for weeks behind the
two failures above
- this repo's own review runs the mixed setup (security on openai-api/gpt-5.5-pro)
* Close the cross-provider key redirect; default the expected set to both names
Review findings on this PR (the pro-tier security pass, fittingly):
- a well-known provider key env may only feed the provider that owns it —
the tokenEnv guard locks NAMES, so without this an auth entry could keep a
locked name but point provider/upstream at a different provider
- the self-review workflow's ECR_EXPECTED_TOKEN_ENV fallback now matches the
committed two-credential config instead of relying on the repo variable
A wedged provider request used to burn a pass's entire wall-clock cap in
silence, and the cross-file pass had no way to spend more than 25 minutes even
when the diff warranted it. Add a no-progress watchdog with one clean-slate
retry, make the finalize/corrective salvage physically tool-free, and give the
cross-file pass the remaining passes window instead of a fixed cap.
Also stop subdividing the cross-file pass by file halves: a left-half/
right-half interaction is invisible to both halves, so splitting deleted the
coverage the pass exists for while reporting success. It now falls back to a
whole-diff no-tools pass.
* Add config-driven trigger policy; fix ai-review:skip matching
Move which-PRs-to-review policy into config (.expo-code-review/config.jsonc
-> review.trigger), so it's a declarative per-repo knob alongside agents/
noise/policy/auth rather than hardcoded in each repo's workflow YAML. ecr ci
now self-gates: skipLabel (ai-review:skip) always wins; trigger "label"
requires an ai-review (or ai-review:<agent>) label; trigger "all" reviews
every non-skipped PR. Default is "all".
Keep per-PR skip as a LABEL, not a config flag: labels are write-gated to
maintainers, so a PR author can't edit config to dodge review (config.jsonc
is read from the PR ref in the auto-workflow).
The workflow if: gate stays available for repos that prefer gating in YAML,
but is fixed to use contains()'s array form for an EXACT match — the old
join()+substring form treated 'ai-review:skip' as containing 'ai-review', so
skip didn't actually skip. It's now a coarse opt-out (run unless skip) on top
of the config policy. Adds shouldReview unit tests.
Co-authored-by: brentvatne <[email protected]>
* Sync templates/workflow.yml gate with the fixed workflow
Follow-up to the same PR: the scaffolded template still shipped the old
substring gate (contains(join(labels), 'ai-review')), so `ecr init` would
give newly adopting repos the bug this PR fixes (ai-review:skip satisfying the
ai-review check). Bring it in line with .github/workflows/expo-code-review.yml
— exact-match opt-out — keeping templates in sync with the code (AGENTS.md).
Co-authored-by: brentvatne <[email protected]>
---------
Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com>
Co-authored-by: brentvatne <[email protected]>
Scaffolded with `ecr init`, with the workflow's run step switched from the
published npm package to `bun run src/cli.ts ci` so each PR is reviewed by
the reviewer code in that PR.