Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
ID: ISSUE-LOCAL-01M298Q7PPM145DQA5C30GRYJA
Title: agent-pr-body.py accepts a body whose Closes #N contradicts the canonical local record
Row: GATE-PR-BODY-TRAILERS
State: OPEN
Kind: bug
GitHub: -
Mirror: PENDING
Availability: FULL
Created: 2026-09-11
Updated: 2026-09-11
Closed: -

## Problem

The repository sets squash_merge_commit_message = PR_BODY, so a body's 'Closes #N' closes the GitHub mirror the moment the squash lands. Local files under .agents/issues are the issue authority. Nothing checked that the branch also carries ISSUE-GH-N.md reading State: CLOSED, so the two authorities diverge at the merge and the authoritative half is left reading OPEN. Observed on four pull requests in one pass: #3101 (Closes #3098 while the record read OPEN), #3095 (Closes #3092 while its own spec gate table read FAILING), #3096 (five closing keywords against four OPEN records and one issue with no local record at all), and #3097 (Closes #3093 against a spec saying the row is not ready to land). check-agent-record.py cannot catch it: it is offline and never sees a pull request body, and it silently continues on an unresolvable bare #N.

## Resolution

-
10 changes: 7 additions & 3 deletions .agents/specs/gate-pr-body-trailers.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# GATE-PR-BODY-TRAILERS: read the bytes the squash will land, in the operator's own shell

**Issue:** [#1263](https://github.com/mudler/vllm.cpp/issues/1263).
**Kind:** one new operator command plus the procedure that names it. No checker
rule changes and no checker is edited: `scripts/check-commit-trailers.py` is not
touched by this row.
**Kind:** one new operator command plus the procedure that names it, and (since
2026-09-11) one rule the command applies itself. `scripts/check-commit-trailers.py`
is still not touched by this row; the added rule lives in `scripts/agent-pr-body.py`,
which this row owns, and it reads the canonical local issue records rather than any
commit trailer.
**Row:** `GATE-PR-BODY-TRAILERS`.

## Now
Expand Down Expand Up @@ -219,6 +221,8 @@ reaches a network.
| `test_the_landing_procedure_names_the_command` | `AGENTS.md` and `.agents/workflow.md` name it, so the entry point cannot be deleted silently. |
| `test_the_suite_is_registered_where_gates_run` | A suite nothing runs is not a gate. |
| `test_the_spec_table_names_exactly_these_cases` | This table is compared with the loaded suite, not sampled, so it cannot go stale inside the change that writes it. |
| `test_closing_an_issue_with_no_local_record_is_refused` | A body's closing keyword closes the GitHub mirror the moment the squash lands, so the branch must also carry the local record reading `State: CLOSED`; three landed pull requests left the authoritative half saying OPEN. |
| `test_a_bare_reference_without_a_closing_keyword_is_accepted` | The gate is narrow deliberately. Open pull requests carry six to ten bare `#N` citations each, most without a local record, and failing those would fire the gate on ordinary work rather than on the defect. |

## Gates

Expand Down
47 changes: 46 additions & 1 deletion scripts/agent-pr-body.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,52 @@ def validate(body: str) -> int:
"UNVERIFIED: the trailer checker rendered no verdict "
f"(exit {result.returncode})"
)
return EXIT_CONTRACT if result.returncode else EXIT_OK
verdict = EXIT_CONTRACT if result.returncode else EXIT_OK
problems = closing_keyword_records(body)
for problem in problems:
print(f"ERROR: {problem}", file=sys.stderr)
return EXIT_CONTRACT if problems else verdict


CLOSING = re.compile(
r"\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#([1-9][0-9]*)\b", re.IGNORECASE
)


def closing_keyword_records(body: str) -> list[str]:
"""Every `Closes #N` in the body whose local issue record does not read CLOSED.

The body IS the squash message, so a closing keyword here closes the GitHub
mirror the moment it lands. Local files are the issue authority, so if the
branch does not also carry `ISSUE-GH-N.md` reading `State: CLOSED`, the two
authorities disagree from the instant of the merge and the local record --
the authoritative one -- is the half left saying OPEN.

Read from the working tree, so run this from the branch being merged; that
is the same tree whose records would land.
"""

problems: list[str] = []
for number in sorted({m.group(1) for m in CLOSING.finditer(body)}, key=int):
matches = sorted((ROOT / ".agents/issues").rglob(f"ISSUE-GH-{number}.md"))
if not matches:
problems.append(
f"body closes #{number} but no .agents/issues/**/ISSUE-GH-{number}.md "
"exists; file it (scripts/agent-issue.py import-github) or drop the keyword"
)
continue
text = matches[0].read_text(encoding="utf-8", errors="replace")
state = next(
(l.split(":", 1)[1].strip() for l in text.splitlines() if l.startswith("State:")),
"",
)
if state != "CLOSED":
problems.append(
f"body closes #{number} but {matches[0].relative_to(ROOT)} reads "
f"State: {state or '(absent)'}; close it locally first "
"(scripts/agent-issue.py close) or drop the keyword"
)
return problems


def pull_request_number(value: str) -> int:
Expand Down
34 changes: 34 additions & 0 deletions tests/scripts/test_agent_pr_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,40 @@ def gh_stub(self, *, stdout: str = "", stderr: str = "", code: int = 0) -> str:
)


class ClosingKeywordRecordTests(ToolHarness):
"""`Closes #N` in a body closes the GitHub mirror the moment the squash lands.

Local files are the issue authority, so the branch must also carry
`ISSUE-GH-N.md` reading `State: CLOSED`. Three landed pull requests
(#3092, #3098 and five on #3096) said `Closes` while their own local record
still read OPEN, which leaves the authoritative half saying OPEN forever.
"""

def test_closing_an_issue_with_no_local_record_is_refused(self) -> None:
text = body(FILLED).replace(
"fix(ROW): the subject line of a body that will be squashed\n",
"fix(ROW): the subject line of a body that will be squashed\n\nCloses #999999.\n",
)
result = self.run_tool("--body-file", self.body_file(text))
self.assertEqual(result.returncode, 1)
self.assertIn(f"ISSUE-GH-{''}999999.md", result.stderr)

def test_a_bare_reference_without_a_closing_keyword_is_accepted(self) -> None:
"""The gate is narrow ON PURPOSE: ordinary citations must not red a body.

Open pull requests carry six to ten bare `#N` citations each, most
without a local record. Failing those would fire the gate on ordinary
work, which AGENTS.md names as the defect rather than the discipline.
"""

text = body(FILLED).replace(
"fix(ROW): the subject line of a body that will be squashed\n",
"fix(ROW): the subject line of a body that will be squashed\n\nRefs #999999.\n",
)
result = self.run_tool("--body-file", self.body_file(text))
self.assertEqual(result.returncode, 0)


class OfflineContractTests(ToolHarness):
def test_the_exact_landed_malformed_value_is_refused(self) -> None:
"""The bytes of 281b4bc76c0e, refused by a command an operator can run."""
Expand Down
Loading