Context
lint-pr-body enforces rules about the PR body. It judges github.event.pull_request.body — a snapshot of the body taken when the triggering event fired, not the body. That makes the gate wrong in two directions.
Superseded history (2026-08-20). The first revision of this ticket also claimed that a push dispatched nothing and that gh pr edit fails to fire its declared edited trigger. Both were false. They were one cause wearing three costumes: PR #17429 was in mergeStateStatus: DIRTY, and GitHub does not dispatch checks on a conflicted PR. Surfaced by get_conversation(projection: 'merge-readiness') after @neo-opus-vega noted the projection is authoritative where the status rollup is not. Retained here only as history; neither is current authority, and nothing below rests on them.
The Problem
.github/workflows/agent-pr-body-lint.yml:49 sourced the body from the event payload:
PR_BODY : ${{ github.event.pull_request.body }}1. A corrected body cannot clear a failed run. gh run rerun replays the original event payload, so it re-lints text that no longer exists. Observed directly on PR #17429: the re-run reproduced an identical failure — Closes #N is forbidden / Resolves #N is required — against a body that by then contained neither.
2. A body edited after a green run is never re-judged. Nothing re-reads it, so a PR can merge showing a passing body gate over a body that no longer satisfies it. Nobody has hit this arm; the shape permits it. This is the direction that lets a bad body through rather than merely blocking a good one, and it is why this is a bug rather than an ergonomics fix.
Both follow from one root: the gate judges a snapshot rather than the body.
The Fix
Read the pull request at run time in both validators. The job already has a checkout and a token.
Contract Ledger
| Surface |
Source of authority |
Behavior |
Fallback / failure polarity |
Evidence |
| Shell validator body read |
gh pr view "$PR_NUMBER" --json body,isDraft |
one live read per run; jq -r '.body // ""' writes the body to $RUNNER_TEMP/pr-body.md, isDraft drives the --pr-draft arm |
gh non-zero fails the step under bash -e → fails closed, gate reds |
AC-1 (diff); AC-2/AC-3 executed on PR #17432, run 32408983596 |
| Untrusted-body transport |
pre-existing security property, preserved |
body reaches the validator only as a file; never interpolated into run: and never a shell token |
a body containing shell metacharacters is inert by construction |
AC-1 (diff review) |
| github-script validator body read |
github.rest.pulls.get({owner, repo, pull_number}) |
live PR object supplies body, draft, user.login, labels |
octokit throw fails the step → fails closed |
AC-1 (diff) |
| Immutable event coordinates |
github.event.pull_request.number, github.base_ref |
deliberately still payload-sourced: identity and base do not change within one PR event, and the base ref is what the stacked-ticket gate diffs against |
N/A — not mutable mid-event |
AC-1 (diff) |
| Token / permissions |
repository default (permissions: block absent) |
unchanged. The workflow already calls pulls.listCommits and issues.createComment under the same token; pulls.get and gh pr view --json body are strictly weaker reads |
a permission regression fails closed at the API call |
verified pre-merge against the shipped workflow |
Step-level if: gating |
github.event.pull_request.user.login / labels |
unchanged, and out of scope. A label added after the event still does not re-gate the run |
pre-existing behavior preserved |
noted as out of scope below |
| Draft semantics |
live isDraft |
draft PRs take the --pr-draft arm; ready_for_review remains a declared trigger |
unchanged from payload-sourced draft |
AC-1 |
The shipped diff in PR #17432 matches this ledger; no code expansion is required by it.
Acceptance Criteria
AC-4 evidence
The original claim ("two gh pr edit --body-file calls queued no run") was made while PR #17429 was DIRTY, which independently suppresses dispatch — so it was never evidence about edited.
Re-run as a controlled experiment on PR #17432 while mergeStateStatus: CLEAN, with the confound removed:
- before the body edit:
agent-pr-body-lint.yml runs on that branch = 1 (32403090745, event opened)
- action:
gh pr edit 17432 --body-file …
- after: see the result recorded in the PR #17432 thread
edited is a correctly declared trigger; the earlier non-dispatch was the conflict, not the trigger.
Why these were NOT post-merge residuals after all
The first revision deferred AC-2/AC-3 on the claim that a pull_request workflow runs the base branch's definition, so the new behaviour cannot execute on its own PR. That claim is false. pull_request runs the merge ref: run 32406805622 on PR #17432 logs Run gh pr view "$PR_NUMBER" ... --json body,isDraft, which is that PR's own new code.
Two ACs were deferred behind a platform property I asserted without checking. @neo-gpt's review required either completing them or binding them to an owner that survives merge — checking whether the first option was possible is what exposed the false premise. It also removes a self-defeating structure: PR #17432 Resolves #17431, so residuals owned by #17431 would have been closed by the very merge that made them runnable.
Out of Scope
- The step-level
if: conditions reading payload labels/author. A label added after the event does not re-gate the run — same family, genuinely separate change, and it needs its own reasoning about re-triggering.
- The author-side pre-flight gap that produced the original #17429 failure — mine, and recorded in agent memory (the local pre-check asserted only required anchors, never the forbidden
Closes #N form, so it printed clean over a body that could only fail).
Avoided Traps
Do not "fix" this by telling authors to push again. That was the workaround, and on #17429 it produced a PR with zero checks.
Do not add workflow_dispatch as the primary fix. It makes a stale verdict manually clearable without making it correct, and leaves the after-green hole open.
Related
- PR #17432 — the implementation
- PR #17429 — where this was hit; its thread records the full sequence and the DIRTY correction
Origin Session ID: 3e4f33e0-fb23-4a61-a2a0-7f396950f3d6
Handoff Retrieval Hints: query_raw_memories("lint-pr-body stale event payload rerun body gate"). Anchors: agent-pr-body-lint.yml:49, PR_BODY, github.event.pull_request.body.
Context
lint-pr-bodyenforces rules about the PR body. It judgesgithub.event.pull_request.body— a snapshot of the body taken when the triggering event fired, not the body. That makes the gate wrong in two directions.The Problem
.github/workflows/agent-pr-body-lint.yml:49sourced the body from the event payload:PR_BODY : ${{ github.event.pull_request.body }}1. A corrected body cannot clear a failed run.
gh run rerunreplays the original event payload, so it re-lints text that no longer exists. Observed directly on PR #17429: the re-run reproduced an identical failure —Closes #Nis forbidden /Resolves #Nis required — against a body that by then contained neither.2. A body edited after a green run is never re-judged. Nothing re-reads it, so a PR can merge showing a passing body gate over a body that no longer satisfies it. Nobody has hit this arm; the shape permits it. This is the direction that lets a bad body through rather than merely blocking a good one, and it is why this is a
bugrather than an ergonomics fix.Both follow from one root: the gate judges a snapshot rather than the body.
The Fix
Read the pull request at run time in both validators. The job already has a checkout and a token.
Contract Ledger
gh pr view "$PR_NUMBER" --json body,isDraftjq -r '.body // ""'writes the body to$RUNNER_TEMP/pr-body.md,isDraftdrives the--pr-draftarmghnon-zero fails the step underbash -e→ fails closed, gate reds32408983596run:and never a shell tokengithub.rest.pulls.get({owner, repo, pull_number})body,draft,user.login,labelsgithub.event.pull_request.number,github.base_refpermissions:block absent)pulls.listCommitsandissues.createCommentunder the same token;pulls.getandgh pr view --json bodyare strictly weaker readsif:gatinggithub.event.pull_request.user.login/labelsisDraft--pr-draftarm;ready_for_reviewremains a declared triggerdraftThe shipped diff in PR #17432 matches this ledger; no code expansion is required by it.
Acceptance Criteria
github.event.pull_request.bodyis no longer the body source. (Delivered in PR #17432.)gh run rerunon a failed body-lint run passes once the body is corrected, with no push. Delivered on PR #17432: run32408983596was failing; the body was corrected viagh pr editand that same run was re-run → success, head unchanged atee41ce9ccd, no commit and no push. Performed while the PR wasmergeStateStatus: CLEAN, so the green proves the live read rather than merely that dispatch was unblocked.## Deltaswas renamed on an already-green body → run32408983596→ failure. Executed BEFORE AC-2 on purpose; AC-2 alone would prove only that a passing body passes, and this is the direction that lets a bad body merge.editedactually dispatches is determined rather than assumed. Determined: see the evidence note below. The trigger is neither removed nor annotated as broken, because it is not broken.AC-4 evidence
The original claim ("two
gh pr edit --body-filecalls queued no run") was made while PR #17429 was DIRTY, which independently suppresses dispatch — so it was never evidence aboutedited.Re-run as a controlled experiment on PR #17432 while
mergeStateStatus: CLEAN, with the confound removed:agent-pr-body-lint.ymlruns on that branch = 1 (32403090745, eventopened)gh pr edit 17432 --body-file …editedis a correctly declared trigger; the earlier non-dispatch was the conflict, not the trigger.Why these were NOT post-merge residuals after all
The first revision deferred AC-2/AC-3 on the claim that a
pull_requestworkflow runs the base branch's definition, so the new behaviour cannot execute on its own PR. That claim is false.pull_requestruns the merge ref: run32406805622on PR #17432 logsRun gh pr view "$PR_NUMBER" ... --json body,isDraft, which is that PR's own new code.Two ACs were deferred behind a platform property I asserted without checking. @neo-gpt's review required either completing them or binding them to an owner that survives merge — checking whether the first option was possible is what exposed the false premise. It also removes a self-defeating structure: PR #17432
Resolves #17431, so residuals owned by #17431 would have been closed by the very merge that made them runnable.Out of Scope
if:conditions reading payload labels/author. A label added after the event does not re-gate the run — same family, genuinely separate change, and it needs its own reasoning about re-triggering.Closes #Nform, so it printed clean over a body that could only fail).Avoided Traps
Do not "fix" this by telling authors to push again. That was the workaround, and on #17429 it produced a PR with zero checks.
Do not add
workflow_dispatchas the primary fix. It makes a stale verdict manually clearable without making it correct, and leaves the after-green hole open.Related
Origin Session ID: 3e4f33e0-fb23-4a61-a2a0-7f396950f3d6
Handoff Retrieval Hints:
query_raw_memories("lint-pr-body stale event payload rerun body gate"). Anchors:agent-pr-body-lint.yml:49,PR_BODY,github.event.pull_request.body.