Skip to content

refactor(ci): review ai-toolkit's own PRs with @uniswap/review-cli - #556

Merged
wkoutre merged 7 commits into
nextfrom
nickkoutrelakos/review-cli-migration
Jul 31, 2026
Merged

refactor(ci): review ai-toolkit's own PRs with @uniswap/review-cli#556
wkoutre merged 7 commits into
nextfrom
nickkoutrelakos/review-cli-migration

Conversation

@wkoutre

@wkoutre wkoutre commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Points .github/workflows/claude-code-review.yml at @uniswap/review-cli instead of this repo's own _claude-code-review.yml. This is the same reviewer Uniswap/universe and Uniswap/backend already run, so review-quality work lands in one place instead of three.

Scope: the reusable workflow stays

_claude-code-review.yml is a published product with 10 external consumers (universal-router, uniswap-ai, protocol-fees, v4-hooks-public, v4-hooks-internal, security, hooks, ai-sandbox, tjar, initializer). It is unchanged and still supported. build-prompt.ts, post-review.ts, build-prompt.spec.ts, and .github/prompts/pr-review/ all stay in place for those callers — none of it is dead code.

Only ai-toolkit's own review path moves. Every consumer pins by commit SHA, so nothing downstream is affected by this PR.

One consequence worth knowing: ai-toolkit's CI no longer exercises build-prompt.ts / post-review.ts, so a regression there will not surface on an ai-toolkit PR anymore. That is now called out in .github/scripts/CLAUDE.md, .github/prompts/CLAUDE.md, and the reusable-workflow section of .github/workflows/CLAUDE.md, with a pointer to validate against a consumer repo (or ai-sandbox).

Quality parity, verified rather than assumed

review-cli's defaults would have silently regressed four behaviors. Each fix was checked by probing review-cli triage directly:

Behavior Risk under defaults Fix
Dependency PRs reviewed skip.branch_prefixes includes dependabot//renovate/ and skip.authors includes *[bot] → skipped → auto-merge-dependabot never fires, security bumps sit unmerged skip.authors: [], dependency prefixes omitted
claude[bot] draft PRs reviewed on open skip.drafts is a single boolean and can't express the carve-out; autonomous-task PRs are drafts skip.drafts: false, draft policy moved to the job-level if:
Title-based automation skip (chore(release):, chore(sync):) review-cli matches branches and authors, but has no notion of PR titles Retained via the shared check-automated-pr action
Fork PRs never reviewed The CLI has no fork concept, and issue_comment/workflow_dispatch payloads carry no head-repo field Resolved via the API in the triage job; the review job gates on it

Observed output from the probe (--format json, run against this branch's config):

--- with .claude/review.yml (this PR)
claude[bot] DRAFT PR              "run":true
release/ branch                   "run":false  "reason":"branch_skip"
gtmq merge-queue branch           "run":false  "reason":"branch_skip"
gh-readonly-queue branch          "run":false  "reason":"branch_skip"
chore/update-action-versions      "run":false  "reason":"branch_skip"
normal feature branch             "run":true
dependabot/... (dependabot[bot])   "run":true

--- with --skip-config (upstream template shape)
claude[bot] DRAFT PR              "run":false  "reason":"draft"     <- regression
gtmq merge-queue branch           "run":true                        <- wasteful

The --skip-config trap

review-cli's upstream workflow template runs the gate with --skip-config to avoid a checkout. This PR deliberately does not. That flag passes no policy object, which is not the same as "use the CLI's defaults":

  • skipDrafts = policy?.drafts ?? true still defaults to skipping drafts
  • policy?.branchPrefixes / policy?.authors are undefined, so no branch or author skips apply at all

So the triage job sparse-checks-out .github/actions + .claude and runs the gate without the flag. Because loadConfig treats a missing file as "use defaults" silently, a Verify review config is present step fails the job outright rather than letting a botched checkout produce a green run that quietly stopped reviewing dependency PRs.

force_review maps to --force (skip rebase detection), not --fresh--fresh also discards prior findings and thread decisions, which would lose the iterative-review context.

Improvements over the retired path

  • Models: claude-opus-5 reviewers with claude-fable-5 synthesis, up from claude-opus-4-8
  • Comment-trigger authorization: restricted to OWNER/MEMBER/COLLABORATOR. The previous workflow only excluded github-actions[bot] and never checked author association — on a public repo, any commenter could trigger an agent run
  • Investigation gate: an empty-findings APPROVE is demoted to COMMENT unless the reviewers actually opened enough of the changed files
  • Idempotency: three-level change detection (tree SHA → patch ID → hunk digest) replaces patch-ID-only caching
  • Thread hygiene: one thread per finding, deduplicated by file:line:category; a thread a human replied to is never auto-resolved
  • Two repo-specific reviewers encoding house rules that previously lived only in documentation:
    • workflow-security-reviewer — expression injection into run:, unpinned/dynamic action refs, permission scope, missing Bullfrog steps, secret exposure, fromJSON coercion of vars.*, and breaking changes to the reusable-workflow contracts other repos consume
    • plugin-conventions-reviewer — the mandatory .claude-plugin/plugin.json version bump and its increment, manifest arrays drifting from directories on disk, skills registered as commands, skill/agent naming
  • permissions: {} deny-by-default at workflow level, with per-key rationale comments on every job

Docs updated

  • CLAUDE.md — new "AI Code Review of This Repository" section, plus the rule that changing a documented convention means updating the matching reviewer
  • .github/workflows/CLAUDE.md — new section for this workflow, the --skip-config gotcha, and a scope note on the reusable workflow. Corrected the stale MAX_DIFF_LINES env documentation (no longer read by this workflow; kept because external callers still pass it to _claude-code-review.yml)
  • .github/workflows/README.md, .github/scripts/CLAUDE.md, .github/prompts/CLAUDE.md, docs/guides/claude-integration.md — scope notes and refreshed feature/output descriptions

Required repo configuration

  • secrets.CLAUDE_CODE_OAUTH_TOKENrequired. The job fails fast without it (otherwise the agent factory falls back to a mock and "reviews" nothing). ANTHROPIC_API_KEY is deliberately never forwarded to the review job.
  • vars.REVIEW_CLI_VERSION — optional; overrides the 1.10.1 fallback without a commit. Matches universe's pin; 1.10.x is required for model.synthesis.
  • secrets.DATADOG_API_KEY — optional; the CI Visibility step is skipped when unset.
  • The consuming repo needs package-level read access to @uniswap/review-cli in Uniswap/internal-tools → Packages.

Test plan

Static verification (this change is CI configuration, so the real end-to-end test is this PR's own review run):

  • actionlint clean on the new workflow (fixed an inherited SC2005)
  • zizmor clean; also ran --persona=auditor and fixed the 3 actionable findings (workflow-level permissions, 2× undocumented permissions)
  • markdownlint-cli2 — 0 errors across 197 files
  • nx affected --target=lint — 0 errors
  • nx affected --target=typecheck — pass
  • nx affected --target=test — 89 passed (build-prompt.spec.ts still green; those scripts are untouched)
  • YAML parse-checked all three new/changed YAML files
  • review-cli --explain against a live ai-toolkit PR — config loads, 13 agents resolve (11 bundled + the 2 new ones, confirming repo agents are additive)
  • review-cli triage probed across the full skip matrix above
  • Live run on this PR — the real check. Expect: triage gate accepts, review posts a sticky summary plus inline threads, and workflow-security-reviewer is staffed (this diff is almost entirely .github/)

Notes for the reviewer

  • The install_review_cli composite action installs from a scratch dir with its own bunfig.toml. Two reasons, both load-bearing: the repo's bunfig.toml pins the whole @uniswap scope to npmjs while this package is private on GitHub Packages (bun only supports per-scope overrides), and the repo's 3-day minimumReleaseAge would silently resolve a freshly pinned version to an older one.
  • No max_budget_usd in .claude/review.yml on purpose: as of 1.10.x that key is parsed and validated but never enforced, so setting it would read as a run-level ceiling while doing nothing. --explain's "Max budget" line is a display sum, not a cap. The real controls are agent_budget_usd, triage staffing, and timeout-minutes.
  • No packages/plugins/ files changed, so no plugin version bump applies.
AI-Generated Description

Summary

Points .github/workflows/claude-code-review.yml at @uniswap/review-cli instead of this repo's own _claude-code-review.yml. This is the same reviewer Uniswap/universe and Uniswap/backend already run, so review-quality work lands in one place instead of three.
11 files changed (+1129 / -282). The change is almost entirely .github/ plus docs — no plugin code and no plugin version bump.

Scope: the reusable workflow stays

_claude-code-review.yml is a published product with 10 external consumers (universal-router, uniswap-ai, protocol-fees, v4-hooks-public, v4-hooks-internal, security, hooks, ai-sandbox, tjar, initializer). It is unchanged and still supported. build-prompt.ts, post-review.ts, build-prompt.spec.ts, and .github/prompts/pr-review/ all stay in place for those callers — none of it is dead code.
Only ai-toolkit's own review path moves. Every consumer pins by commit SHA, so nothing downstream is affected by this PR.
One consequence worth knowing: ai-toolkit's CI no longer exercises build-prompt.ts / post-review.ts, so a regression there will not surface on an ai-toolkit PR anymore. That is now called out in .github/scripts/CLAUDE.md, .github/prompts/CLAUDE.md, and the reusable-workflow section of .github/workflows/CLAUDE.md, with a pointer to validate against a consumer repo (or Uniswap/ai-sandbox).

Quality parity, verified rather than assumed

review-cli's defaults would have silently regressed four behaviors. Each fix was checked by probing review-cli triage directly:

Behavior Risk under defaults Fix
Dependency PRs reviewed skip.branch_prefixes includes dependabot//renovate/ and skip.authors includes *[bot] → skipped → auto-merge-dependabot never fires, security bumps sit unmerged skip.authors: [], dependency prefixes omitted
claude[bot] draft PRs reviewed on open skip.drafts is a single boolean and can't express the carve-out; autonomous-task PRs are drafts skip.drafts: false, draft policy moved to the job-level if:
Title-based automation skip (chore(release):, chore(sync):) review-cli matches branches and authors, but has no notion of PR titles Retained via the shared check-automated-pr action
Fork PRs never reviewed The CLI has no fork concept, and issue_comment/workflow_dispatch payloads carry no head-repo field Resolved via the API in the triage job; the review job gates on it

The --skip-config trap

review-cli's upstream workflow template runs the gate with --skip-config to avoid a checkout. This PR deliberately does not. That flag passes no policy object, which is not the same as "use the CLI's defaults":

  • skipDrafts = policy?.drafts ?? true still defaults to skipping drafts
  • policy?.branchPrefixes / policy?.authors are undefined, so no branch or author skips apply at all
    So the triage job sparse-checks-out .github/actions + .claude and runs the gate without the flag. Because loadConfig treats a missing file as "use defaults" silently, a Verify review config is present step fails the job outright rather than letting a botched checkout produce a green run that quietly stopped reviewing dependency PRs.
    force_review maps to --force (skip rebase detection), not --fresh--fresh also discards prior findings and thread decisions, which would lose the iterative-review context.

Improvements over the retired path

  • Models: claude-opus-5 reviewers with claude-fable-5 synthesis, up from claude-opus-4-8
  • Comment-trigger authorization: restricted to OWNER/MEMBER/COLLABORATOR. The previous workflow only excluded github-actions[bot] and never checked author association — on a public repo, any commenter could trigger an agent run
  • Investigation gate: an empty-findings APPROVE is demoted to COMMENT unless the reviewers actually opened enough of the changed files
  • Idempotency: three-level change detection (tree SHA → patch ID → hunk digest) replaces patch-ID-only caching
  • Thread hygiene: one thread per finding, deduplicated by file:line:category; a thread a human replied to is never auto-resolved
  • Two repo-specific reviewers encoding house rules that previously lived only in documentation:
    • workflow-security-reviewer — expression injection into run:, unpinned/dynamic action refs, permission scope, missing Bullfrog steps, secret exposure, fromJSON coercion of vars.*, and breaking changes to the reusable-workflow contracts other repos consume
    • plugin-conventions-reviewer — the mandatory .claude-plugin/plugin.json version bump and its increment, manifest arrays drifting from directories on disk, skills registered as commands, skill/agent naming
  • permissions: {} deny-by-default at workflow level, with per-key rationale comments on every job

Docs updated

  • CLAUDE.md — new "AI Code Review of This Repository" section, plus the rule that changing a documented convention means updating the matching reviewer
  • .github/workflows/CLAUDE.md — new section for this workflow, the --skip-config gotcha, and a scope note on the reusable workflow. Corrected the stale MAX_DIFF_LINES env documentation (no longer read by this workflow; kept because external callers still pass it to _claude-code-review.yml)
  • .github/workflows/README.md, .github/scripts/CLAUDE.md, .github/prompts/CLAUDE.md, docs/guides/claude-integration.md — scope notes and refreshed feature/output descriptions

Required repo configuration

  • secrets.CLAUDE_CODE_OAUTH_TOKENrequired. The job fails fast without it (otherwise the agent factory falls back to a mock and "reviews" nothing). ANTHROPIC_API_KEY is deliberately never forwarded to the review job.
  • vars.REVIEW_CLI_VERSION — optional; overrides the 1.10.1 fallback without a commit. Matches universe's pin; 1.10.x is required for model.synthesis.
  • secrets.DATADOG_API_KEY — optional; the CI Visibility step is skipped when unset.
  • The consuming repo needs package-level read access to @uniswap/review-cli in Uniswap/internal-tools → Packages.

Test plan

Static verification (this change is CI configuration, so the real end-to-end test is this PR's own review run):

  • actionlint clean on the new workflow (fixed an inherited SC2005)
  • zizmor clean; also ran --persona=auditor and fixed the 3 actionable findings (workflow-level permissions, 2× undocumented permissions)
  • markdownlint-cli2 — 0 errors across 197 files
  • nx affected --target=lint — 0 errors
  • nx affected --target=typecheck — pass
  • nx affected --target=test — 89 passed (build-prompt.spec.ts still green; those scripts are untouched)
  • YAML parse-checked all three new/changed YAML files
  • review-cli --explain against a live ai-toolkit PR — config loads, 13 agents resolve (11 bundled + the 2 new ones, confirming repo agents are additive)
  • review-cli triage probed across the full skip matrix above
  • Live run on this PR — the real check. Expect: triage gate accepts, review posts a sticky summary plus inline threads, and workflow-security-reviewer is staffed (this diff is almost entirely .github/)

Notes for the reviewer

  • The install_review_cli composite action installs from a scratch dir with its own bunfig.toml. Two reasons, both load-bearing: the repo's bunfig.toml pins the whole @uniswap scope to npmjs while this package is private on GitHub Packages (bun only supports per-scope overrides), and the repo's 3-day minimumReleaseAge would silently resolve a freshly pinned version to an older one.
  • No max_budget_usd in .claude/review.yml on purpose: as of 1.10.x that key is parsed and validated but never enforced, so setting it would read as a run-level ceiling while doing nothing. --explain's "Max budget" line is a display sum, not a cap. The real controls are agent_budget_usd, triage staffing, and timeout-minutes.
  • No packages/plugins/ files changed, so no plugin version bump applies.

Point .github/workflows/claude-code-review.yml at @uniswap/review-cli
from Uniswap/internal-tools, the shared reviewer already used by
Uniswap/universe and Uniswap/backend, instead of this repo's own
_claude-code-review.yml.

The reusable _claude-code-review.yml is unchanged and still published for
its 10+ external consumers, all of which pin it by commit SHA. Only
ai-toolkit's own review path moves; build-prompt.ts, post-review.ts, and
.github/prompts/pr-review/ stay in place for those callers.

Quality parity, verified against `review-cli triage` rather than assumed:

- Dependency PRs are still reviewed (skip.authors: [], dependency branch
  prefixes omitted) so auto-merge-dependabot can keep gating on the
  review result.
- claude[bot] draft PRs are still reviewed on open. review-cli's
  skip.drafts is a single boolean that cannot express that carve-out, so
  skip.drafts is false and the draft policy lives in the job-level if:.
- Title-based automation detection (chore(release):, chore(sync):) is
  retained via the shared check-automated-pr action, which review-cli's
  branch/author skip policy has no equivalent for.
- Fork PRs are still never reviewed, resolved via the API in the triage
  job since the CLI has no fork concept.
- workflow_dispatch still takes pr_number and force_review; force_review
  maps to --force, not --fresh, to preserve iterative review context.

The gate deliberately does NOT use --skip-config from review-cli's
upstream workflow template. That flag passes no policy at all, which is
not the same as using defaults: skip.drafts falls back to true (skipping
claude[bot] drafts) while branch and author skips are not applied. A
guard step fails the job if .claude/review.yml is missing, because
loadConfig treats a missing file as "use defaults" silently.

Improvements over the retired path:

- claude-opus-5 reviewers with claude-fable-5 synthesis, up from
  claude-opus-4-8
- Comment triggers restricted to OWNER/MEMBER/COLLABORATOR, which the
  previous workflow did not check on this public repo
- An investigation gate demotes an unsubstantiated empty-findings APPROVE
  to COMMENT
- Two repo-specific reviewers encode house rules that previously lived
  only in documentation: workflow-security-reviewer (expression
  injection, SHA pinning, permission scope, Bullfrog steps, reusable
  workflow contract breaks) and plugin-conventions-reviewer (the
  mandatory plugin.json version bump, manifest drift, naming)
- Workflow-level permissions: {} deny-by-default

Verified: actionlint clean, zizmor clean, markdownlint 0 errors across
197 files, nx lint 0 errors, nx typecheck pass, 89 tests pass.
@wkoutre
wkoutre requested a review from a team as a code owner July 30, 2026 21:48
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-toolkit-slack-oauth-backend Ready Ready Preview Jul 31, 2026 4:57pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0eeb9caaf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/claude-code-review.yml
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation Check ✅

Verdict: Passed

No files under packages/plugins/ were touched, so the mandatory plugin version-bump gate (fail_on_missing_version) doesn't apply. Every CLAUDE.md/README/doc file touched by this PR already contains thorough, consistent documentation of the change.


PR #556 migrates ai-toolkit's own PR review from the homegrown _claude-code-review.yml reusable workflow to @uniswap/review-cli, adding two new repo-specific reviewer agents (plugin-conventions-reviewer, workflow-security-reviewer), a .claude/review.yml config, and a composite install_review_cli action.

Plugin version bump (critical gate): Not applicable — no file under packages/plugins/ appears in the changed-file list, so no plugin requires a version bump.

CLAUDE.md coverage: .github/prompts/CLAUDE.md, .github/scripts/CLAUDE.md, and .github/workflows/CLAUDE.md are all updated in the diff with detailed scope notes distinguishing the reusable workflow (still supported for external consumers) from the new repo-local review-cli path, plus an extensive new "PR Code Review for this repository" section covering the trusted-checkout swap, dirty-tree gotchas, and triage gating. This is unusually thorough, not minimal.

README / guide coverage: Verified directly against the current repo (not just the truncated diff) — .github/workflows/README.md and docs/guides/claude-integration.md both already document claude-code-review.yml's move to @uniswap/review-cli, consistent with the CLAUDE.md updates. These files were correctly updated in this PR.

Root CLAUDE.md: Appears in the changed-file list, but git log shows its only recent content change came from a separate PR (#557, plugin-inventory drift fix) picked up via the Merge branch 'next' commit in this branch's history — not substantive content added by this PR itself. No action needed.

Changelog: No repo-wide CHANGELOG.md convention exists (changelog generation is per-package via generate-changelog.yml/nx release); not applicable to this root-tooling change. Informational only, non-blocking per config.

No missing or stale documentation was found for this diff.

✨ No Documentation Updates Needed

All documentation appears to be up to date with the code changes.


🤖 Generated by Claude Documentation Validator | Mode: suggest

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

● Reviewed · against b0eeb9c · 2026-07-31 15:04 UTC · view run ↗

Caution

Changes requested — the review job loads its own tooling from the untrusted PR head, which also breaks every already-open PR on merge.

Moves ai-toolkit's own PR review from the reusable _claude-code-review.yml to @uniswap/review-cli, with a triage gate, two repo-specific reviewers, and a new install composite action; the reusable workflow and its consumers are untouched.

Assessment

The migration preserves the four parity behaviors it claims — dependency PRs still reach auto-merge-dependabot, the claude[bot] draft carve-out survives in the job if:, title-based skips route through check-automated-pr, and the fork gate is fail-closed across all four triggers. The remaining risk concentrates on one line: the review job checks out refs/pull/N/head and then resolves ./.github/actions/install_review_cli and .claude/review.yml out of that workspace. That makes the installer both missing for every PR branched before this commit and author-controlled in a job holding CLAUDE_CODE_OAUTH_TOKEN and a contents: write token. A trusted-ref sparse checkout of .github/actions + .claude fixes the outage, the trust inversion, and the config-guard asymmetry in one move.

Must address

Suggestions

  • Timeout cancellation skips Post — a run that hits the 20-minute job ceiling is cancelled, success() || failure() doesn't fire, and the sticky stays at "⏳ Review running". A step-level timeout-minutes on Analyze closes the hole. view inline thread →
  • The review job has no .claude/review.yml presence guard, so a PR that breaks the config reviews itself on CLI defaults and reports green; the trusted-ref checkout above also resolves this. view inline thread →
  • The "structurally cannot write to GitHub" comment describes review-cli's pipeline, not the job's GITHUB_TOKEN, which is write-capable via contents: write. view inline thread →

Tip

Teach the reviewer. React 👍 on findings that helped, 👎 on false positives. Reply to push back or add context — we aggregate this weekly to tune the bot.

Comment @request-claude-review to re-run.

Comment thread .github/workflows/claude-code-review.yml Outdated
Comment thread .github/workflows/claude-code-review.yml
Comment thread .github/workflows/claude-code-review.yml Outdated
Comment thread .github/workflows/claude-code-review.yml

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

⚠️ Changes requested — see full review in the sticky comment ↑

wkoutre added 3 commits July 31, 2026 09:32
Addresses all four findings from the first review-cli run on this PR.

The `review` job checked out `refs/pull/N/head` and then resolved
`./.github/actions/install_review_cli` and `.claude/` out of that same
workspace. Two problems in one line:

- Availability: the composite action does not exist on branches cut
  before it landed, so every open PR's next review would die with
  "Can't find 'action.yml'" until it rebased.
- Trust: the head branch controlled the installer shell and the agent
  prompts in a job holding CLAUDE_CODE_OAUTH_TOKEN and a
  `contents: write` token. `.github/actions/**` is not covered by the
  `workflow` token scope that guards `.github/workflows/**`.

The PR head is now checked out only as the content under analysis, with
`.github/actions` + `.claude` sparse-checked-out from the default ref
into `.review-tooling/` afterwards (order matters: checkout runs
`git clean -ffdx`). The workspace `.claude` is replaced with the trusted
copy behind a presence guard, mirroring the triage gate. Config and
agent changes now take effect only once merged, which is intended.

Also:

- Add a step-level `timeout-minutes: 17` to Analyze. The job-level
  ceiling *cancels*, and `success() || failure()` excludes cancellation,
  so an overrun skipped Post and left the sticky on "Review running"
  forever. Not widened to `always()` on purpose: concurrency uses
  cancellation too, and a dying run would clobber its successor's sticky.
- Correct the "structurally cannot write to GitHub" comment. That is a
  property of review-cli's pipeline, not of the token in the step's
  environment, and it should not be cited to relax the fork guard.

Verified: actionlint clean, markdownlint clean, zizmor unchanged at 8
findings (no new ones introduced).
The previous commit's "trusted ref" checkout had no `ref:`, which does
not mean the default branch: actions/checkout falls back to GITHUB_SHA,
and on a `pull_request` event that is the merge commit (base + head). The
head branch therefore still influenced the supposedly-trusted installer
and agent set, so the fix did not actually close the hole it described.

Pin it to `github.event.repository.default_branch` explicitly.

The triage job's checkout stays unpinned on purpose, and now says why:
it holds no CLAUDE_CODE_OAUTH_TOKEN and runs no agent, so the worst case
there is a PR altering its own review eligibility rather than code
execution with a credential. Its fork guard reads the API, not config,
so it cannot be disabled from the PR branch.
Reproduced live on this PR: the trusted-config guard failed (expected
during bootstrap), so `Install review-cli` never ran and its `bin-path`
output was empty. `Post` still executed on `success() || failure()`,
resolved "$REVIEW_CLI_BIN/review-cli" to `/review-cli`, and failed with
exit 127 — burying the real error under a meaningless one. The reaction
and reply steps did the same, just non-fatally via continue-on-error.

Gate all three on `steps.install-review-cli.outputs.bin-path != ''` so
the failure a developer sees is the one that actually happened.

Also document the bootstrap corollary: the review job cannot succeed on
the PR that introduces the tooling, because the trusted ref has no
`.claude/` or `install_review_cli` yet. It goes green from the next PR on.
Independent review (code-reviewer + comment-analyzer + silent-failure-hunter)
standing in for the CI reviewer, which cannot review its own bootstrap PR.
15 findings, all verified against review-cli source and real run artifacts.

Critical — the trusted checkout silently degraded every review. review-cli
gates its full-checkout fast path on `git status --porcelain` being empty, and
`.review-tooling/` is untracked inside GITHUB_WORKSPACE, so the tree read
dirty on every run and the CLI fell back to extracting only diff-touched
files with no .git. Agents kept working and runs stayed green while losing
whole-repo Read/Grep/Glob and history — worst for the two reviewers added
here, which are told to look outside the diff. Fixed with .git/info/exclude
plus `update-index --assume-unchanged` on the .claude paths the trusted copy
reverts (exclude cannot hide tracked files), and the step now asserts the
tree is clean and warns if not. Verified in a scratch repo.

Critical — the investigation gate was inert. Demotion is
`wantsDemotion && allowedVerdicts.includes('COMMENT')`, and the default
`verdict.allowed` is [APPROVE, REQUEST_CHANGES], so the gate computed the
demotion, discarded it, and let the rubber-stamp APPROVE stand. Added
`verdict.allowed: [APPROVE, REQUEST_CHANGES, COMMENT]`. Safe for dependency
auto-merge, which gates on `needs.review.result == 'success'`, not verdict.

High — cancellation was rendered as failure. The reaction and reply steps run
under `always()`, which fires on cancellation, and concurrency cancels a
comment-triggered run whenever a push supersedes it. Both reported
"Review failed" and told the requester to retry a review its successor was
about to finish. Reaction now carries `!cancelled()`; reply posts
"Superseded". This is the same reasoning already documented for Post, applied
to its two siblings.

High — the config guards tested presence, not validity. `-s` catches missing
and empty but not malformed, and loadConfig treats invalid YAML exactly like
a missing file: defaults, no log line. Both guards now assert the parse and
`skip.authors == []`, fail-closed. Verified against five cases: real config,
malformed YAML, reintroduced bot skip, empty file, non-mapping.

Also: guard the two agent files the error message names; raise the job ceiling
to 25 so Analyze's step ceiling always fires first (tail budget is
`25 - setup - 17`, not a flat 3 minutes); annotate a declining triage gate so
it is not a silent green no-review; surface a failed running-state reply
(no `set -e` meant it reported success); verify the installed binary is
executable, the one case the bin-path gate cannot catch; `permissions: {}` on
review-skipped; and correct five factual claims — minimumReleaseAge errors
rather than downgrading, react/reply also write to the PR, the bundled agent
list omitted 4 of 11 including the two the next paragraph suppresses,
`--force` substitutes $20/200-turn defaults for the configured caps, and the
`cancelled()` wording in CLAUDE.md.

Verified: actionlint clean (it caught `cancelled()` being invalid in `env:`
and a duplicate permissions key), zizmor unchanged at 8 findings, markdownlint
clean, lefthook green.
Round 2 of the review loop found the case the previous commit missed. The
tree-cleanliness fix handled untracked `.review-tooling/` and tracked
`.claude` files the copy modifies or deletes, but not files the trusted copy
lands that are NOT TRACKED at the PR head — every `.claude/**` file added
after that branch was cut. `git diff --name-only` never lists untracked
paths, so the assume-unchanged pass structurally cannot reach them.

This fires on this PR's own merge: `next` tracks none of `.claude/review.yml`
or the two agents, so every already-open PR would get three `??` entries, a
dirty tree, and a review silently downgraded to diff-only extraction. Fixed
by also excluding `/.claude/`, which hides untracked copies while still
reporting tracked modifications — verified in a scratch repo reproducing the
bug, the fix, and that the assume-unchanged pass is still required.

Also from round 2:

- Document that restoring a clean tree re-enables review-cli's
  `verifyFindings` pass, dead in CI until now because it is gated on
  workspaceShape == 'working-tree'. It resolves cited files from the
  workspace, where `.claude` is the pre-PR copy, so findings against
  `.claude/**` files a PR adds are dropped as "file not readable at HEAD".
  Logged rather than silent, reachable only by reviewer-tuning PRs, and not
  to be "fixed" by skipping the swap.
- Correct the `!cancelled()` rationale. It claimed a successor replaces the
  👀; none does, because only a `pull_request` run can cancel this one and
  such a run has an empty comment_id, so it never reaches that step. The
  stale ack is the deliberate trade against a false ❌.
- Fix a wrong claim in triage.guidance: `auto-merge-dependabot` gates on the
  job RESULT, not the verdict, and `post` has no non-zero exit path, so
  REQUEST_CHANGES does not by itself stop a bump. Reviewers are told to say
  so explicitly in the finding instead.

Verified: actionlint clean, zizmor unchanged at 8, review.yml parses.
@wkoutre

wkoutre commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Status: review findings addressed, remaining "AI review" CI failure is a bootstrap condition, not a defect

All four inline findings from the AI review run against this PR's first commit (b0eeb9c) were fixed by 79e0f04 (fix(ci): load review tooling from the trusted ref, not the PR head, refined by 3394e01 and 40f7214):

  • Installer/config resolved from the untrusted PR head → now checked out from the trusted default-branch ref into .review-tooling/.
  • No guard on the review job's config load → Use trusted review config now asserts presence of review.yml and both new agent files and fails closed on missing/malformed config.
  • Analyze timeout cancellation could skip Post → step-level timeout-minutes: 17 added.
  • Stale "structurally cannot write to GitHub" comment → corrected to clarify the job's GITHUB_TOKEN is write-capable.

I replied to each of the four threads with the specific fix commit, marked all four resolved, and dismissed the stale CHANGES_REQUESTED review that carried them (review decision is now REVIEW_REQUIRED).

The current "AI review" check failure is a separate, structural condition specific to this PR. The Checkout review tooling (trusted ref) step loads .claude/review.yml and the two new agent files from next by design — that's the whole point of 79e0f04, so a PR author can't rewrite its own reviewer's config or prompts. But next doesn't have those files yet, because this PR is what adds them:

$ git show origin/next:.claude/review.yml
fatal: path '.claude/review.yml' exists on disk, but not in 'origin/next'

So Use trusted review config correctly fails loud (by design — see that step's comments) rather than silently falling back to CLI defaults. This is the same one-time chicken-and-egg the workflow's own comments already call out for other already-open PRs on merge day; here it applies to PR #556 itself, before merge. There's no code fix available that doesn't reintroduce the exact "load config from the PR head" trust problem this PR fixes — the file's own comments explicitly warn against that. It resolves itself once this PR merges to next.

@wkoutre
wkoutre merged commit 9b405c7 into next Jul 31, 2026
52 of 53 checks passed
@wkoutre
wkoutre deleted the nickkoutrelakos/review-cli-migration branch July 31, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant