fix(review-runs): collect Step 4's outcome signals in a tested script - #1027
Conversation
tend-agent
left a comment
There was a problem hiding this comment.
The blind spot is real and the fix is the right shape — one added query, joined to the prose that already frames in-thread corrections. Two notes on the recipe, one verified as wrong.
updated: does accept a full Z timestamp — GitHub's search date qualifiers take optional time information, and gh passes it through. Measured on this repo just now with SINCE=2026-08-20T07:59:59Z: updated:>=$SINCE returns 9 PRs, updated:>=${SINCE%Z}+00:00 returns 9, updated:>=${SINCE%T*} returns 10. So the truncation isn't required, and the sentence explaining it states something a future run will read as ground truth about the API. The sibling recipe in review-reviewers already passes the full timestamp (--search "updated:>$WINDOW_START"), so this also leaves the two skills disagreeing on a point one of them asserts as fact. Suggestions inline for both the command and the prose.
Separately, and not introduced here: the same argument the PR body makes for re-reading $SINCE applies to $BOT_LOGIN in the adjacent conjunct of the same select(). Unset, .user.login != "" is true for every review — including every tend-review COMMENT body — so the query reports the bot talking to itself as maintainer corrections. That fails toward false positives rather than the false negative this PR closes, and the block above (issues/pulls/comments) has it too, so it's a Step 4-wide gap rather than something this diff owes. Worth a BOT_LOGIN=$(gh api user --jq '.login') next to the SINCE= line in both blocks if you're touching this again.
…_LOGIN
The review recipe truncated $SINCE to a date on the claim that `--search`
takes no time component. It does: measured on this repo, `updated:>=$SINCE`
and `updated:>=${SINCE%Z}+00:00` both return 9 PRs where the date-only form
returns 10, the extra one (#1018) updated at 01:28:24Z — before the cutoff.
Pass the timestamp and drop the day of over-selection. This also stops the
skill asserting, as ground truth a future run reads, something that
contradicts the sibling recipe in review-reviewers.
Also re-read $BOT_LOGIN alongside $SINCE in all three Step 4 blocks. Shell
variables don't survive between tool calls, and unset it makes
`.user.login != """ true for every entry, so the step reports the bot's own
reviews as maintainer corrections. In this repo's last window that is 8
bodied reviews, all the bot's own, every one a false positive.
|
Both notes applied in 6862d7b; replies inline on the two suggestions. On Left VerificationTimestamp granularity, on The row date-only admits is #1018, The revised loop run verbatim returns zero non-bot rows on this repo for that window; with the bot filter dropped it returns the 8 rows above, confirming the empty is genuine and not a broken query.
|
tend-agent
left a comment
There was a problem hiding this comment.
Both notes are applied correctly. I re-measured the timestamp claim independently rather than take the reply's numbers: on this repo with SINCE=2026-08-20T07:59:59Z, updated:>=$SINCE returns 9, the +00:00 form 9, date-only 10, and an unqualified gh pr list returns 200 — the third number is what rules out the qualifier being parsed-and-discarded, which the two matching counts alone wouldn't. I also ran the revised loop verbatim over a wider window (2026-08-18) and cross-checked its candidate list against a per-PR enumeration that doesn't use --search at all: both find the same 20 bodied reviews, so the updated: candidate list has no false negatives for in-window reviews.
One new thing, introduced by this commit: the rewritten comment on the disposition block says an unset $BOT_LOGIN "matches nobody", which is the opposite of what happens and contradicts the clause right after it. gh pr list --author "" applies no author filter at all — on this repo it returns {"max-sixty":67,"tend-agent":132,"worktrunk-bot":1} where --author tend-agent returns only tend-agent — and in the other two blocks .user.login != "" is true for every entry. Both directions are "stops filtering", not "matches nobody". Given the PR's own thesis is that a wrong factual claim in a skill gets read as ground truth by a future run, worth correcting in place. Suggestion inline.
An empty `--author` applies no author filter and `.user.login != ""` is true for every entry, so both directions over-report rather than matching nobody.
|
can we simplify? |
…plicing Three queries in one block set $SINCE/$BOT_LOGIN once instead of three times, and jq reads them via $ENV rather than splicing them into the filter string.
|
Yes — simplified in 8ee0e3b, net 10 lines shorter than the previous revision and now +26/-5 against Step 4's three queries are one block instead of three, so Same coverage: I ran the block verbatim out of the file. On this repo it returns 6 dispositions, 10 non-bot comments, 0 bodied non-bot reviews for the last window; pointed at |
tend-agent
left a comment
There was a problem hiding this comment.
Ran the collapsed block verbatim out of the file rather than trusting the reply's numbers. It works, and the two mechanisms the collapse depends on both hold: $ENV.SINCE / $ENV.BOT_LOGIN resolve under gh's bundled gojq (not just system jq), and closedAt is null for open PRs, so select(.closedAt > $ENV.SINCE) still excludes them with the splice gone. On this repo's window the reviews loop returns 18 bodied reviews with the login filter dropped and 0 with it on — all 18 tend-agent — which is both the false-positive count the filter prevents and proof the empty result is a real empty rather than a broken query. review-reviewers genuinely doesn't share the blind spot: it already loops pulls/$pr/reviews in its repo-wide sweep.
One structural note, prompted by the "can we simplify?" ask rather than by a defect in the diff. The block is now 32 lines carrying three separately-verified edge cases — pagination direction on both endpoints, the empty-bodied review container, updated: windowing the PR while submitted_at windows the review. running-in-ci's Scripts over prose recipes names that threshold directly: "When a skill's code block needs edge-case handling or grows past a couple of dozen lines, put the logic in a tested script and leave the skill a one-line invocation with the intent … A prose recipe gets no shellcheck and no tests; every session re-derives its correctness." A scripts/review-runs-corrections.sh "$SINCE" beside the other nine, with a generator/tests/ case, would leave Step 4 a single line and would be the deeper version of the simplification — and Step 4's failure mode is the outward kind the tests would guard, since a silently-empty query becomes "no maintainer corrections" in the tracking issue, which later runs read as ground truth under Gate 1.
Not a blocker and not something I'd expand this PR to do — the diff is correct as written, and prose-vs-script is a call for you rather than something I should decide by pushing it.
The block had grown to 32 lines carrying three separately-verified edge cases (pagination direction on both endpoints, the empty-bodied review container, `updated:` windowing the PR while `submitted_at` windows the review), which is the threshold running-in-ci names for putting logic in a script. Step 4 is now a one-line invocation. Extraction also closes the fail-open class the earlier revisions kept patching in prose: the script exits 2 when the anchor or the bot login is missing, rather than reporting an empty window that becomes "no maintainer corrections" in the tracking issue.
|
Took the script, in 01c6ac2 — it's the better answer to "can we simplify?" than the collapse was, and the deciding factor is the one the review names: Step 4's failure mode is outward. A silently-empty query becomes "no maintainer corrections" in the tracking issue, which later runs read as ground truth under Gate 1, so the fail-open inputs are exactly the complexity Step 4 is now one line and the skill is net −20 lines against VerificationLive, not just under the fakes. On this repo with an 18h window the script returns 1 disposition (#1025, merged), 2 non-bot comments (both Tests: missing anchor and empty login each exit 2 with no
|
tend-agent
left a comment
There was a problem hiding this comment.
The extraction is the right call, and the three window edges are pinned properly. One fail-open survives it, in the same class the script exists to close.
for pr in $(gh pr list … --search …) doesn't propagate the candidate query's failure. Bash's set -e doesn't check the exit status of a command substitution in a for word list, so a failed --search yields an empty list, the loop body never runs, and reviews reduces to [] with the script exiting 0. Step 4 then reads that as the all-clear it defines — "Empty dispositions, comments, and reviews is the all-clear" — and writes "no maintainer corrections" into the tracking issue, which is the outward claim the whole PR is about. Ran it against the script as committed, with a fake gh that fails only on the --search call:
$ review-runs-corrections.sh 2026-01-02T00:00:00Z
search: API rate limit exceeded
{ "since": "2026-01-02T00:00:00Z", "bot": "tend-agent",
"dispositions": [], "comments": [], "reviews": [] }
$ echo $?
0
Worth saying why the other two paths don't have this: DISPOSITIONS=$(… | jq …) is an assignment, so pipefail plus set -e catch it, and in the comments block the gh api is the loop body, so its failure exits the subshell and fails the pipeline. Only the reviews block puts a gh call in a for list — and it's the call most likely to fail, since --search draws on the search API's 30 req/min budget rather than the 5000/h core one, and Step 4 runs after the log downloads and token report.
Suggestion inline hoists the candidate list into its own assignment, which set -e does check. Applied it locally: shellcheck stays clean (SC2086 isn't raised for a for word list, so no disable comment is needed) and all 12 tests still pass.
Smaller, and a call for you rather than a defect: the extraction moved the "since filters on updated_at, so an older comment edited inside the window is a real hit, not a broken filter" note into the script's header, where the run reading Step 4 won't see it. Rows still carry both timestamps, so a session now meets a created before the anchor with nothing on the page explaining why it qualified — and that direction discards a real correction.
…earch does A failed --search inside the for word list is invisible to set -e, so the loop is skipped and reviews: [] reads as an all-clear. Hoist the candidate list into its own assignment, which set -e does check, and restore the updated_at note to Step 4 where the run reading it will see it.
|
Both notes applied in a730ffd; reply inline on the suggestion. Took the second note too — the VerificationThe fail-open, reproduced against the script as committed (fake Live happy path unchanged: on this repo over a 6h window the script returns 1 disposition, 2 non-bot comments, 0 bodied non-bot reviews in 4.6s.
|
tend-agent
left a comment
There was a problem hiding this comment.
Last open finding is closed. Verified the hoist rather than taking the reply's reproduction: set -e really does treat the two forms differently — R=$(for x in $(fail); do …; done | jq -s .) reaches the next line with R=[] and exits 0, while CANDIDATES=$(fail) aborts the script with rc=1. The new test is a genuine regression pin, not a tautology: checked out at 01c6ac2 it fails with assert 0 != 0 against the pre-fix script and passes against a730ffd.
Checked the other three fail-open shapes in the same script while I was in there, and they hold as the header claims: REPO="${GITHUB_REPOSITORY:-$(gh repo view …)}" propagates a failing substitution out of the parameter-expansion default, and a gh api failure in the comments loop body fails the pipeline under pipefail. So the reviews block was the only one, and it no longer is.
The --limit 200 on both pr list calls is the one thing I looked at and decided not to raise as a finding — Step 1 floors the anchor at 49 hours, and a 49h window on this repo is 10 updated PRs against a 200 cap, while the dispositions list would need a bot PR to sit open ~50 days (the oldest open one is 4 days, 3 open in total). Recording it only so the next reader doesn't re-derive it.
No approval because I authored the PR, not because anything is outstanding. Nothing new on the incremental; CI is green.
review-runsStep 4 cross-checks outcomes withissues/commentsandpulls/comments, neither of which returns a review body —pulls/commentscarries inline review comments only. A maintainerCHANGES_REQUESTEDwhose correction is written in the body therefore returns nothing from both calls, and the run writes "no maintainer corrections" with corrections sitting in the window.Step 4's three queries — dispositions, thread comments, and the missing review bodies — now live in
plugins/tend-ci-runner/scripts/review-runs-corrections.sh, withgenerator/tests/test_review_runs_corrections.pypinning the window edges. The skill is a one-line invocation, net −20 lines. Extraction also closes the fail-open class the prose kept patching: an unset anchor compares less than every timestamp and--author ""applies no author filter, so the script exits 2 on a missing anchor or login rather than reporting an empty window that reads as an all-clear.Verified against live API data (details below), and the script run against both this repo and
max-sixty/leaf.Verification
Review bodies are absent from
pulls/comments. Review4988584496on #1024 (COMMENTED, non-empty body, submitted inside the window) has no representation in the repo-wide review-comments endpoint — not by review id, not by body text:Every entry the endpoint does return carries a
path— inline comments, each belonging to a review record.The script returns the missed reviews. Pointed at
max-sixty/leafover the window in #1026 (where both prescribed queries returned zero non-bot entries), thereviewskey carries the twoCHANGES_REQUESTEDrows this PR exists to catch. On this repo over an 18h window it returns 1 disposition, 2 non-bot comments, 0 bodied non-bot reviews, in 4.3s.(.body | length) > 0is load-bearing. Empty-bodied review containers from a human exist on this repo today — e.g. review4988620443(max-sixty, #1024) and4987925324(max-sixty, #1022), bothCOMMENTEDwith no body. GitHub creates one per standalone inline reply, so without the filter every thread where anyone replied inline reports a correction with no text in it.updated:takes a full timestamp. An earlier revision truncated$SINCEto a date on the claim that the qualifier accepts no time component. It does. On this repo withSINCE=2026-08-20T07:59:59Z: fullZform returns 9 PRs, the+00:00form 9, date-only 10, unqualifiedgh pr list200. The extra PR the date-only form admits is #1018, updated2026-08-20T01:28:24Z— before the cutoff — so the time component is honoured at sub-day granularity rather than silently truncated. The sibling recipe inreview-reviewers(--search "updated:>$WINDOW_START") already passes the full timestamp.Two deltas from the recipe proposed in the issue:
--paginatewithper_page=100on the reviews call (reviews page at 30 oldest-first, so the unpaginated call drops the newest — exactly where a window's reviews are), and thesubmitted_atfilter inside the loop, sinceupdated:windows the PR rather than the review.Tests. 12 cases over a fake
ghrunning the script's real filters: missing anchor and unresolvable login both exit 2 before any API call; open PRs excluded from dispositions (closedAtnull); bot entries excluded from all three lists;--paginateasserted on both comment endpoints; an older comment edited into the window reported with both timestamps; the empty-bodied container excluded; a pre-window review on an in-window PR excluded.review-reviewers— flagged in the issue as possibly sharing the blind spot — does not: it already callspulls/{n}/reviewsper PR in both its bot-output census and its acceptance check.uv run pytest(581 passed) anduvx pre-commit run --files …both pass.Closes #1026 — automated triage