diff --git a/product/skills/admin-bypass-sweep/SKILL.md b/product/skills/admin-bypass-sweep/SKILL.md index becb9df3..bc6b6ec1 100644 --- a/product/skills/admin-bypass-sweep/SKILL.md +++ b/product/skills/admin-bypass-sweep/SKILL.md @@ -223,18 +223,48 @@ fate — merged or genuinely blocked — is decided. ## Step 6: Prove the final state Before reporting results, re-query every PR number touched — do not trust -running tallies kept during execution: +running tallies kept during execution. A merged tally may not be computed +from `gh pr view` state alone; GitHub's PR state is not proof that the PR's +merge commit reached the trunk. ```bash +verify="engine/hooks/gh-write-verification/verify_pr_landed_on_trunk.sh" +trunk="" # for example, master + for pr in ; do - gh pr view $pr --repo / --json number,state,mergedAt,title \ - --jq '"#\(.number)\t\(.state)\t\(.mergedAt // "-")\t\(.title)"' + title="$(gh pr view "$pr" --repo / --json number,title \ + --jq '"#\(.number)\t\(.title)"')" || title="#${pr}\t" + + verifier_status=0 + verifier_output="$("$verify" --repo <owner>/<repo> "$pr" "$trunk" 2>&1)" || verifier_status=$? + + case "$verifier_status" in + 0) + printf '%s\tmerged\t%s\n' "$title" "$verifier_output" + ;; + 1) + printf '%s\tmerged-but-not-on-trunk\t%s\n' "$title" "$verifier_output" + ;; + 3) + printf '%s\tunchecked\t%s\n' "$title" "$verifier_output" + ;; + *) + printf '%s\tunchecked\tverify_pr_landed_on_trunk.sh exited %s: %s\n' \ + "$title" "$verifier_status" "$verifier_output" + ;; + esac done ``` -Report merged count, blocked count, and the specific PR numbers/titles in -each bucket — a summary count alone hides which specific work is still -stuck. +Branch only on `engine/hooks/gh-write-verification/verify_pr_landed_on_trunk.sh` +exit codes: exit 0 is `merged`, exit 1 is `merged-but-not-on-trunk`, and +exit 3 is `unchecked`. Exit 3 is never reported as merged. Only exit 0 +increments the merged count; exit 1 and exit 3 must be counted in their own +buckets and named with their PR numbers/titles. + +Report merged count, merged-but-not-on-trunk count, unchecked count, blocked +count, and the specific PR numbers/titles in each bucket — a summary count +alone hides which specific work is still stuck. ## Why this exists diff --git a/product/skills/admin-bypass-sweep/tests/stack_branch_landing_fixture.md b/product/skills/admin-bypass-sweep/tests/stack_branch_landing_fixture.md new file mode 100644 index 00000000..78afb031 --- /dev/null +++ b/product/skills/admin-bypass-sweep/tests/stack_branch_landing_fixture.md @@ -0,0 +1,29 @@ +# Stack-branch landing fixture + +PR #1789 was reported by GitHub as merged after it landed against a stack +branch instead of the trunk. + +Old Step 6 input: + +```text +#1789 MERGED 2026-09-09T10:11:12Z Retarget child stack branch +``` + +Old Step 6 outcome: + +```text +merged +``` + +New Step 6 verifier result: + +```text +exit 1 +FAIL: PR #1789 in owner/repo reports MERGED but abc123 is not on origin/master +``` + +New Step 6 outcome: + +```text +merged-but-not-on-trunk +``` diff --git a/product/skills/admin-bypass-sweep/tests/test_step_6_verifier_tally.py b/product/skills/admin-bypass-sweep/tests/test_step_6_verifier_tally.py new file mode 100644 index 00000000..486b8409 --- /dev/null +++ b/product/skills/admin-bypass-sweep/tests/test_step_6_verifier_tally.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +SKILL_DIR = Path(__file__).resolve().parents[1] +SKILL = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") +FIXTURE = (Path(__file__).resolve().parent / "stack_branch_landing_fixture.md").read_text(encoding="utf-8") + + +def step_6() -> str: + match = re.search(r"^## Step 6:.*?(?=^## )", SKILL, re.M | re.S) + if match is None: + raise AssertionError("Step 6 section not found") + return match.group(0) + + +class TestStep6VerifierTally(unittest.TestCase): + def setUp(self) -> None: + self.step = step_6() + self.compact_step = " ".join(self.step.split()) + + def test_step_6_names_the_trunk_ancestry_verifier(self): + self.assertIn("engine/hooks/gh-write-verification/verify_pr_landed_on_trunk.sh", self.step) + + def test_step_6_maps_all_documented_status_codes(self): + for code, outcome in ( + ("exit 0", "merged"), + ("exit 1", "merged-but-not-on-trunk"), + ("exit 3", "unchecked"), + ): + self.assertIn(code, self.step) + self.assertIn(outcome, self.step) + + def test_gh_pr_view_state_alone_cannot_drive_the_merged_tally(self): + self.assertIn("A merged tally may not be computed from `gh pr view` state alone", self.compact_step) + self.assertNotIn("state,mergedAt", self.step) + self.assertIn("Only exit 0 increments the merged count", self.compact_step) + + def test_exit_3_is_never_merged(self): + self.assertIn("Exit 3 is never reported as merged", self.step) + + def test_stack_branch_fixture_is_no_longer_counted_as_merged(self): + self.assertIn("#1789\tMERGED", FIXTURE) + self.assertIn("Old Step 6 outcome:\n\n```text\nmerged\n```", FIXTURE) + self.assertIn("exit 1", FIXTURE) + self.assertIn("New Step 6 outcome:\n\n```text\nmerged-but-not-on-trunk\n```", FIXTURE) + + +if __name__ == "__main__": + unittest.main()