Skip to content

PR body gate (2) make-pr preflight runs the PR-description validator - #841

Merged
mergify[bot] merged 15 commits into
mainfrom
plan/pr-body-gate-2-make-pr-preflight-runs-the-pr-description-validator
Sep 24, 2026
Merged

mergify[bot] merged 15 commits into
mainfrom
plan/pr-body-gate-2-make-pr-preflight-runs-the-pr-description-validator

Conversation

@EdbertChan

@EdbertChan EdbertChan commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Before a PR goes up, a local check reads its description and says pass or fail. People trust that pass and publish.

The problem: that check only looked for claims about history. It never ran the format rules the required GitHub check runs.

So a dozen descriptions got a local pass, then failed the required check after publishing. Each one needed a manual fix.

The fix: the local check now also runs the same format checker. It fails when that checker fails, or when the checker cannot run at all.

Review Claim

The local pre-publish check now fails any PR description that the required description check on GitHub would reject, and it fails when that rule check cannot run.

Review Lane

behavior

Review Unit

engine-runtime

Safety Invariant

The change can only add a failure or an unchecked notice. A description that passes engine/skills/draft-pr/scripts/validate-pr-body.mjs today is never newly blocked: validate_body() returns the validator's own exit code when it is 0 or 1, and describe() returns status or schema_status, so a validator exit 0 adds no failure.

Slice Rationale

One script, one claim: the description step in engine/skills/make-pr/scripts/preflight.py also runs the schema validator. The one-line realpath change in scripts/ci/check_no_new_comments.py rides along. scripts/check_no_new_comments.py is a symlink to ci/check_no_new_comments.py; run through it, the origin/main version resolved the wrong repo root and exited 1 with ModuleNotFoundError: No module named 'detect'. With realpath it exits 0.

Non-goals

  • No change to the validator (engine/skills/draft-pr/scripts/validate-pr-body.mjs), scripts/pr/validate-pr-body-local.mjs, the hook, or the make-pr SKILL.md rules.
  • No Python copy of the validator's rules; preflight shells out so the two cannot drift.
  • No overlap with files open PR Turn drafter-core PR rules off unless CATSTACK_DRAFTER_CORE=1 #742 changes.

Test Plan

Test Plan
  • python3 -m unittest discover -s engine/skills/make-pr/tests -v — includes the new test_preflight.py cases: the cat-mode: fleet upkeep runs from one script #795 description fixture (tests/fixtures/pr795-failing-body.md, plan sections not collapsed, code names in Summary) makes preflight exit non-zero and print the validator's errors; a passing description still prints ok preflight passed; missing node, a timeout, or an unexpected exit code prints description unchecked: <reason> and fails.
  • python3 scripts/check_skill_test_coverage.py --base origin/main --head HEAD
  • python3 scripts/check_skill_file_refs.py
  • python3 scripts/check_no_new_comments.py
  • bash scripts/scrub-handoff-artifacts.sh

Revert Plan

Revert Plan
  • Safe to revert? Yes
  • Revert command: git revert <merge-sha>
  • Post-revert steps: None. Preflight goes back to checking only history claims, and the required PR Body check in CI still catches format failures after publication.
  • Data migration? No

🤖 Generated with Claude Code


Note

Low Risk
Developer-only preflight and CI helper path resolution; may block publish when Node or schema validation is unavailable, with no change to production runtime behavior.

Overview
make-pr preflight now runs the same Node PR-body schema validator (validate-pr-body.mjs) that the required GitHub check uses, in addition to the existing history-claims description_check. describe() fails if either step fails, and if the validator cannot run (no Node, missing script, timeout, or unexpected exit), preflight treats that as unchecked and fails rather than passing.

New validate_body() shells out to the validator with a 120s timeout and surfaces its stdout/stderr in preflight output. Tests add a #795-style failing body fixture, TestDescriptionSchemaValidator (validator errors, full preflight failure, pass path, and unchecked edge cases), and stub validate_body in the history-check unit test so responsibilities stay split.

scripts/ci/check_no_new_comments.py resolves its directory with os.path.realpath instead of abspath, so invoking the script via the scripts/check_no_new_comments.py symlink resolves the correct repo root and can import detect.

Reviewed by Cursor Bugbot for commit 9a9c913. Bugbot is set up for automated code reviews on this repo. Configure here.

EdbertChan and others added 14 commits September 24, 2026 00:58
description_check only read the description's prose for claims about the
repo's past, so preflight printed "ok preflight passed" for a body the
required PR Body check then rejected after publication (#780-#789, #793,
#795). describe() now also runs
engine/skills/draft-pr/scripts/validate-pr-body.mjs on the same file and
prints its errors under the gate line.

Exit 1 from the validator fails preflight. No node, a missing validator,
a timeout, or any other exit code prints "description unchecked: ..." and
fails too -- a check that could not run is not a pass.

The #795 description lands as a fixture: it exits 1 with Test Plan and
Revert Plan outside <details> and code names in the Summary, and the new
tests show preflight failing on it and still passing a valid body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ew claim: make-pr preflight runs the PR-description validator on --body-file and fails when it fails, so it no longer prints 'preflight passed' for a description the required PR Body check rejects.

Review lane: policy
Safety invariant: The change can only add a failure or an UNCHECKED notice; a PR description that passes engine/skills/draft-pr/scripts/validate-pr-body.mjs today is never newly blocked.
Effectiveness measurement: A preflight test feeds the original #795 description (Test Plan and Revert Plan not inside <details>, code names in Summary), which the validator rejects with exit 1, and asserts preflight exits non-zero and prints the validator's errors; a passing description still gives 'ok preflight passed'; a validator that cannot run gives an unchecked failure, not a pass.
Slice rationale: One script, one claim: preflight's description step also runs the schema validator.
Architectural effect: preflight's describe() step covers the same rules as the required PR Body check.
Goal: Stop preflight from approving a PR description the required check will reject.
Motivation: A reflect pass found PR descriptions on catstack PRs #780-#789, #793 and #795 failing the required PR Body check after publication, and the user had to ask for a manual fix of every PR. In catstack the guard stayed silent: fed `mergify stack push` and `gh pr create --body-file <failing body>` it exited 0 with no output, while the same stack push in the Invoker checkout fired. Read at origin/main: engine/skills/make-pr/scripts/preflight.py describe() only runs description_check, then main() prints 'ok preflight passed'; the session that opened #795 got that line and then ran gh pr create.
Alternative considerations: Leaving the validator to CI was set aside because the failure then shows up only after publication. Reimplementing the rules in Python was set aside because it would drift from the validator.
Implementation details: In engine/skills/make-pr/scripts/preflight.py describe(), after description_check, run `node engine/skills/draft-pr/scripts/validate-pr-body.mjs --body-file <file>` from the repo root with a timeout. Print its output lines indented like the other gates. Exit 1 from the validator fails preflight; a missing node binary, a timeout, or any other exit code prints 'description unchecked: <reason>' and fails preflight. Add the tests to engine/skills/make-pr/tests/test_preflight.py with the #795 body as a fixture file under engine/skills/make-pr/tests/fixtures/.
Non-goals: No change to the validator, the hook, or the make-pr SKILL.md rules. Do not edit engine/skills/draft-pr/scripts/validate-pr-body.mjs, scripts/pr/validate-pr-body-local.mjs, or any other file open PR #742 changes; call the validator as it is. If a change would overlap an open PR, stop and report instead.
Layer: domain
Feature state: active
Files:
- engine/skills/make-pr/scripts/preflight.py
- engine/skills/make-pr/tests/test_preflight.py
- engine/skills/make-pr/tests/fixtures/pr795-failing-body.md
Change types:
- engine/skills/make-pr/scripts/preflight.py: modify
- engine/skills/make-pr/tests/test_preflight.py: modify
- engine/skills/make-pr/tests/fixtures/pr795-failing-body.md: create
Acceptance criteria:
- `python3 -m unittest discover -s engine/skills/make-pr/tests -v` exits 0.
- `python3 scripts/check_skill_test_coverage.py --base origin/main --head HEAD` exits 0.
- `python3 scripts/check_skill_file_refs.py` exits 0.
- `python3 scripts/check_no_new_comments.py` exits 0.

Exit code: 0
…w claim: `python3 -m unittest discover -s engine/skills/make-pr/tests -v` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 -m unittest discover -s engine/skills/make-pr/tests -v`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 0
…w claim: `python3 scripts/check_no_new_comments.py` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_no_new_comments.py`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 1
…w claim: `python3 scripts/check_skill_test_coverage.py --base origin/main --head HEAD` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_skill_test_coverage.py --base origin/main --head HEAD`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 0
…w claim: `python3 scripts/check_skill_file_refs.py` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_skill_file_refs.py`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 0
…w claim: `python3 scripts/check_no_new_comments.py` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_no_new_comments.py`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 1
…w claim: `python3 scripts/check_no_new_comments.py` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_no_new_comments.py`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Solution:
  Review claim: `python3 scripts/check_no_new_comments.py` passes on the finished branch.
Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_no_new_comments.py`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.
…w claim: `python3 scripts/check_no_new_comments.py` passes on the finished branch.

Review lane: proof
Safety invariant: Verification is read-only and alters no repository file.
Effectiveness measurement: The command's exit code is the direct measurement.
Slice rationale: One check per proof task.
Architectural effect: None; verification only.
Goal: Prove the slice.
Motivation: Running the check is the proof.
Alternative considerations: The full suite was not required because the slice touches one component with its own tests.
Implementation details: Run `python3 scripts/check_no_new_comments.py`.
Non-goals: No mutations.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 0
…o ephemeral inter-task handoff files remain in the worktree before the merge gate.

Review lane: cleanup
Safety invariant: The scrub script only checks for known handoff artifact names and never touches source, tests, or other repository files.
Effectiveness measurement: The script exits non-zero if any handoff artifact remains.
Slice rationale: Required terminal scrub for every implementation workflow.
Architectural effect: None; hygiene only.
Goal: Leave the branch free of handoff artifacts.
Motivation: Handoff files must not reach the PR.
Alternative considerations: Manual cleanup was set aside as non-deterministic.
Implementation details: Run scripts/scrub-handoff-artifacts.sh.
Non-goals: No product edits.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

Exit code: 0
…c4e653a5-a4933bc3 — Review claim: No ephemeral inter-task handoff files remain in the worktree before the merge gate.

Review lane: cleanup
Safety invariant: The scrub script only checks for known handoff artifact names and never touches source, tests, or other repository files.
Effectiveness measurement: The script exits non-zero if any handoff artifact remains.
Slice rationale: Required terminal scrub for every implementation workflow.
Architectural effect: None; hygiene only.
Goal: Leave the branch free of handoff artifacts.
Motivation: Handoff files must not reach the PR.
Alternative considerations: Manual cleanup was set aside as non-deterministic.
Implementation details: Run scripts/scrub-handoff-artifacts.sh.
Non-goals: No product edits.
Layer: app_regression
Feature state: active
Layer exception: allowed. Verification and the terminal scrub run after the docs commit so they check the final branch the PR will carry.
Acceptance criteria:
- The command exits 0.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9a9c913. Configure here.

Comment thread engine/skills/make-pr/scripts/preflight.py Outdated
@mergify

mergify Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Queued — the merge queue status continues in this comment ↓.

@EdbertChan

Copy link
Copy Markdown
Owner Author

Mergify repair stopped: unresolved bot review thread PRRT_kwDOT3uYWs6lQXLR. The retry cap was reached for current head 9a9c913.

The required PR Body check passes --changed-files-file, which is how the
validator rejects changed file and folder names in the Summary and a
Review Unit that does not match the diff. Preflight passed only
--body-file, so those descriptions passed locally and failed after
publish. describe() now takes the changed paths preflight already
computed and writes them to a temp file for the validator.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Change-Id: Id36a64869b7cfb2b76b912fd3cf81a88a39dd344
@EdbertChan

Copy link
Copy Markdown
Owner Author

@Mergifyio queue

@mergify

mergify Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 28 minutes 47 seconds in the queue, including 27 minutes 45 seconds running CI.

Required conditions to merge
  • check-success = lint
  • check-success = test
  • check-success = validate

@mergify mergify Bot added the queued label Sep 24, 2026
@mergify
mergify Bot merged commit 4bccb0d into main Sep 24, 2026
4 checks passed
@mergify mergify Bot removed the queued label Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants