docs(loop-lane): define the cross-lane PR hold — label-only, hold-then-explain (9.0.0) - #2512
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ac537e353
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 2m 41s —— View job Code review
Scope: Checks performed:
No correctness or maintainability issues found. The new clause is internally consistent with the rest of the convention (correctly distinguishes tracker-item escalation from PR holds, doesn't conflict with the existing "no lane creates labels" rule since it only applies a pre-existing label), and the factual claims about CI enforcement check out against the actual workflow file. |
…n-explain (9.0.0) Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
4ac537e to
ea33415
Compare
|
Claude has reviewed this PR 2 times. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
…n-explain (9.0.0) (#2512) ## Summary Adds a **Cross-lane PR hold** subsection to the loop-lane convention's §2 escalation contract, closing the gap the 2026-07-25 incident exposed: an escalating lane decided PR #1351 must not merge, drafted its explanation first, and applied `do-not-merge` ~30 minutes later — 3m20s **after** the merge. Three independent investigations on the issue converged on the same root cause: the server-side enforcement (org-ruleset-required `do-not-merge` status check, no bypass actors) worked correctly on both sides of the merge boundary; what no convention defined was *when to engage it*. Holding a PR was undifferentiated from escalating a tracker item, and the two acts fail differently — a PR hold carries a hard real-time deadline. The clause states three rules: - **`do-not-merge` is the only cross-lane hold; a PR comment is never one** (comments are advisory by construction — the same night, an explicit "do not merge as written" comment on #1326 held nothing). - **Hold first, explain second** — the label goes on the moment the hold is decided, before the escalation comment is drafted. - **Freshness re-read before any hold action** — a hold decision drafted against a stale snapshot can target an already-merged PR. Per the convention's own versioning rule, a change to the escalation contract is a major bump: `8.1.0` → `9.0.0`, CHANGELOG entry included. This implements the recommended remedy from the issue's decision-ready triage summary (Option A, parts 1 and 3 — the convention clause plus the retitle correcting the issue's falsified framing, done on the issue directly). Part 2 (a `babysit_merge.py` label backstop) is deliberately **not** included: the ruleset-required check already enforces the hold server-side with no bypass actors and demonstrably held during the incident; a wrapper-level duplicate narrows only the label-apply→check-report race, which the freshness-re-read rule addresses at the behavioral layer. If future evidence shows the ruleset gate insufficient, that is its own scoped change. ## Test plan - `npx markdownlint-cli2@0.23.2` on both edited files — 0 issues. - Docs-only change to a convention document; no scripts, hooks, or plugin behavior surfaces touched. CI's markdown/link gates cover the rest. ## Related Fixes #1409 --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
|
Claude finished @kyle-sexton's task in 37s —— View job Code review
Result: skipped (skip-gate condition 1 — PR is closed, not open). Per For reference, the merged PR already carries a completed review from this lane (posted 2026-08-12T19:51:50Z, no correctness/maintainability issues found) plus two Codex review comments on |
`verify-security-review-evidence.sh` runs under `set -euo pipefail` and
called its scope helper bare:
pr_touches_security_paths "$base_ref"
local in_scope=$?
The helper signals OUT-of-scope by RETURNING NON-ZERO. Under `set -e` a
bare call with a non-zero return aborts the shell immediately, so the
script died right there — exit 1, no message — and the "guard not
applicable" branch below it was unreachable dead code. Every out-of-scope
pull request FAILED the guard instead of being waved through.
Observed on run 31637054526 (PR #2512): the diff touches only
`docs/conventions/loop-lane/*.md`, the lane correctly SKIPPED, and
`security-review-evidence` failed beside it with a step log containing
nothing but `##[error]Process completed with exit code 1`. The empty log
is the signature — `set -e` aborts before any echo can run.
The obvious one-line fix (`|| in_scope=$?`) clears the red check but
leaves something worse, and ShellCheck says so via SC2310, which this
repo enables deliberately in `.shellcheckrc` ("catches the bash trap
where errexit silently stops working"). The helper runs `python3`, and a
crashing interpreter also exits 1 — so with out-of-scope encoded as
"returns 1", a BROKEN scope check is indistinguishable from a NEGATIVE
one, and the guard would skip itself and silently pass a pull request it
exists to check. Fail-open is the wrong direction for a security guard.
Separate the channels instead: the helper prints its verdict
(`in-scope` / `out-of-scope`) on stdout and reserves a non-zero exit for
a genuine fault. The caller consumes it through command substitution,
which keeps `set -e` live for the helper, and treats any unrecognised
verdict as a fault rather than a pass.
Verified: self-tests 6 pass / 0 fail, including a regression that models
the bare call in a separate `bash -c` process (a `( … )` subshell will
not do — bash suppresses `set -e` for the whole dynamic extent of a
command whose status is tested, so `$( … )` inside `[[ … ]]` makes the
bug unreproducible in the very harness meant to catch it), plus static
guards that fail if either older shape returns. `shellcheck --rcfile
.shellcheckrc` clean on both files, with SC2310 no longer firing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeEvWwrK8KdQTFb5n9CrUP
…PR (#2523) ## Summary `scripts/verify-security-review-evidence.sh` runs under `set -euo pipefail` and called its scope helper bare: ```bash pr_touches_security_paths "$base_ref" local in_scope=$? ``` The helper signals OUT-of-scope by **returning non-zero**. Under `set -e`, a bare call with a non-zero return aborts the shell immediately — so the script died on that line, exit 1, no message, and the `"diff does not touch security-relevant paths — guard not applicable"` branch below was unreachable dead code. Every out-of-scope pull request FAILED the guard instead of being waved through. Observed on [run 31637054526](https://github.com/melodic-software/claude-code-plugins/actions/runs/31637054526) (PR #2512): the diff touches only `docs/conventions/loop-lane/*.md`, `security-review / security-review` correctly **SKIPPED**, and `security-review-evidence` failed beside it with a step log containing nothing but `##[error]Process completed with exit code 1`. The empty log is the signature — `set -e` aborts before any `echo` can run. Reproduced standalone: ``` $ cat setE.sh set -euo pipefail f() { return 1; } main() { f "x"; local rc=$?; echo "REACHED with rc=$rc"; } main $ bash setE.sh; echo "exit=$?" exit=1 # "REACHED" never prints ``` ## Fix — and why not the one-liner The obvious fix is `pr_touches_security_paths "$base_ref" || in_scope=$?`. It clears the red check and introduces something worse, which **ShellCheck flags directly via SC2310** — a check this repo enables on purpose in `.shellcheckrc` ("catches the bash trap where errexit silently stops working inside if/while/&&/||"). The helper runs `python3`, and a crashing interpreter also exits 1. With out-of-scope encoded as "returns 1", a **broken** scope check is indistinguishable from a **negative** one — the guard would skip itself and silently pass a PR it exists to check. Fail-open is the wrong direction for a security guard. So the two channels are separated instead: the helper prints its verdict (`in-scope` / `out-of-scope`) on **stdout** and reserves a non-zero **exit** for a genuine fault. The caller consumes it via command substitution, which keeps `set -e` live for the helper, and treats any unrecognised verdict as a fault rather than a pass. ## Verification Run in this worktree — commands I actually ran, with real output: - `bash scripts/verify-security-review-evidence.sh.test.sh` -> **6 pass, 0 fail**, `All checks passed.` - `shellcheck --rcfile .shellcheckrc scripts/verify-security-review-evidence.sh scripts/verify-security-review-evidence.sh.test.sh` -> **clean**, with SC2310 no longer firing on the guard (it fired on the `||` variant, which is how the fail-open hole was found). New regression cases: - a non-zero-returning helper called bare under `set -e` kills the script — modelled in a **separate `bash -c` process** deliberately. A `( … )` subshell will not do: bash suppresses `set -e` for the whole dynamic extent of a command whose status is being tested, and `$( … )` inside `[[ … ]]` is exactly that context, which made the bug unreproducible in the very harness meant to catch it. My first attempt at this test failed for that reason, not because the fix was wrong. - static guards that fail if either older shape (bare call, or `||`-suppressed call) returns - a static guard that fails if the unrecognised-verdict catch-all is removed **Not verified here:** that the three currently-red PRs (#2512, #2504, #2499) go green. They will only pick this up once their branches carry it — the evidence workflow runs from the PR head. ## Related Third distinct defect found in this one file today, which is itself the finding: it infers another workflow's behaviour by reading that workflow's logs. #2517 (grep matched the lane action's echoed source) only became reachable once the `cursor[bot]` outage was fixed; this one only became reachable once #2517 let execution past the grep. No linked issue — filing was interrupted; the detail is captured here instead. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Summary
Adds a Cross-lane PR hold subsection to the loop-lane convention's §2 escalation contract, closing the gap the 2026-07-25 incident exposed: an escalating lane decided PR #1351 must not merge, drafted its explanation first, and applied
do-not-merge~30 minutes later — 3m20s after the merge. Three independent investigations on the issue converged on the same root cause: the server-side enforcement (org-ruleset-requireddo-not-mergestatus check, no bypass actors) worked correctly on both sides of the merge boundary; what no convention defined was when to engage it. Holding a PR was undifferentiated from escalating a tracker item, and the two acts fail differently — a PR hold carries a hard real-time deadline.The clause states three rules:
do-not-mergeis the only cross-lane hold; a PR comment is never one (comments are advisory by construction — the same night, an explicit "do not merge as written" comment on docs(review): reconcile stale code-review framing in quality-gate pr.md and README #1326 held nothing).Per the convention's own versioning rule, a change to the escalation contract is a major bump:
8.1.0→9.0.0, CHANGELOG entry included.This implements the recommended remedy from the issue's decision-ready triage summary (Option A, parts 1 and 3 — the convention clause plus the retitle correcting the issue's falsified framing, done on the issue directly). Part 2 (a
babysit_merge.pylabel backstop) is deliberately not included: the ruleset-required check already enforces the hold server-side with no bypass actors and demonstrably held during the incident; a wrapper-level duplicate narrows only the label-apply→check-report race, which the freshness-re-read rule addresses at the behavioral layer. If future evidence shows the ruleset gate insufficient, that is its own scoped change.Test plan
npx markdownlint-cli2@0.23.2on both edited files — 0 issues.Related
Fixes #1409