Skip to content

fix(pr-merge): wrap the refusal comment's reasons to 100 columns - #11103

Merged
kyle-sexton merged 3 commits into
mainfrom
fix/refusal-comment-wrap
Oct 30, 2026
Merged

kyle-sexton merged 3 commits into
mainfrom
fix/refusal-comment-wrap

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

No related issue: enable-auto-merge batch item 4 (refusal comment wrap)

Summary

enable-auto-merge's standing refusal comment (refusalBody) put each reason on one fenced line.
GitHub scrolls a code-block line sideways instead of wrapping it, and open lanes App pull requests
carry reason lines of up to 347 columns. Reasons now go through the quote() helper from #10968:
each line wraps at a space to 100 columns, continuation lines are indented two spaces, and more
than 20 lines fold into a <details> block.

The parts that tools read stay byte-identical: the escalation and babysit-merge-risk marker
lines, the line naming the head (prose wraps when GitHub renders it), the whole-review record
lines, and the last-line unarmed-since and partial marks.

Fix

  • quote() and wrapLine() move from pr-unblock/verify-notes.mjs into
    pr-unblock/address-feedback.mjs, which enable-auto-merge already imports. quote() gains a
    <details> summary parameter whose default keeps verify-notes' text, so its comments do not
    change.

  • fenced() trims each reason line and collapses runs of whitespace to one space. A wrap at a
    space then joins back exactly.

  • New refusalReasons(body, person, head, label, {since, partial, records}) finds the fence by
    its markers, which also works inside <details>. It tries two formats:

    • the wrapped format: it joins continuation lines, then round-trips the result through the new
      refusalBody;
    • the old one-line format: it round-trips the raw lines through the old renderer.

    Anything else returns null, so a comment it cannot read keeps its label.

  • The readers:

    Reader Old-format comment Wrapped comment
    personReasons and holdPathOnly (through it) old-format branch wrapped branch
    nativeRefusal in clear-escalations.mjs old-format branch wrapped branch; replaces slice(5, -1)
    laneCause unchanged, reads the reason nativeRefusal returns gets the whole joined reason
    refusalWrites a prior comment equal to the old render of the same reason counts as unchanged equal to the new render, as before
    prMergeEscalations untouched; the head line did not move untouched
    ci-status jq (pr-require-checks.yml) untouched untouched; it trims each line and joins with spaces

    Two reader changes need calling out:

    • Before this PR, a wrapped person code after "; " (";\n security-finding") read as a
      lane cause in laneCause and cleared the label. The new test shows it.
    • Without the refusalWrites change, an old-format person refusal at an unchanged head whose
      label a person removed would be posted again and relabeled.
  • The readers that look only at marker lines or the last line are unaffected: sinceAt,
    isPartial, recordLines, priorRefusal, liftBlocker and foreignMarker (the new body adds
    no <!-- line), and the Python _self_written and babysit_whole_review.records.

Verification

All results are local runs.

  • node --test .github/scripts/pr-merge/enable-auto-merge.test.mjs: 186 pass.
  • node --test .github/scripts/pr-merge/clear-escalations.test.mjs: 105 pass.
  • node --test .github/scripts/pr-unblock/verify-notes.test.mjs: 24 pass, unchanged by the move.
  • bash scripts/ci-hold-human-gated.test.sh: exit 0; shellcheck is clean.
  • Tests run against main's sources:
    • enable-auto-merge: 10 fail, covering the new wrap, round-trip and unchanged-refusal tests and
      the fixtures that now expect wrapped bodies;
    • clear-escalations: 1 fails (the wrapped security-finding case cleared the label).
  • With the old-format branches removed from this PR's code:
    • enable-auto-merge: 6 fail, including the existing tests that keep an old-format prior
      comment;
    • clear-escalations: 1 fails.
  • scripts/check-changelog-fragments.sh --check and --check-required origin/main: pass.

Related

🤖 Generated with Claude Code

kyle-sexton and others added 3 commits October 29, 2026 21:00
enable-auto-merge's refusal comment put each reason on one fenced line,
which GitHub scrolls sideways (up to 347 columns measured). Reasons now
go through address-feedback's quote(): wrapped at a space to 100
columns, continuations indented two spaces, more than 20 lines folded
into <details>. The marker lines and the head line are unchanged.

refusalReasons() joins wrapped lines back and also accepts a comment
posted in the old one-line format; personReasons, holdPathOnly and
clear-escalations' nativeRefusal read through it, and refusalWrites
treats an old-format comment with the same reason as unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 30, 2026 01:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T01:28:30.086301Z 430c3a5 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 430c3a5c233b0047f8e7fe7db4ee39a5295cf456: 0 finding(s), 0 as inline comments.

No security issues found in #11103 at 430c3a5.

critical-findings: 0

I read the whole diff. I read the surrounding code in enable-auto-merge.mjs for holdPathOnly, approvalLift and refusalWrites. The PR touches the merge-hold trust controls enable-auto-merge.mjs and clear-escalations.mjs, so I checked each for a weakened hold. None of the checks below found one.

  • nativeRefusal in clear-escalations.mjs:
    • It still requires an exact round trip: refusalBody(true, reason, head, label) with since null, partial false and no records must equal the stored body byte for byte. This is the same check as before, moved into refusalReasons.
    • The caller's other gates are untouched: lanes-App author, unedited comment, READER_HOLDS prefix.
    • The old-format branch accepts only bodies the old renderer produced exactly. A person or PR author cannot make it accept anything it did not accept before.
  • Decoding is unambiguous:
    • fenced() trims every reason line and collapses whitespace. No true reason line can start with two spaces.
    • A line starting with two spaces is therefore always a wrap continuation. The decoded reasons are exactly the ones the lane rendered, so HOLD_PATH_LINE, pathOnlyRisk and READER_HOLDS see the same reasons as before.
    • A PR-controlled filename containing a newline gives the same result as before the change. The per-line HOLD_PATH_LINE test still fails on it.
  • The laneCause behavior change tightens a hold: a person code after a wrapped "; " (for example security-finding) now keeps the label instead of reading as a lane cause. The new test covers this case.
  • approvalLift keeps its independent checks: the hold-path-only lift still needs the file-listing check (pathHoldKind), the cost-hold check, and a trusted lifter whose approval binds the head. The wrap change does not touch any of these.
  • refusalWrites: the added prior.body === unwrapped branch only suppresses a repost when the prior comment is the old-format render of the same reason, head, label and records. It keeps the label state logic as it was. It cannot suppress an escalation for a changed reason.
  • quote() and wrapLine() move to address-feedback.mjs:
    • The code is unchanged, and the new summary parameter defaults to the old text.
    • It still replaces control characters and neutralizes triple backticks, so quoted text stays inert in the fence.
    • Reason text only reaches the <details> summary as a line count, not as content.
  • Out of scope for this diff: pr-require-checks.yml, pr-merge.yml and the Python modules are not changed. Changes to the manifests, the token broker, the kill switch and the trusted-actor list are absent. The PR context carries no embedded instructions.
  • Cost: none. The change spends no money and adds no billed resource.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed 430c3a5c233b0047f8e7fe7db4ee39a5295cf456: 0 finding(s), 0 as inline comments.

No findings. I read the full diff and the surrounding enable-auto-merge code; I did not run any tests. The test counts in the PR description are the author's claims, unverified by me.

Checked by reading:

  • refusalReasons rebuilds the comment with refusalBody (wrapped format) or the old renderer and returns the reasons only on a byte-identical match, so a lookalike comment returns null.
  • fenced() collapses whitespace, so a wrap at a single space joins back exactly.
  • refusalWrites treats an old-format comment equal to the old render as unchanged, so a person-removed label stays off.
  • nativeRefusal in clear-escalations.mjs now reads the whole joined reason, so a person's code after a wrapped "; " keeps the label.
  • The jq in pr-require-checks.yml (not in this diff) finds the fence by content and trims and joins lines, so it still reads wrapped and <details>-folded reasons. The new cases in scripts/ci-hold-human-gated.test.sh cover both.
  • Marker lines and the line naming the head stay out of the wrapped fence.
  • The quote()/wrapLine() move keeps verify-notes' output unchanged, since the new summary parameter defaults to its old text.

No cost or critical-security hold applies.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 04b04f4 Oct 30, 2026
79 checks passed
@kyle-sexton
kyle-sexton deleted the fix/refusal-comment-wrap branch October 30, 2026 01:53
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