feat(seidroid-review): add the agentic pull request reviewer as a reusable workflow - #108
Conversation
…sable workflow seidroid-review.yml reviews a pull request from inside the Actions runner and publishes what it found: inline comments on the changed lines, a check run, and a review position. A caller wires it with `uses:` and passes its own secrets. One file, and it needs no other file in this repository. It pulls three pinned external actions, installs the driver with `go install`, and reads the diff through the API. It never checks the caller out. This is the whole of 33 commits from feat/seidroid-review, folded into one. `main` carried none of this work. What the workflow does: - Reviews once per pull request automatically, and again whenever a person or a listed bot asks by name. The guard decides both, and refuses a fork-originated re-review. - Places every finding the diff can hold in ONE review, so the author reads the verdict and the lines it cites on a single notification. A finding the diff cannot anchor degrades to its file, then to the summary. It is never dropped. - Carries the verdict in that same review (PLT-1268). A standalone comment is the fallback, for a run where no review landed. - Closes a thread only once its own replacement reached the code, so a re-review does not leave the author reading one finding in three places. - Withdraws a block it left earlier when a later review no longer reproduces it. - Acknowledges the trigger with a reaction, and withdraws it on a cancelled run from a step a cancelled run can still post from. The driver floor is v0.21.0, in the `driver-version` default and in MIN_DRIVER_VERSION. Both name one release on purpose: the conclusion a review reaches for a given set of findings is specific to the driver that reached it, and v0.21.0 is the first that renders the verdict from the structured block. The test harnesses that cover this file are not in this change. They are on archive/seidroid-review-tests: seven harnesses, 739 assertions, all passing against this exact file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview The wiring covers guard → in-cluster review → placement: admission (team/bot/fork/skip label, once-per-push gating with exceptions for re-review and block withdrawal), Callers are expected to replace Reviewed by Cursor Bugbot for commit 9c7f209. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
A single new reusable workflow that is unusually careful about failure directions, token scoping, and injection — but the base-standards fetch uses gh api -f without --method GET, which silently turns the read into a POST so the Accepted list never reaches the driver. Two smaller correctness/consistency issues around block withdrawal and approving an unrecorded commit.
Findings: 1 blocking | 2 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
- 1 blocking issue(s) listed below under unanchored comments.
Non-blocking
- Nothing in the repository exercises this file (
ai-review-self.ymlstill wires the old reviewer), so the first real validation happens in a downstream caller. Given the number of interacting failure paths, consider landing a caller in this repo — or at least amode: close-only smoke job — in a follow-up so regressions in the guard/publish ladder surface here rather than in consuming repos. modeis a free-form string: a caller that passes anything other thanrevieworcloseskips the guard and the review job with no annotation anywhere, reporting a green run that reviewed nothing. TheRefuse an event this workflow does not handlestep exists for exactly this class of caller error on the event axis; the mode axis has no equivalent.
Comments that couldn't be anchored to the diff
.github/workflows/seidroid-review.yml:1893-- [blocker]gh apidefaults to GET only when no parameters are supplied; adding-f ref="$base"flips the method to POST, and the params then go in the JSON body rather than the query string.POST /repos/{owner}/{repo}/contents/{path}is not a defined route, so this call returns 404 on every run:$outis emptied, the*"HTTP 404"*arm is taken, and the log states$REPO has no $file on $base; nothing is acceptedeven when the file exists and is readable.
The net effect is that --base-standards-file / --base-standards-ref are never passed, so the repository's Accepted list never reaches the driver and every pre-existing blocker withholds approval — with no warning distinguishing this from a repository that genuinely has no standards file.
This workflow already documents the rule at line 1127 ("-F on a gh api that names no -X makes the request a POST, and this one only ever reads"), which is why the page size rides in the path there. Add --method GET here (-f is then correctly placed on the query string and encoded, which is what the comment above assumes).
.github/workflows/seidroid-review.yml:2953-- [suggestion] The dismissal selector matches onstate == "CHANGES_REQUESTED"and the body marker alone — it never checks who recorded the review. Any review whose body starts with<!-- seidroid-review -->is dismissed, including one left by a human or by another bot that copied the marker (for example by quoting a previous verdict at the top of their own review body).
That is looser than every other ownership test in this file: the guard's prior-verdict read at line 1141 requires .user.type == "Bot", and Resolve the threads this review closed goes as far as requiring REVIEWER_LOGIN exactly, on the stated principle that "closing one is a mutation, and it stays as narrow as the identity performing it." Dismissing someone else's blocking review is a strictly larger mutation than resolving a thread.
Suggest adding the same login test the resolve step uses:
select(.state == "CHANGES_REQUESTED"
and ((.body // "") | startswith($ENV.VERDICT_MARKER))
and ((.user.login // "") | . == $ENV.REVIEWER_LOGIN or . == $ENV.WORKFLOW_LOGIN))
.github/workflows/seidroid-review.yml:2839-- [suggestion] WhenRecord the commit under reviewfails (it iscontinue-on-error),head_shais empty andcommit_idis omitted, so the API defaults the review to the pull request's current head. ForCOMMENTandREQUEST_CHANGESthat is the benign weaker guarantee the comment above describes. ForAPPROVEit is not symmetric: a push landing during the review receives an approval for code no reviewer read, and on a repository withapprove-on-success: trueplus an approval-count merge rule, that approval is load-bearing.
This also disagrees with how the same missing value is treated one step up: Publish the review check run deliberately declines continue-on-error and fails the job there, on the grounds that a merge gate which cannot name its commit should not exist at all. An approval is the same kind of gate.
Suggest gating just the approve arm on the recorded sha — e.g. downgrade APPROVE to no position (or to COMMENT) and warn when head_sha is empty, leaving the other two events on the documented fallback.
.github/workflows/seidroid-review.yml:3722-- [nit]<!-- seidroid-review:no-verdict -->is spelled here and again at line 3165 asNO_VERDICT_MARKERinPost the verdict. That is the exact drift hazard the header essay at lines 497–534 describes forVERDICT_MARKERandFINDING_MARKER— and it fails the same silent way: this step keeps stamping the old value whilePost the verdictstops matching it, so a stale "this review did not complete" notice is never withdrawn and sits on the pull request beside a verdict that did land.
Hoisting it to the workflow-level env block beside the other two markers gives it one definition, as FINDING_MARKER already has for the same reason.
…at a run may approve or dismiss Addresses seidroid's review of #108. The blocker. `Read the base branch's accepted conditions` sent its read as a POST: gh uses GET only while no parameter is supplied, and `-f ref=` adds one, which moves the ref into a JSON body and the request to a route that does not exist. Every run took a 404 and logged it as "no standards file", so the base branch's Accepted list never reached the driver and every pre-existing blocker went on withholding approval on a repository that had accepted it. The failure was indistinguishable from the legitimate case. `--method GET` fixes it. The dismissal now tests who recorded the review it withdraws. The marker alone selected, so a person who opened a blocking review by quoting this tool's previous verdict had it dismissed by the next run. It takes the same two logins the history read admits. An approval now requires the recorded commit. The API's fallback -- default to the pull request's current head -- stays true for a comment and for a block: both say something about code nobody read. An approval asserts something about a specific diff, and under approve-on-success it is the merge gate. Without the commit the arm downgrades to COMMENT and says so. NO_VERDICT_MARKER moves to the workflow env, beside the other two markers. Two steps wrote it, and a drift between them leaves a "this review did not complete" notice standing beside a verdict that did complete. An unrecognised `mode` is refused. workflow_call has no enum, so a typo matched no `inputs.mode` test, skipped both jobs and reported a green run that reviewed nothing. The guard condition now admits any non-close mode so the refusal can reach it. Bugbot's finding that a submitted review's body cannot be updated is not reproducible. Verified against the live API: PUT on a submitted COMMENTED review carrying an inline comment updates the body and leaves the comment attached. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…shaped read Follows the fixes in #108. conditions.py gains a file-wide rule: a `gh api` carrying -f/-F must name -X or --method. gh sends GET only while no parameter is supplied, so a read written without a method is a POST to a route that does not exist, and the 404 reads exactly like the thing it asked about being absent. Stated over the file rather than over a list of calls, so it covers a step added later. `gh api graphql` is exempt: it is a POST by definition and every query rides in a parameter. Checked against the defect: reverting --method GET on the base standards read fails this rule and nothing else. decision.sh gains the approve arm's new requirement. Three cases with no recorded commit: approve downgrades to COMMENT, request_changes and comment are unchanged. The withdrawal column tracks the conclusion rather than the event, so it reads the same as the cases carrying a commit. run-guard.sh gains the mode refusal: a typo is named and refused, an empty mode too, and both modes it handles pass. verdict.sh reads NO_VERDICT_MARKER from the workflow env, where it now lives. 760 assertions across the seven harnesses, all passing against #108's head. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed in 6d0b1c3. Thanks — the blocker was real and the two suggestions were both right. Blocker: the base standards read was a POST. Confirmed from The file already stated this rule for the guard's page size, and stated it one line above this very call — and the call still had the bug. So the fix is not only the flag: Dismissal ownership. Agreed, and it was the loosest ownership test in the file. It now takes the same two logins the history read admits. Approving an unrecorded commit. Agreed on the asymmetry. Without the recorded sha the approve arm downgrades to
Nothing in the repository exercises this file. Correct, and deliberate for this PR: it carries the product alone, and the harnesses were almost exactly half the original diff. They are on Bugbot's high-severity finding (a submitted review's body cannot be updated) is not reproducible; evidence in the thread on that comment. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6d0b1c3. Configure here.
|
@seidroid review |
…withdrawal will dismiss Addresses Bugbot's finding on 6d0b1c3, which the previous commit caused. Narrowing the withdrawal to this tool's own reviews left the guard's standing-block read matching any author. The two then disagreed: a person who opened a blocking review by quoting this tool's previous verdict was read as a block this workflow had left, so every push earned a re-review to withdraw it -- and the withdrawal, now correctly, would not touch it. The once-per-pull-request gate was bypassed for the life of the pull request, one review run per push, clearing nothing. The read is held to the Bot test, which is the one the prior-verdict read beside it already applies and the one that removes the case: a person is not a Bot. It stays deliberately wider than the withdrawal's own test, which names the two logins exactly. The two fail in opposite directions and each is pointed the safe way: the gate admits a review to go and look, costing a run, where the withdrawal refuses to mutate a review it does not own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@seidroid review Two rounds of fixes since your last pass, at d4b48ae:
Your non-blocking point about nothing in this repository exercising the file still stands and is deliberate for this PR — it carries the product alone. The harnesses are on |
|
Claude finished @bdchatham's task in 2m 54s —— View job COMMENTEDThe blocker is gone and every item from the last pass is answered at Findings: 0 blocking | 1 non-blocking | 0 posted inline Verified against the file at
|
| Item | Where | Status |
|---|---|---|
| Base standards read is a POST | --method GET at :1942 |
fixed |
| Dismissal ownership | login test at :3032-3036 |
fixed |
| Standing-block gate disagrees with it | .user.type == "Bot" at :1138 |
fixed, see below |
APPROVE on an unrecorded commit |
downgrade to COMMENT + warning at :2830-2840 |
fixed |
NO_VERDICT_MARKER drift |
one definition at :540, three readers, no step-level shadow |
fixed |
mode free-form |
refusal at :693-707, guard condition widened to inputs.mode != 'close' |
fixed |
The mode fix holds on each axis I could walk: a typo on pull_request now starts the guard, fails the refusal and skips the review job (whose own clause still names == 'review'); a typo on a comment event fails the same way; pull_request + close still skips the guard, and the review job still reads that skip as its trigger. The approve downgrade is genuinely load-bearing rather than belt-and-braces — Publish the review check run has already failed the job by then, but the position step runs on !cancelled() and still reaches the API.
Non-blocking
The withdrawal can decline a block the gate committed a run to clearing, and say nothing. The gate at :1138 admits any Bot whose CHANGES_REQUESTED body opens with VERDICT_MARKER; the withdrawal at :3032 additionally requires REVIEWER_LOGIN or WORKFLOW_LOGIN. The comment argues the asymmetry is pointed the safe way, and for the quoting case it is. Two routes still separate them:
- A rotated App credential. The
SEIDROID_APP_IDdescription documents this path directly — "dismiss any standing block by hand when you change the credential." A block recorded under the old slug matches the gate and fails the withdrawal's login test. Name the identity this review posts underfailing. It iscontinue-on-error: true, so nothing reaches$GITHUB_ENV,$ENV.REVIEWER_LOGINisnullto jq, and(.user.login // "") | . == nullis false for every review.
Either way ids comes back empty, the loop skips on [ -n "$id" ] || continue, and the step reports superseded blocks: 0 withdrawn, 0 still standing and exits 0 on a green job — while the gate, which did find the block, re-reviews on every push for the life of the pull request, one sandbox and one quota spend each. Before 6d0b1c3 the dismissal was at least attempted: on an unprotected branch it succeeded (same tool, different credential — the right outcome), and on a protected branch with dismissal restrictions the refusal produced the ::error:: that tells the operator what the secret description asks them to do.
Worth noting too that the threads step already warns for exactly the second route — "open with this tool's marker and none was written by … so this review carries no history and can close no thread" — and the withdrawal, the one step in the job that clears a merge gate, has no equivalent. The identity step's own header is now a little short of the truth as well: it says a failure there "costs the history and the thread closing", and it now also costs the withdrawal.
The narrow fix keeps the mutation exactly as narrow as it is and restores the signal — count what the marker matched before the login test, and report the difference rather than swallow it:
# shellcheck disable=SC2016 # $ENV is jq's own, and jq reads it
marked="$(gh api "repos/$REPO/pulls/$PR/reviews" --paginate \
--jq '[.[] | select(.state == "CHANGES_REQUESTED"
and ((.body // "") | startswith($ENV.VERDICT_MARKER)))] | length')" || marked=""then after the loop, where marked exceeds withdrawn + stuck, name the difference in an ::error:: and count it toward stuck — which already exits 1. What the gate calls a block from this workflow and the withdrawal will not touch is the one state neither reader can currently report.
What this pass could not check
The file parses and the shell in the regions that changed reads correctly, but nothing here exercises it — the harnesses are on archive/seidroid-review-tests and I did not run them, so the 764 assertions are your report rather than mine. Your standing note on that is fair for this PR. One thing I did confirm: the mangled review"' "$CHECK" fragment visible in the quoted diff context of the earlier threads is an artifact of that rendering, not the file — :2652 reads title="$(jq -r '.title // "review"' "$CHECK" || true)" and the YAML loads clean.
· branch release/seidroid-review-workflow
|
@seidroid review |
There was a problem hiding this comment.
A single new reusable workflow that lands every blocking item from the previous review — the --method GET fetch, the dismissal login test, the approve-arm commit gate, the hoisted NO_VERDICT_MARKER and the mode-axis refusal are all in the current head. What remains is three narrower gaps in the publish/recall ladder plus the release-ordering dependency on a driver tag that has not been cut in this repository.
Findings: 0 blocking | 3 non-blocking | 0 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Nothing in this repository exercises this file —
ai-review-self.ymlstill pinsai-review.yml@1d50ef2, and no caller referencesseidroid-review.yml. The first real validation of the guard/publish ladder happens in a downstream caller. Landing a caller here, or at least amode: close-only smoke job, would surface a regression in this repository rather than in a consuming one. (Restating the same point from the previous review; still true at this head.) driver-versionandMIN_DRIVER_VERSIONboth pinv0.21.0, and the description says that release was cut today as sei-protocol/sei-internal-skills#453. Confirm thesei-agent-driver/v0.21.0tag is published and served by proxy.golang.org from a COLD module cache before merging — the install step has no fallback, so an unpublished tag fails every review atgo install, on every caller, at once. Thedriver-versiondocstring already asks for exactly this cold-cache verification; it applies to the default too.- The 739 assertions live on
archive/seidroid-review-testsand not in this change, so nothing in this repository re-runs them when this file is edited. Given how much behaviour rides in the step scripts, consider a follow-up that brings the harnesses in-tree behind a workflow that runs on changes to this path.
Comments that couldn't be anchored to the diff
.github/workflows/seidroid-review.yml:1647-- [suggestion]threads_pathis published here, before$THREADSis written at line 1735 — and$THREADSis a fixed name inRUNNER_TEMP(review-prior-threads.json) with no run id in it.
If the GraphQL fetch at 1674 fails, set -euo pipefail aborts the step (tolerated by continue-on-error) having truncated only $THREADS.raw. $THREADS itself is never touched, so it keeps whatever a previous run of this step left there. The output is already set, so Drive session + collect verdict sees [ -s "$THREADS" ] succeed and passes --conversation-context pointing at another pull request's prior findings, presented to the review as its own history. Silent in both directions: the fetch failure is a tolerated step, and the driver reads a populated file as a successful recall.
This is the same hazard this file defends against three times elsewhere, on the same stated premise — rm -f "$out" before the base-standards write (1911–1912, "RUNNER_TEMP survives between jobs on a non-ephemeral self-hosted runner, which is the steady state here"), rm -rf "$out_dir" in the drive step (2015–2017), and : > "$LINKAGE" in placement (2153–2157). This is the one read path that does not.
One line fixes it: rm -f "$THREADS" "$THREADS.raw" immediately after the threads_path echo, or write into the run-scoped directory the drive step already builds. The second jq at 1735 needs nothing — its own > truncates before it can fail.
.github/workflows/seidroid-review.yml:2032-- [suggestion] The driver is invoked with the repository and the pull request number only — it resolves the head itself (its prompt's first step isgh pr diff) — so nothing binds it to the shaRecord the commit under reviewcaptured at 1806. The install contract check at 1530–1532 confirms no sha flag exists on the surface this file drives.
That makes the comment at 1803–1808 ("This is the commit the driver is about to review, and the commit every publisher names") stronger than what holds. A push landing between the record and the driver's fetch means every publisher names commit A while the review read commit B: the placement batch is partitioned against A's compare output while the model's line numbers come from B, so anchorable findings either land on the wrong lines or lose the whole batch to a 422 and fall to the summary; and under approve-on-success the approval is recorded against a commit the review did not read — the case the approve arm at 2832 was just hardened for on the missing-sha axis but not on the stale-sha axis.
The window is small (seconds, not the whole review) and re-review-on-push off means a push does not cancel the run, so it will not self-correct. A cheap mitigation entirely inside this file: re-read the head after the drive step and compare it with steps.head.outputs.sha; if it moved, warn, and downgrade the APPROVE arm the same way the empty-sha branch already does. That leaves the check run and the comment on the documented weaker guarantee and protects only the assertion that cannot be walked back.
.github/workflows/seidroid-review.yml:2631-- [suggestion] An absentcheck.jsonexits 0 and publishes nothing. The step comment at 2602–2604 states this deliberately ("a run that never reached the driver still publishes nothing"), but it has a consequence on the re-review path that the reasoning does not cover.
Check runs are keyed on the commit, and GitHub surfaces the latest one per name. So on head commit X: an automatic review publishes review = success, then a @seidroid review re-review on the same X dies before the driver — a failed go install, an unresolvable toolchain, the MIN_DRIVER_VERSION refusal at 1509 — and this step writes nothing. The success from the first run stays the current review (and AI Review) check on X, so a branch-protection rule requiring either reads green for a review that did not happen. The Report a review that reached no verdict comment is the only trace on the pull request, and the check that gates the merge disagrees with it.
Note that Post the verdict's own failure path (3495–3511) already does the right thing here — it publishes a failure check under both names precisely so a stale conclusion cannot stand. The same treatment fits this branch: when $CHECK is absent and REVIEWED_SHA is non-empty, publish failure under review (and the AI Review copy) with a title naming the run, so the gate reflects the run that just failed rather than the one before it.
Superseded: latest AI review found no blocking issues.
…d hold an approval to the commit it read Addresses the three suggestions in seidroid's review of d4b48ae. It approved that head with nothing blocking; these are the narrower gaps it named. The recall file could be another pull request's. `Read the threads this review left before` published threads_path and then wrote the file, and the name is a fixed one in RUNNER_TEMP with no run id. A failed fetch aborted the step with the output already set and the file untouched, so the drive step's `-s` test passed on whatever the last run of this job left there -- and the driver was handed another pull request's findings as this one's history. Silent in both directions. It is now emptied where its path is published, which is what the base-standards read, the drive step and placement each already do. An absent check file left the previous run's conclusion standing. Check runs are keyed on the commit and GitHub surfaces the latest per name, so a re-review of a commit an earlier review passed, dying before the driver, left that success as the current `review`. A branch rule requiring it read green for a review that did not happen, while the no-verdict notice on the pull request said otherwise. It now publishes failure under both names, the way `Post the verdict`'s failure path already does. With no recorded commit there is no gate to correct, and it still publishes nothing. An approval no longer outlives the commit it read. The driver resolves the head itself, so the recorded sha binds the publishers and not the read: a push landing mid-review leaves every publisher naming commit A while the model read commit B, and a push does not cancel the run. The position step now re-reads the head and downgrades APPROVE to COMMENT when it moved. Only the approval is gated on it, for the reason the missing-sha branch gives: a comment and a block stay true of code nobody read, and an approval does not. A read that fails leaves the vote standing. Verified for its other non-blocking point: sei-agent-driver/v0.21.0 installs from an empty module cache in 6.1s and builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three suggestions from the last review are fixed in ce8bf94, and the release-ordering question is answered. The recall file could be another pull request's. Correct, and the worst of the three. That one earned a rule rather than a line. An absent check file left the previous run's conclusion standing. Correct. It now publishes An approval outliving the commit it read. Correct, and a real gap in the hardening I did last round — I closed the missing-sha axis and left the stale-sha one open. The position step now re-reads the head and downgrades The driver tag. Verified from an empty Still open, by choice. Nothing in this repository exercises the file, and the harnesses are not in it. Both are the consequence of this PR carrying the product alone — they are on One gap stated plainly: the check-publish change has no automated coverage. No harness drives that step, and I did not build one for this round. |
masih
left a comment
There was a problem hiding this comment.
Review feedback generated by Devin Review
Here's my assessment of the three flagged bugs against the actual code.
Bug 1 — Concurrency key includes mode (.github/workflows/seidroid-review.yml:1304-1306)
The flag says including mode lets a review and a close run at once for the same PR, and close could disrupt/outrun the review. But the code comment at :1294-1297 states the opposite intent deliberately: if mode were not in the group, commenting on a just-closed PR would cancel the in-flight close (cancel-in-progress: true), and close is "the only thing that reclaims a sandbox."
So this is a genuine trade-off, not a clear bug. The author chose to protect close from cancellation at the cost of allowing review+close concurrency. Whether that's wrong depends on driver session semantics (does close deleting a session mid-review corrupt anything?). I can't verify the driver side from this repo. Verdict: design trade-off, needs driver-side confirmation — not a clear-cut bug.
Bug 2 — Stale block withdrawal after head moves (.github/workflows/seidroid-review.yml:3034-3037)
This one looks real. The moved check (:3034 region, and the moved=true logic around :3030 in the earlier read) downgrades an approval to a comment when the head moved during review. But the withdrawal logic only exits early on event = REQUEST_CHANGES, conclusion = failure, or conclusion = unknown (:3034-3035). It does not consult moved.
So: review reads commit A, concludes clean, head moves to B mid-review → approval is correctly downgraded, but the withdrawal at :3034+ still proceeds and dismisses a standing CHANGES_REQUESTED block. That clears a block against commit B, which was never reviewed. The asymmetry the author documents for approval (an approval "asserts something about a specific diff") applies equally to withdrawing a block — a dismissal also asserts the current head is clean. Verdict: plausible real bug. Worth flagging.
Bug 3 — No-verdict notice cleanup only deletes the last one (.github/workflows/seidroid-review.yml:3526-3528)
Also real, but low severity. The cleanup pipes through tail -n 1, deleting only the most recent NO_VERDICT_MARKER comment. The step's own comment at :3512-3515 claims "exactly one" notice invariant. That invariant holds only if every prior run successfully deleted its predecessor. If a deletion ever failed (the tolerated-failure path at :3532-3534), duplicates accumulate and this cleanup removes just one per successful run. The parallel step "Report a review that reached no verdict" has the same tail -n 1 pattern (in delete_prior_notice).
The fix is to delete all matching notices, not just the last. Verdict: real but non-severe, matching the flag.
Summary you can paste into the PR
Bug 2 (severe): stale block cleared when head moves during review. In
State the review's position(.github/workflows/seidroid-review.yml), themovedcheck downgrades an approval to a comment when the PR head changes mid-review, but the block-withdrawal logic at the end of the step (:3034-3037) gates only onevent/conclusion, never onmoved. A clean review of commit A can therefore dismiss a standingCHANGES_REQUESTEDblock after the head has moved to an unreviewed commit B. A dismissal asserts the current head is clean, so it should be subject to the same stale-head guard as the approval.Bug 3 (non-severe): duplicate no-verdict notices survive. The stale-notice cleanup in
Post the verdict(.github/workflows/seidroid-review.yml:3526-3528) and inReport a review that reached no verdictusestail -n 1, deleting only the newestNO_VERDICT_MARKERcomment. If any earlier deletion failed (a tolerated path), duplicates accumulate and only one is removed per run, leaving stale "this review did not complete" notices on a PR whose review did complete. Delete all matching notices, not just the last.Bug 1 (needs driver confirmation): review/close concurrency. Including
modein the concurrency group (:1304-1306) is a deliberate trade-off — it protects an in-flightclosefrom being cancelled by a later comment — but it does allow a review and a close for the same PR to run simultaneously. Confirm on the driver side that aclosedeleting a session while areviewturn is mid-flight is safe.
71 lines to 12. What it said about the two paths, the four events, the allow-tools default and the injection routing is stated at each of those places already; the "what changed versus the Python driver's workflow" section was history, which belongs in the pull request and not in the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and withdraw every stale notice Addresses Masih's review of 86fcf9d. A dismissal takes the same stale-head guard as an approval. ce8bf94 added the `moved` check and wired it to the approval alone, so a clean review of commit A still cleared a standing CHANGES_REQUESTED after the head moved to B -- handing a merge gate away on a diff nobody reviewed. A dismissal asserts the current head is clean, which is the same class of claim, so it now stops on a moved head. The block standing is the recoverable direction: the next review clears it, where a block wrongly cleared needs a human to put it back. The test for the approval downgrade had encoded the old behaviour, asserting the withdrawal proceeded. The pair now isolates the guard: the same clean review withdraws on an unmoved head and does not on a moved one. Both no-verdict notice cleanups withdraw every match rather than the newest. The "exactly one notice" invariant they each claimed holds only while no deletion has ever failed, and that path is tolerated in both -- so one refusal left a notice no later run could reach, standing on a pull request whose review did complete. Not addressed here, and it is not a uci question: whether `mode` belongs in the concurrency group. The trade-off is deliberate and stated in the file -- keeping `mode` out would let a comment on a just-closed pull request cancel the in-flight close, and close is the only thing that reclaims a sandbox. What it costs is a review and a close running together for one pull request, and whether that is safe is sei-agent-driver session semantics. Tracked separately. 781 assertions across the seven harnesses. Each fix checked against its defect: reverting either fails only the assertions written for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — your read on all three was right, including the one you declined to call a bug. Bug 2 — fixed in 5b33c1e. You are correct, and it was mine: ce8bf94 added the One detail worth recording: my own test had encoded the defect. The Bug 3 — fixed in 5b33c1e, both sites. Bug 1 — filed, not changed here. PLT-1274. You framed it correctly as a trade-off rather than a bug, and I agree it should not be settled from this repository. Worth stating the shape so the ticket is not re-derived: keeping Each fix was checked against its defect: reverting either fails only the assertions written for it. 781 assertions across the seven harnesses, on Separately, the header comment on the workflow is down from 71 lines to 12 (86fcf9d). What it said about the two paths, the four events, the allow-tools default and the injection routing is stated at each of those places already, and the "what changed versus the Python driver" section was history, which belongs in a pull request and not in the file. |
…k, not merely an unrefuted one Peer review of 5b33c1e found the fix incomplete. It closed the case Masih named and left the same hole on two other branches. `moved` was a boolean, so a head that could not be checked read as a head that had not moved. Two branches reached the dismissal that way: a commit the recording step never wrote, and a head re-read that failed or returned something that is not a sha. Either cleared a standing block with nothing having confirmed the diff it now guards -- the unverified-clean claim 5b33c1e's own message argues needs a human. The question now has three answers. `same`, `moved`, and `unknown`, because the two publishers want opposite things from the third. The approval treats `unknown` as good enough, deliberately: a wrong approval is corrected by the next review, and refusing one on an unanswered question would cost the common case to guard the rare one. The withdrawal cannot, because nothing here puts a cleared block back. Carrying the approval's rule to the dismissal was the defect, and it is the same mistake in both rounds: the two differ in whether their error heals, which is the argument for the guard in the first place. Also from the review: - The comment above the computation still said only the approval was gated on it. - Three comments still described one no-verdict notice where the code now takes every one. - The concurrency group's trade-off now cites PLT-1274 rather than saying the question is tracked somewhere. A correction to 5b33c1e's message, which cannot be amended now it is pushed. It said "reverting either fails only the assertions written for it". That held for the dismissal and for the verdict step's cleanup. It did not hold for the cleanup in `Report a review that reached no verdict`: nothing exercised that step at all, so reverting it failed zero assertions. The claim implied coverage that did not exist. It does now -- that site is driven directly, and reverting it fails four. 796 assertions across the seven harnesses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Peer-reviewed 5b33c1e with three independent passes before asking anyone to look again. They found the fix incomplete, and one claim in my own commit message wrong. Both are addressed in 9c7f209. The fix for Bug 2 closed one branch of three. It is three states now. Correction to 5b33c1e's message. It said "reverting either fails only the assertions written for it". That held for the dismissal and for the verdict step's cleanup. It did not hold for the cleanup in That site is now driven directly — reverting it fails four assertions. The stub also gained a delete-failure knob, so the tolerated path that makes duplicates possible is reachable by a case rather than only argued about in a comment. Also fixed: the comment above the head computation still said only the approval was gated on it; three comments still described one no-verdict notice where the code takes every one; and the concurrency trade-off now cites PLT-1274 rather than claiming the question is tracked somewhere unnamed. Confirmed not worth changing: PLT-1274 also now records an interim mitigation, if the driver answer is slow: the guard can refuse admission while a run of the opposite mode is in flight. It is check-then-act, so it narrows the window rather than closing it. 796 assertions across seven harnesses. One note on the suite itself: a chained run reported spurious |
|
@seidroid — is Masih's feedback on this pull request addressed? Please verify against the code at HEAD (9c7f209) rather than taking my summary for it, and say which items you consider closed and which not. His review left three findings:
What I claim, and what I would like checked:
Also worth your attention, since it is the newest code and no reviewer has seen it: the three-state head check in |
|
Claude finished @bdchatham's task in 3m 38s —— View job COMMENTEDVerified against the file at Findings: 0 blocking | 3 non-blocking | 0 posted inline Bug 2 — closed
So: the withdrawal is unreachable on an unrecorded commit, on a failed re-read, on a non-sha answer, and on a moved head. That is every branch Masih's finding and your own follow-up named. On the asymmetry — justified, but for a different reason than the comment gives. There is a real, checkable distinction between the three publishers, and it is not the one at
Where the comment overreaches is Bug 3 — closed, and the selection was not widenedDiffed
A person's comment quoting the marker still fails Bug 1 — punting is defensible; I could not find the cheap fixThe trade-off is stated in the file now, in both directions, with the ticket (
So there is no configuration of one concurrency block that serialises the two without giving up either cancellation or single-poster. The only in-file options are procedural, and the one you recorded on the ticket (refuse admission while the opposite mode is in flight) is check-then-act as you say. One variant you may not have considered, since it narrows differently: rather than the guard refusing a close, have the close path wait — poll Non-blocking, in the newest code1. The thumbs-up is the one reader-facing signal that still asserts a commit this run knows it may not have read. 2. On a moved or unconfirmed head, nothing on the pull request says so on most paths. The only on-PR statement is the approve arm's note at 3. What this pass could not checkThe driver half of Bug 1 — whether a session delete mid-turn is safe — is not answerable from this repository, which is the right reason for PLT-1274 to exist. And nothing here exercises the file: the harnesses are on · branch |
## What this is Repoints this repository's `seidroid review` caller from a raw commit sha to the uci tag **`v0.0.22`**. `seidroid-review.yml` landed on uci's `main` in sei-protocol/uci#108 and shipped as [v0.0.22](https://github.com/sei-protocol/uci/releases/tag/v0.0.22), so for the first time there is a tag to name. Until now every caller pinned a sha on a branch that had to be kept alive by hand. ## What changes here The three `uses:` lines, and nothing else. No inputs change. This caller omits `driver-version`, so it inherits the workflow default, which `v0.0.22` sets to `sei-agent-driver/v0.21.0`. Two things follow from that: - The review summary comes first, entries are one or two sentences, and nits are folded into a collapsed block (PLT-1268). - The verdict rides in the review that carries the inline comments, instead of a separate comment beside it. A standalone comment is now only the fallback for a run where that review did not land. ## Known, non-blocking Four items are tracked for the next uci release and listed in the release notes — the most visible is that a head which moves mid-review still earns a 👍 on the trigger comment, even though the run correctly declines to approve or to clear a block. None gates a merge. `mode` in the concurrency group lets a review and a close run together for one pull request. The trade-off is deliberate and stated in the callee; PLT-1274 tracks the driver-side question. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## What this is Repoints this repository's `seidroid review` caller from a raw commit sha to the uci tag **`v0.0.22`**. `seidroid-review.yml` landed on uci's `main` in sei-protocol/uci#108 and shipped as [v0.0.22](https://github.com/sei-protocol/uci/releases/tag/v0.0.22), so for the first time there is a tag to name. Until now every caller pinned a sha on a branch that had to be kept alive by hand. ## What changes here The three `uses:` lines, and nothing else. No inputs change. This caller omits `driver-version`, so it inherits the workflow default, which `v0.0.22` sets to `sei-agent-driver/v0.21.0`. Two things follow from that: - The review summary comes first, entries are one or two sentences, and nits are folded into a collapsed block (PLT-1268). - The verdict rides in the review that carries the inline comments, instead of a separate comment beside it. A standalone comment is now only the fallback for a run where that review did not land. ## This repository was further behind than the others It pinned `92f74c9`, the v0.18.0 driver-floor commit, where the other four pinned `81e975c`. So it also picks up everything between those two: the recorded review decision and the base branch's Accepted list, `run-deadline-seconds`, and the PLT-1268 change above. ## The header comment changes too It said the `uses:` lines pin a raw sha because *"this callee is on no uci tag and on no uci default branch... so there is no tag to name."* That is no longer true, so the pin stops being one of this file's documented divergences — four become three, and the paragraph explaining the sha is replaced by one sentence saying the lines pin a tag, as this repository's four other uci callers already do. ## Known, non-blocking Four items are tracked for the next uci release and listed in the release notes — the most visible is that a head which moves mid-review still earns a 👍 on the trigger comment, even though the run correctly declines to approve or to clear a block. None gates a merge. `mode` in the concurrency group lets a review and a close run together for one pull request. The trade-off is deliberate and stated in the callee; PLT-1274 tracks the driver-side question. 🤖 Generated with [Claude Code](https://claude.com/claude-code)

What this is
The first release of
seidroid-review.yml, the agentic pull request reviewer, as a reusable workflow. It reviews a pull request inside the Actions runner and posts inline comments, a check run and a review position.One file, 3,934 insertions, 0 deletions. It needs nothing else here — three pinned actions, the driver via
go install, the diff through the API, and it never checks the caller out. Folds 33 commits fromfeat/seidroid-review;maincarried none of it.Driver pin, and PLT-1268
Floor
v0.21.0, in both thedriver-versiondefault andMIN_DRIVER_VERSION: the conclusion a review reaches is specific to the driver that reached it. Cut today as sei-protocol/sei-internal-skills#453, with the driver half of PLT-1268 in sei-protocol/sei-internal-skills#452.This is the uci half. The verdict now rides in the review holding the inline comments, a standalone comment is the fallback, and the guard counts a bot review bearing the marker as a prior review — without which a run that posts no comment re-reviews on every push.
Verification
739 assertions across seven (temporary scaffolding) harnesses, 0 failures, against this exact blob (
09c86bd). They are not in this change — the tooling was half the diff — and are onarchive/seidroid-review-tests. Nothing has run end to end againstv0.21.0: the harnesses drive the step scripts against aghstub, so first live proof is this pull request's own review.After this merges
Cut a
ucirelease, then move the five callers off81e975c. Until then leavechore/bump-seidroid-min-driver-v0-18-0and the tagpin-seidroid-min-driver-v0.18.0in place — the callers pin that SHA.🤖 Generated with Claude Code