LearnNewsExamplesServices
Frontmatter
id17431
titleThe PR body gate judges a payload snapshot, so a corrected body cannot produce a green verdict — and an edited one keeps a stale green
stateClosed
labels
bugai
assigneesneo-opus-grace
createdAtAug 20, 2026, 8:16 PM
updatedAtAug 20, 2026, 9:57 PM
githubUrlhttps://github.com/neomjs/neo/issues/17431
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 20, 2026, 9:57 PM

The PR body gate judges a payload snapshot, so a corrected body cannot produce a green verdict — and an edited one keeps a stale green

neo-opus-grace
neo-opus-grace commented on Aug 20, 2026, 8:16 PM

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 -efails 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-1 — Both validators read the PR at run time; github.event.pull_request.body is no longer the body source. (Delivered in PR #17432.)
  • AC-2gh run rerun on a failed body-lint run passes once the body is corrected, with no push. Delivered on PR #17432: run 32408983596 was failing; the body was corrected via gh pr edit and that same run was re-run → success, head unchanged at ee41ce9ccd, no commit and no push. Performed while the PR was mergeStateStatus: CLEAN, so the green proves the live read rather than merely that dispatch was unblocked.
  • AC-3 — The inverse: a body edited to REMOVE a required anchor after a green run goes red. Delivered on PR #17432: ## Deltas was renamed on an already-green body → run 32408983596failure. 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.
  • AC-4 — Whether edited actually 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-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.

tobiu referenced in commit b75e5d0 - "fix(ci): the PR body gate reads the live body, not the event payload (#17431) (#17432) on Aug 20, 2026, 9:57 PM
tobiu closed this issue on Aug 20, 2026, 9:57 PM