fix(ci): PR body check must ignore issue references inside code, and accept Refs - #126
Conversation
…accept Refs The closing-keyword check stripped HTML comments but not code, so a keyword inside a code span or fenced block satisfied it. GitHub creates no link from those, so the check could report that the board updates on merge when it will not. PR #124 hit this: its only Closes was inside backticks, in the sentence explaining why it used Refs instead, and the check passed while closingIssuesReferences stayed empty. Strips fenced blocks, tilde fences and inline spans into a prose variable used for the reference check only. Fences are stripped before inline spans, or the inline pattern eats a fence's delimiters and leaves its body behind as prose. The by:agent raw-output rule keeps reading the unstripped body, since it requires a fence to be present. Also accepts Refs #n, because a PR that completes no issue still has to name what it belongs to. A plan, one step of a larger task, or a docs correction should not have to claim it closes something to get a green check. Only Closes moves the board; the two are not interchangeable. AGENTS.md is amended to match, and its approval count corrected from one review to the two that are actually enforced. Closes #125 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
|
Sharpening one line in Not verified, which said the workflow had not yet run in CI. It has now, and it ran this branch's version — the run for the body check reports What that run does and does not prove:
A discriminating CI run is not reachable before merge. PR #124 is the body that separates old from new, but its head branch predates this fix, and a So: 17 local controls for behaviour, one CI run for executability, and no CI evidence of the behaviour change until this is on |
… testable Both reviewers blocked the previous attempt. Stripping code with regexes was incomplete: an unterminated fence, a double-backtick span, a quad fence wrapping a shorter run, and a four-space indented block each smuggled a keyword past it while GitHub created no link. The regex approach is an arms race against CommonMark, so it is abandoned for the closing question. GitHub already computes closingIssuesReferences, which is exactly what the check wants to know, so the workflow asks for it and passes the answer in. An unset answer reads as unknown rather than none: the script degrades to a text search and says on stdout that the board update is not verified, so a guessed result cannot be mistaken for a checked one. Refs still needs a text search, being our convention rather than GitHub's, so strip_code survives for that path only, where a false positive claims nothing about the board. It is now correct for all four shapes above: fences are matched by a line scanner honouring CommonMark's rule that a closing run must be at least as long as the opening one, which no regex backreference can express, and spans are run-length matched. The logic moves out of the YAML heredoc into launchpad/scripts/pr_body_check.py with 30 committed controls that import it. The suite it replaces tested a hand-copied replica and would have passed unchanged had the real check been reverted. These fail when strip_code is neutered to the identity, when the GitHub answer is ignored, and when parse_closing_refs always reports empty. The workflow now checks out the base commit, not the PR head, so a pull request cannot supply the checker that judges it. Closes #125 Closes #123 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…y trusted Checking out the base commit meant the checker did not exist on the PR that introduces it, so the check failed with a missing file. Which commit supplies the checker is a trust decision: for a same-repo PR GitHub already runs the workflow from the head, so head code executes either way and using the base adds no safety while preventing a change to the checker from testing itself. For a fork PR GitHub runs the base workflow, so fork code never executes today and must not start now. Same-repo PRs therefore use the head; forks are judged by the trunk's checker. A missing checker fails loudly rather than skipping, because an absent check is not a passing body. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
A reviewer noted that pointing at reviewDecision overclaims: it reports REVIEW_REQUIRED and exposes no count. The figure of two is stated only in GitHub's merge box on an open PR, which is the one place it is readable without admin:org. Names all three configuration endpoints that report nothing, so the next person does not rediscover them one at a time. Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…p a false security claim Both reviewers read the rewrite and blocked it. Every finding is applied. The checkout comment claimed that pinning the ref stopped a fork supplying the checker that judges it. That was wrong: pull_request runs the workflow from the merge ref of the pull request for forks too, so a fork already owns every step and can overwrite or skip the script whichever commit is checked out. Running the base workflow for forks is pull_request_target behaviour, which this does not use. The conditional ref bought nothing and stopped a change to the checker from testing itself, so it is gone. The comment now states the real boundary: a read-only token with no secrets, and two required human approvals. The four-space indent strip is removed. It deleted ordinary nested markdown, so a reference on a nested bullet or a wrapped continuation line vanished and the check rejected a compliant pull request for having no reference at all. A false block is worse than the false pass this file was written to fix. Since GitHub now answers the closing question, the only cost of not stripping indented code is an indented Refs being counted, which claims nothing about the board. Fence scanning gains three corrections: blockquote tolerance, so a quoted fenced block is recognised rather than leaking its contents as prose; a separate closing pattern, because a closer carries nothing but its run and trailing whitespace, and accepting junk after it ended blocks early while opening spurious new ones; and a fence-character match, since CommonMark does not allow a backtick fence to be closed by a tilde run. parse_closing_refs now rejects booleans, which bool subclassing int had let through as 1. The degraded note distinguishes a closing keyword from a Refs match instead of printing one line for both. Controls grow from 30 to 44. New ones drive main and capture stdout, because check returning the right note is not the same as a reader seeing it: suppressing notes on a passing degraded run defeated the whole purpose of the feature and no control noticed. Eleven mutations are now caught, including the four that survived the previous suite. Closes #125 Closes #123 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Review — recommend mergeThird-pass review, focused on the fixes the two earlier rounds asked for. I re-ran the suite Confirmed
Finding 1 — a blockquoted unterminated fence swallows the rest of the body (new false block)
The Cheap fix in the existing line scanner: when a fence is open, treat a line whose blockquote depth The converse leak — a quoted ``` closing an unquoted fence, so later lines survive as prose — is Finding 2 — nothing runs these 44 controlsThe stated reason for moving the logic out of YAML is that nothing could test it there. Nothing A path-filtered job would close it: - run: python3 -m unittest discover -s launchpad/scripts -t launchpad/scriptsThis is the finding I would most want recorded, because it decides whether the testability MinorThe On the two-issue scopeClosing #125 and #123 together is the right call — #123 is the same four lines of §6 that this Review by Claude Code ( |
|
Both findings from the review above are now filed, so nothing here is left only in a comment:
Both are on milestone M0 - Buzz MVP and labelled Recommendation is unchanged: merge. Neither finding warrants a third blocking round, and A merge was attempted and correctly refused — Comment by Claude Code ( |
tucktuck101
left a comment
There was a problem hiding this comment.
Approving on the strength of the third-pass review above (#126 (comment)). Both findings are filed as #145 and #146 on milestone M0 rather than blocking a third round: #145 is a narrow false-block that arrives with this branch, and #146 wires the 44 controls into CI so the mutation evidence outlives the merge.
Merging with admin privileges at @tucktuck101's direction, which bypasses the second approval that §6 — as rewritten by this very diff — requires. Recorded here rather than left to the audit log.
Review by Claude Code (claude-opus-5[1m]) on behalf of @tucktuck101.
…t disagrees STEP 4 of #116, built the way Serina chose rather than the way the plan prescribed. The plan said to reuse launchpad-pr-check.yml's body regex. That is the mechanism sibling PR #126 is replacing as bug #125, and it is wrong in a way this record must not repeat. Probed against four real pull requests before deciding: - fork #92 — the body carries a visible `Closes #n` and GitHub reports NOTHING, because the PR's base was not the default branch. Merging it closes no issue, and a regex says the board updates. - fork #86 — closes TWO issues. The plan's `re.search` reports one. A control now reproduces the single search and requires the record to hold more than it. - upstream block#5695 — the only keyword sits in an unfilled `<!-- Fixes block#1234 -->` placeholder. GitHub ignores it; so does the text half, which strips comments first. - fork #124 — `Refs #116`, correctly not a closing reference. So closingIssuesReferences decides `present` and the issue list, the body still supplies which keyword was written, and a disagreement between them is recorded instead of resolved silently — a disagreement is the shape of #125 and is signal a reviewer wants. An unreadable GitHub answer yields `present: null` — unknown, never false. "We could not ask" and "it closes nothing" differ on whether the board updates, and closing_refs is therefore skip-only: the run still exits 0 carrying the skip. closing_issue grew from three fields to six, added to the module's enumerated field list in the same commit as the plan requires. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…y resolves INTERFACE.md for #116, so #117, #118 and #119 have something to depend on that is not the source. The three rules a consumer can rely on: null means "not read" and never "nothing there"; every null field has a skip entry naming the endpoint that answered; and exit 2 leaves stdout empty so there is no half-record to mistake for a whole one. #120's CONTAINMENT.md names #116 in its contract table: must call fetch.fetch_all, must never concatenate surfaces or build a prompt. Both halves are addressed rather than assumed. The "must never" half holds today — every author-controlled surface is a separately labelled JSON string, nothing is concatenated, no prompt exists, and the AST controls keep the record module unable to reach a model at all. The "must call" half cannot hold yet: fetch.fetch_all is on an unmerged branch, importing it is impossible and copying it would create the second copy this work was told not to make. It resolves by rebase after #120 merges, and the injected runner is already the seam that makes adopting their fetcher an adapter rather than a rewrite. Also raised for the reviewer: #120 put its tree at launchpad/review-agent/ while this plan assumed launchpad/scripts/, where PR #126 is also adding a script. They should not stay split, and which way they converge is not this PR's call. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…t disagrees STEP 4 of #116, built the way Serina chose rather than the way the plan prescribed. The plan said to reuse launchpad-pr-check.yml's body regex. That is the mechanism sibling PR #126 is replacing as bug #125, and it is wrong in a way this record must not repeat. Probed against four real pull requests before deciding: - fork #92 — the body carries a visible `Closes #n` and GitHub reports NOTHING, because the PR's base was not the default branch. Merging it closes no issue, and a regex says the board updates. - fork #86 — closes TWO issues. The plan's `re.search` reports one. A control now reproduces the single search and requires the record to hold more than it. - upstream block#5695 — the only keyword sits in an unfilled `<!-- Fixes block#1234 -->` placeholder. GitHub ignores it; so does the text half, which strips comments first. - fork #124 — `Refs #116`, correctly not a closing reference. So closingIssuesReferences decides `present` and the issue list, the body still supplies which keyword was written, and a disagreement between them is recorded instead of resolved silently — a disagreement is the shape of #125 and is signal a reviewer wants. An unreadable GitHub answer yields `present: null` — unknown, never false. "We could not ask" and "it closes nothing" differ on whether the board updates, and closing_refs is therefore skip-only: the run still exits 0 carrying the skip. closing_issue grew from three fields to six, added to the module's enumerated field list in the same commit as the plan requires. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…y resolves INTERFACE.md for #116, so #117, #118 and #119 have something to depend on that is not the source. The three rules a consumer can rely on: null means "not read" and never "nothing there"; every null field has a skip entry naming the endpoint that answered; and exit 2 leaves stdout empty so there is no half-record to mistake for a whole one. #120's CONTAINMENT.md names #116 in its contract table: must call fetch.fetch_all, must never concatenate surfaces or build a prompt. Both halves are addressed rather than assumed. The "must never" half holds today — every author-controlled surface is a separately labelled JSON string, nothing is concatenated, no prompt exists, and the AST controls keep the record module unable to reach a model at all. The "must call" half cannot hold yet: fetch.fetch_all is on an unmerged branch, importing it is impossible and copying it would create the second copy this work was told not to make. It resolves by rebase after #120 merges, and the injected runner is already the seam that makes adopting their fetcher an adapter rather than a rewrite. Also raised for the reviewer: #120 put its tree at launchpad/review-agent/ while this plan assumed launchpad/scripts/, where PR #126 is also adding a script. They should not stay split, and which way they converge is not this PR's call. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Both High findings from the final branch review, on a branch now rebased onto launchpad. #116. HIGH — the record asked eight questions and none of them was the one readable question about the gate. launchpad/AGENTS.md §6 was rewritten by merged PR #126: the launchpad branch requires at least two approving reviews, the ruleset enforcing that is invisible without admin:org — three endpoints report nothing — and a live PR's reviewDecision confirms review IS required without exposing the count. This record published `configured: false` and never asked. On a branch that needs two approvals, that told #119 and #121 nothing gates it, with the evidence one call away: the governing property of this whole stage, broken by the stage itself. There are two gates, so the record now asks about both. `configured` answers "is a required STATUS CHECK visible"; `review_required` answers "is a REVIEW gate in force". The count is not inferred — reviewDecision does not carry it and §6's figure could drift. required_gate grew from two keys to five, added to the enumerated field list in the same commit. The stale §6 quotation in the docstring is replaced by what §6 says now. Two recorded fixtures, because the signal genuinely differs by base: PR 86 on launchpad answers REVIEW_REQUIRED, PR 92 on a topic branch answers "" — gh renders a GraphQL null as an empty string, so "" means "not required" and treating it as unrecognised would turn a readable answer into a malformed one. HIGH — the no-model AST check named two files and pr-preflight.py was not one of them, for three commits, while the suite reported the property as proved. An inference call in the entry point — the file the issue calls "the script" and the first one a reader opens — left all controls green. The files to check are now enumerated from the directory, so a new pre-flight module fails closed instead of going unchecked; the allowlist itself stays hand-written, because that is what makes an unanticipated import fail. Scoped to this stage's files: #126's pr_body_check.py shares the directory and its imports are not this suite's to police. MEDIUM, and the rebase is what made it real — the harness discovered all of launchpad/scripts, which now holds #126's suite. Plain discovery there runs 156 tests, not this stage's 112. A regression in a module this branch does not own would abort the harness and be reported as the pre-flight's; worse, a red foreign control would let every pre-flight mutant register as "killed" while proving nothing. SUITE and NO_MODEL_ONLY now name this stage's two test modules. Rebased onto launchpad: 0 behind, 15 ahead, all 15 DCO-signed. 112 own controls (156 with #126's), 24/24 mutants killed, 15/15 branches checked, 15/15 forbidden imports refused — the two new injections being the entry point ones that would have caught the second finding. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
…for them The final branch review left eight non-blocking findings. Six were one-line corrections inside files this PR is still adding, so they are fixed here: filing an issue to correct a docstring and then merging the wrong docstring is worse than fixing it. The seventh is #148, because it needs a real StatusContext response that no PR in either repository currently carries. The eighth cannot be fixed retroactively — the plan's GATES ordering was not held — and stays disclosed in the PR body. Two docstrings that misstated their own evidence: - preflight_core said PR 86's body writes one `Closes #79`. It writes two, on consecutive lines; the under-reporting belongs to the plan's `re.search`, which a control reproduces. As written it argued that the body-regex mechanism was unfixable, weakening a decision whose real support is PR 92 and #125. - fetch_all's own docstring said "seven reads" and was stale twice over — the module header and a control both say nine. Read.name was written at 24 sites and read at none, so a skip entry silently dropped which read failed: build_pr reports under "pr" when `meta` is what broke. Skip entries now carry `source`, added to the record's enumerated field list in the same commit, with a control asserting source != field on exactly that case. The mutation harness promised a byte-for-byte restore verification that only its first phase performed. Two phases were added later and neither verified, in a tool that rewrites tracked source. All three now restore through one helper that checks its own work. record-delete-fixture.sh compared by branch name — the exact recording error README.md and record.sh both name, which once produced an empty file list for a real six-file PR. It reads base.sha now. The committed fixture was always sound; the script's own call was dead output. The plan's OPEN section said #110 is open. #110 decided 45 minutes after that file was committed, and its comment names #116's workflow invocation among what it unblocks. The plan merges and a PR body does not, so the correction belongs in the file. 113 own controls (157 with #126's), 24/24 mutants killed, 15/15 branches checked, 15/15 forbidden imports refused. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Two High, five Medium, four Low. #116. HIGH — the check list was capped at first:100 and totalCount, the field that would say so, was fetched and read by nothing. A PR with more than a hundred contexts would have published the first page as the whole list, with no skip and exit 0, and required_gate.configured is computed from that list — so a required check past the boundary read as "no gate configured". This is the exact shape the truncated reason exists for on the trees API, with the detecting value already in hand. It now fails closed. One correction to the review that found it: its reproduction reported "totalCount says 120" against the real fixture. The fixture and the live API both report 47 contexts and totalCount 47, verified twice. The defect is real and latent, not observed — and a control now pins that PR 86 is genuinely un-truncated, so the guard cannot be mistaken for one that never had a case. HIGH — the unreadable-branch_rules return dropped the three review_* keys every other return spreads. A consumer written to INTERFACE.md's five-key contract got a KeyError whenever the rules probe 403s, while the run exited 0 — the one failure "null means not read" cannot express, because the key was absent rather than null. The control that was supposed to cover this asserted `configured` alone. MEDIUM — checks were unpinned to any commit while build_diff refuses to return without a head sha. The rollup is reached through commits(last:1), resolved server-side at its own moment, and its oid was fetched and dropped. A push between the two reads gave a diff for commit A and checks for commit B with nothing able to say so. They must now agree. MEDIUM — "enumerated from the directory" was a hand-written list wearing a filename prefix: a stage module not called preflight* escaped discovery, the allowlist and the no-model check entirely. Discovery is by exclusion now — a new file is ours until someone names it otherwise here, with a reason — and a control creates an unanticipated module to prove it fails closed. MEDIUM — the controls ran nowhere. Deferring CI to #119 was right for a live pre-flight run and wrong for the suite: it needs no token and no network, proved under `env -i` with no HOME and no GH_TOKEN, 165 tests green. #126 had already built the workflow it belongs in. The mutation harness runs there too, because 24/24 in a PR body is a claim and not a gate. MEDIUM — the PR body moved out of launchpad/plans/. Committing it there answered the plan's own reserved question ("whether plans belong in this repository at all is unresolved... one git rm either way") by accumulation, and put a second artefact kind in a directory launchpad/AGENTS.md §3 does not list. It sits beside the code it describes instead, and the decision stays the reviewer's. MEDIUM — the body also asserted the plan still calls #110 open, four paragraphs after saying that was fixed, and after it was. LOW — a stale test count in the harness comment; the plan's six "24 checks" claims, corrected in place the way its #110 line was, with the recorded 47 beside each; a contract table naming the read "review_decis"; and the branch-rules skip filed bare where its two siblings are dotted, so neither selector found all three. Three branch mutations added for the new guards, and one for the return that dropped the review half — the phase that catches a missing branch, which is how these would have been caught the first time. 165 controls, 24/24 mutants, 18/18 branches, 15/15 injected imports. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
The mechanical findings from the third final review. #116. HIGH — the previous commit added a CI job to .github/workflows and left four statements saying no workflow file is added, one of them in the plan's OPEN section. The worst was not the contradiction but which sentence went stale: the security rationale argued that `pull_request` is the right trigger BECAUSE no workflow is added here. That argument was gone and nothing replaced it, while the Not-verified list told a reviewer the branch touches no workflow — so they would skip the one file in the diff that executes on every PR. All four now describe what exists: a job that runs this stage's controls and its mutation harness, needing no credential, which is why #110 never gated it; invoking the pre-flight against a live PR still needs the scoped token and stays with #119. The trigger rationale is rewritten to argue why THIS job is safe — a read-only token, no secrets, no persisted credentials, two approving reviews to merge, and a defeated control suite that misleads a reviewer without being able to write anywhere. MEDIUM — the new job's checkout dropped the `persist-credentials: false` that the sibling job argues for at length, in the job that runs the harness: code that rewrites and executes repository source by design, from the fork's own commit. Both checkouts now set it. MEDIUM — the pasted evidence in the PR body no longer reproduced: 113 and 157 tests against 121 and 165, a 15-row branch listing against 18, and one paragraph claiming 13 injected imports where its own pasted block said 15. Re-pasted from a current run, including the `env -i` run that backs the no-credential claim. MEDIUM — INTERFACE.md's counts were stale again, one commit after 34d50e4 existed only to make them exact. 157 -> 165, 113 -> 121. LOW — the body described ">100 checks" as an unverified edge. It is now a hard refusal: exit 2, no record, a truncated skip. Said so, and stated plainly that pagination is unimplemented and untracked, so a reviewer can decide whether refusal is the permanent answer. Left alone deliberately, because they are judgement calls rather than mechanics: scoping the CI job with a paths filter so another task's new .py under launchpad/scripts does not fail this stage's allowlist; whether a raced commit-desync deserves its own SKIP reason instead of `malformed`; and whether launchpad/AGENTS.md §3 should list scripts/ and plans/, which is a governance file and inherited from #126 rather than introduced here. 121 own controls (165 with #126's), 24/24 mutants, 18/18 branches, 15/15 imports. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
8c2a0e9's message claimed four statements about the workflow were corrected and that the ">100 checks" note was rewritten. Its diff corrected three of the four and never touched the note. The fourth statement was in the plan's OPEN section — the one place the previous commit named explicitly as the reason the fix mattered, and the one it missed. Root cause, because it is the interesting part: those edits were applied with str.replace and no assertion. When the search text did not match — the OPEN sentence had been reworded by an earlier commit, and the checks bullet had been split in two — replace returned the string unchanged and the script reported success. A no-op edit read as a completed one, and the commit message asserted it. That is the same defect this whole branch exists to prevent, in the process that writes it rather than in the code it writes: an absence reading as a value. Every substitution here asserts its target is present first. Also corrected: the ">100 checks" bullet now states what the code does — refuse with exit 2 and a truncated skip rather than truncate — and says plainly that pagination is unimplemented and untracked, so a reviewer can decide whether refusal is the permanent answer. NOT fixed here, because it is not this branch's: launchpad/README.md says one approving review while launchpad/AGENTS.md §6 says two. The review sourcing that to this branch is mistaken — AGENTS.md is not in this diff. #126 changed §6 when it merged and left README behind. It is a real contradiction for a new contributor and belongs in its own issue against launchpad. Refs #116 Signed-off-by: Serina Mcfall <serina.mcfall@gmail.com>
Summary
The PR body check accepted a closing keyword written inside code, which GitHub ignores, so it
could report that the board updates on merge when it would not. The closing question is now
answered by GitHub rather than by a regex, and the check moves out of the workflow's YAML
heredoc into a script with committed tests that import it.
Related issue
Closes #125
Closes #123
Issue type
Bug
Agent provenance
Objective
Three files: a new
launchpad/scripts/pr_body_check.pyholding the check, its committed testsuite, and the workflow reduced to fetching GitHub's answer and invoking the script.
Impacted components
Approach and rejected alternatives
Rejected, after review: stripping code with regexes. The first attempt on this branch did
that, and both reviewers blocked it with executed evidence. Four ordinary markdown shapes
smuggled a keyword past it while GitHub created no link:
That is an arms race against CommonMark, and the check would have kept losing it.
Chosen: ask GitHub.
closingIssuesReferencesis precisely what the check wants to know —whether the board will update — and GitHub computes it authoritatively. The workflow queries
it and passes the answer in, so there is no markdown to misparse for the closing question.
Verified in CI on this PR: the run reports
CLOSING_REFS: [123,125]andreference: GitHub will close #123, #125 on merge.An unknown answer is not an empty one. If the query fails,
CLOSING_REFSis unset, and thescript treats that as unknown rather than none: it degrades to a text search and prints
that the board update is not verified, so a guessed result cannot be read as a checked one.
Rejected: failing the build on a GitHub hiccup, which punishes the author for an outage; and
silently passing, which is the fail-open this whole issue is about.
Refsstill needs a text search, being our convention rather than GitHub's, sostrip_codesurvives for that path only. Fences are matched by a line scanner, not aregex, because CommonMark requires a closing run at least as long as the opening one and
recannot express "backreference, or longer". Spans are run-length matched by backreference. It
handles the four shapes above, plus blockquoted fences, info strings, openers indented up to
three spaces, mismatched fence characters, and closers carrying trailing junk — every one of
which a reviewer demonstrated leaking.
Removed: the four-space indent strip, which was worse than the bug. It deleted any line
starting with four spaces, and CommonMark ties that rule to the container's content column,
so ordinary nested markdown was being erased:
GitHub renders both lines as prose. The check deleted the second one and rejected the PR for
having no issue reference — a false block, which is worse than the false pass this branch
exists to fix: the false pass merely failed to catch something, while this actively obstructed
an author who had done nothing wrong. Since GitHub now answers the closing question, the only
cost of not stripping indented code is an indented
Refsbeing counted, which claims nothingabout the board.
The logic left the YAML because nothing could test it there. The suite it replaces copied
the regexes by hand and would have passed unchanged had the real check been reverted or
replaced with
sys.exit(0)— a reviewer demonstrated exactly that. The 30 controls now importthe module the workflow runs.
Withdrawn: the claim that pinning the checkout protected anything. An earlier revision of
this branch pinned the ref so a fork could not supply the checker that judges it. A reviewer
showed that reasoning is wrong:
on: pull_requestruns the workflow from the PR's own mergeref for forks too, so a fork already owns every step in the file and can overwrite or skip the
script whichever commit is checked out. Running the base workflow for forks is
pull_request_targetbehaviour, which this workflow deliberately does not use. The conditionalref bought nothing and stopped a change to the checker from testing itself, so it is gone, and
the comment now states the real boundary instead of inventing one. A missing checker still
fails loudly; an absent check is not a passing body.
#123 is fixed in the same commit range rather than its own PR: it is a change to the same
four lines of §6. This PR therefore closes two issues, bending §6's one-issue-one-PR rule, at
@serina-mcfall's direction. A reviewer who disagrees should ask for the §6 lines to be split
out rather than accept the precedent silently.
Verification
Command run:
Raw output:
And the bug case, which must fail — a body whose only keyword is inside code:
The mutation block is the part that matters, and it is stated at the size it earns. Eleven
specific mutations were tried, including the four a reviewer found surviving the previous
30-control suite; all eleven now turn it red and restoring turns it green. That is not a claim
that no mutation survives — a suite cannot prove that — only that these eleven do not, and that
the four known holes are closed.
The controls that most needed adding drive
main()and capture stdout.check()returning theright note is not the same as a reader seeing it: a reviewer found that printing notes only
on failure left a passing degraded run silent about being unverified, defeating the feature's
entire purpose, and nothing in the suite noticed.
Not verified
now read this branch twice and blocked it twice; every finding from both passes is applied and
evidenced above, but nobody has re-read the result.
documented
pull_requestsemantics, not observation — including the corrected claim.main()andread its output, but I did not force a real GraphQL failure in a live run.
strip_coderemains best-effort and I make no claim it handles every CommonMark codeform. It handles the four shapes that defeated the first attempt and the five more a reviewer
found defeating the second. One known residual: a code span whose content wraps a soft line
break (
`Closes\n#9`) is not stripped. It guards onlyRefs, so the cost is bounded inboth directions now that the indent strip is gone.
Issue typeand provenance-table assertions still read the unstripped body, so theyremain satisfiable from inside code. Out of scope here and noted in bug: PR body check accepts a closing keyword inside code, which GitHub ignores — the board will not update #125.
launchpad-issue-check.ymlfor the same class of defect..samplefiles under.git/hooks/.Security implications
Two changes worth a reviewer's attention, in opposite directions.
Improved: a check satisfiable by text inside a code block is satisfiable by quoted
content, and PR bodies quote diffs, logs and other people's text routinely. Taking the closing
answer from GitHub removes that surface for the assertion that mattered. #120 covers the
general form of this concern for the review agent.
New surface, and the honest description of it. The workflow now checks out a commit and
executes a script from it, which the inline version did not. An earlier revision of this branch
claimed the checkout ref made that safe against forks. That claim was wrong and is
withdrawn —
on: pull_requestruns the workflow from the PR's own merge ref for forks too, soa fork owns every step here regardless of what is checked out, and no arrangement of
actions/checkoutchanges that.The real boundary, which is enough for a check of this kind: a fork-triggered run receives a
read-only
GITHUB_TOKENwith no repository secrets, and merging requires two approving reviewsfrom people with write access. A defeated body check misleads a reviewer; it cannot merge
anything or write anywhere.
persist-credentials: falseis set and the added permission ispull-requests: read.This check is not a security control and should not be cited as one. Its job is to catch an
honest mistake in a PR body. Anyone who wants a guarantee against a deliberately hostile PR
needs
pull_request_targetwith a hardened checkout, which is a different design decision thanthis bug fix.
Escalations
1. Two review rounds, twelve findings from the second, all applied — and the fixes are
themselves unreviewed. Round one blocked the regex approach; round two blocked the rewrite
that replaced it, and was right to. The most serious thing it caught was mine: I wrote a
security rationale into a workflow comment and into this PR body that does not hold for the
trigger in use. A wrong security claim in a file people read for guidance is worse than no
claim, and I would rather that be the headline of this escalation than the code fixes.
2. This PR closes two issues, per the Approach section. Raising it rather than letting it
pass unremarked.
3. The base-checkout bootstrap problem cost a red CI run, visible in this PR's history: the
first attempt pinned the checkout to the base commit, where the script does not yet exist, so
the check failed with a missing file. Fixed by the conditional ref. Recording it because the
same trap waits for any future PR that moves a checker into a script.
4. A bare
gh prcommand in these worktrees targetsblock/buzz, not this fork. Whilemarking #124 ready I ran
gh pr ready 124without--repoand it resolved to upstream,attempting to operate on their PR #124 — it failed only because that PR is closed. I have set
a default repo in the plan worktree; the other worktrees still have none, and
pr-gateforbids--repo, so the combination will keep pointing at upstream. Not filed yet; say the word.