{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreifqryup2uun4ntjy4emjzrcnknihi6avjlvlo7jpxytcewidnr7da",
"uri": "at://did:plc:6u4awktizhivwgqxl5j67h4k/app.bsky.feed.post/3mjktyyqplqu2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiao32pp7omqylgtk2qy3nmmk7qumvcygqg6u5uytjjqfp7ggd2nui"
},
"mimeType": "image/webp",
"size": 39240
},
"description": "Things move fast. I recently wrote about using the adversarial models with my feature-workflow plugin a few weeks ago — ripping out the internal self-review skill and replacing it with external reviewers. Different model, read-only, posting structured critiques on the PR. The core insight was that a different model catches things the implementing model doesn't, because it isn't agreeable to its own work. That part was right and hasn't changed.\n\n\nBut the process was cumbersome and time consuming.",
"path": "/moving-the-reviewer-to-github/",
"publishedAt": "2026-04-15T21:35:13.000Z",
"site": "https://www.subaud.io",
"tags": [
"the adversarial models",
"github.com/schuettc/claude-code-plugins"
],
"textContent": "Things move fast. I recently wrote about using the adversarial models with my feature-workflow plugin a few weeks ago — ripping out the internal self-review skill and replacing it with external reviewers. Different model, read-only, posting structured critiques on the PR. The core insight was that a different model catches things the implementing model doesn't, because it isn't agreeable to its own work. That part was right and hasn't changed.\n\nBut the process was cumbersome and time consuming. To get a review I ran `/feature-submit` in Claude Code, switched to a second terminal where `gemini` was waiting, pasted the PR URL, read the findings, switched back to Claude, described what needed to change, iterated. With three or four rounds per feature, it became difficult to keep track of terminals, even with using `cmux`.\n\nThe plugin is now at v9.2.3, the reviewer runs in GitHub Actions, triggered by a PR label and I don't have to switch terminals anymore.\n\n## Setup\n\nOne command wires the whole thing up. `/feature-init` asks for a branch prefix (feature/, feat/, whatever), a target branch (dev or main), a reviewer (gemini, codex, or none), and an API key if you picked a reviewer. It writes `.feature-workflow.yml` with those settings, drops `.github/workflows/feature-review.yml` plus the plan and impl review prompts into the repo, uploads the API key as a GitHub repo secret via gh secret set, and flips on the repo-level \"Allow GitHub Actions to approve pull requests\" setting so the bot's review approvals actually land. Every skill that manages branches — `/feature-review-plan`, `/feature-review-impl`, `/feature-ship` — reads the prefix and target from `.feature-workflow.yml`, so feature branches get named consistently and PRs target the right base without me typing it each time.\n\nThere's also `/feature-init --update`, which refreshes just the workflow file and the review prompts from the current plugin templates without touching config, the API secret, or existing features. I've used it several times as I've iterated on the prompts and the workflow gating — edit the templates in claude-code-plugins, run `--update` in my project, commit the refreshed `.github/` files, and the next PR gets the new behavior.\n\n## Labels as the Trigger\n\n`/feature-review-plan` and `/feature-review-impl` still do the git work — branch off `dev`, commit, push, open or update a draft PR. The new step is that they add a label at the end.\n\n\n gh pr edit <pr-number> --add-label plan-review\n\n\nA GitHub Actions workflow listens for `pull_request` `labeled` events and runs the reviewer inside the Action runner.\n\n\n on:\n pull_request:\n types: [labeled, synchronize]\n\n jobs:\n plan-review:\n if: contains(github.event.pull_request.labels.*.name, 'plan-review') && !contains(github.event.pull_request.labels.*.name, 'impl-review')\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v5\n with:\n fetch-depth: 0\n - id: gemini\n uses: google-github-actions/run-gemini-cli@v0\n with:\n prompt: ${{ steps.prompt.outputs.value }}\n gemini_api_key: ${{ secrets.GOOGLE_API_KEY }}\n - name: Post review\n if: always() && steps.gemini.outputs.summary != ''\n run: bash .github/scripts/post-review.sh\n\n\nLabels specifically, not `workflow_dispatch` or commit triggers. They're scoped to the PR, visible in the GitHub UI, and remove-then-add is a cheap way to re-run a review without pushing a new commit or filing out a dispatch form.\n\n## Two Phases, Two Labels\n\nThe plan/impl split already existed in the previous version. `plan-review` runs against a PR containing only `idea.md` and `plan.md`. `impl-review` runs against the same PR once implementation lands. The two jobs in the workflow have mutually-exclusive label guards:\n\n\n if: contains(github.event.pull_request.labels.*.name, 'plan-review') && !contains(github.event.pull_request.labels.*.name, 'impl-review')\n\n\n## The Submit Skill Stops\n\n`/feature-review-impl` does its git work, swaps labels, and stops. The skill has explicit language telling Claude not to start reviewing the code locally after submitting:\n\n\n Display the following to the user, then **STOP**. Do NOT launch any code review agents, do NOT run any review skills, do NOT analyze the code further. Your job is done.\n\n\n## What It's Caught\n\nThe clearest example I have was a potential P0 privacy leak. Private and unlisted records were surfacing in a public feed. The feature went through three rounds of plan review before a single line of code was written, and each round was blocked with a different correctness finding.\n\nRound one: the plan fixed the write path in one Lambda and claimed a sibling Lambda was already correct. Gemini pointed at the exact line — `!== 'private'` — and noted that unlisted records are not 'private', so they leak. Plan was updated to tighten the gate to `=== 'public'`.\n\nRound two: a third leak path in a third Lambda on the client reconnect path, same bug. Also a DynamoDB semantics issue in the cleanup script — `visibility <> :public` evaluates to `UNKNOWN` against rows where the visibility attribute is missing entirely, so legacy rows without the attribute would stay leaked.\nFix: `attribute_exists(gsi2pk) AND (attribute_not_exists(visibility) OR visibility <> :public)`.\n\nRound three: a spread-inheritance bug. The write-path fix was structured as `{...existing, ...(visibility === 'public' ? {gsi2pk: 'PUBLIC', ...} : {})}`. If `existing` already held a leaked `gsi2pk='PUBLIC'` from a prior retry, the conditional spread is empty for non-public records, so the stale key gets inherited from `...existing`.\nFix: explicitly set `gsi2pk: undefined` with DynamoDB's `removeUndefinedValues: true`.\n\nThree correctness bugs, all against a markdown file, with no code written yet. Any one of these could have been annoying to deal with down the road and all were caught just by pushing the plan to GitHub.\n\nIn another scenario, I ran the `impl-review` while finishing up a different feature. An upstream client had changed its event payload between versions, renaming a boolean field. The Lambda I'd just written was still keying off the old field name and defaulting to false when it was missing, so every event from the new client version was being persisted with the wrong value. The same review pass flagged a routing bug where a special-case event type was being tagged as a generic event because the kind switch ran before the special-case check, a hydration-on-load call passing a field that's always null at init time, and a reorder-buffer livelock where unsequenced events could strand behind a missing sequence number.\n\nAll four issues were caught and corrected just by going through my standard Claude Code process.\n\n## What Changed About How I Use It\n\nThe review is asynchronous now. I run `/feature-review-impl`, the skill stops, and I go do something else while the Action runs. When the review comments land on the PR, I pull them into Claude's context with `gh pr view <n> --comments`. The PR is the single record of the review history — commits and comments in one place, not scattered across terminal scrollback. None of the review state is local. If I switch machines mid-feature the PR is still there with the review still attached.\n\n## The v9.x Hardening\n\nMost of what changed between v9.0.0 and v9.2.3 was bugs I hit running features through the system. v9.1.0 moved the review-posting step into the workflow itself via `post-review.sh` because the Gemini action's built-in PR-review posting produced malformed output on certain verdicts. v9.2.1 fixed verdict parsing and added the `plan-review` gating from earlier in this post, both after hitting the bugs in slay-the-spire and upstreaming the fixes the same day. v9.2.3 synced the review prompts with prettier formatting.\n\n## Sequence Flow\n\n\n sequenceDiagram\n participant CC as Claude Code\n participant GH as GitHub (PR)\n participant GA as GitHub Actions\n participant GM as Gemini CLI\n\n CC->>GH: git push feature/<id>\n CC->>GH: gh pr create --draft (or update)\n CC->>GH: gh pr edit --add-label plan-review\n Note right of CC: Skill stops here\n\n GH-->>GA: pull_request labeled event\n GA->>GA: checkout repo (fetch-depth: 0)\n GA->>GA: load .github/review-prompt-plan.md\n GA->>GM: run-gemini-cli with prompt + PR ref\n GM->>GH: gh pr view <n> --json (diff, plan.md)\n GM-->>GA: review summary + verdict\n GA->>GH: post-review.sh → gh pr review --comment\n\n Note over CC,GH: Async — I go do something else\n\n CC->>GH: gh pr view <n> --comments\n GH-->>CC: review findings\n CC->>CC: iterate, re-submit\n\n\nSource: github.com/schuettc/claude-code-plugins.",
"title": "Moving the Reviewer to GitHub",
"updatedAt": "2026-05-05T16:27:28.640Z"
}