Commit Graph
13 Commits
Author SHA1 Message Date
Philippe Loulidi 82c466aac8 self-review: switch to the claude-code engine on the Max subscription (#43) 2026-07-31 19:42:54 -06:00
Philippe Loulidi 6bbc1fbd7a Adopt LLP: design docs in llp/, @ref annotations, ref-check in CI (#38)
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.
2026-07-31 01:20:51 -06:00
Brent Vatne 601b19afc1 Trusted base-commit configuration by default; isolate the runtime from PR-head config
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)
2026-07-26 22:29:18 -07:00
Brent Vatne 188f8521bf Detect provider rate limits and wait them out; subscription-aware concurrency (#32)
* 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
2026-07-26 19:44:03 -07:00
Brent Vatne 43b31a4879 Mixed Codex-subscription + API-key auth; GPT by default; per-pass model reporting (#30)
* 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
2026-07-26 18:19:26 -07:00
Brent Vatne 3aeb82ba0d Detect stalled model requests; let the cross-file pass use the whole budget (#27)
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.
2026-07-26 18:00:51 -07:00
Philippe Loulidi 57d879789b Publish to npm from CI on version tags (tag is the version source of truth) (#26) 2026-07-24 14:31:22 -06:00
Philippe Loulidi 31f289b793 Ship the canonical config guard as ecr verify-config; replace the awk sweep (#12) 2026-07-24 14:29:34 -06:00
expo-tuft[bot]andbrentvatne 97473bab52 Add oxlint + oxfmt and wire into CI (#10)
Co-authored-by: brentvatne <[email protected]>
2026-07-23 11:08:33 -07:00
expo-tuft[bot]andbrentvatne 1e9aea2542 Config-driven trigger policy + fix ai-review:skip matching (#7)
* 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]>
2026-07-23 10:50:01 -07:00
Brent Vatne 8ea2a50a1c Add self-review setup: ecr init scaffold + CI workflow running from source
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.
2026-07-22 23:08:30 -07:00
Brent Vatne 2166a7a81d Use bun as the package manager (bun.lock, bun-based CI) 2026-07-22 18:29:29 -07:00
Brent Vatne 63d62924f3 Add CI: typecheck, build, unit tests 2026-07-22 18:28:01 -07:00