Skip to content

feat(release): T-5 reconcile guards — check_pr_closes_issue.sh + check_reconcile.sh (APR-RELEASE-001 §6, kaizen) - #3200

Merged
noahgift merged 3 commits into
mainfrom
PMAT-1098-t5-reconcile
Sep 13, 2026
Merged

feat(release): T-5 reconcile guards — check_pr_closes_issue.sh + check_reconcile.sh (APR-RELEASE-001 §6, kaizen)#3200
noahgift merged 3 commits into
mainfrom
PMAT-1098-t5-reconcile

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

APR-RELEASE-001 §6 T-5 Reconcile is a hard gate of every train (operator ruling 2026-09-13, kaizen). Measured on the 0.67.0 train: 308 open issues, 424 opened vs 124 closed in 30 d, 48 merged PRs of which 9 carried a closing reference, 880 remote branches of which 847 had no PR. Filing was the work product; closing was nobody's.

Two guards, each with a must-RED/must-GREEN self-test (mutation-checked: neutering the fail path turns the self-test red):

  • scripts/check_pr_closes_issue.sh — a PR body that cites #N must use Closes/Fixes/Resolves, or carry no-close: <reason>. 9-case table.
  • scripts/check_reconcile.sh <tag> --prev-tag <prev> --json OUT — the five predicates R1 fixed-but-open, R2 missing closing refs, R3 PR-less branches older than 14 d, R4 DIRTY PRs older than a train, R5 closure/arrival; exit 1 unless R1..R4 are 0, exit 2 on env. 17-case offline self-test via RECONCILE_INPUT_DIR.

First real receipt (v0.66.0→v0.67.0, after archiving 776 dead branches to refs/archive/): R1=0 R2=28 R3=0 R4=1 R5=22/61=0.36. Wiring R2 into ci.yml and check_reconcile.sh into the autopilot's close step follows in the next PR.

no-close: the spec amendment lives on PR #3164; this PR adds the guards only.

🤖 Generated with Claude Code

…eck_reconcile.sh

APR-RELEASE-001 section 6: R-2 (PR body must close what it cites, or say
why not via no-close:) and the five T-5 reconcile predicates (R1..R5),
each factored into file-driven functions so --self-test never touches
the network. Both self-tests carry must-RED/must-GREEN twins per
predicate (mutation proof).

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@noahgift
noahgift enabled auto-merge September 13, 2026 08:09
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3200 head=7b452797f33729ec75adf5defc9f0d0fd6d432ff verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

…very cargo-free guard with no args

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_roadmap_diff_additive.sh reported base id(s) missing at head: this branch forked before they landed on main, and a plain update takes one side of the file whole. Resolved as main's roadmap plus this branch's own new entries, each at its sorted slot among same-prefix peers. The other three roadmap guards pass on the result.

Pmat-Ticket: PMAT-1098

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@noahgift

Copy link
Copy Markdown
Contributor Author

Ran this guard against the real tree before it merged — check_reconcile.sh v0.67.0 --prev-tag v0.66.0 — and recorded the result as a T-5 ledger receipt in #3212. The
self-test is green (17 cases) and the live path works. Three findings about the
predicates, for whoever picks them up:

R2 needs an epoch. check_pr_closes_issue.sh is not on main yet, so every one of the
35 PRs in v0.66.0..v0.67.0 merged before any such check existed — R2 = 35 of them, and
it cannot ever reach zero for that window. Worse, most of those citations are correct:
#3160 and #3175 filed their follow-ups (#3147#3159, #3170#3174) and cite them
without closing, which is what a backlog-opening PR should do. The no-close: line is
exactly the remedy for that shape, and those bodies could not carry it. Counting only PRs
merged after the guard was armed on main would make R2 a ratchet instead of a permanent
red.

R3 conflates dead with stranded. It named feat/y2-calibrate and
feat/y1-7bgarbage. Both carry unique unmerged work. The first holds evidence/perf-060/
— 791 insertions and the only measurement of mini's throughput that exists, which matters
now that mini is a full-time build host. Deleting it to zero the counter would have
destroyed it; it is landed as #3211 instead and R3 fell 2 → 1. Suggest splitting the
predicate: no open PR and no unique commits = dead; no open PR with unique commits =
stranded.

R4 is volatile against main. It read 0 at 11:44Z and 1 at 11:50Z, because #3164,
#3162 and #3201 merged as one queue group in between and made #3041 dirty. Against a
moving base it reports the queue's timing rather than the tree's health.

None of this blocks this PR — the guard measures what it says it measures. It is what the
first real run found.

Merged via the queue into main with commit cf44694 Sep 13, 2026
18 of 19 checks passed
@noahgift
noahgift deleted the PMAT-1098-t5-reconcile branch September 13, 2026 13:15
noahgift added a commit that referenced this pull request Sep 13, 2026
…he table was not hermetic once it started resolving

TWO MORE, found by measuring the first change instead of trusting it.

1. A REFERENCE TO A CLOSED ISSUE OWES NO REASON. "left open forever" is
   this guard's whole purpose, so demanding a `no-close:` line for an issue
   that is already closed is the same false positive as demanding one for a
   sibling PR. Of the references still flagged after PR refs were dropped,
   #2706, #338 and #532 were all already closed; the reason a body would
   have had to write is "it is closed", which the API already knows.

   Someone else closing it still counts, and that is not a loophole: the
   outcome R-2 wants is a closed issue, not a particular author closing it.
   What it cannot do is pass on an OPEN one — mutation M5 (drop `issue`
   too) turns six rows red, including the three that shipped with the guard.

2. THE SELF-TEST STOPPED BEING HERMETIC THE MOMENT THE GUARD RESOLVED
   ANYTHING. The rows cite "#123" and "#1", and once ref_kind existed those
   were no longer fixtures — they became live API calls against real issues
   in this repository. Both are closed here, so `refs-only` and
   `noclose-empty`, two must-RED rows, started PASSING for a reason that
   had nothing to do with what they test. Caught because they went red, not
   because anyone reasoned about it.

   Every row is pinned to the stub now: no network, no token,
   deterministic. 9001 a PR, 9002 an open issue, 9004 closed, 9003
   unresolvable, everything else an issue.

AND ONE ROW THAT IS NOT THE SEAM. Sixteen rows pinned to a stub prove the
DECISION and say nothing about the RESOLVER. `gh-error-fails-closed`
shadows `gh` with one that exits 1 — a runner with no token, a rate limit,
or no network — and requires the body to still FAIL. Mutation M8 (resolver
error answers `closed`) kills it; without that row, the only way this
change could make the guard laxer than it was would be invisible.

`gh` is shadowed rather than removed from PATH because it lives in
/usr/bin beside the grep and sed this script needs: an empty PATH tests
nothing but exit 127, which is what the first attempt measured.

MEASURED EFFECT on the 31 open PRs: 8 passing before any of this, 13 after
PR refs were dropped, 16 now. The 15 that still fail every one cite a
genuinely OPEN issue with no keyword and no reason — which is the debt §6
R-2 exists to surface, not noise to tune away.

Self-test 16/16, all hermetic.

Refs #3200
Pmat-Ticket: PMAT-1098
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noahgift added a commit that referenced this pull request Sep 13, 2026
…aller further out

guard-tree caught it on this PR's own branch:

    FAIL R2-positive: expected [10], got []
    self-test FAILED: 1 case(s).

check_reconcile.sh's R2 row delegates to check_pr_closes_issue.sh, and its
fixture body is `Refs #5`. Once that script began resolving references, #5
stopped being a fixture: it is a real, already-CLOSED issue in this
repository, so the new rule dropped it and R2 correctly reported no
offenders — while the table still expected [10].

This is the same defect I fixed inside check_pr_closes_issue.sh's own
table in the previous commit, one caller further out, and I did not look
for the second caller. Extending a script's behaviour extends it for
everyone who calls it; the old proof does not transfer, and neither do the
old fixtures.

Pinned to a stub that answers `issue` for every number — which is what
these fixtures were always written to mean. Exported, because
r2_missing_closing_ref runs the close script as a child process.

Mutation-verified: unsetting the pin reproduces the CI failure exactly,
`FAIL R2-positive: expected [10], got []`, so the pin is load-bearing.

check_reconcile.sh is the ONLY other caller — grepped rather than assumed —
and its bare path is the self-test, so guard_tree makes no API calls.

Self-test 17/17.

Refs #3200
Pmat-Ticket: PMAT-1098
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant