You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Nightly's Step 6 survey reads its file list straight from nightly_survey_files.py and reviews every path in it. Nothing between the script and the review checks whether a path already has an open PR against it. On a repo whose PRs merge, that costs nothing — the previous pass's PR is gone by the time the bucket comes round. On a repo with a backlog it costs a full review per aliased file, every cycle, forever.
The aliasing is exact, not approximate
The rotation is deterministic in both directions: the day picks the bucket (unix_day / 86400 % 28) and the path picks the bucket (cksum(path) % 28). So bucket N returns to the same file set every 28 days. Once a repo's default branch has been static longer than the cycle, every bucket whose last pass left an unmerged PR re-derives that PR's findings on the way round — not occasionally, but on the exact anniversary.
That threshold was crossed on max-sixty/cargo-affected: main static 31 days against a 28-day cycle, 36 open bot PRs, roughly a third of them survey-shaped.
What it cost on the run that tripped it
tend-nightly34195750626 (2026-09-08, bucket 12/28) drew tests/functional/clean.rs and tests/functional/db_has_function_ranges.rs. It read both, found the unreachable || contains("no coverage data found") alternative and a pub → pub(crate) error in a module doc, branched, wrote both fixes, rustup component add llvm-tools, installed cargo-nextest, cargo build --tests, ran the full functional suite, clippy --all-targets and fmt --check — and then checked the queue and found max-sixty/cargo-affected#83, opened 2026-08-11 (28 days earlier, same bucket), touching the same two files with the same two fixes. The branch was discarded unpushed. Run cost $3.09 / 54 turns, most of it on that path.
Two earlier nightlies on the same repo landed the same way with less sunk cost — 33724452938 (09-03) found two of three survey defects already carried by open PRs #77 and #99, and 33950286183 (09-05) declined to file after its src/selection.rs finding turned out to be covered by #81.
Why the existing guards don't cover it
Step 8's orienting projection (gh issue list/gh pr list --json number,title) matches on titles. A survey PR's title describes the fix (tests: correct two inaccurate statements in surveyed scenarios), which a freshly re-derived finding has no reason to phrase the same way.
running-in-ci's "Fetch the prior rejection before re-deriving a fix" (added since 0.1.14, present on main) does search by path, and firing per finding before writing code would have saved the build and test cycle above. But it is downstream of the read: the survey has already spent its review turns on the file by the time any finding exists to search for. It also depends on the model choosing the right <symbol> for each finding, where the survey already holds the exact path.
The pre-gh pr create recheck is last by construction — it caught this one, which is why nothing wrong shipped, but only after everything was spent.
Suggested direction
Design is yours. The cheapest shape looks like one call at the top of Step 6, before any file is read:
gh pr list --state open --limit 200 --json number,files \
--jq '[.[] | {n: .number, paths: [.files[].path]}]'> /tmp/open-pr-files.json
Then drop from today's bucket any path that appears in it, noting the skipped paths and the PRs that cover them in the Step 9 summary so the skip is visible rather than silent. A file with an open PR against it isn't a good survey target regardless of the aliasing: whatever the survey finds there is either already in that PR or belongs as a comment on it, and the PR is the place a maintainer is already looking.
This is a property of the rotation rather than of any one repo, so it will surface anywhere a backlog outlives the 28-day cycle — most visibly on repos where the bot is the only author.
Verification notes
Bucket confirmed by running the script in the same 24h window: nightly-survey-files.sh → bucket=12/28 files=4, listing the two test files plus two generated tend-*.yaml (skipped per the checklist).
gh pr view 83 --json files returns exactly tests/functional/clean.rs, tests/functional/db_has_function_ranges.rs; its diff makes the same two edits the nightly's Edit tool calls made, re-read from the session log at claude-session-logs-c508d87b.
Step 6 on tend main (plugins/tend-ci-runner/skills/nightly/SKILL.md) carries no dedup between the script invocation and "Apply the review checklist below to each file in full".
Nightly's Step 6 survey reads its file list straight from
nightly_survey_files.pyand reviews every path in it. Nothing between the script and the review checks whether a path already has an open PR against it. On a repo whose PRs merge, that costs nothing — the previous pass's PR is gone by the time the bucket comes round. On a repo with a backlog it costs a full review per aliased file, every cycle, forever.The aliasing is exact, not approximate
The rotation is deterministic in both directions: the day picks the bucket (
unix_day / 86400 % 28) and the path picks the bucket (cksum(path) % 28). So bucket N returns to the same file set every 28 days. Once a repo's default branch has been static longer than the cycle, every bucket whose last pass left an unmerged PR re-derives that PR's findings on the way round — not occasionally, but on the exact anniversary.That threshold was crossed on max-sixty/cargo-affected:
mainstatic 31 days against a 28-day cycle, 36 open bot PRs, roughly a third of them survey-shaped.What it cost on the run that tripped it
tend-nightly34195750626(2026-09-08, bucket 12/28) drewtests/functional/clean.rsandtests/functional/db_has_function_ranges.rs. It read both, found the unreachable|| contains("no coverage data found")alternative and apub→pub(crate)error in a module doc, branched, wrote both fixes,rustup component add llvm-tools, installedcargo-nextest,cargo build --tests, ran the full functional suite,clippy --all-targetsandfmt --check— and then checked the queue and found max-sixty/cargo-affected#83, opened 2026-08-11 (28 days earlier, same bucket), touching the same two files with the same two fixes. The branch was discarded unpushed. Run cost $3.09 / 54 turns, most of it on that path.Two earlier nightlies on the same repo landed the same way with less sunk cost —
33724452938(09-03) found two of three survey defects already carried by open PRs #77 and #99, and33950286183(09-05) declined to file after itssrc/selection.rsfinding turned out to be covered by #81.Why the existing guards don't cover it
gh issue list/gh pr list --json number,title) matches on titles. A survey PR's title describes the fix (tests: correct two inaccurate statements in surveyed scenarios), which a freshly re-derived finding has no reason to phrase the same way.running-in-ci's "Fetch the prior rejection before re-deriving a fix" (added since0.1.14, present onmain) does search by path, and firing per finding before writing code would have saved the build and test cycle above. But it is downstream of the read: the survey has already spent its review turns on the file by the time any finding exists to search for. It also depends on the model choosing the right<symbol>for each finding, where the survey already holds the exact path.gh pr createrecheck is last by construction — it caught this one, which is why nothing wrong shipped, but only after everything was spent.Suggested direction
Design is yours. The cheapest shape looks like one call at the top of Step 6, before any file is read:
Then drop from today's bucket any path that appears in it, noting the skipped paths and the PRs that cover them in the Step 9 summary so the skip is visible rather than silent. A file with an open PR against it isn't a good survey target regardless of the aliasing: whatever the survey finds there is either already in that PR or belongs as a comment on it, and the PR is the place a maintainer is already looking.
This is a property of the rotation rather than of any one repo, so it will surface anywhere a backlog outlives the 28-day cycle — most visibly on repos where the bot is the only author.
Verification notes
nightly-survey-files.sh→bucket=12/28 files=4, listing the two test files plus two generatedtend-*.yaml(skipped per the checklist).gh pr view 83 --json filesreturns exactlytests/functional/clean.rs, tests/functional/db_has_function_ranges.rs; its diff makes the same two edits the nightly'sEdittool calls made, re-read from the session log atclaude-session-logs-c508d87b.createdAtis2026-08-11T06:41:20Z; the nightly ran2026-09-08T06:41:08Z— 28 days and 12 seconds apart, which is the rotation, not a coincidence.main(plugins/tend-ci-runner/skills/nightly/SKILL.md) carries no dedup between the script invocation and "Apply the review checklist below to each file in full".