Repository navigation
feat: implement issue #1995 — info_status_pattern covers only SonarCloud — Codex/Qodo/CodeRabbit usage-limit notices are still undispositionable and still strand PRs - #2000
Conversation
…oud — Codex/Qodo/CodeRabbit usage-limit notices are still undispositionable and still strand PRs
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Dev-Lead — waiting on PR blockers (intent: review-changes)PR: #2000 |
|
Note @don-petry I reviewed this PR and no code changes were needed, but I can't mark it done yet: required checks |
|
No description provided. |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-01T20:14:39Z. |
There was a problem hiding this comment.
Code Review
This pull request introduces generalized info-status patterns for service notices (such as usage-limit, trial-ended, and review-limit messages) from Codex, CodeRabbit, and Qodo to prevent these non-finding comments from blocking the maintainer-comment gate. It also adds comprehensive unit tests to verify that service notices are auto-cleared while actual review findings continue to block. The review feedback suggests tightening the regex patterns in reviewer-sources.tsv to prevent overly broad matching, and updating the BATS assertions to check for specific non-zero exit codes instead of using generic non-zero checks to avoid false positives on unexpected test failures.
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
Acknowledged: this is Qodo's "trial has ended" service notice — it carries no review finding, so no code action is needed. This PR (#1995) teaches the maintainer-comment gate to auto-clear exactly this notice via the |
|
Acknowledged: this is Codex's "You have reached your Codex usage limits for code reviews" out-of-quota notice — no review finding, so no code action is needed. This PR (#1995) teaches the maintainer-comment gate to auto-clear this notice via the |
|
Acknowledged: this is CodeRabbit's auto-generated rate-limit/summary notice — no actionable review finding, so no code action is needed. This PR (#1995) teaches the maintainer-comment gate to auto-clear CodeRabbit's "Review limit reached … prepaid credits" notice via its info_status_pattern in scripts/lib/reviewer-sources.tsv. |
|
Acknowledged: SonarCloud reports Quality Gate Passed — a clean status re-posted on every push with no finding, so no action is needed. The |
Dev-Lead — fix-reviews (applied)Changes committed and pushed. Requested items addressed:
|
|
Automated review paused — review engines are unavailable. Every configured review engine (Claude → Copilot → Gemini) reported a usage/rate limit or was otherwise unavailable, so no review could be generated for this run. This is a single, deduplicated notice: the agent will re-review automatically on its next scheduled run once capacity recovers, and will not post repeat notices in the meantime. |
Resolved — the `needs-human-review` hold was lifted; dev-lead has picked this item up. Click to expand the prior hold notice.dev-lead is withholding action on this item. It is labeled To re-enable automated pickup: remove the |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-01T20:25:41Z. |
Superseded by automated re-review at
|
Superseded by automated re-review at
|
…dy start Reconciles this PR with #1998 (#1993), which pins the same Codex notice. The unanchored substring would clear any Codex comment that merely quotes the notice further down, including one carrying a real finding. Anchoring on ^ keeps both observed forms of the notice (short "usage limit." and long "usage limits for code reviews") while a quoted occurrence still blocks. Adds a negative test to the registry suite and to the gate suite; both fail without the anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-01T21:35:33Z. |
Dev-Lead — fix-bot-comment (no-changes)Agent reasoning |
…otices only Two review findings on #2000, plus a regression I introduced in 6f05785. 1. pr-review finding 1 (MEDIUM, blocking). maintainer-resolve-comment.sh authorized the registered-bot path by login only. Now that Codex, CodeRabbit and Qodo are registered, that let a maintainer minimize any of their FINDINGS as RESOLVED. The bot path now also requires the comment body to match the author's info_status_pattern (new pure helper mrc_bot_body_matches, the same case-sensitive jq test() the gate uses). The snapshot query reads `body`. A finding, or SonarCloud's "Quality Gate failed", is refused with exit 3. 2. CodeRabbit security review (moderate). A matching notice prefix cleared the whole comment, including finding text after it. Each pattern now matches the bot's COMPLETE known notice body, anchored at both ends (^ ... \s*$), with the observed optional parts (dashboard/settings links, billing link, the second sentence). 3. Regression. The start anchor in 6f05785 rejected Qodo's REAL trial notice, which reads "<!-- qodo:billing-blocked -->\n\n**ⓘ Qodo reviews are paused ...**". The pattern now allows the bold and ⓘ wrapper. Tests: the real Codex and Qodo bodies posted on these PRs clear; each notice followed by a finding blocks; the bot-path refusals. The new resolve-comment tests fail without the script change. The advisory_gate projection is still 5 rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R
|
Addressed in 1b86c65:
The advisory projection is still 5 rows. Generated by Claude Code |
's branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
cubic P3: the shim's JSON used \\n, so the body decoded to a literal backslash-n rather than a newline, unlike a real GitHub response. The SonarCloud pass and fail fixtures now use \n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R
|
CI checks on this PR are still running. The PR-review sweep re-reviews this PR automatically once the checks complete — no action is needed. Posted by the donpetry-bot PR-review cascade. |
|
Advisory bots were rate-limited; auto-approval is withheld until they recover. pr-review-sweep will re-review this PR after 2026-10-01T22:12:27Z. |
…t-codex-connector "usage limits" notices so they stop blocking the maintainer gate and spawning dev-lead runs (#1998) * feat: implement issue #1993 — Stop the Codex noise: auto-clear chatgpt-codex-connector "usage limits" notices so they stop blocking the maintainer gate and spawning dev-lead runs * fix(reviews): address review comments [skip ci-relay] * feat: implement issue #1995 — info_status_pattern covers only SonarCloud — Codex/Qodo/CodeRabbit usage-limit notices are still undispositionable and still strand PRs * fix(reviews): address review comments [skip ci-relay] * fix(reviews): address review comments [skip ci-relay] * fix(reviewer-sources): anchor the Codex usage-limit pattern to the body start Reconciles this PR with #1998 (#1993), which pins the same Codex notice. The unanchored substring would clear any Codex comment that merely quotes the notice further down, including one carrying a real finding. Anchoring on ^ keeps both observed forms of the notice (short "usage limit." and long "usage limits for code reviews") while a quoted occurrence still blocks. Adds a negative test to the registry suite and to the gate suite; both fail without the anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix(reviewer-sources): end-anchor the Codex notice pattern; negatives reuse each pattern's literals cubic (PRRT_kwDOR9SdIs6oGf77): the negative cases shared no tokens with their patterns, so loosening an anchor would still pass. The Codex pattern also matched review prose that opens with the notice's words and then carries a finding. - Codex pattern is now ^You have reached your Codex usage limits?( for code reviews)?\. — it matches both observed notice forms and nothing that continues the sentence. - Each registry test gains a negative built from its own pattern's literals (Codex, CodeRabbit, Qodo). The new Codex negative matches under the previous pattern and fails the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix(dev-lead-intent): warn when the info-status classifier cannot read the registry cubic (PRRT_kwDOR9SdIs6oGWHh): matches_info_status_pattern swallowed registry read failures. With TRUSTED_BOTS preset the top-level registry bootstrap never runs, so an unreadable registry silently routed every Codex usage-limit notice back to a full fix-bot-comment engine run, which is the spend #1993 removes. The classifier still fails closed (routes normally) but now emits a ::warning:: on stderr when the helper is missing, the registry cannot be read, or the pattern fails to evaluate, matching how the rest of the script reports registry problems. New intent test covers the unreadable-registry path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix(reviewer-sources): start-anchor the CodeRabbit and Qodo notice patterns graphite (both rows): the patterns were unanchored substrings, so any review body that merely mentions the notice text mid-body was auto-cleared. - Both patterns are now anchored to the body start, allowing leading HTML marker comments (^\s*(<!--[^>]*-->\s*)*). Qodo prefixes its real trial notice with <!-- qodo:billing-blocked -->, which a bare ^ would reject. - The Qodo trial sentence is also end-anchored (\.), like the Codex one. - Registry and gate tests: graphite's mid-body examples now block, and the marker-prefixed Qodo notice still clears. The CodeRabbit negatives fail without the anchor. - The column docs now spell out the anchoring convention. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix(info-status): bot path checks the body; patterns match complete notices only Two review findings on #2000, plus a regression I introduced in 6f05785. 1. pr-review finding 1 (MEDIUM, blocking). maintainer-resolve-comment.sh authorized the registered-bot path by login only. Now that Codex, CodeRabbit and Qodo are registered, that let a maintainer minimize any of their FINDINGS as RESOLVED. The bot path now also requires the comment body to match the author's info_status_pattern (new pure helper mrc_bot_body_matches, the same case-sensitive jq test() the gate uses). The snapshot query reads `body`. A finding, or SonarCloud's "Quality Gate failed", is refused with exit 3. 2. CodeRabbit security review (moderate). A matching notice prefix cleared the whole comment, including finding text after it. Each pattern now matches the bot's COMPLETE known notice body, anchored at both ends (^ ... \s*$), with the observed optional parts (dashboard/settings links, billing link, the second sentence). 3. Regression. The start anchor in 6f05785 rejected Qodo's REAL trial notice, which reads "<!-- qodo:billing-blocked -->\n\n**ⓘ Qodo reviews are paused ...**". The pattern now allows the bold and ⓘ wrapper. Tests: the real Codex and Qodo bodies posted on these PRs clear; each notice followed by a finding blocks; the bot-path refusals. The new resolve-comment tests fail without the script change. The advisory_gate projection is still 5 rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R --------- Co-authored-by: don-petry <{}+don-petry@users.noreply.github.com> Co-authored-by: donpetry-bot <{}+donpetry-bot@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Resolve test conflicts: keep this branch's single-escaped SonarCloud fixture bodies (the shim's printf expands \n), and keep main's two case-sensitivity / foreign-author Codex negative tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R
Dev-Lead — fix-bot-comment (no-changes)Agent reasoning |
|
Superseded by automated re-review at
|
Dev-Lead — fix-bot-comment (no-changes)Agent reasoning |
donpetry-bot
left a comment
There was a problem hiding this comment.
Automated review — APPROVED ✓
Risk: LOW
Reviewed commit: 2c77e41d28d7c9fdd5c2675ba36bb490404c6c26
Review mode: triage-approved (single reviewer)
Summary
After main absorbed the #1995 implementation (it landed with #1998's squash), this PR's net diff is just a 2-line test-fixture change in tests/dev-lead/unit/test_maintainer_resolve_comment.bats. The fake gh bodies now use JSON \n escapes instead of \\n, so the parsed comment body has real newlines like a real SonarCloud comment instead of a literal backslash-n. Test-only change, LOW risk. The triage-approved assessment holds.
Linked issue analysis
Closes #1995. Every acceptance criterion is met on main, which this branch is merged up to:
scripts/lib/reviewer-sources.tsvdeclaresinfo_status_patternentries forchatgpt-codex-connector,coderabbitaiandqodo-code-review.- Each pattern is anchored at both ends (
^ … \s*$) on the complete notice text. tests/test_reviewer_sources.batsandtest_maintainer_comment_gate.batscover positive and negative cases.--reasonis still mandatory on the bot path.- The registry header documents the rule FOR NO-FINDING SERVICE NOTICES ONLY (#1995).
mrc_bot_body_matches also stops a registered bot's finding from being minimized on the bot path. With Closes #1995, the issue will close correctly once this merges.
Findings
No blocking findings.
- The fixture change is correct. Inside the shim's JSON string,
\ndecodes to a newline, while\\ndecoded to a literal backslash-n, which did not represent a real comment body. The SonarCloud pattern uses[\s\S]*, so the 'passed' case still matches. The 'Quality Gate failed' case is still refused (exit 3, no writes). - Non-blocking: the PR title and body still describe the full feature, although the remaining diff is only a test fix. That is a cosmetic mismatch and does not need to change before merging.
- No unresolved review threads. The bot notices (Qodo trial-ended, Codex usage-limit, CodeRabbit) are no-finding service notices that have already been acknowledged.
- MCP secret scan:
run_secret_scanningis not available in this run. gitleaks passed.
CI status
All required checks pass: bats, unit-tests, shellcheck/Lint, CodeQL, SonarCloud, gitleaks, actionlint, AgentShield, and the stub/standards guards. The dev-lead / dispatch|ci-relay|resume runs were CANCELLED; those are automation jobs superseded by newer runs, not code checks. The dependency-audit ecosystem jobs were SKIPPED because the ecosystems don't apply to this repo.
Reviewed automatically by the PR-review agent (single-reviewer mode: opus 5.5 [opus 4.8, opus 4.7]). Reply if you need a human review.
donpetry-bot
left a comment
There was a problem hiding this comment.
Automated review — APPROVED ✓
Risk: LOW
Reviewed commit: 2c77e41d28d7c9fdd5c2675ba36bb490404c6c26
Review mode: triage-approved (single reviewer)
Summary
Confirmation review at 2c77e41. Since #1998's squash, main already holds the #1995 implementation. This PR's net diff is now a 2-line test-fixture fix in tests/dev-lead/unit/test_maintainer_resolve_comment.bats: the fake gh bodies use '\n' instead of '\n'. The shim JSON then decodes to real newlines, as in a real SonarCloud comment. The change is test-only and LOW risk. The triage-approved assessment holds.
Linked issue analysis
Closes #1995, which is already CLOSED. Its implementation reached main through #1998 (2d65b55). scripts/lib/reviewer-sources.tsv carries anchored info_status_pattern rows for chatgpt-codex-connector, coderabbitai and qodo-code-review, plus the registry doc block. The keyword on an already-closed issue is harmless. This PR is the remaining test-fixture follow-up.
Findings
No blocking findings.
- The fixture fix is correct. The SHIM heredoc is unquoted, so
$bodyis substituted verbatim and backslashes in the expanded value are not reprocessed. A JSON\nnow decodes to a real newline. The old\\ndecoded to a literal backslash-n. - Both affected tests still hold. The SonarCloud 'passed' fixture still matches its pattern. The 'Quality Gate failed' case is still refused with exit 3 and no writes.
- Prior cycle findings are all resolved on main. The bot path requiring a body match through
mrc_bot_body_matchesand the anchored Codex, CodeRabbit and Qodo patterns are both in place. No new issues were found. - Review threads: there are no unresolved threads. The Qodo, Codex and CodeRabbit comments are no-finding service notices, and each has already been acknowledged.
- Non-blocking: the PR title and body still describe the full feature, but the remaining diff is only a test fix. This is cosmetic.
- Secret scan: the run_secret_scanning MCP tool is not available in this run. gitleaks passed.
- Local bats run: I could not run bats locally because it is not installed on this runner. CI
batsandunitboth passed.
CI status
All code and quality checks are green: bats, unit, unit-tests, shellcheck/ShellCheck/Lint, CodeQL, SonarCloud (Quality Gate passed), gitleaks, actionlint, AgentShield and the stub/standards guards. The dev-lead dispatch, ci-relay and resume runs show CANCELLED or PENDING, and pr-auto-review / check-and-dispatch is QUEUED. These are automation orchestration runs, not code checks. The dependency-audit ecosystem jobs were SKIPPED because those ecosystems don't apply to this repo.
Reviewed automatically by the PR-review agent (single-reviewer mode: opus 5.5 [opus 4.8, opus 4.7]). Reply if you need a human review.
… section (#2008 AC4) rl_scope returned "" for any CodeRabbit summary that carried an architecture_review section, so a summary with both a security review and the `rate limited by coderabbit.ai` block was never detected as rate-limited. pr-review then never withheld for it or scheduled the retry, and the throttled code review was silently lost. That is the owner's AC4 review item at 8429b59 and pr-review's cycle-1 finding. rl_scope now returns the rate-limited block whenever it is present, with or without a security section, and "" otherwise. The security section is still evidence in its own right: its findings are held by the maintainer gate and dispositioned through reviewer_sources_finding_section_pattern, which this scope does not touch. Tests: PR #2000's fixture (throttled code review + security finding) is now detected as rate-limited (fails on the previous script), and a security section that only mentions a rate limit is not a notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R
…ew is throttled separately from its code review — findings in both sections must be addressed, and an in-place edit after disposition must re-open the comment (#2009) * feat: implement issue #2008 — CodeRabbit's Security Architecture Review is throttled separately from its code review — findings in both sections must be addressed, and an in-place edit after disposition must re-open the comment * chore: dev-lead update (review-changes) [skip ci-relay] * fix(reviews): address review comments [skip ci-relay] * fix(reviews): address review comments [skip ci-relay] * test(advisory-gate): raise line budget 600→615 for #2008 edit-time merge The producer-path lastEditedAt merge in get_advisory_bot_states (50bdc85) took scripts/lib/advisory-review-gate.sh to 603 lines, failing "Advisory gate: script is minimal" in the unit job. Raise the budget with the same rationale-comment convention as earlier raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix(reviews): address review comments [skip ci-relay] * fix(dev-lead-fix-reviews): converge superseded dispositions before skipping stale candidates (#2008) When the stale gate skips a candidate (disposition predates the last edit or timestamp is unreadable), the superseded disposition replies were not converged to OUTDATED. This allowed a comment edited across multiple passes to accumulate duplicate disposition replies. Now the convergence loop runs before the skip, ensuring exactly one authorized disposition is visible even when the candidate cannot yet be minimized. Fixes cubic P3 finding about operational-hygiene gap in dev-lead-fix-reviews.sh. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * fix: count only trusted-author run markers in the stale-disposition dedup (#2008) stale_disposition_needs_dispatch suppressed the #2008 edit re-dispatch when any comment carried a success-shaped `<!-- dev-lead-fix-reviews pr=<N> ... intent=fix-reviews status=... -->` marker posted after the edit, whoever wrote it. An outside commenter could paste one and keep a stale disposition from being re-checked. Only markers from OWNER/MEMBER/COLLABORATOR authors (as dev-lead's own markers are) now count. Addresses CodeRabbit's Security Architecture finding on the #2009 summary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix: honor an active rate-limit hold in the retry sweep, including the #2008 edit re-dispatch - scan_pr_for_rate_limits read each marker's reset= and check= with Python-style named groups `(?P<r>...)`. jq's Oniguruma rejects that syntax ("undefined group option"), the `|| true` swallowed the error, and the reset came back empty, so is_reset_in_future never held a PR: every cron cycle re-dispatched a still-rate-limited fix-ci/fix-reviews pass. Use `(?<r>...)`. - The #2008 stale-edit re-dispatch ran whenever nothing else was dispatched, including while a hold was still active, spending a pass that would hit the same limit. Track an active hold and skip the edit re-dispatch until it resets; once it resets, the retry dispatch covers the edit in one run. (cubic P1 on dev-lead-retry.sh:456.) - Tests: scan-level cases for an active and an expired hold; the active case fails on the previous script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix: count a pass from when it started in the #2008 stale-edit dedup (cubic P1) stale_disposition_needs_dispatch suppressed the edit re-dispatch when a fix-reviews success marker was POSTED at/after the bot edit. A pass that started before the edit and finished after it posted such a marker without ever reading the edited body, so the re-dispatch was skipped and the stale disposition stayed until some later edit. - dev-lead-fix-reviews.sh stamps every terminal marker with read_at=, the time the pass started (PASS_STARTED_AT, a lower bound on when it read the PR's comments; validated as an ISO timestamp). - The dedup compares read_at= against the edit, and falls back to the marker's createdAt only for legacy markers without the stamp. - Tests: a pass that started before the edit but finished after it no longer suppresses the dispatch (fails on the previous scripts); the marker carries read_at=; and the COLLABORATOR trust case (cubic P3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R * fix: detect a throttled CodeRabbit code review even beside a security section (#2008 AC4) rl_scope returned "" for any CodeRabbit summary that carried an architecture_review section, so a summary with both a security review and the `rate limited by coderabbit.ai` block was never detected as rate-limited. pr-review then never withheld for it or scheduled the retry, and the throttled code review was silently lost. That is the owner's AC4 review item at 8429b59 and pr-review's cycle-1 finding. rl_scope now returns the rate-limited block whenever it is present, with or without a security section, and "" otherwise. The security section is still evidence in its own right: its findings are held by the maintainer gate and dispositioned through reviewer_sources_finding_section_pattern, which this scope does not touch. Tests: PR #2000's fixture (throttled code review + security finding) is now detected as rate-limited (fails on the previous script), and a security section that only mentions a rate limit is not a notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fdDZDb4mhT65MU1izeA4R --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: donpetry-bot <{}+donpetry-bot@users.noreply.github.com>



Problem
info_status_pattern covers only SonarCloud — Codex/Qodo/CodeRabbit usage-limit notices are still undispositionable and still strand PRs
From the issue: This is the generalization that #1918's review flagged ("do not special-case SonarCloud, or the next reviewer added to the council reopens it").
chatgpt-codex-connectoris that next case, and it is live now.Risk
Low — changes automation shell logic under scripts/, covered by shellcheck (--severity=warning) and the bats suite.
Test plan
Tests added/updated:
tests/dev-lead/unit/test_maintainer_comment_gate.bats,tests/test_reviewer_sources.bats. Verification:bash scripts/dev-lead-lint.sh(shellcheck --severity=warning) ran pre-commit; the bats suite runs in CI.Rollback
Revert this PR. No non-revertible side effects (no tags, migrations, or external state).
Monitoring
This PR's Lint (shellcheck) and bats checks show pass/fail; watch subsequent dev-lead / pr-review runs for behavioral regressions.
Closes #1995