Skip to content

feat(seidroid-review): let a pull request ask for nits - #94

Merged
bdchatham merged 2 commits into
feat/seidroid-reviewfrom
feat/nit-opt-in
Sep 6, 2026
Merged

feat(seidroid-review): let a pull request ask for nits#94
bdchatham merged 2 commits into
feat/seidroid-reviewfrom
feat/nit-opt-in

Conversation

@bdchatham

@bdchatham bdchatham commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

An author can now ask for a polish pass. seidroid-review.yml never passed
--include-nits, so IncludeNits was false on every review and the driver's nit
setting was unreachable from a caller. This wires it to a label on the reviewed
pull request, and adds the flag to the install step's contract check.

Label, not a boolean input

The person who wants nits is the author of one pull request. A boolean input keys
off the caller's configuration, so it turns nits on for every pull request in the
repository or for none. A label is set per pull request, by the author, without a
workflow edit.

nitpick-label matches ai-review.yml's input of the same name and its
ai: nitpick default (ai-review.yml:66-70), so a repository running both tools
adds one label rather than two. Its two consumers there are the prompt
(ai-review.yml:783) and the poster (ai-review.yml:799, 838, 896), both fed
from one label read in the preflight resolve step (ai-review.yml:268).

Empty disables the check, which is skip-review-label's semantics in this file
rather than ai-review.yml's.

One semantic does not transfer. ai-review.yml re-runs on a labeled event when
the changed label is the nitpick one. This workflow reviews a pull request once
and states that a relabel earns no second review, so adding the label starts
nothing. The author labels the pull request and comments @seidroid review. The
input says so.

Where the read goes, and what it costs

Its own step in the review job, Read the nit setting from the pull request,
immediately before the driver invocation. It costs one GET /repos/{o}/{r}/pulls/{n}
on the review path only.

Not in the guard's Admit the request. The guard's skip-label read runs under
GH_TOKEN, which is the App token with no fallback; a caller that configured no
App would never be able to opt in. Answering that caller needs GATE_TOKEN, which
is a separate gh api call whichever job it lives in — so sharing the guard's call
would mean restructuring a fail-open/fail-closed admission check for a signal that
decides nothing about admission. The guard also denys by exit 0 mid-step, so an
output added after the label check is not written on a denied path.

Not inside the drive step either, which is the tighter constraint. That step
deliberately carries no GitHub token, and its env reaches the driver process. A
GH_TOKEN there would hand the reviewing agent a GitHub credential.

The read uses any(.labels[]?.name; . == $ENV.NITPICK_LABEL), the shape #93 gives
the guard's own label check. It answers a failed read differently, and on purpose:
the skip label withholds work, so refusing on a signal nobody could read is the
safe answer there; this label asks for advice, so refusing would spend the review
to protect the polish pass. A failed read warns and leaves nits off. The comment
says so beside the code.

The install contract check

The install step reads review --help and refuses a driver missing any long flag
this file passes, before a session opens or quota is spent. --include-nits is now
on that list. Without it a driver that dropped or renamed the flag would pass the
check and fail inside Drive session + collect verdict, after install had already
admitted it — which is the failure the check exists to move earlier.

The list is confirmed complete against the argv the drive step actually builds,
not against the list as written; see report 3 below.

What turning nits on changes on the pull request

Off does not mean dropped. Read against sei-agent-driver at v0.15.0, which is
both the driver-version default and MIN_DRIVER_VERSION after #95:

label absent label present
a nit-grade observation nitRule sends it to non_blockers: prose in the verdict comment and in the check run's Non-blocking section, no thread on the code reported inline with severity nit: a comment thread on the line
a nit the review placed inline anyway dropped — PlaceableFindings (findings.go:128), the counts (findings.go:373 via countFindings), and the check summary, which renders only the line-less buckets placed and counted
a prior thread a nit restates supersedes nothing, because no comment posts superseded, and resolved once the comment is on the code

So the label chooses where a nit lands, not whether the review makes one. The
prompt states the current setting on both settings and says it replaces an earlier
one (prompt.go:527-548) — load-bearing here, because the session outlives the run
and a first turn told to leave nits out still holds that instruction.

Verification

Nothing ran on a GitHub runner. Three step scripts — Install the review driver,
Read the nit setting and Drive session + collect verdict — were extracted from
the shipped file with a YAML parser and run under bash with stubs. The harness
asserts each step's if and the INCLUDE_NITS wiring against the file, so a
rebase that changes one fails the harness rather than passing it. The gh stub
runs the shipped --jq filter through real jq; the go stub serves a driver
whose reported version and review --help flag set the case controls.

1. driver argv

case                 nit step                    --include-nits  drive rc
label present        include_nits='true'         yes             0
label absent         include_nits='false'        no              0
no labels at all     include_nits='false'        no              0
no labels key        include_nits='false'        no              0
near-miss labels     include_nits='false'        no              0
caller renamed it    include_nits='true'         yes             0
label read fails     include_nits='false'        no              0
label input empty    skipped                     no              0
close mode           skipped                     no              0
every input set      include_nits='true'         yes             0

every input set exists so the union of flags below is the whole surface. Its argv:

review sei-protocol/uci 42 --out .../verdict.md --findings-out .../findings.json \
  --check-out .../check.json --conversation-context .../threads.json \
  --guidelines-file REVIEW.md --extra-instructions "be terse" --include-nits \
  --trigger-id 999

Close mode: review sei-protocol/uci 42 --close.

2. install contract check

case                         version   mode     rc   annotation
help names every flag        v0.15.0   review   0
help drops --include-nits    v0.15.0   review   1    ...does not accept `review` --include-nits
help drops --check-out       v0.15.0   review   1    ...does not accept `review` --check-out
driver below the floor       v0.14.0   review   1    ...is older than v0.15.0
below the floor, close       v0.14.0   close    0

The stub help gives half the flags a cobra shorthand (-x, --out string), so the
check is exercised against the shape its own comment says it must tolerate.

3. contract list against real argv

Parsed out of the shipped install script and compared with the union of long flags
the drive step actually built across all ten cases:

contract list: --out --findings-out --check-out --close --conversation-context
               --guidelines-file --extra-instructions --include-nits --trigger-id
argv built:    --check-out --close --conversation-context --extra-instructions
               --findings-out --guidelines-file --include-nits --out --trigger-id
built but NOT in the contract list:        none
in the contract list but never built here: none

Both directions are assertions, so wiring a flag without listing it, or listing one
the workflow never passes, fails the harness.

The real sei-agent-driver@v0.15.0 was installed from the proxy and its
review --help names exactly --check-out --close --conversation-context --extra-instructions --findings-out --guidelines-file --help --include-nits --out --trigger-id — the contract list plus --help.

The --jq filter was also run through gh's own engine (github.com/cli/go-gh/v2/pkg/jq
v2.16.0): label present true, absent false, empty array false, no labels
key false, ai: nitpicky false, AI: Nitpick false.

actionlint 1.7.12 on the same path with the same invocation: base 30f5c09
gives 4 findings, all SC2102:info; this branch gives 4, all SC2102:info.
shellcheck -S info on all three extracted scripts: clean. The file parses under
yaml.safe_load; 19 workflow_call inputs.

Not verified: any live run, and the flag's effect on a real model turn.

🤖 Generated with Claude Code

@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Workflow-only wiring to an existing driver flag; failures degrade to the previous behavior (nits off, review still runs).

Overview
Authors can opt into inline nit threads on a single pull request by adding the configurable nitpick-label (default ai: nitpick), aligned with ai-review.yml so one label works for both reviewers.

The workflow adds a preflight step that reads PR labels (outside the driver step so no GitHub token reaches the agent) and sets include_nits. The drive step passes --include-nits only when that label is present. The install contract check now requires the driver to accept --include-nits.

Without the label, nits are still surfaced in the verdict/check summary as non-blocking prose, not dropped. With the label, nits post on the relevant lines and can supersede prior threads. Label read failures fail open (review runs with nits off); relabeling alone does not re-trigger a review.

Reviewed by Cursor Bugbot for commit cd3a736. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions 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.

Clean, well-guarded wiring of a per-PR ai: nitpick label to the driver's --include-nits flag: the read is fail-open, the token stays out of the drive step's env, and the label reaches jq via $ENV rather than string splicing. Two non-blocking points: the new flag was not added to the driver contract check that enumerates every flag this workflow passes, and the step repeats a GET /pulls/{n} the step directly above it already makes with the same token.

Findings: 0 blocking | 1 non-blocking | 0 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • The new opt-in path has no automated coverage in the repo; the PR body describes a local harness that extracts the two step scripts and asserts the if expression and INCLUDE_NITS wiring, but nothing in .github/ runs it, so a future rebase that breaks the wiring fails silently rather than in CI. Worth landing that harness alongside the change if it is meant to hold.

Comments that couldn't be anchored to the diff

  • .github/workflows/seidroid-review.yml:1459 -- [suggestion] --include-nits is not added to the driver contract check's flag list (line 1146-1147). That loop is documented as covering "every long flag ... which this workflow passes", and --conversation-context is in it despite also being passed conditionally — so the convention is that a conditionally-passed flag still belongs there.

Impact is limited today: review mode enforces MIN_DRIVER_VERSION = v0.14.0, which has the flag, so the gap only bites a newer driver that renames or drops --include-nits — precisely the "too new, or simply not this driver" case the check exists to catch. In that case a nit-labelled PR would fail inside the drive step, after the credential is held and the session is open, rather than at install time.

Adding --include-nits to the for flag in ... list closes it.

  • .github/workflows/seidroid-review.yml:1380 -- [suggestion] This is a second GET /repos/{o}/{r}/pulls/{n} against the same object the Record the commit under review step fetched immediately above (line 1345), with the identical token expression, identical REPO/PR, and an if that differs only by the label check. The PR description justifies not sharing the guard's read (different identity, early exit 0 on deny), but that reasoning does not apply to the adjacent step — neither constraint holds there.

One gh api capturing the JSON once and emitting both sha and include_nits would drop the duplicate call. The trade-off is that a failed read would then cost both signals instead of one, which is worth stating in the comment either way — right now the file makes the same request twice without noting that it does.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eaec656. Configure here.

Comment thread .github/workflows/seidroid-review.yml
bdchatham and others added 2 commits September 6, 2026 15:22
The driver takes --include-nits and its prompt reads it on both settings; the
workflow passed it on neither, so every review ran with nits off and no author
could ask otherwise.

The ask is a label on the reviewed pull request, `nitpick-label`, matching
ai-review.yml's input of the same name and default. The person who wants a
polish pass is the author of one pull request, not the person who wired the
workflow, and a boolean input cannot be set per pull request.

The read is its own step in the review job rather than a line in the guard. The
guard reads the same object for the skip label under the App identity alone,
which cannot answer a caller that configured no App; sharing that call would
mean restructuring a fail-open/fail-closed admission check for a signal that
decides nothing about admission. It costs one read of the pull request.

Not in the drive step itself: the read needs a GitHub token, and that step's env
reaches the driver process.

Off is unchanged, and off means redirected. Without the label the driver sends a
nit-grade finding to the non-blocking bucket, where the verdict comment and the
check summary carry it as prose and no thread opens on the line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The install step checks `review --help` for every long flag this file passes and
refuses a driver missing one, before a session opens or quota is spent.
--include-nits was not on that list, so a driver that dropped or renamed it
passed the check and failed later inside `Drive session + collect verdict`,
after install had already admitted it.

The label read now takes `.labels[]?.name`, the shape the guard's own label check
uses. A payload carrying no labels key answers false rather than failing the read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bdchatham

Copy link
Copy Markdown
Contributor Author

Label read failures fail open (continue-on-error, warning, nits off)

Right on the behaviour, and worth splitting the term: the review fails open, the
opt-in fails closed. #93 makes the guard's skip-label read refuse on an unreadable
label, so a reader meeting both needs to see the difference is chosen — the skip
label withholds work, this one asks for advice.

Two changes since the commit you read (eaec656cd3a736): --include-nits is
now in the install step's contract check, which had let a driver missing the flag
past install and into the drive step; and the read takes .labels[]?.name, the
shape #93 gives the guard.

@github-actions github-actions 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.

Wires the previously unreachable --include-nits driver flag to a per-PR ai: nitpick label, with a dedicated read step and the flag added to the install contract check. Logic, failure handling (continue-on-error, unset output → nits off), token placement and injection safety ($ENV in jq, env-routed values) all check out; only a minor redundancy in the API read.

Findings: 0 blocking | 2 non-blocking | 1 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • Behaviour of --include-nits itself lives in the driver (sei-agent-driver), so the input's claims about where a nit lands and what it may supersede can't be verified from this repo. Worth a one-line pointer to the driver docs if that contract ever drifts, but the version floor (v0.15.0) plus the contract check covers the flag's existence.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

# the review that was asked for, carrying one fewer kind of finding.
read_ok=true
# shellcheck disable=SC2016 # $ENV is jq's own, and jq reads it
carries="$(gh api "repos/$REPO/pulls/$PR" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This is the same GET /repos/{o}/{r}/pulls/{n} the immediately preceding Record the commit under review step already makes (line 1346), under the same token, the same REPO/PR env and a compatible if/continue-on-error. The PR description explains why the read isn't shared with the guard or folded into the drive step, but not why it isn't folded into this adjacent step, which would cost one API call instead of two and one --jq change.

The counter-argument is failure isolation — merged, a single failed read loses both sha and the nit setting — so this is fine as-is if that's the intent; it just isn't stated anywhere the way the other two placement decisions are.

@bdchatham
bdchatham merged commit bc93b4f into feat/seidroid-review Sep 6, 2026
6 checks passed
@bdchatham
bdchatham deleted the feat/nit-opt-in branch September 6, 2026 22:32
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