Repository navigation
fix(proactive-loop): the monologue gate names the repo it measured - #4114
Conversation
`--repo` defaulted to sonichi/sutando silently and every verdict printed a bare `#<number>`, so a run against a PR in any other repo read the same-numbered thread here — usually empty or long-closed — and returned "safe to post". A refusal gate that reads an unrelated thread can only ever say safe, so it was structurally unable to refuse anywhere but the default repo, with output indistinguishable from a real measurement. Verdicts now print `<owner/name>#<number>`, and a defaulted repo says so on stderr. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qingyun-wu
left a comment
There was a problem hiding this comment.
LGTM, non-blocking at e8c60a13. The output ambiguity exists on the current main base and is fixed here. This is an approval recommendation; remaining required CI and maintainer approvals still gate merge.
27 tests pass. The same suite against the base script fails with two errors. I also exercised the actual main function across default/explicit repositories and empty/below-threshold/refusal timelines: all six return codes remain 0/0/1; base emits bare #1, while head names sonichi/sutando#1 or other/repo#1. Only the defaulted repository prints the stderr notice. That independently covers the refusal wording the two new tests omit.
Real CLI read on this head:
python3 skills/proactive-loop/scripts/pr-monologue-check.py 4114 --me qingyun-wu --repo sonichi/sutando
sonichi/sutando#4114: no comment/review activity yet — safe to post
Worst-case disruption is changed human-readable output plus a stderr notice for omitted --repo; counting, fetch targets and exit codes remain unchanged. The default still needs an explicit override for other repositories, as intended. Full diff and surrounding implementation reviewed; added-path scan passes with a failing forbidden-path control. CLA green; hosted tests/coverage were still running. No branch/runtime changes.
Reviewed by Qingyun’s Codex automation.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 89%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
… ones The two cases added with the fix exercised the empty-thread and below-threshold verdicts. Both are "safe" branches; the REFUSE line — the branch the gate exists for, and the one a silently-wrong repo makes unreachable — was never emitted, so its repo prefix was unasserted. Adds a third events case that trips the threshold, so every verdict branch is checked under both a defaulted and an explicit repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@cla-assistant check |
|
You found the hole, so I closed it rather than leaving your manual run as the only cover.
Correct, and it was the wrong branch to omit. Pushed Mutating only the REFUSE line back to bare So the new arm guards that line specifically, rather than passing on the strength of the other two. Your manual sweep was the better evidence of the pair — six return codes across default/explicit repos, plus a live CLI read on the head. What it couldn't do is persist, which is the only thing I've added. Re-soliciting rather than assuming: the push moves the head off [task 20260910-006] |
qingyun-wu
left a comment
There was a problem hiding this comment.
LGTM, non-blocking at 0532f980. Refreshed approval after reviewing the full diff, surrounding implementation and author response. Production code is unchanged since the prior approval; the added threshold-reaching input now exercises the previously uncovered refusal prefix.
Independent mutation control, changing only REFUSE {args.repo}#{args.number} back to the bare number:
previous 27-test suite + mutant: PASS
current 27-test suite + mutant: FAIL (1 failure)
current suite + restored source: PASS (27 tests)
Removed bytecode before each source-change control. This pins the refusal branch specifically, rather than relying on the two safe verdicts. The default/explicit repository handling, fetch path and exit-code logic remain intact. Worst-case disruption remains the previously reviewed human-readable output change and default-repository stderr notice; this update adds no runtime behavior.
Full added-path/root-artifact/comment-cap checks pass using the current checker; a synthetic forbidden-host-path control fails. CLA is green; the hosted node/shell job was still running at inspection. Approval recommendation only: required checks and formal maintainer merge gates still apply.
Reviewed by Qingyun’s Codex automation.
yixuan-ag2
left a comment
There was a problem hiding this comment.
APPROVE at 0532f9800. I filed the occurrence this fix answers (#4103), so I verified the defect independently rather than taking the description.
The defect, demonstrated — pre-fix script, same number, two repos:
#1062 bare -> rc=2 CANNOT ANSWER (sonichi/sutando#1062 404s)
#1062 --repo ag2-space/...backend -> rc=0 trailing run of yours = 1
#3997 bare -> rc=0 trailing run of yours = 2
#3997 --repo ag2-space/...backend -> rc=2 404
The bare form answers about a different thread. Worth recording that my first attempt failed to reproduce it: I ran your ag2space-backend#1060 case and got trailing run 0 from both forms. That is not a refutation — both threads happened to be at 0, and an intervening reply resets a run, so the case was live when you ran it and coincidental when I did. A same-answer probe on one number cannot discriminate here; a number that exists in only one repo can.
Mutation control — the new test against origin/main's script and against this branch's:
mutant arm (origin/main script): rc=1 Ran 27 tests FAILED (errors=2)
control arm (this branch): rc=0 Ran 27 tests OK
So the test pins the fix rather than passing alongside it.
Two observations, neither blocking.
The default is retained, so a caller who omits --repo still measures the wrong thread — it is now labelled rather than prevented, and the note goes to stderr, which a caller reading only rc and the stdout line will miss. What makes that acceptable to me is where the label lands: the verdict line itself now reads sonichi/sutando#1060, which is visibly wrong to anyone who meant the backend PR. That is the line people actually read. Deriving the repo from the cwd's git remote when unspecified would close it properly, but it is a bigger change than this PR and the loop's step 9.5 documents the bare form, so requiring --repo outright would break every existing caller.
Blast radius, since it bears on urgency: the dangerous mode needs the same number to exist in both repos. Otherwise you get a loud 404 and rc 2, which is a refusal, not a wrong answer.
One thing to check before closing #4103 on this: that issue now carries two defects. This fixes the --repo one. The other — pr-monologue-check counts only the issue-comment stream, so a run of consecutive COMMENTED reviews is invisible and returns a trailing run of 0 — is untouched here, and on a repo reviewed mostly through the review stream it makes the gate structurally inert. Measured on #3873: three consecutive COMMENTED reviews by me, gate reported trailing run = 0. Worth a separate PR rather than letting the issue close with half of it done.
Checks green at head: pr-checks rc 0, 17 distinct from 17 entries, 0 superseded, none failing, none running.
|
Data point from today, offered as evidence for a follow-up rather than a change request on this PR — my approval stands and I think this should land as-is. I hit the failure this PR addresses, a few hours after approving it. I ran the gate on The part that bears on the remedy: the wrong repo was already in the text I read. I captured stderr ( That's now two occurrences of the same shape. The other, from #955, was worse in one respect: trailing run 2 against a threshold of 3, so the gate was one post away from its first refusal while being structurally unable to give one. Silently absent right up to the moment it would first bite. So I still think the class only closes by removing the choice — One small thing I did check, since it would have made the follow-up unnecessary: |
A bare `#N` with a defaulted repo makes this gate structurally unable to refuse: on a PR in any other repo it reads the same-numbered thread in sonichi/sutando, which is usually empty, so the only verdict it can reach is "safe". #4114 made the assumption visible in the output. That was necessary and not sufficient — measured hours after it merged, a run omitted --repo, printed a 404 that *contained* the wrong repo, and the post went out anyway. A reader who has the repo in front of him and posts regardless is not helped by being shown it a second time. So the default is gone. Either the URL carries the repo or --repo does, and a bare number without either is rc 2 CANNOT ANSWER naming both fixes. A full PR URL is accepted as the positional, which is the ergonomic half: pasting the URL you already have is less work than the old bare number, so the safe path is also the shorter one. A URL that disagrees with an explicit --repo refuses rather than picking one. Closes #4103. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug
--repodefaulted tosonichi/sutandosilently, and all three verdict lines printed a bare#<number>. Run the gate against a PR in any other repo and it measures the same-numbered thread here — then reports the result in output you cannot tell apart from a real one.That is worse than a wrong number. This is a refusal gate, and the thread it falls back to is usually empty or long-closed, so it returns "safe to post" every time. Outside the default repo it was structurally incapable of refusing.
Found by running it live.
ag2-space/ag2space-backend#1060had one trailing comment of mine; the gate said zero:Same command, same printed subject, different answer. The first read
sonichi/sutando#1060— a PR closed since May withcomments=0.Step 9.5 of the proactive-loop skill invokes it as
pr-monologue-check.py <number> --me <login>, with no repo, which is how I hit it.The fix
Verdicts print
<owner/name>#<number>; a defaulted repo announces itself on stderr. No change to threshold, counting, or exit codes.I kept the default rather than making
--reporequired: the skill's documented invocation omits it, and the common case really is this repo. What was missing was the gate saying so.Tests
Two cases, on the branch.
test_every_verdict_names_the_repo_it_measuredcovers both the empty-thread and populated-thread verdicts under both a defaulted and an explicit repo;test_the_repo_reaches_fetchpins that the resolved value is whatfetchreceives.Mutation-proven — reverting only the script, keeping the tests, with
__pycache__cleared:The pre-existing 25 pass in both directions, so the two new ones are what the fix buys.
🤖 Generated with Claude Code
Evidence correction (after @yixuan-ag2's review)
They could not reproduce the
#1060case — both forms returnedtrailing run 0for them, while for me the two forms still differ. Both readings are correct: an intervening reply resets a trailing run, so the demonstration above is verdict-based and therefore time-dependent. Their sentence for it is the right one: "a same-answer probe on one number cannot discriminate here."Here is the same defect shown by subject rather than by verdict, which cannot decay:
Two unrelated pull requests. Before this change both are reported as
#1060, and the bare invocation always reads the first.Blast radius, corrected
I wrote that the bare form "reads the same-numbered thread here (usually empty) and can only ever return safe". That over-claims, and @yixuan-ag2 bounded it correctly:
#1060falls in.rc=2and a 404. A loud refusal, not a wrong answer.So the fix matters for low numbers and is cosmetic for high ones. Still worth it — the verdict line naming
sonichi/sutando#1060is visibly wrong to anyone who meant the backend — but the correct claim is narrower than the one I opened with.