From 9b0343432def953c7487b10f906443a0497fe0ef Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Thu, 17 Sep 2026 15:21:24 +0200 Subject: [PATCH 1/6] =?UTF-8?q?PMAT-3445:=20plan=20=E2=80=94=20read=20the?= =?UTF-8?q?=20milestone=20at=20the=20cut,=20not=20only=20at=20T-5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.68.x autopilot tag step reads no milestone; the only read is the close step, after publish. Plan for a milestone gate, a versioned tag step that calls it, and the spec/contract wiring. Grilled by a width-3 quorum before code lands. Pmat-Ticket: PMAT-3445 Co-Authored-By: Claude Opus 5 --- docs/audits/impl-PMAT-3445-plan.md | 122 +++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 docs/audits/impl-PMAT-3445-plan.md diff --git a/docs/audits/impl-PMAT-3445-plan.md b/docs/audits/impl-PMAT-3445-plan.md new file mode 100644 index 0000000000..545c2d85c7 --- /dev/null +++ b/docs/audits/impl-PMAT-3445-plan.md @@ -0,0 +1,122 @@ +# PMAT-3445 plan — the milestone being cut is read at the cut, not only at T-5 + +Ticket: #3445 (P1, milestone 0.69.0). Design discussion #3418, plan and evidence #3427. +Status: PLAN, grilled by a width-3 quorum before code lands. + +## 1. Mechanism, measured + +| Fact | Command | Value | +|---|---|---| +| 0.68.0's milestone at the tag | `gh api repos/paiml/aprender/milestones/5` (issue #3445 body) | open 2 / closed 93 at 06:35Z; #3091 reopened 05:10Z | +| What the 2 open items are today | `gh api 'repos/paiml/aprender/issues?milestone=5&state=open'` | #3091 (issue, P1) and #3450 (PR, `release: 0.68.1`) — **the milestones API `open_issues` counts PRs** | +| Where the tag is cut | `/mnt/nvme-raid0/agent-wt/rel-068-1-autopilot/autopilot.sh`, `if run_step tag` block (untracked, one copy per train) | `git tag -a` → `git push origin ` → `gh release create`; **no milestone read** | +| The only milestone read in the train | same file, `close` step | `open_issues == 0` or refuse to close the milestone — **after** tag, assets and publish | +| T-5 | `scripts/check_reconcile.sh` | R1–R4; no milestone predicate | +| Live now | `ps` | the 0.68.1 autopilot (pid 3021358) waits on #3450 against milestone 5, which holds #3091 open | +| Next cut | `docs/specifications/06x-release-schedule.md` §3 | 0.69.0 leaves 2026-09-17T18:00Z; milestone 6 holds 126 open (123 issues + 3 PRs) | + +Why the defect exists: the reconcile reads the milestone at one instant; an item reopened after that instant is invisible, and nothing re-reads at the cut. + +## 2. Design + +### G1 — `scripts/check_milestone_cut.sh` + +``` +check_milestone_cut.sh [--repo O/R] [--json OUT] +check_milestone_cut.sh --self-test # also the bare run (guard_tree.sh runs guards bare) +``` + +- **Universe:** every open item in the milestone, issues AND pull requests — + `gh api --paginate 'repos/O/R/issues?milestone=&state=open&per_page=100'`. PRs count because + the milestone's own `open_issues` counts them, and that is the number the ticket quotes. +- **Title → number** via `milestones?state=all`. Zero or two matches → exit 2. +- **Count cross-check:** `len(universe) != milestone.open_issues` → exit 2. A paginated read that dropped + rows must not be able to pass. +- **Vacuity:** `open_issues + closed_issues == 0` → exit 2. An empty milestone means the wrong one was named. +- **Carry record** (the ticket's "explicit `slipped_from:` decision"): a comment on the item with a line + `slipped_from: ` — anchored at line start, the milestone title matched whole (so `0.69.00` and + `0.69.0-rc` are not `0.69.0`), optional reason after whitespace. The term is the one + `06x-release-schedule.md` §4 step 1 and D-5 already use. +- **A reopen voids an earlier carry:** the comment must be newer than the item's latest `reopened` + event (`issues//events`). This is the #3091 shape exactly. +- **Verdict:** exit 0 when every open item is carried (vacuously when none are open); exit 1 naming each + uncarried item `#N [labels] title`; exit 2 on any environment or consistency failure + (gh missing/unauthenticated, unresolvable milestone, count mismatch, vacuity). Never a silent pass. +- **Receipt** (`--json`): `{milestone, number, open, closed, carried:[{number, comment_url, created_at}], + uncarried:[{number, kind, title, labels, reopened_at}], verdict}`. +- **Structure:** the predicate reads FILES (`milestone.json`, `items.json`, `comments/.json`, + `events/.json`); the live path fetches into a temp dir and calls the same function. The self-test + builds fixtures and never touches the network (the `check_reconcile.sh` pattern). + +Self-test case table: + +| Case | Input | Exit | +|---|---|---| +| C1 | open 0, closed 5 | 0 | +| C2 | one open issue, no comments | 1, names it | +| C3 | one open PR, no comments | 1, names it as `pr` | +| C4 | open issue, comment `slipped_from: M` | 0 | +| C5 | comment `slipped_from: ` | 1 | +| C6 | carry comment OLDER than the latest reopen (#3091) | 1 | +| C7 | carry comment NEWER than the latest reopen | 0 | +| C8 | `slipped_from: M` mid-line in prose | 1 | +| C9 | `slipped_from: M0` / `slipped_from: M-rc` (prefix trap) | 1 | +| C10 | open 0, closed 0 | 2 | +| C11 | milestone says open 3, list holds 2 | 2 | +| C12 | two open, one carried, one not | 1, names only the uncarried | + +Mutation proof, recorded in the commit body: neuter the predicate to admit everything → the self-test +goes RED (C2, C3, C5, C6, C8, C9, C12); neuter the reopen rule → C6 goes RED. + +### G2 — the decision surface: `scripts/release_tag.sh` + +The tag is decided in an untracked per-train autopilot copy. A precondition written only into the spec +depends on the next copy remembering it — the defect class this ticket is. So the tag step itself +becomes a versioned script the autopilot calls: + +``` +release_tag.sh --version X.Y.Z --commit --milestone --notes <file> [--repo O/R] [--dry-run] +release_tag.sh --self-test +``` + +Order, each step fail-closed: `check_milestone_cut.sh <milestone>` (exit ≠ 0 → `STOP` naming the items, +no tag) → the tag does not already exist locally or on the remote → `git tag -a` → `git push` → +`gh release create --verify-tag`. `--dry-run` runs the gate and prints the three write commands without +running them. Its self-test stubs `gh`/`git` on PATH and asserts that a RED gate means zero write calls. + +### G3 — wiring and contract + +- `docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md` §4: T-0 row reads the milestone + (`check_milestone_cut.sh`) at the freeze; T-3 Promote runs it again immediately before the tag, + through `release_tag.sh`. A RED read is SKIPPED, like any red step. +- `docs/specifications/06x-release-schedule.md` §4 step 1 (read at freeze), step 4 (tag through + `release_tag.sh`), §7 ships-table row. +- `contracts/release-schedule-06x-v1.yaml`: equation `milestone_settled_at_cut`, falsification tests + running both self-tests. +- `scripts/guard_tree.sh` picks both scripts up bare (both are cargo-free and self-test when bare). +- The running 0.68.1 autopilot is another session's file and is running. It is not edited here. Its + owner gets the one-line replacement for its tag block. This is a named gap in the receipt, not a claim + that it is wired. + +## 3. First-green proof before it may block (live, recorded in the receipt) + +| Run | Expected | +|---|---| +| `check_milestone_cut.sh 0.67.0` (closed, 0 open / 50 closed) | 0 | +| `check_milestone_cut.sh 0.66.0` (0 open / 10 closed) | 0 | +| `check_milestone_cut.sh 0.68.0` | 1, names #3091 and #3450 | +| `release_tag.sh --dry-run --version 0.68.99 --milestone 0.68.0 …` (the rehearsal the ticket names) | non-zero, `STOP` names #3091, no tag on the remote | + +## 4. Phases + +| Phase | Scope | Acceptance | +|---|---|---| +| P1 gate | `scripts/check_milestone_cut.sh` | `bash scripts/check_milestone_cut.sh --self-test` = 0; the mutation turns it RED; `bashrs lint` clean; the three live rows of §3 | +| P2 decision surface | `scripts/release_tag.sh` | `bash scripts/release_tag.sh --self-test` = 0; the dry-run rehearsal row of §3 | +| P3 wiring | the two specs, the contract | `pv validate contracts/release-schedule-06x-v1.yaml`; `cargo test -p aprender-core --test readme_contract test_documented_paths_exist`; `bash scripts/guard_tree.sh --dry-run --no-cargo` lists both as `run:` | + +## 5. Consequence that must be surfaced, not hidden + +The 0.69.0 cut is due today at 18:00Z with 126 open items in milestone 6. When this gate is wired, +that cut stops until every item is closed or carried. That is the gate working. The remedy is the +existing freeze/spill (06x §4 step 1, APR-RELEASE-001 §6.3), not a waiver. From 465cd96f2ecad3416cdfcb419e5edf9111a743b3 Mon Sep 17 00:00:00 2001 From: Noah Gift <noah.gift@gmail.com> Date: Thu, 17 Sep 2026 15:38:48 +0200 Subject: [PATCH 2/6] =?UTF-8?q?PMAT-3445:=20check=5Fmilestone=5Fcut.sh=20?= =?UTF-8?q?=E2=80=94=20the=20milestone=20being=20cut=20must=20hold=200=20o?= =?UTF-8?q?pen=20items=20at=20the=20cut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.68.0 was tagged while milestone 0.68.0 held #3091 (P1, reopened 85 min before the tag) and #3450. The autopilot's tag step read no milestone; the only read was the close step, after publish. This gate exits 0 only when the named milestone has zero open issues AND pull requests, exit 1 naming each open item with its carry/close remedy, exit 2 when it cannot judge (unresolvable or duplicate title, empty milestone, a listed-vs-open_issues disagreement, gh missing/unauthenticated, a failed read). Plan v2 applies the width-3 grill's 3/3 consensus: carry = re-milestone (no comment/event scraping), no release_tag.sh (guard_tree runs only check_*.sh; it would be dark or refused as unwired), every read paginated as JSON lines. Self-test (also the bare run): 18 checks, S1..S16, no network (gh stubbed). Mutations, each on a scratch copy: (a) judge always passes -> RED: S2 S3 S4 S12 S15 S16 (6 of 18) (b) count cross-check deleted -> RED: S6 S7 (2 of 18) (c) --paginate dropped (items) -> RED: S15 (1 of 18) bashrs lint --level error: exit 0. First-green proof, live 2026-09-17: check_milestone_cut.sh 0.67.0 -> 0 (0 open, 50 closed) check_milestone_cut.sh 0.66.0 -> 0 (0 open, 10 closed) check_milestone_cut.sh 0.68.0 -> 1 (#3091 issue [bug,P1], #3450 pr) check_milestone_cut.sh 0.69.0 -> 1 (48 open at the read; milestone API open=48) Pmat-Ticket: PMAT-3445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- docs/audits/impl-PMAT-3445-plan.md | 172 +++++++------ scripts/check_milestone_cut.sh | 399 +++++++++++++++++++++++++++++ 2 files changed, 497 insertions(+), 74 deletions(-) create mode 100644 scripts/check_milestone_cut.sh diff --git a/docs/audits/impl-PMAT-3445-plan.md b/docs/audits/impl-PMAT-3445-plan.md index 545c2d85c7..b8db4c018d 100644 --- a/docs/audits/impl-PMAT-3445-plan.md +++ b/docs/audits/impl-PMAT-3445-plan.md @@ -1,21 +1,23 @@ # PMAT-3445 plan — the milestone being cut is read at the cut, not only at T-5 Ticket: #3445 (P1, milestone 0.69.0). Design discussion #3418, plan and evidence #3427. -Status: PLAN, grilled by a width-3 quorum before code lands. +Status: **v2**. v1 was grilled by a width-3 quorum and returned 3/3 `do-not-implement-as-written` +(§6). v2 applies every consensus edit. ## 1. Mechanism, measured | Fact | Command | Value | |---|---|---| | 0.68.0's milestone at the tag | `gh api repos/paiml/aprender/milestones/5` (issue #3445 body) | open 2 / closed 93 at 06:35Z; #3091 reopened 05:10Z | -| What the 2 open items are today | `gh api 'repos/paiml/aprender/issues?milestone=5&state=open'` | #3091 (issue, P1) and #3450 (PR, `release: 0.68.1`) — **the milestones API `open_issues` counts PRs** | -| Where the tag is cut | `/mnt/nvme-raid0/agent-wt/rel-068-1-autopilot/autopilot.sh`, `if run_step tag` block (untracked, one copy per train) | `git tag -a` → `git push origin <tag>` → `gh release create`; **no milestone read** | -| The only milestone read in the train | same file, `close` step | `open_issues == 0` or refuse to close the milestone — **after** tag, assets and publish | +| What the 2 open items are today | `gh api 'repos/paiml/aprender/issues?milestone=5&state=open'` | #3091 (issue, P1) and #3450 (PR, `release: 0.68.1`). **The milestones API `open_issues` counts PRs** | +| Where the tag is cut | `/mnt/nvme-raid0/agent-wt/rel-068-1-autopilot/autopilot.sh:104` `if run_step tag` (untracked, one copy per train) | `git tag -a` → `git push origin <tag>` → `gh release create`; **no milestone read** | +| The only milestone read in the train | same file `:239`, the `close` step | `open_issues == 0` or refuse to close. This runs **after** tag, assets and publish | | T-5 | `scripts/check_reconcile.sh` | R1–R4; no milestone predicate | -| Live now | `ps` | the 0.68.1 autopilot (pid 3021358) waits on #3450 against milestone 5, which holds #3091 open | +| Live | `ps` | the 0.68.1 autopilot (pid 3021358) waits on #3450 against milestone 5, which holds #3091. Warned on #3450 | | Next cut | `docs/specifications/06x-release-schedule.md` §3 | 0.69.0 leaves 2026-09-17T18:00Z; milestone 6 holds 126 open (123 issues + 3 PRs) | -Why the defect exists: the reconcile reads the milestone at one instant; an item reopened after that instant is invisible, and nothing re-reads at the cut. +Why the defect exists: the reconcile reads the milestone at one instant. An item reopened after that +instant is invisible, and nothing re-reads it at the cut. ## 2. Design @@ -26,77 +28,88 @@ check_milestone_cut.sh <milestone-title> [--repo O/R] [--json OUT] check_milestone_cut.sh --self-test # also the bare run (guard_tree.sh runs guards bare) ``` -- **Universe:** every open item in the milestone, issues AND pull requests — - `gh api --paginate 'repos/O/R/issues?milestone=<number>&state=open&per_page=100'`. PRs count because - the milestone's own `open_issues` counts them, and that is the number the ticket quotes. -- **Title → number** via `milestones?state=all`. Zero or two matches → exit 2. -- **Count cross-check:** `len(universe) != milestone.open_issues` → exit 2. A paginated read that dropped - rows must not be able to pass. -- **Vacuity:** `open_issues + closed_issues == 0` → exit 2. An empty milestone means the wrong one was named. -- **Carry record** (the ticket's "explicit `slipped_from:` decision"): a comment on the item with a line - `slipped_from: <M>` — anchored at line start, the milestone title matched whole (so `0.69.00` and - `0.69.0-rc` are not `0.69.0`), optional reason after whitespace. The term is the one - `06x-release-schedule.md` §4 step 1 and D-5 already use. -- **A reopen voids an earlier carry:** the comment must be newer than the item's latest `reopened` - event (`issues/<n>/events`). This is the #3091 shape exactly. -- **Verdict:** exit 0 when every open item is carried (vacuously when none are open); exit 1 naming each - uncarried item `#N <issue|pr> [labels] title`; exit 2 on any environment or consistency failure - (gh missing/unauthenticated, unresolvable milestone, count mismatch, vacuity). Never a silent pass. -- **Receipt** (`--json`): `{milestone, number, open, closed, carried:[{number, comment_url, created_at}], - uncarried:[{number, kind, title, labels, reopened_at}], verdict}`. -- **Structure:** the predicate reads FILES (`milestone.json`, `items.json`, `comments/<n>.json`, - `events/<n>.json`); the live path fetches into a temp dir and calls the same function. The self-test - builds fixtures and never touches the network (the `check_reconcile.sh` pattern). +**The rule: exit 0 only when the milestone has 0 open items.** Issues and pull requests both count, +because the close step (`autopilot.sh:239`) and 06x §4 step 8 close on `open_issues`, which counts +both. A gate that ignored PRs could pass while close still refused. + +**Carry means re-milestoning** (grill consensus, 3/3). An item that does not ride this train is moved +to the next milestone (`gh issue edit N --milestone <next>` / `gh pr edit N --milestone <next>`), with +a `slipped_from: X.Y.0` comment as the record. That is the term 06x §4 step 1 and D-5 already use, and +it leaves the milestone at 0 open, which is what close needs. The gate reads no comments and no events. +A reopen before the cut shows up as an open item, which is the #3091 shape exactly. + +**When it runs.** Twice, and never while the train's own bump PR is open: +1. At the freeze (06x §4 step 1, APR-RELEASE-001 T-0), before the bump PR is opened. +2. Immediately before `git tag` (06x §4 step 4, APR-RELEASE-001 T-3), after the bump PR has merged. + This is the binding read. + +**Live reads**, each paginated as JSON lines (`gh api --paginate --jq '.[]'`) so that more than 100 +items cannot turn into concatenated arrays: +`repos/O/R/milestones?state=all&per_page=100` and `repos/O/R/issues?milestone=<n>&state=open&per_page=100`. + +**Exit 2, never a pass**, when: gh or python3 is missing; gh is unauthenticated; a gh read fails; the +title matches 0 or 2+ milestones (compared as a literal string, so `0.69.0` never matches `0a69b0` and +`0.69` never matches `0.69.0`); `open_issues + closed_issues == 0` (vacuous: the wrong milestone was +named); the item list length differs from `open_issues`; any listed item is not open or carries a +different milestone number. + +**Exit 1** prints one line per open item, `#N <issue|pr> [labels] title`, then the remedy for each +item (close it, or run the exact `gh … edit N --milestone <next>` command), then a summary. +**Exit 0** prints `PASS milestone <M> (#n): 0 open, <c> closed`. + +`--json OUT` writes `{milestone, number, open, closed, items:[{number, kind, title, labels, url}], verdict}`. + +**Structure:** the judge reads two FILES (`milestones.jsonl`, `items.jsonl`). The live path fetches into +a temp dir and calls the same judge. The self-test builds fixtures and stubs `gh` on PATH; it never +touches the network. This is the `check_reconcile.sh` pattern. Self-test case table: | Case | Input | Exit | |---|---|---| -| C1 | open 0, closed 5 | 0 | -| C2 | one open issue, no comments | 1, names it | -| C3 | one open PR, no comments | 1, names it as `pr` | -| C4 | open issue, comment `slipped_from: M` | 0 | -| C5 | comment `slipped_from: <other milestone>` | 1 | -| C6 | carry comment OLDER than the latest reopen (#3091) | 1 | -| C7 | carry comment NEWER than the latest reopen | 0 | -| C8 | `slipped_from: M` mid-line in prose | 1 | -| C9 | `slipped_from: M0` / `slipped_from: M-rc` (prefix trap) | 1 | -| C10 | open 0, closed 0 | 2 | -| C11 | milestone says open 3, list holds 2 | 2 | -| C12 | two open, one carried, one not | 1, names only the uncarried | - -Mutation proof, recorded in the commit body: neuter the predicate to admit everything → the self-test -goes RED (C2, C3, C5, C6, C8, C9, C12); neuter the reopen rule → C6 goes RED. - -### G2 — the decision surface: `scripts/release_tag.sh` - -The tag is decided in an untracked per-train autopilot copy. A precondition written only into the spec -depends on the next copy remembering it — the defect class this ticket is. So the tag step itself -becomes a versioned script the autopilot calls: - -``` -release_tag.sh --version X.Y.Z --commit <sha> --milestone <title> --notes <file> [--repo O/R] [--dry-run] -release_tag.sh --self-test -``` - -Order, each step fail-closed: `check_milestone_cut.sh <milestone>` (exit ≠ 0 → `STOP` naming the items, -no tag) → the tag does not already exist locally or on the remote → `git tag -a` → `git push` → -`gh release create --verify-tag`. `--dry-run` runs the gate and prints the three write commands without -running them. Its self-test stubs `gh`/`git` on PATH and asserts that a RED gate means zero write calls. +| S1 | open 0, closed 5 | 0, prints PASS | +| S2 | one open issue | 1, names `#10 issue` | +| S3 | one open PR | 1, names `#11 pr`, remedy is `gh pr edit` | +| S4 | two open (issue + PR) with labels | 1, names both with their labels | +| S5 | open 0, closed 0 | 2 (vacuous) | +| S6 | `open_issues` 3, list holds 2 | 2 | +| S7 | `open_issues` 0, list holds 1 | 2 (a stale count cannot pass) | +| S8 | no milestone with the title | 2 | +| S9 | two milestones with the title | 2 | +| S10 | titles `0a69b0` (1 open) and `0.69.0` (0 open): judge `0.69.0` → 0; judge `0.69` → 2 | literal match | +| S11 | a listed item carries another milestone number, or is closed | 2 | +| S12 | 150 open items as JSON lines (the pagination shape) | 1, summary counts 150 | +| S13 | live path, `gh` absent from PATH | 2 | +| S14 | live path, stub `gh` that fails `auth status` | 2 | +| S15 | live path, stub `gh` serving the S2 fixture; the stub refuses a call without `--paginate` or a query without `state=open` | 1, names `#10` | +| S16 | S2 with `--json` | verdict `RED`, `items[0].number == 10` | + +Mutation proof, recorded in the commit body: (a) the judge always passes → S2, S3, S4, S12, S15 go RED; +(b) delete the count cross-check → S6, S7 go RED; (c) drop `--paginate` from the live read → S15 goes RED. + +### G2 — dropped + +v1 proposed `scripts/release_tag.sh`. All three lanes rejected it: `guard_tree.sh` runs only +`scripts/check_*.sh` (`scripts/guard_tree.sh:18`), and `scripts/check_guards_are_wired.sh:122-126` +treats any `scripts/*.sh` with a `--self-test)` arm as a guard that must be wired. So it would be either +refused as unwired or silently dark. No CI home exists without a workflow edit. + +The tag's decision surface therefore stays in the untracked per-train autopilot. That is a **named +gap**, not a claim of wiring. What this PR does about it: +- The spec text names the exact call and its position (§G3). +- The live 0.68.1 train is warned on its bump PR (#3450). +- A follow-up ticket versions the autopilot's tag step so the next per-train copy cannot omit the read. ### G3 — wiring and contract -- `docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md` §4: T-0 row reads the milestone - (`check_milestone_cut.sh`) at the freeze; T-3 Promote runs it again immediately before the tag, - through `release_tag.sh`. A RED read is SKIPPED, like any red step. -- `docs/specifications/06x-release-schedule.md` §4 step 1 (read at freeze), step 4 (tag through - `release_tag.sh`), §7 ships-table row. -- `contracts/release-schedule-06x-v1.yaml`: equation `milestone_settled_at_cut`, falsification tests - running both self-tests. -- `scripts/guard_tree.sh` picks both scripts up bare (both are cargo-free and self-test when bare). -- The running 0.68.1 autopilot is another session's file and is running. It is not edited here. Its - owner gets the one-line replacement for its tag block. This is a named gap in the receipt, not a claim - that it is wired. +- `docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md` §4: the T-0 row reads the milestone + at the cut; the T-3 row re-reads it immediately before the tag. A RED read makes the step RED + (SKIPPED, like any red step). Carry means re-milestoning. +- `docs/specifications/06x-release-schedule.md` §4 step 1 (read at freeze; carry = re-milestone plus + `slipped_from:`) and step 4 (re-read immediately before the tag), plus a §11 amendment row. +- `contracts/release-schedule-06x-v1.yaml`: equation `milestone_settled_at_cut`; falsification tests + (a) the self-test and (b) both specs name the gate at the cut; `qa_gate.checks` entry. +- `scripts/guard_tree.sh` picks up `check_milestone_cut.sh` bare, which runs the self-test. ## 3. First-green proof before it may block (live, recorded in the receipt) @@ -105,18 +118,29 @@ running them. Its self-test stubs `gh`/`git` on PATH and asserts that a RED gate | `check_milestone_cut.sh 0.67.0` (closed, 0 open / 50 closed) | 0 | | `check_milestone_cut.sh 0.66.0` (0 open / 10 closed) | 0 | | `check_milestone_cut.sh 0.68.0` | 1, names #3091 and #3450 | -| `release_tag.sh --dry-run --version 0.68.99 --milestone 0.68.0 …` (the rehearsal the ticket names) | non-zero, `STOP` names #3091, no tag on the remote | +| `check_milestone_cut.sh 0.69.0` | 1, 126 items (more than 100: the paginated read) | +| Rehearsal: a copy of the autopilot's `tag` block with the gate as its first line, `git`/`gh` write verbs stubbed to a call log, milestone 0.68.0 | `STOP` names #3091; the call log holds 0 write calls | ## 4. Phases | Phase | Scope | Acceptance | |---|---|---| -| P1 gate | `scripts/check_milestone_cut.sh` | `bash scripts/check_milestone_cut.sh --self-test` = 0; the mutation turns it RED; `bashrs lint` clean; the three live rows of §3 | -| P2 decision surface | `scripts/release_tag.sh` | `bash scripts/release_tag.sh --self-test` = 0; the dry-run rehearsal row of §3 | -| P3 wiring | the two specs, the contract | `pv validate contracts/release-schedule-06x-v1.yaml`; `cargo test -p aprender-core --test readme_contract test_documented_paths_exist`; `bash scripts/guard_tree.sh --dry-run --no-cargo` lists both as `run:` | +| P1 gate | `scripts/check_milestone_cut.sh` | `bash scripts/check_milestone_cut.sh --self-test` = 0; mutations (a)(b)(c) each turn it RED; `bashrs lint` has no errors; the four live rows of §3 | +| P2 wiring | the two specs, the contract | `pv validate contracts/release-schedule-06x-v1.yaml` = 0; `cargo test -p aprender-core --test readme_contract test_documented_paths_exist` = 0; `bash scripts/guard_tree.sh --dry-run --no-cargo` prints `run: scripts/check_milestone_cut.sh`; `bash scripts/check_guards_are_wired.sh` = 0 | +| P3 rehearsal + review | scratch only | the §3 rehearsal row; the width-3 quorum on the diff | ## 5. Consequence that must be surfaced, not hidden -The 0.69.0 cut is due today at 18:00Z with 126 open items in milestone 6. When this gate is wired, +The 0.69.0 cut is due today at 18:00Z with 126 open items in milestone 6. Where this gate is called, that cut stops until every item is closed or carried. That is the gate working. The remedy is the existing freeze/spill (06x §4 step 1, APR-RELEASE-001 §6.3), not a waiver. + +## 6. Grill record (v1) + +Width 3, `grillme`, review-only; lanes gemini-3.1-pro-high, gemini-3.8-flash-high, +gemini-3.7-flash-high; author family claude. Verdicts 3/3 `do-not-implement-as-written`. Consensus +edits, all applied above: carry = re-milestone and require open == 0; drop `release_tag.sh`; +paginate every read; add the metacharacter, zero-item and pagination cases; run the gate only when the +bump PR is not open. A width-1 goal lane had meanwhile implemented v1 (commit `d14914db8`, lane worktree +only). It was superseded, not merged: v1 semantics, and `gh api --paginate` without `--jq` writes +concatenated arrays that `json.load` rejects above 100 items. That would be an exit 2 on 0.69.0 itself. diff --git a/scripts/check_milestone_cut.sh b/scripts/check_milestone_cut.sh new file mode 100644 index 0000000000..90bcb1ff8d --- /dev/null +++ b/scripts/check_milestone_cut.sh @@ -0,0 +1,399 @@ +#!/usr/bin/env bash +# check_milestone_cut.sh <milestone-title> [--repo O/R] [--json OUT] | --self-test +# +# APR-RELEASE-001 section 4 (T-0, T-3) and 06x-release-schedule.md section 4 +# steps 1 and 4 (PMAT-3445, #3445). The milestone a release train cuts must +# hold ZERO open items -- issues AND pull requests -- at the instant of the cut. +# +# WHY: v0.68.0 was tagged 2026-09-17T06:35Z while milestone 0.68.0 held two +# open items, one of them #3091, a P1 defect reopened 85 minutes before the +# tag. The T-5 reconcile reads the milestone at one instant, so an item +# reopened after it is invisible; the autopilot's tag step read no milestone +# at all, and its only read was the close step, after publish. +# +# THE RULE: exit 0 only when open_issues == 0. Pull requests count because +# the milestone is closed on open_issues and GitHub counts them there. An item +# that does not ride this train is CARRIED by moving it to the next milestone +# (`gh issue edit N --milestone <next>`, or `gh pr edit`) with a +# `slipped_from: X.Y.0` comment -- never by leaving it open in this one. +# +# WHEN: at the freeze, before the train's bump PR opens, and again +# immediately before `git tag`, after the bump PR has merged. Never while the +# bump PR is open: it sits in the milestone and reads RED. +# +# Exit 0 = zero open items in the milestone. +# Exit 1 = at least one open item; each is named with its remedy. +# Exit 2 = cannot judge, never a silent pass: gh or python3 missing, gh +# unauthenticated, a read failed, the title matches 0 or 2+ +# milestones (compared literally), the milestone has no items at all, +# or the listed items disagree with the milestone's own open_issues. +# +# No arguments runs the self-test, because guard_tree.sh runs every guard +# bare. The self-test builds fixtures and stubs gh on PATH; it never touches +# the network. + +set -euo pipefail + +SELF_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" + +usage() { + printf 'usage: %s <milestone-title> [--repo O/R] [--json OUT] | --self-test\n' "$(basename "$0")" >&2 + exit 2 +} + +# judge_from_dir DIR TITLE [JSON_OUT] +# DIR holds milestones.jsonl and items.jsonl, one JSON object per line: the +# shape `gh api --paginate --jq '.[]'` writes, so a read of more than one page +# is never a concatenation of arrays. Prints the verdict and returns 0, 1 or 2. +judge_from_dir() { + python3 -c ' +import json, sys + +d, title, json_out = sys.argv[1], sys.argv[2], sys.argv[3] + +def env(msg): + print("ENV: " + msg, file=sys.stderr) + sys.exit(2) + +def read_lines(name): + rows = [] + try: + with open(d + "/" + name, encoding="utf-8") as f: + for n, raw in enumerate(f, 1): + raw = raw.strip() + if not raw: + continue + try: + obj = json.loads(raw) + except ValueError: + env("%s line %d is not a JSON object" % (name, n)) + if not isinstance(obj, dict): + env("%s line %d is not a JSON object" % (name, n)) + rows.append(obj) + except OSError as e: + env("cannot read %s: %s" % (name, e)) + return rows + +matches = [m for m in read_lines("milestones.jsonl") if m.get("title") == title] +if len(matches) != 1: + env("title \"%s\" matches %d milestone(s); exactly 1 is required" % (title, len(matches))) +m = matches[0] +number = m.get("number") +open_n = m.get("open_issues") +closed_n = m.get("closed_issues") +if not isinstance(open_n, int) or not isinstance(closed_n, int): + env("milestone \"%s\" carries no integer open_issues/closed_issues" % title) +if open_n + closed_n == 0: + env("milestone \"%s\" (#%s) has no items at all -- the wrong milestone was named" % (title, number)) + +items = read_lines("items.jsonl") +if len(items) != open_n: + env("milestone \"%s\" (#%s) reports open_issues=%d but the read listed %d item(s)" % (title, number, open_n, len(items))) +for it in items: + if it.get("state") != "open" or (it.get("milestone") or {}).get("number") != number: + env("listed item #%s is not an open item of milestone #%s" % (it.get("number"), number)) + +rows = [] +for it in sorted(items, key=lambda i: i.get("number", 0)): + rows.append({ + "number": it.get("number"), + "kind": "pr" if "pull_request" in it else "issue", + "title": it.get("title", ""), + "labels": [lb.get("name", "") for lb in (it.get("labels") or [])], + "url": it.get("html_url", ""), + }) + +verdict = "RED" if rows else "PASS" +if json_out: + with open(json_out, "w", encoding="utf-8") as f: + json.dump({"milestone": title, "number": number, "open": open_n, "closed": closed_n, + "items": rows, "verdict": verdict}, f, indent=2, sort_keys=True) + f.write("\n") + +if not rows: + print("PASS milestone %s (#%s): 0 open, %d closed -- the cut may proceed" % (title, number, closed_n)) + sys.exit(0) + +for r in rows: + print("#%s %s [%s] %s" % (r["number"], r["kind"], ",".join(r["labels"]), r["title"])) +for r in rows: + print(" remedy #%s: close it, or carry it: gh %s edit %s --milestone <next> && gh %s comment %s --body \"slipped_from: %s\"" + % (r["number"], r["kind"], r["number"], r["kind"], r["number"], title)) +print("RED milestone %s (#%s): %d open item(s) -- no tag until each is closed or carried" % (title, number, len(rows))) +sys.exit(1) +' "$1" "$2" "${3:-}" +} + +# fetch_live REPO TITLE DIR -- writes DIR/milestones.jsonl and DIR/items.jsonl. +# A title that does not resolve to exactly one milestone leaves items.jsonl +# empty; judge_from_dir then names the resolution failure (exit 2). +fetch_live() { + repo="$1" + title="$2" + dir="$3" + gh api --paginate --jq '.[]' "repos/${repo}/milestones?state=all&per_page=100" > "${dir}/milestones.jsonl" || return 1 + number="$(python3 -c ' +import json, sys +nums = [] +with open(sys.argv[1], encoding="utf-8") as f: + for raw in f: + if raw.strip(): + obj = json.loads(raw) + if obj.get("title") == sys.argv[2]: + nums.append(obj.get("number")) +print(nums[0] if len(nums) == 1 else "") +' "${dir}/milestones.jsonl" "$title")" || return 1 + if [ -z "$number" ]; then + : > "${dir}/items.jsonl" + return 0 + fi + gh api --paginate --jq '.[]' "repos/${repo}/issues?milestone=${number}&state=open&per_page=100" > "${dir}/items.jsonl" || return 1 +} + +# --------------------------------------------------------------------------- +# self-test +# --------------------------------------------------------------------------- + +ST_FAILS=0 +ST_CASES=0 + +# st_check CASE WANT_RC GOT_RC OUTFILE [NEEDLE ...] +st_check() { + c="$1" + want="$2" + got="$3" + out="$4" + shift 4 + ST_CASES=$((ST_CASES + 1)) + if [ "$got" != "$want" ]; then + printf 'FAIL %s: expected exit %s, got %s\n' "$c" "$want" "$got" >&2 + sed 's/^/ /' "$out" >&2 + ST_FAILS=$((ST_FAILS + 1)) + return 0 + fi + for needle in "$@"; do + if ! grep -qF -- "$needle" "$out"; then + printf 'FAIL %s: output lacks [%s]\n' "$c" "$needle" >&2 + sed 's/^/ /' "$out" >&2 + ST_FAILS=$((ST_FAILS + 1)) + fi + done +} + +# st_judge FIXDIR CASE WANT_RC TITLE [NEEDLE ...] +st_judge() { + fx="$1" + c="$2" + want="$3" + t="$4" + shift 4 + rc=0 + judge_from_dir "$fx" "$t" "" > "${fx}/out" 2>&1 || rc=$? + st_check "$c" "$want" "$rc" "${fx}/out" "$@" +} + +# st_ms TITLE NUMBER OPEN CLOSED -- one milestones.jsonl line +st_ms() { + printf '{"title":"%s","number":%s,"open_issues":%s,"closed_issues":%s}\n' "$1" "$2" "$3" "$4" +} + +# st_item NUMBER MILESTONE_NUMBER KIND STATE LABEL -- one items.jsonl line +st_item() { + pr="" + if [ "$3" = pr ]; then + pr=',"pull_request":{"url":"u"}' + fi + printf '{"number":%s,"state":"%s","milestone":{"number":%s},"title":"item %s","labels":[{"name":"%s"}]%s}\n' \ + "$1" "$4" "$2" "$1" "$5" "$pr" +} + +self_test() { + fx="$(mktemp -d)" || return 2 + + # S1 zero open, some closed -> PASS + st_ms M 7 0 5 > "${fx}/milestones.jsonl" + : > "${fx}/items.jsonl" + st_judge "$fx" S1 0 M "PASS milestone M (#7): 0 open, 5 closed" + + # S2 one open issue -> RED, named + st_ms M 7 1 5 > "${fx}/milestones.jsonl" + st_item 10 7 issue open bug > "${fx}/items.jsonl" + st_judge "$fx" S2 1 M "#10 issue [bug] item 10" "gh issue edit 10 --milestone <next>" "1 open item(s)" + + # S3 one open PR -> RED, named as pr, remedy through gh pr + st_item 11 7 pr open release > "${fx}/items.jsonl" + st_judge "$fx" S3 1 M "#11 pr [release] item 11" "gh pr edit 11 --milestone <next>" + + # S4 an issue and a PR -> both named with their labels + st_ms M 7 2 5 > "${fx}/milestones.jsonl" + { st_item 11 7 pr open release; st_item 10 7 issue open P1; } > "${fx}/items.jsonl" + st_judge "$fx" S4 1 M "#10 issue [P1] item 10" "#11 pr [release] item 11" "2 open item(s)" + + # S5 an empty milestone is the wrong milestone, not a pass + st_ms M 7 0 0 > "${fx}/milestones.jsonl" + : > "${fx}/items.jsonl" + st_judge "$fx" S5 2 M "has no items at all" + + # S6 the read lost an item + st_ms M 7 3 5 > "${fx}/milestones.jsonl" + { st_item 10 7 issue open a; st_item 11 7 issue open b; } > "${fx}/items.jsonl" + st_judge "$fx" S6 2 M "open_issues=3 but the read listed 2" + + # S7 a stale zero count cannot pass over a listed open item + st_ms M 7 0 5 > "${fx}/milestones.jsonl" + st_item 10 7 issue open a > "${fx}/items.jsonl" + st_judge "$fx" S7 2 M "open_issues=0 but the read listed 1" + + # S8 no milestone carries the title + st_ms Other 7 0 5 > "${fx}/milestones.jsonl" + : > "${fx}/items.jsonl" + st_judge "$fx" S8 2 M "matches 0 milestone(s)" + + # S9 two milestones carry the title + { st_ms M 7 0 5; st_ms M 8 0 2; } > "${fx}/milestones.jsonl" + st_judge "$fx" S9 2 M "matches 2 milestone(s)" + + # S10 the title is compared literally: dots are not wildcards, a prefix is not a match + { st_ms 0a69b0 1 1 0; st_ms 0.69.0 2 0 3; } > "${fx}/milestones.jsonl" + : > "${fx}/items.jsonl" + st_judge "$fx" S10a 0 0.69.0 "PASS milestone 0.69.0 (#2)" + st_judge "$fx" S10b 2 0.69 "matches 0 milestone(s)" + + # S11 a listed item from another milestone, or a closed one, is a bad read + st_ms M 7 1 5 > "${fx}/milestones.jsonl" + st_item 10 8 issue open a > "${fx}/items.jsonl" + st_judge "$fx" S11a 2 M "is not an open item of milestone #7" + st_item 10 7 issue closed a > "${fx}/items.jsonl" + st_judge "$fx" S11b 2 M "is not an open item of milestone #7" + + # S12 more than one page of items (150) -> every one counted + st_ms M 7 150 5 > "${fx}/milestones.jsonl" + : > "${fx}/items.jsonl" + i=1000 + while [ "$i" -lt 1150 ]; do + st_item "$i" 7 issue open a >> "${fx}/items.jsonl" + i=$((i + 1)) + done + st_judge "$fx" S12 1 M "#1000 issue" "#1149 issue" "150 open item(s)" + + # S16 --json records the verdict and the items + st_ms M 7 1 5 > "${fx}/milestones.jsonl" + st_item 10 7 issue open bug > "${fx}/items.jsonl" + rc=0 + judge_from_dir "$fx" M "${fx}/receipt.json" > "${fx}/out" 2>&1 || rc=$? + python3 -c ' +import json, sys +r = json.load(open(sys.argv[1], encoding="utf-8")) +ok = r["verdict"] == "RED" and r["open"] == 1 and [i["number"] for i in r["items"]] == [10] +print("json-receipt " + ("ok" if ok else "WRONG: %r" % r)) +' "${fx}/receipt.json" >> "${fx}/out" 2>&1 || true + st_check S16 1 "$rc" "${fx}/out" "json-receipt ok" + + # The live path, with gh stubbed on PATH. SELF_PATH is re-executed so main() + # and fetch_live() are exercised exactly as a release step calls them. + stub="${fx}/stub" + mkdir -p "$stub" + + # S13 gh absent -> ENV. The PATH holds only the tools the script needs before its gh check. + for tool in dirname basename python3; do + ln -s "$(command -v "$tool")" "${stub}/${tool}" + done + rc=0 + env PATH="$stub" "$BASH" "$SELF_PATH" M > "${fx}/out" 2>&1 || rc=$? + st_check S13 2 "$rc" "${fx}/out" "gh is not on PATH" + rm -f "${stub}/dirname" "${stub}/basename" "${stub}/python3" + + # S14 gh present but unauthenticated -> ENV + printf '#!/bin/sh\n[ "$1" = auth ] && exit 1\nexit 0\n' > "${stub}/gh" + chmod +x "${stub}/gh" + rc=0 + PATH="${stub}:${PATH}" bash "$SELF_PATH" M > "${fx}/out" 2>&1 || rc=$? + st_check S14 2 "$rc" "${fx}/out" "gh is not authenticated" + + # S15 a stub gh serving the S2 fixture. It refuses a read that does not + # paginate as JSON lines, and an items query that is not state=open for + # the resolved milestone number -- so the live query itself is under test. + cat > "${stub}/gh" <<'STUB' +#!/bin/sh +case "$1" in + auth) exit 0 ;; + api) ;; + *) echo "stub gh: unexpected verb: $*" >&2; exit 9 ;; +esac +case " $* " in + *' --paginate --jq .[] '*) ;; + *) echo "stub gh: read without --paginate --jq .[]: $*" >&2; exit 9 ;; +esac +case "$*" in + *'repos/o/r/milestones?state=all&per_page=100'*) cat "$STUB_DIR/milestones.jsonl" ;; + *'repos/o/r/issues?milestone=7&state=open&per_page=100'*) cat "$STUB_DIR/items.jsonl" ;; + *) echo "stub gh: unexpected read: $*" >&2; exit 9 ;; +esac +STUB + chmod +x "${stub}/gh" + rc=0 + STUB_DIR="$fx" PATH="${stub}:${PATH}" bash "$SELF_PATH" M --repo o/r > "${fx}/out" 2>&1 || rc=$? + st_check S15 1 "$rc" "${fx}/out" "#10 issue [bug] item 10" + + rm -rf "${fx:?}" + if [ "$ST_FAILS" -ne 0 ]; then + printf 'self-test FAILED: %s of %s check(s).\n' "$ST_FAILS" "$ST_CASES" >&2 + return 1 + fi + printf 'self-test OK: %s case(s).\n' "$ST_CASES" + return 0 +} + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + +main() { + if [ $# -eq 0 ] || [ "${1:-}" = "--self-test" ]; then + self_test + exit $? + fi + case "$1" in + -h|--help) sed -n '2,33p' "$SELF_PATH"; exit 0 ;; + -*) usage ;; + esac + + title="$1" + shift + repo="paiml/aprender" + json_out="" + while [ $# -gt 0 ]; do + [ $# -ge 2 ] || usage + case "$1" in + --repo) repo="$2" ;; + --json) json_out="$2" ;; + *) usage ;; + esac + shift 2 + done + + if ! command -v gh >/dev/null 2>&1; then + printf 'ENV: gh is not on PATH.\n' >&2 + exit 2 + fi + if ! command -v python3 >/dev/null 2>&1; then + printf 'ENV: python3 is not on PATH.\n' >&2 + exit 2 + fi + if ! gh auth status >/dev/null 2>&1; then + printf 'ENV: gh is not authenticated.\n' >&2 + exit 2 + fi + + input_dir="$(mktemp -d)" || exit 2 + trap 'rm -rf "${input_dir:?}"' EXIT + if ! fetch_live "$repo" "$title" "$input_dir"; then + printf 'ENV: a gh read of %s failed.\n' "$repo" >&2 + exit 2 + fi + rc=0 + judge_from_dir "$input_dir" "$title" "$json_out" || rc=$? + exit "$rc" +} + +main "$@" From ffc68fe47a53c031de3be75bd6ab2fe09703908a Mon Sep 17 00:00:00 2001 From: Noah Gift <noah.gift@gmail.com> Date: Thu, 17 Sep 2026 15:41:36 +0200 Subject: [PATCH 3/6] PMAT-3445: both release specs read the milestone at the cut; contract equation milestone_settled_at_cut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 06x-release-schedule.md §4 step 1 reads the milestone at the freeze (before the bump PR opens) and step 4 re-reads it immediately before the tag (after the bump merges) — the binding read, the one that would have seen #3091 reopened 85 min before v0.68.0. Carry = re-milestone plus a `slipped_from:` comment, never an item left open (step 8 closes on open_issues). APR-RELEASE-001 §4 T-0 and T-3 rows carry the same two reads. contracts/release-schedule-06x-v1.yaml: equation milestone_settled_at_cut, RS0-INV-007, FALSIFY-REL-06X-009 (the self-test) and -010 (both specs name the gate), qa_gate check. Verified: pv validate (pinned pv 0.68.0 via verifier_pin.sh) 0 errors; pv lint PASS; FALSIFY-010 exit 0; cargo test -p aprender-core --test readme_contract 15 passed; guard_tree.sh --dry-run --no-cargo prints `run: scripts/check_milestone_cut.sh`; check_guards_are_wired, check_no_timing_in_required, check_assertions_exclude, check_pass_grep_anchored, check_sourced_libs_option_neutral, check_bashrs_gate all exit 0. Pmat-Ticket: PMAT-3445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- contracts/release-schedule-06x-v1.yaml | 27 +++++++++++++++++++ docs/specifications/06x-release-schedule.md | 5 ++-- .../APR-RELEASE-001-train-and-build-kaizen.md | 4 +-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/contracts/release-schedule-06x-v1.yaml b/contracts/release-schedule-06x-v1.yaml index 8371113c40..bbd047d6c3 100644 --- a/contracts/release-schedule-06x-v1.yaml +++ b/contracts/release-schedule-06x-v1.yaml @@ -66,6 +66,15 @@ equations: - "A row never waits on a later train" - "A blocked row carries no date of its own — its expiry is its latest blocker's train" + milestone_settled_at_cut: + formula: "for every train vX.Y.0: at the freeze (before the bump PR opens) and immediately before `git tag` (after it merges), open_issues(milestone X.Y.0) == 0, where open_issues counts issues AND pull requests; read by `scripts/check_milestone_cut.sh X.Y.0`" + domain: "the GitHub milestone the train cuts, read live at each of the two instants" + codomain: "exit code of scripts/check_milestone_cut.sh: 0 settled, 1 open items named, 2 cannot judge" + invariants: + - "An item that does not ride the train is carried by moving it to the next milestone with a `slipped_from: X.Y.0` comment, never by leaving it open (§4 step 8 closes on open_issues)" + - "The pre-tag read is binding: an item reopened after the freeze is visible only there (v0.68.0 shipped with #3091 reopened 85 min before the tag, #3445)" + - "An empty milestone, an unresolvable or duplicate title, or a read that disagrees with open_issues is exit 2, never a pass" + proof_obligations: - id: RS0-INV-001 type: invariant @@ -97,6 +106,11 @@ proof_obligations: property: "Each train left inside its window or the amendment log says why" formal: "the closing comment of each release epic carries planned/actual/delta" applies_to: train_leaves_in_window + - id: RS0-INV-007 + type: invariant + property: "No tag is cut while its milestone holds an open issue or pull request" + formal: "scripts/check_milestone_cut.sh X.Y.0 exit 0 immediately before `git tag vX.Y.0`; its self-test exits 0 and each of its three recorded mutations turns it RED" + applies_to: milestone_settled_at_cut falsification_tests: - id: FALSIFY-REL-06X-001 @@ -147,6 +161,18 @@ falsification_tests: test: "grep -q 'docs/specifications' scripts/check_no_claim_literals.sh" if_fails: "A ratio typed into the schedule without a receipt would pass unread" + - id: FALSIFY-REL-06X-009 + rule: "The milestone gate can fail and does not pass vacuously (PMAT-3445)" + prediction: "check_milestone_cut.sh --self-test exits 0: an open issue, an open PR, 150 open items, a lost or stale count, an empty milestone, a missing or duplicate or regex-lookalike title, and an unpaginated live read are each judged as their case table says" + test: "bash scripts/check_milestone_cut.sh --self-test" + if_fails: "The cut gate no longer refuses what it exists to refuse, and a tag can again be cut over an open P1 (v0.68.0, #3091)" + + - id: FALSIFY-REL-06X-010 + rule: "Both release specs read the milestone at the cut" + prediction: "06x-release-schedule.md §4 and APR-RELEASE-001 §4 each name scripts/check_milestone_cut.sh" + test: "grep -q 'check_milestone_cut.sh' docs/specifications/06x-release-schedule.md && grep -q 'check_milestone_cut.sh' docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md" + if_fails: "The gate exists and no protocol calls it — the release step that cuts the tag reads nothing again" + qa_gate: id: F-REL-06X-001 name: "06x release schedule — trains leave in their window, every tag carries four apr binaries, PRs fit in 20 minutes, the spec describes the tree" @@ -157,3 +183,4 @@ qa_gate: - "pr_budget_twenty_minutes" - "spec_describes_the_tree" - "obligation_dag_is_acyclic_and_forward" + - "milestone_settled_at_cut" diff --git a/docs/specifications/06x-release-schedule.md b/docs/specifications/06x-release-schedule.md index 8d6db8b8a7..113dd0eaf6 100644 --- a/docs/specifications/06x-release-schedule.md +++ b/docs/specifications/06x-release-schedule.md @@ -281,10 +281,10 @@ The milestone already holds 15 issues; with the 14 rows above the train is over Numbered so an autopilot (`/mnt/nvme-raid0/agent-wt/rel-066-autopilot/` is the 0.66 precedent) can run it fail-closed, with a `STOP <step>` line naming the refusing gate. -1. **Freeze the platform.** `gh pr list --state open --label <train>`: rows still open move to the next epic with `slipped_from:`. No new pushes while the queue drains (`[[feedback_push_cadence_starves_the_merge_queue]]`). +1. **Freeze the platform.** `gh pr list --state open --label <train>`: rows still open move to the next epic with `slipped_from:`. No new pushes while the queue drains (`[[feedback_push_cadence_starves_the_merge_queue]]`). Then read the milestone, before the bump PR opens: `bash scripts/check_milestone_cut.sh X.Y.0` must exit 0, meaning 0 open issues **and** PRs. An item that does not ride this train is **carried by moving it**: `gh issue edit N --milestone <next>` (or `gh pr edit`) plus a `slipped_from: X.Y.0` comment. It is never carried by leaving it open here, because step 8 closes the milestone on `open_issues` (PMAT-3445, #3445). 2. **Bump.** `bash scripts/bump-version.sh X.Y.0`; `CHANGELOG.md` section from the epic's rows; the bump PR carries the receipt(s). 3. **Pre-publish dogfood — FULL.** `bash scripts/dogfood.sh --phase pre-publish` on the intel pool: the whole-workspace nextest line, GPU crates per-package, `aprender-compute`, the integration list, `make coverage-check` (`COV_FLOOR := 88`), `pv lint contracts/`, the PR-budget row (68-E1), the fleet-utilization row (68-C2), and the **release-assets row** (67-A1, on the *previous* tag until this one exists). NO-GO stops the train (§1.1). -4. **Merge the bump, tag, release.** `gh release create vX.Y.0 --target <main tip>`; `binary-release.yml` fires on `release: published`: 4 `apr` + 4 `pv` builds, verify-apr-assets, smoke-cuda on yoga and gx10. +4. **Merge the bump, re-read the milestone, tag, release.** Once the bump PR has merged, and immediately before the tag, run `bash scripts/check_milestone_cut.sh X.Y.0` again. Exit ≠ 0 → `STOP tag`, no tag. The script names every open item, and an item reopened after the freeze shows up here: v0.68.0 was tagged with #3091 reopened 85 min earlier. This is the binding read; step 1's read is the early one. Then `gh release create vX.Y.0 --target <main tip>`; `binary-release.yml` fires on `release: published`: 4 `apr` + 4 `pv` builds, verify-apr-assets, smoke-cuda on yoga and gx10. 5. **Verify assets by command, never by eye.** scripts/check_release_assets.sh vX.Y.0 (owed by 67-A1; until it lands, the §2 A acceptance query) — 8 apr files, 8 pv files. 6. **Cascade.** `scripts/check_publish_preflight.sh` then the crates.io drain (multi-pass, `[[feedback_crates_io_cascade_auth_config_traps]]`). 7. **Post-publish dogfood** on gx10 and yoga (+ intel): install the release asset (from 0.68 through the installer), `apr devices --json`, the parity protocol at c=1, the 7B story (67-C3; from 0.69 the recipe smoke), receipts under `evidence/dogfood/X.Y.0/<host>.json`. @@ -457,4 +457,5 @@ Invariants (checked by the contract's `obligation_dag_is_acyclic_and_forward`): | 2026-09-10 | operator: *"(add these to priorities)"* — three P0 five-whys tickets | rows 67-A1 (P0-1 #3082), 67-C2 (P0-2 #3083), 67-E1/E2 (P0-3 #3084) | | 2026-09-10 | infra#500 landed the same day (another session): disposable Docker runners `gx10-eph` / `yoga-eph` online; #3086 moves the CUDA asset build into `rust:1.93.0-bullseye` on them (glibc 2.31 floor) | G7 re-measured 12:30Z; §2 C "which runner" rule; 67-A1 CPU lane follows #3086; 67-D0 containment = ephemeral runner first, group restriction second | | 2026-09-10 | `agy /teamwork-preview` review, single lane, verdict FAIL (§10) | F1 G9/67-D0 re-measured; F2 G14; F3 remedy (smoke-cpu) with premise refuted; F4 67-E2 mirror-the-PR + over-cap workspace check + root-manifest FULL, R2; F5 §2 B rescoping stated; F6 67-F1 near-tie/defect rule | +| 2026-09-17 | #3445 (measured: v0.68.0 tagged 06:35Z with milestone 0.68.0 at open 2, #3091 reopened 05:10Z; the autopilot's tag step read no milestone) | §4 steps 1 and 4 read the milestone with `scripts/check_milestone_cut.sh` at the freeze and immediately before the tag. Carry = re-milestone + `slipped_from:`. Contract equation `milestone_settled_at_cut`, FALSIFY-REL-06X-009/010. Plan grilled 3/3 `do-not-implement-as-written` at v1, applied in v2 (`docs/audits/impl-PMAT-3445-plan.md`) | diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index 95a85330d2..cb18f6b3fb 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -110,10 +110,10 @@ Live `forjar.yaml` beats this table. Record the diff in the receipt and continue | Step | Action | Skip if | |---|---|---| -| **T-0 Cut** | cut sha = `main` HEAD; bump minor; `CHANGELOG` from merged PR titles since last tag | tag `v0.N.0` exists | +| **T-0 Cut** | **precondition, before the bump PR opens:** `bash scripts/check_milestone_cut.sh 0.N.0` exits 0, meaning the milestone holds 0 open issues and PRs. An item not riding the train is carried by moving it to the next milestone with a `slipped_from: 0.N.0` comment (§6.3 spill), never by leaving it open. Then cut sha = `main` HEAD; bump minor; `CHANGELOG` from merged PR titles since last tag | tag `v0.N.0` exists | | **T-1 Deep** | `deep` green on cut sha (**the check is `deep`, not `ci / deep`** — §4.3): full tests, doctests, `--no-default-features`, feature matrix (§4.1), GPU, every `cargo run --example` (§4.2) | green run recorded for this sha | | **T-2 Dogfood** | `apr-dogfood` skill go/no-go receipt; `apr-cookbook` current; release notes generated | receipt exists for this sha | -| **T-3 Promote** | tag; clean-room on the tag; GitHub release with T-2 receipt attached | release `v0.N.0` exists | +| **T-3 Promote** | **immediately before the tag, after the bump PR has merged:** `bash scripts/check_milestone_cut.sh 0.N.0` exits 0 again. Exit ≠ 0 is RED and there is no tag. The T-0 read is not enough, because an item reopened between the reads is invisible to it (v0.68.0 shipped with #3091 reopened 85 min before the tag, #3445). Then tag; clean-room on the tag; GitHub release with T-2 receipt attached | release `v0.N.0` exists | | **T-4 Publish** | **automated** — the autopilot runs `cascade-drain.sh` after T-3's preflight; record cascade wall minutes; attended minutes are 0 by construction | all crates at `0.N.0` on crates.io | | **T-5 Reconcile** | **hard gate (operator 2026-09-13: kaizen)** — the §6 reconcile predicates hold, receipt `docs/build-ledger/<date>/<sha>-reconcile.json` committed; the train has no `DONE` line without it | receipt exists for this sha and every predicate reads 0 | From dfe0852941cd2ddeaca8a170c8b5e2c21efca0bf Mon Sep 17 00:00:00 2001 From: Noah Gift <noah.gift@gmail.com> Date: Thu, 17 Sep 2026 15:45:23 +0200 Subject: [PATCH 4/6] PMAT-3445: implementation receipt and estimates row Verification table (claimed vs rerun), dispatch ledger with the 3/3 v1 grill and the superseded goal lane, the autopilot-tag rehearsal (STOP, 0 write calls), I-3 attempted=2 running_peak=2 slots=3, findings F-1..F-8. The untracked autopilot tag step is filed separately; it is named in the receipt. Pmat-Ticket: PMAT-3445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- docs/audits/impl-PMAT-3445-receipt.md | 117 ++++++++++++++++++++++++++ docs/audits/impl-estimates.jsonl | 1 + 2 files changed, 118 insertions(+) create mode 100644 docs/audits/impl-PMAT-3445-receipt.md diff --git a/docs/audits/impl-PMAT-3445-receipt.md b/docs/audits/impl-PMAT-3445-receipt.md new file mode 100644 index 0000000000..e04d19052f --- /dev/null +++ b/docs/audits/impl-PMAT-3445-receipt.md @@ -0,0 +1,117 @@ +# impl-PMAT-3445 receipt — the milestone being cut is read at the cut + +## Identity + +| Field | Value | +|---|---| +| ticket | PMAT-3445 (GitHub #3445, P1, milestone 0.69.0) | +| kind | code (`kind-gate.sh PMAT-3445 docs/roadmaps/roadmap.yaml --base origin/main` → exit 0, `kind=code`) | +| branch | `PMAT-3445-t0-milestone-gate`, cut from `origin/main` @ `dc6fb687c` (the #3448 merge that filed the ticket) | +| discover.json sha256 (prefix) | `f1120b4c9bc805a9` — `gate_cmd=make gate`, `gate_cmd_fallback=false`, `required_check=ci / gate,workspace-test`, `contracts_dir=contracts`, `quorum_tool=agy` | +| orchestrator model | admitted `opus` (`model-gate.sh` → `model=opus-5 class=opus decision=admit`). An earlier Fable session was refused by the same gate (`model fable-5-1 not permitted`) and made no edit; the operator relaunched on Opus | +| phase boundary | `phase-boundary: ticket=PMAT-3445 phase=2 orch_model=opus change=none recorded=0` | + +Status-line join (`[U]`, not measured in this run): statusLine `session_id` = hook `session_id`; `tasks[].id` = hook +`agent_id`; `transcript_path` present on subagentStatusLine stdin. NotRun, with no command executed for them. + +## What landed + +| File | Change | +|---|---| +| `scripts/check_milestone_cut.sh` | new gate: exit 0 only when the named milestone holds 0 open issues and PRs; 1 names each open item plus its carry/close remedy; 2 when it cannot judge | +| `docs/specifications/06x-release-schedule.md` | §4 step 1 reads the milestone at the freeze; step 4 re-reads it immediately before the tag (the binding read); §11 amendment row | +| `docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md` | §4 T-0 and T-3 rows carry the same two reads; carry = re-milestone plus `slipped_from:` | +| `contracts/release-schedule-06x-v1.yaml` | equation `milestone_settled_at_cut`, RS0-INV-007, FALSIFY-REL-06X-009/010, qa_gate check | +| `docs/audits/impl-PMAT-3445-plan.md` | plan v2 plus the v1 grill record | + +## Plan, routing, dispatch ledger + +| Phase | Route (`route.sh`, verbatim) | Trigger | Executor | Outcome | +|---|---|---|---|---| +| P0 plan grill | `route=agy-plan w=1.00 basis=absent effort=1[U]` | Q2 (plan artifact) | `paiml-agy-delegate` (opus), quorum `grillme` width 3, writes=false | 3/3 `do-not-implement-as-written` (gemini-3.1-pro-high, gemini-3.8-flash-high, gemini-3.7-flash-high; author family claude); agy conversations `d22ad04f…`, `e8b9932a…`, `8959b9a4…`; children=3; `--concurrent-scope scripts/check_milestone_cut.sh` on every lane; no BLIND, no KEPT | +| P1 gate | `route=agy-goal w=1.00 basis=absent note=fable-binding effort=1[U] bucket_collision=true` | — | `paiml-agy-delegate` (opus), goal width 1, writes=true → **superseded**, then **direct** | the delegate hit maxTurns (30) and was not resumed; its lane (conversation `ba5e4680…`, gemini-3.1-pro-high) committed `d14914db8` in its own worktree under v1 semantics; the reducer read NO-VERDICT (goal output has no verdict field). Not merged: v1 design rejected, and `gh api --paginate` without `--jq` writes concatenated arrays that `json.load` rejects above 100 items. Rewritten directly for v2 (deviation from R-4, recorded) | +| P2 wiring | `route=self` (orchestration-class edits to specs and contract) | — | direct | all acceptance commands exit 0 (below) | +| P3 rehearsal + pre-PR review | `route=self` / quorum via `quorum-review.sh` | Phase 4 | rehearsal direct; review = the artifact `docs/audits/quorum-PMAT-3445.json` | see that artifact | + +Slots and I-3: `transcript-gate.sh <session dir>` → `PASS transcript-gate: attempted=2 denied=0 stalled=0 running_peak=2 slots=3`. +(Run without the argument, it swept the worktree's project dir and reported a vacuous 0/0. Finding F-5.) + +## Verification — claimed vs re-run by the orchestrator + +| Acceptance | Lane/worker claim | Orchestrator rerun | +|---|---|---| +| `bash scripts/check_milestone_cut.sh --self-test` | goal lane: "exits 0" (v1 file, superseded) | exit 0, `self-test OK: 18 case(s).` (v2) | +| bare run = self-test | — | exit 0, same line | +| mutation (a) judge always passes | lane: "mutations turned self-test RED" (no output) | exit 1; RED S2 S3 S4 S12 S15 S16 | +| mutation (b) count cross-check deleted | — | exit 1; RED S6 S7 | +| mutation (c) `--paginate` dropped from the items read | — | exit 1; RED S15 | +| `bashrs lint --no-ignore --level error` | lane: "no errors" | exit 0 (plain lint: 0 errors, 26 warnings; precedent `check_reconcile.sh`: 0 errors, 76 warnings) | +| live `check_milestone_cut.sh 0.67.0` | lane: exit 0 | exit 0 — `0 open, 50 closed` | +| live `check_milestone_cut.sh 0.66.0` | — | exit 0 — `0 open, 10 closed` | +| live `check_milestone_cut.sh 0.68.0` (at `465cd96f2`) | lane: exit 1 naming #3091 | exit 1 — `#3091 issue [bug,P1]`, `#3450 pr [] release: 0.68.1` | +| live `check_milestone_cut.sh 0.69.0` | — | exit 1 — 48 open (milestone API `open_issues=48`) | +| `pv validate contracts/release-schedule-06x-v1.yaml` (pv pinned by `scripts/verifier_pin.sh`, 0.68.0) | lane 1: passes; lane 2: needs qa_gate wiring (both addressed) | exit 0, `0 error(s), 0 warning(s)`; `pv lint` PASS | +| FALSIFY-REL-06X-010 | — | exit 0 | +| `cargo test -p aprender-core --test readme_contract` | — | exit 0, 15 passed | +| `guard_tree.sh --dry-run --no-cargo` | — | prints `run: scripts/check_milestone_cut.sh` | +| `check_guards_are_wired.sh`, `check_no_timing_in_required.sh`, `check_assertions_exclude.sh`, `check_pass_grep_anchored.sh`, `check_sourced_libs_option_neutral.sh`, `check_bashrs_gate.sh` | lanes 2+3 predicted wired-guard refusal for a `release_tag.sh` (dropped) | all exit 0 | +| `cargo fmt --all -- --check`, `cargo test -p aprender-contracts --lib`, `cargo deny check advisories` | — | exit 0, exit 0 (1538 passed), exit 0 (`advisories ok`) | +| `make gate` (`gate_cmd`) | — | **NotRun locally**; CI's required checks `ci / gate` and `workspace-test` are the gate of record for this PR | + +### Rehearsal (the ticket's falsifier) + +A copy of `rel-068-1-autopilot/autopilot.sh:103-112` (the `tag` block, verbatim) with +`bash scripts/check_milestone_cut.sh "$MS_TITLE" … || die …` as its first line, against milestone 0.68.0. +`git tag`/`git push`/`gh release` were stubbed to a call log; every other `git`/`gh` call went to the real binaries. + +- exit 1; STATUS: `STOP milestone 0.68.0 is not settled: #3450 pr [] release: 0.68.1;` +- write calls logged: **0**; `git ls-remote --tags origin 'v0.68.99*'` → 0 refs. +- #3091 was not named because between the `465cd96f2` read and the rehearsal, #3091 was **carried**: + `2026-09-17T13:40:30Z demilestoned 0.68.0` / `milestoned 0.70.0` (issue events). This came after this run's + warning on #3450. The rehearsal therefore ran on a real milestone holding exactly one open item and named it. + +## Jidoka + +| Phase | Defect | Owner | Whys | +|---|---|---|---| +| P0 | plan v1 admitted carried items that stay open, and proposed a dark `release_tag.sh` | PMAT-3445 (this plan) | the close step closes on `open_issues` → a comment-based carry leaves it non-zero → the plan read the ticket's "or carries `slipped_from:`" as a gate clause instead of the 06x step 1 re-milestone practice → the untracked autopilot was not read before planning → fixed in v2 by re-milestoning | + +No gate went red in P1 or P2 after v2. `.pmat/jidoka.jsonl` is not a tracked file in this repository, so this row is its record. + +## Estimates + +K̂ = 24 (`basis=first-run[U]`). `estimate.sh aprender 3` exits ENV: 47 measured rows, none enters a total. +The ticket body's own Ĵ = 24 turns `[A]` was borrowed. K = 48. Actual: `k_measured` = 88 distinct main-thread +assistant message ids at receipt time, over the whole session, including the refused Fable turns before the +goal was declared. The overrun came from P0 (grill 3/3 against v1), the superseded goal lane, and the redesign. +Row appended to `docs/audits/impl-estimates.jsonl`. + +## Findings and gaps + +- **F-1 (gap, filed #3454, milestone 0.70.0).** The tag decision surface is an untracked per-train autopilot + copy. This PR puts the read into both specs and the contract; no running autopilot calls it. The live 0.68.1 + train (pid 3021358, `autopilot.sh 3450 wait dryrun`) was warned on #3450. +- **F-2 (consequence).** Milestone 0.69.0 held 48 open items at the last read, and its cut is due + 2026-09-17T18:00Z. Where the gate is called, that cut stops until they are closed or carried. +- **F-3.** `pmat hooks install --strict --force` fails in a linked worktree (`Not a directory (os error 20)`, + `.git` is a file). Commits ran the shared `core.hooksPath` pre-commit and carry `Pmat-Ticket:` by hand. +- **F-4.** The edit hook keys `active-ticket` by the session's cwd repository (`…/124170554`), not by the + edited file's repository (`discover.sh --state-dir` in the worktree gives `…/3607376464`). +- **F-5.** `transcript-gate.sh` with no argument resolved the worktree's project dir, not the session's, and + reported a vacuous 0/0. With the explicit session dir: attempted=2, running_peak=2. +- **F-6.** The goal-lane delegate hit maxTurns 30 while polling its lane. `lane-reduce` marks a goal lane + NO-VERDICT/partial because the goal schema carries `outcome`, not `verdict`. +- **F-8 (environment, not this diff).** `guard_tree.sh --no-cargo` on this host: 62 checks, 2 FAIL, both + `tool_version`. `scripts/cb200_baseline.txt` and `scripts/complexity_baseline.txt` were recorded under + pmat 3.40.1, and this host runs pmat 3.40.2. `check_complexity_ratchet.sh` measured base `dc6fb687c` = 673 + and merge `ffc68fe47` = 673 functions over threshold, delta +0, same `.rs` universe. This PR changes no + `.rs` file. Classified as a host instrument drift; CI's fleet pmat is the comparand of record. +- **F-7.** A peer session reported this worktree's base as predating #3448. Measured: + `git merge-base --is-ancestor <#3448 merge> HEAD` → true. + +## Verdict + +`DONE` requires: merged green on `ci / gate` + `workspace-test`, and the quorum artifact +`docs/audits/quorum-PMAT-3445.json` agreed. At this commit the local gates above PASS, and the mutation was +observed RED. The `pv` contract and the invalidated spec claims ride in the same PR. Merge state is read from +the PR, not from this file. diff --git a/docs/audits/impl-estimates.jsonl b/docs/audits/impl-estimates.jsonl index 636f7d46c7..f3f6f6b577 100644 --- a/docs/audits/impl-estimates.jsonl +++ b/docs/audits/impl-estimates.jsonl @@ -46,3 +46,4 @@ {"repo":"aprender","ticket":"PMAT-989","phase":"design-quorum","mode":"delegate(quorum:3)","est":5,"actual":1,"basis":"est=first-run[U]; actual=one delegate dispatch (74,787 tokens), record docs/audits/pp-066-r0-design-quorum.md"} {"repo":"aprender","ticket":"PMAT-989","phase":"P_1-P_4","mode":"direct","est":5,"actual":22,"basis":"est=first-run[U]; actual=orchestrator bash calls (RED table, registry, apr devices + catalogue, contracts)"} {"repo":"aprender","ticket":"PMAT-989","phase":"P3-review","mode":"delegate(quorum:3)+direct","est":5,"actual":4,"basis":"est=first-run[U]; actual=orchestrator bash calls applying the 3/3 mergeable-with-changes verdict (dedup, cuda reasons, strict schema, registry-sourced overrides)"} +{"repo":"aprender","ticket":"PMAT-3445","phase":"all","mode":"delegate(quorum:3,goal:1)+direct","est":24,"actual":88,"unit":"turn","basis":"est=issue #3445 body J=24 [A] borrowed (estimate.sh ENV: 0 of 47 rows pool); actual=k_measured distinct main-thread assistant message ids, whole session incl. refused Fable turns"} From b5a09b0ba0ce50f07b73f79019046eb975c87bcd Mon Sep 17 00:00:00 2001 From: Noah Gift <noah.gift@gmail.com> Date: Thu, 17 Sep 2026 16:01:47 +0200 Subject: [PATCH 5/6] =?UTF-8?q?PMAT-3445:=20admit=20the=20train's=20own=20?= =?UTF-8?q?release=20epic=20=E2=80=94=20it=20is=20open=20at=20every=20cut?= =?UTF-8?q?=20by=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found at pre-merge review (delegate-measured, orchestrator re-measured): each train's release epic sits in its own milestone (06x §5) and is closed at §4 step 8, after publish — #3078 was open in milestone 0.67.0 at the v0.67.0 tag (tagged 07:35Z, epic closed 09:14Z). The binding pre-tag read would have been RED at every cut. The gate now admits exactly one item: an issue labelled `epic` whose title is literally `EPIC: release train <M>` followed by end or whitespace, printed as ADMITTED; two claimants exit 2. A topic epic, a PR, an unlabelled look-alike and another train's epic stay ordinary open items. Self-test 25 checks (S17-S23 added). Mutations on scratch copies: (d) admission removed -> RED S17 S22 S23 (e) admission widened to any `epic` label -> RED S18 S20 S21 (a)(b)(c) still RED (a: 11 checks, b: S6 S7, c: S15) Live: 0.69.0 -> 1, ADMITTED #3080 + 24 other open; 0.70.0 -> 1, ADMITTED #3081 + 28; 0.67.0/0.66.0 -> 0. pv validate 0 errors, pv lint PASS, readme_contract 15 passed, FALSIFY-010 exit 0, check_no_claim_literals 0. 06x §4 step 1 no longer claims step 8 "closes on open_issues" (step 8 does not say so); the refusal is attributed to the train driver. Pmat-Ticket: PMAT-3445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- contracts/release-schedule-06x-v1.yaml | 11 +-- docs/audits/impl-PMAT-3445-plan.md | 10 +++ docs/audits/impl-PMAT-3445-receipt.md | 29 ++++++- docs/specifications/06x-release-schedule.md | 6 +- .../APR-RELEASE-001-train-and-build-kaizen.md | 2 +- scripts/check_milestone_cut.sh | 82 +++++++++++++++---- 6 files changed, 113 insertions(+), 27 deletions(-) diff --git a/contracts/release-schedule-06x-v1.yaml b/contracts/release-schedule-06x-v1.yaml index bbd047d6c3..2c6f36d747 100644 --- a/contracts/release-schedule-06x-v1.yaml +++ b/contracts/release-schedule-06x-v1.yaml @@ -67,13 +67,14 @@ equations: - "A blocked row carries no date of its own — its expiry is its latest blocker's train" milestone_settled_at_cut: - formula: "for every train vX.Y.0: at the freeze (before the bump PR opens) and immediately before `git tag` (after it merges), open_issues(milestone X.Y.0) == 0, where open_issues counts issues AND pull requests; read by `scripts/check_milestone_cut.sh X.Y.0`" + formula: "for every train vX.Y.0: at the freeze (before the bump PR opens) and immediately before `git tag` (after it merges), open(milestone X.Y.0) \\ {the train's release epic} == ∅, where open counts issues AND pull requests and the release epic is the single issue labelled `epic` titled `EPIC: release train X.Y.0 …`; read by `scripts/check_milestone_cut.sh X.Y.0`" domain: "the GitHub milestone the train cuts, read live at each of the two instants" codomain: "exit code of scripts/check_milestone_cut.sh: 0 settled, 1 open items named, 2 cannot judge" invariants: - "An item that does not ride the train is carried by moving it to the next milestone with a `slipped_from: X.Y.0` comment, never by leaving it open (§4 step 8 closes on open_issues)" - "The pre-tag read is binding: an item reopened after the freeze is visible only there (v0.68.0 shipped with #3091 reopened 85 min before the tag, #3445)" - - "An empty milestone, an unresolvable or duplicate title, or a read that disagrees with open_issues is exit 2, never a pass" + - "An empty milestone, an unresolvable or duplicate title, a read that disagrees with open_issues, or two open items claiming to be the release epic is exit 2, never a pass" + - "The release epic is admitted, never silently: it is printed as ADMITTED; a topic epic, a PR, an unlabelled look-alike or another train's epic is an ordinary open item" proof_obligations: - id: RS0-INV-001 @@ -108,8 +109,8 @@ proof_obligations: applies_to: train_leaves_in_window - id: RS0-INV-007 type: invariant - property: "No tag is cut while its milestone holds an open issue or pull request" - formal: "scripts/check_milestone_cut.sh X.Y.0 exit 0 immediately before `git tag vX.Y.0`; its self-test exits 0 and each of its three recorded mutations turns it RED" + property: "No tag is cut while its milestone holds an open issue or pull request other than the train's own release epic" + formal: "scripts/check_milestone_cut.sh X.Y.0 exit 0 immediately before `git tag vX.Y.0`; its self-test exits 0 and each of its five recorded mutations turns it RED" applies_to: milestone_settled_at_cut falsification_tests: @@ -163,7 +164,7 @@ falsification_tests: - id: FALSIFY-REL-06X-009 rule: "The milestone gate can fail and does not pass vacuously (PMAT-3445)" - prediction: "check_milestone_cut.sh --self-test exits 0: an open issue, an open PR, 150 open items, a lost or stale count, an empty milestone, a missing or duplicate or regex-lookalike title, and an unpaginated live read are each judged as their case table says" + prediction: "check_milestone_cut.sh --self-test exits 0: an open issue, an open PR, 150 open items, a lost or stale count, an empty milestone, a missing or duplicate or regex-lookalike title, an unpaginated live read, the release epic (admitted), and a topic epic, an unlabelled look-alike, another train's epic, an epic-titled PR and two claimants (not admitted) are each judged as their case table says" test: "bash scripts/check_milestone_cut.sh --self-test" if_fails: "The cut gate no longer refuses what it exists to refuse, and a tag can again be cut over an open P1 (v0.68.0, #3091)" diff --git a/docs/audits/impl-PMAT-3445-plan.md b/docs/audits/impl-PMAT-3445-plan.md index b8db4c018d..35dce5ec44 100644 --- a/docs/audits/impl-PMAT-3445-plan.md +++ b/docs/audits/impl-PMAT-3445-plan.md @@ -144,3 +144,13 @@ paginate every read; add the metacharacter, zero-item and pagination cases; run bump PR is not open. A width-1 goal lane had meanwhile implemented v1 (commit `d14914db8`, lane worktree only). It was superseded, not merged: v1 semantics, and `gh api --paginate` without `--jq` writes concatenated arrays that `json.load` rejects above 100 items. That would be an exit 2 on 0.69.0 itself. + +## 7. Amendment v2.1 — pre-merge review + +The review delegate found that each train's release epic (06x §5: label `epic`, milestone X.Y.0) is closed +at §4 step 8, after publish. #3078 was open in milestone 0.67.0 at the v0.67.0 tag. v2's rule (0 open items) +would therefore have read RED at every cut. v2.1 admits exactly one item: an issue labelled `epic` whose title +is literally `EPIC: release train <M>` followed by end or whitespace. It is printed as `ADMITTED`, and two +claimants exit 2. Cases S17–S23; mutations (d) admission removed → S17 S22 S23 RED, (e) admission widened to +any `epic` label → S18 S20 S21 RED. + diff --git a/docs/audits/impl-PMAT-3445-receipt.md b/docs/audits/impl-PMAT-3445-receipt.md index e04d19052f..473d537bc0 100644 --- a/docs/audits/impl-PMAT-3445-receipt.md +++ b/docs/audits/impl-PMAT-3445-receipt.md @@ -18,7 +18,7 @@ Status-line join (`[U]`, not measured in this run): statusLine `session_id` = ho | File | Change | |---|---| -| `scripts/check_milestone_cut.sh` | new gate: exit 0 only when the named milestone holds 0 open issues and PRs; 1 names each open item plus its carry/close remedy; 2 when it cannot judge | +| `scripts/check_milestone_cut.sh` | new gate: exit 0 only when the named milestone holds no open issue or PR except the train's own release epic (printed `ADMITTED`); 1 names each open item plus its carry/close remedy; 2 when it cannot judge | | `docs/specifications/06x-release-schedule.md` | §4 step 1 reads the milestone at the freeze; step 4 re-reads it immediately before the tag (the binding read); §11 amendment row | | `docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md` | §4 T-0 and T-3 rows carry the same two reads; carry = re-milestone plus `slipped_from:` | | `contracts/release-schedule-06x-v1.yaml` | equation `milestone_settled_at_cut`, RS0-INV-007, FALSIFY-REL-06X-009/010, qa_gate check | @@ -40,16 +40,20 @@ Slots and I-3: `transcript-gate.sh <session dir>` → `PASS transcript-gate: att | Acceptance | Lane/worker claim | Orchestrator rerun | |---|---|---| -| `bash scripts/check_milestone_cut.sh --self-test` | goal lane: "exits 0" (v1 file, superseded) | exit 0, `self-test OK: 18 case(s).` (v2) | +| `bash scripts/check_milestone_cut.sh --self-test` | goal lane: "exits 0" (v1 file, superseded) | exit 0, `self-test OK: 25 case(s).` (v2 + release-epic rule) | | bare run = self-test | — | exit 0, same line | -| mutation (a) judge always passes | lane: "mutations turned self-test RED" (no output) | exit 1; RED S2 S3 S4 S12 S15 S16 | +| mutation (a) judge always passes | lane: "mutations turned self-test RED" (no output) | exit 1; RED S2 S3 S4 S12 S15 S16 S18 S19 S20 S21 S23 | | mutation (b) count cross-check deleted | — | exit 1; RED S6 S7 | | mutation (c) `--paginate` dropped from the items read | — | exit 1; RED S15 | +| mutation (d) release-epic admission removed | — | exit 1; RED S17 S22 S23 | +| mutation (e) admission widened to any `epic` label | — | exit 1; RED S18 S20 S21 | | `bashrs lint --no-ignore --level error` | lane: "no errors" | exit 0 (plain lint: 0 errors, 26 warnings; precedent `check_reconcile.sh`: 0 errors, 76 warnings) | | live `check_milestone_cut.sh 0.67.0` | lane: exit 0 | exit 0 — `0 open, 50 closed` | | live `check_milestone_cut.sh 0.66.0` | — | exit 0 — `0 open, 10 closed` | | live `check_milestone_cut.sh 0.68.0` (at `465cd96f2`) | lane: exit 1 naming #3091 | exit 1 — `#3091 issue [bug,P1]`, `#3450 pr [] release: 0.68.1` | -| live `check_milestone_cut.sh 0.69.0` | — | exit 1 — 48 open (milestone API `open_issues=48`) | +| live `check_milestone_cut.sh 0.69.0` | — | at `465cd96f2`: exit 1, 48 open. After the epic rule: exit 1, `ADMITTED #3080` (release epic), 24 other open | +| live `check_milestone_cut.sh 0.70.0` | — | exit 1, `ADMITTED #3081`, 28 other open | +| `check_no_claim_literals.sh` (a review lane saw "universe EMPTY" in its sandbox) | — | exit 0 on the tree | | `pv validate contracts/release-schedule-06x-v1.yaml` (pv pinned by `scripts/verifier_pin.sh`, 0.68.0) | lane 1: passes; lane 2: needs qa_gate wiring (both addressed) | exit 0, `0 error(s), 0 warning(s)`; `pv lint` PASS | | FALSIFY-REL-06X-010 | — | exit 0 | | `cargo test -p aprender-core --test readme_contract` | — | exit 0, 15 passed | @@ -106,6 +110,23 @@ Row appended to `docs/audits/impl-estimates.jsonl`. pmat 3.40.1, and this host runs pmat 3.40.2. `check_complexity_ratchet.sh` measured base `dc6fb687c` = 673 and merge `ffc68fe47` = 673 functions over threshold, delta +0, same `.rs` universe. This PR changes no `.rs` file. Classified as a host instrument drift; CI's fleet pmat is the comparand of record. +- **F-9 (defect found at pre-merge review, fixed in this PR).** The review delegate measured, and the + orchestrator re-measured, that each train's release epic sits in its own milestone and is closed after + publish. #3078 (milestone 0.67.0) was closed 2026-09-13T09:14Z, and v0.67.0 was tagged 07:35Z. As first written, + the binding pre-tag read would therefore have been RED at every cut. The gate now admits exactly that one item: + an issue, labelled `epic`, whose title is literally `EPIC: release train <M>` followed by end or whitespace. It + prints the item as `ADMITTED`; two claimants exit 2. Cases S17–S23 and mutations (d) and (e) cover it. The + 0.67.0/0.66.0 first-green reads were taken after those milestones closed, not at their cut instants. That limit + is stated here and not claimed away. +- **F-10 (rail defect, measured by the review delegate from agy transcripts).** In the `quorum-review.sh` + round at `dfe085294` (3/3 PASS, 0 findings), lanes 2 and 3 read lane 1's verdict file through the + script's `$WORK` path before finishing (lane 2 at 13:49:04Z, lane 3 at 13:49:16Z). Independence held for + lane 1 only. All three returned a `tree_witness` copied from another session's lane clone, and the rail + stages and checks no witness. Lane 1 opened no file. That round's 3/3 is recorded here as one + uncontaminated PASS plus two PASSes cast after seeing it. It also missed F-9, which the delegate found. The + round is superseded by the round on the fixed head. +- **F-11.** 06x §4 step 1 originally said step 8 "closes the milestone on `open_issues`". Step 8 says only + "close the milestone". The text now attributes the `open_issues` refusal to the train driver. - **F-7.** A peer session reported this worktree's base as predating #3448. Measured: `git merge-base --is-ancestor <#3448 merge> HEAD` → true. diff --git a/docs/specifications/06x-release-schedule.md b/docs/specifications/06x-release-schedule.md index 113dd0eaf6..d4354e7659 100644 --- a/docs/specifications/06x-release-schedule.md +++ b/docs/specifications/06x-release-schedule.md @@ -281,10 +281,10 @@ The milestone already holds 15 issues; with the 14 rows above the train is over Numbered so an autopilot (`/mnt/nvme-raid0/agent-wt/rel-066-autopilot/` is the 0.66 precedent) can run it fail-closed, with a `STOP <step>` line naming the refusing gate. -1. **Freeze the platform.** `gh pr list --state open --label <train>`: rows still open move to the next epic with `slipped_from:`. No new pushes while the queue drains (`[[feedback_push_cadence_starves_the_merge_queue]]`). Then read the milestone, before the bump PR opens: `bash scripts/check_milestone_cut.sh X.Y.0` must exit 0, meaning 0 open issues **and** PRs. An item that does not ride this train is **carried by moving it**: `gh issue edit N --milestone <next>` (or `gh pr edit`) plus a `slipped_from: X.Y.0` comment. It is never carried by leaving it open here, because step 8 closes the milestone on `open_issues` (PMAT-3445, #3445). +1. **Freeze the platform.** `gh pr list --state open --label <train>`: rows still open move to the next epic with `slipped_from:`. No new pushes while the queue drains (`[[feedback_push_cadence_starves_the_merge_queue]]`). Then read the milestone, before the bump PR opens: `bash scripts/check_milestone_cut.sh X.Y.0` must exit 0, meaning no open issue **or** PR except this train's own release epic (§5), which is open until step 8 by design and is named as `ADMITTED`. An item that does not ride this train is **carried by moving it**: `gh issue edit N --milestone <next>` (or `gh pr edit`) plus a `slipped_from: X.Y.0` comment. It is never carried by leaving it open here, because step 8 closes the milestone, and the train driver refuses to close one whose `open_issues` is not 0 (PMAT-3445, #3445). 2. **Bump.** `bash scripts/bump-version.sh X.Y.0`; `CHANGELOG.md` section from the epic's rows; the bump PR carries the receipt(s). 3. **Pre-publish dogfood — FULL.** `bash scripts/dogfood.sh --phase pre-publish` on the intel pool: the whole-workspace nextest line, GPU crates per-package, `aprender-compute`, the integration list, `make coverage-check` (`COV_FLOOR := 88`), `pv lint contracts/`, the PR-budget row (68-E1), the fleet-utilization row (68-C2), and the **release-assets row** (67-A1, on the *previous* tag until this one exists). NO-GO stops the train (§1.1). -4. **Merge the bump, re-read the milestone, tag, release.** Once the bump PR has merged, and immediately before the tag, run `bash scripts/check_milestone_cut.sh X.Y.0` again. Exit ≠ 0 → `STOP tag`, no tag. The script names every open item, and an item reopened after the freeze shows up here: v0.68.0 was tagged with #3091 reopened 85 min earlier. This is the binding read; step 1's read is the early one. Then `gh release create vX.Y.0 --target <main tip>`; `binary-release.yml` fires on `release: published`: 4 `apr` + 4 `pv` builds, verify-apr-assets, smoke-cuda on yoga and gx10. +4. **Merge the bump, re-read the milestone, tag, release.** Once the bump PR has merged, and immediately before the tag, run `bash scripts/check_milestone_cut.sh X.Y.0` again. The release epic is the one admitted open item, and it is closed at step 8. Exit ≠ 0 → `STOP tag`, no tag. The script names every open item, and an item reopened after the freeze shows up here: v0.68.0 was tagged with #3091 reopened 85 min earlier. This is the binding read; step 1's read is the early one. Then `gh release create vX.Y.0 --target <main tip>`; `binary-release.yml` fires on `release: published`: 4 `apr` + 4 `pv` builds, verify-apr-assets, smoke-cuda on yoga and gx10. 5. **Verify assets by command, never by eye.** scripts/check_release_assets.sh vX.Y.0 (owed by 67-A1; until it lands, the §2 A acceptance query) — 8 apr files, 8 pv files. 6. **Cascade.** `scripts/check_publish_preflight.sh` then the crates.io drain (multi-pass, `[[feedback_crates_io_cascade_auth_config_traps]]`). 7. **Post-publish dogfood** on gx10 and yoga (+ intel): install the release asset (from 0.68 through the installer), `apr devices --json`, the parity protocol at c=1, the 7B story (67-C3; from 0.69 the recipe smoke), receipts under `evidence/dogfood/X.Y.0/<host>.json`. @@ -457,5 +457,5 @@ Invariants (checked by the contract's `obligation_dag_is_acyclic_and_forward`): | 2026-09-10 | operator: *"(add these to priorities)"* — three P0 five-whys tickets | rows 67-A1 (P0-1 #3082), 67-C2 (P0-2 #3083), 67-E1/E2 (P0-3 #3084) | | 2026-09-10 | infra#500 landed the same day (another session): disposable Docker runners `gx10-eph` / `yoga-eph` online; #3086 moves the CUDA asset build into `rust:1.93.0-bullseye` on them (glibc 2.31 floor) | G7 re-measured 12:30Z; §2 C "which runner" rule; 67-A1 CPU lane follows #3086; 67-D0 containment = ephemeral runner first, group restriction second | | 2026-09-10 | `agy /teamwork-preview` review, single lane, verdict FAIL (§10) | F1 G9/67-D0 re-measured; F2 G14; F3 remedy (smoke-cpu) with premise refuted; F4 67-E2 mirror-the-PR + over-cap workspace check + root-manifest FULL, R2; F5 §2 B rescoping stated; F6 67-F1 near-tie/defect rule | -| 2026-09-17 | #3445 (measured: v0.68.0 tagged 06:35Z with milestone 0.68.0 at open 2, #3091 reopened 05:10Z; the autopilot's tag step read no milestone) | §4 steps 1 and 4 read the milestone with `scripts/check_milestone_cut.sh` at the freeze and immediately before the tag. Carry = re-milestone + `slipped_from:`. Contract equation `milestone_settled_at_cut`, FALSIFY-REL-06X-009/010. Plan grilled 3/3 `do-not-implement-as-written` at v1, applied in v2 (`docs/audits/impl-PMAT-3445-plan.md`) | +| 2026-09-17 | #3445 (measured: v0.68.0 tagged 06:35Z with milestone 0.68.0 at open 2, #3091 reopened 05:10Z; the autopilot's tag step read no milestone) | §4 steps 1 and 4 read the milestone with `scripts/check_milestone_cut.sh` at the freeze and immediately before the tag. Carry = re-milestone + `slipped_from:`. The train's own release epic (§5, closed at step 8) is the one admitted open item: the review measured #3078 open in milestone 0.67.0 at the v0.67.0 tag. Contract equation `milestone_settled_at_cut`, FALSIFY-REL-06X-009/010. Plan grilled 3/3 `do-not-implement-as-written` at v1, applied in v2 (`docs/audits/impl-PMAT-3445-plan.md`) | diff --git a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md index cb18f6b3fb..53e31ac9e6 100644 --- a/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md +++ b/docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md @@ -110,7 +110,7 @@ Live `forjar.yaml` beats this table. Record the diff in the receipt and continue | Step | Action | Skip if | |---|---|---| -| **T-0 Cut** | **precondition, before the bump PR opens:** `bash scripts/check_milestone_cut.sh 0.N.0` exits 0, meaning the milestone holds 0 open issues and PRs. An item not riding the train is carried by moving it to the next milestone with a `slipped_from: 0.N.0` comment (§6.3 spill), never by leaving it open. Then cut sha = `main` HEAD; bump minor; `CHANGELOG` from merged PR titles since last tag | tag `v0.N.0` exists | +| **T-0 Cut** | **precondition, before the bump PR opens:** `bash scripts/check_milestone_cut.sh 0.N.0` exits 0, meaning the milestone holds no open issue or PR except this train's own release epic. That epic is closed at the end of the train, and the script names it as admitted. An item not riding the train is carried by moving it to the next milestone with a `slipped_from: 0.N.0` comment (§6.3 spill), never by leaving it open. Then cut sha = `main` HEAD; bump minor; `CHANGELOG` from merged PR titles since last tag | tag `v0.N.0` exists | | **T-1 Deep** | `deep` green on cut sha (**the check is `deep`, not `ci / deep`** — §4.3): full tests, doctests, `--no-default-features`, feature matrix (§4.1), GPU, every `cargo run --example` (§4.2) | green run recorded for this sha | | **T-2 Dogfood** | `apr-dogfood` skill go/no-go receipt; `apr-cookbook` current; release notes generated | receipt exists for this sha | | **T-3 Promote** | **immediately before the tag, after the bump PR has merged:** `bash scripts/check_milestone_cut.sh 0.N.0` exits 0 again. Exit ≠ 0 is RED and there is no tag. The T-0 read is not enough, because an item reopened between the reads is invisible to it (v0.68.0 shipped with #3091 reopened 85 min before the tag, #3445). Then tag; clean-room on the tag; GitHub release with T-2 receipt attached | release `v0.N.0` exists | diff --git a/scripts/check_milestone_cut.sh b/scripts/check_milestone_cut.sh index 90bcb1ff8d..e3a5c7dc19 100644 --- a/scripts/check_milestone_cut.sh +++ b/scripts/check_milestone_cut.sh @@ -11,9 +11,14 @@ # reopened after it is invisible; the autopilot's tag step read no milestone # at all, and its only read was the close step, after publish. # -# THE RULE: exit 0 only when open_issues == 0. Pull requests count because -# the milestone is closed on open_issues and GitHub counts them there. An item -# that does not ride this train is CARRIED by moving it to the next milestone +# THE RULE: exit 0 only when the milestone holds no open item except the +# train's own release epic. Pull requests count: GitHub counts them in +# open_issues, and the train driver refuses to close a milestone while that +# number is not 0. The release epic (06x section 5: label `epic`, title +# `EPIC: release train X.Y.0 ...`) is open at every cut by design -- it is +# closed at section 4 step 8, after publish -- so it is the one ADMITTED item, +# named in the output; two claimants are exit 2. Any other item that does not +# ride this train is CARRIED by moving it to the next milestone # (`gh issue edit N --milestone <next>`, or `gh pr edit`) with a # `slipped_from: X.Y.0` comment -- never by leaving it open in this one. # @@ -93,25 +98,44 @@ for it in items: if it.get("state") != "open" or (it.get("milestone") or {}).get("number") != number: env("listed item #%s is not an open item of milestone #%s" % (it.get("number"), number)) -rows = [] -for it in sorted(items, key=lambda i: i.get("number", 0)): - rows.append({ +def row(it): + return { "number": it.get("number"), "kind": "pr" if "pull_request" in it else "issue", "title": it.get("title", ""), "labels": [lb.get("name", "") for lb in (it.get("labels") or [])], "url": it.get("html_url", ""), - }) + } + +# The OWN release epic of the train (06x section 5: label epic, milestone X.Y.0) is +# closed at section 4 step 8, after publish, so it is open at every cut by +# design. It is the one admitted item: an issue, labelled epic, whose title is +# literally "EPIC: release train <title>" followed by the end or whitespace. +# Two claimants are ambiguous and widen nothing: exit 2. +epic_prefix = "EPIC: release train " + title +def is_release_epic(r): + rest = r["title"][len(epic_prefix):] + return (r["kind"] == "issue" and "epic" in r["labels"] and r["title"].startswith(epic_prefix) + and (rest == "" or rest[0].isspace())) + +all_rows = [row(it) for it in sorted(items, key=lambda i: i.get("number", 0))] +admitted = [r for r in all_rows if is_release_epic(r)] +if len(admitted) > 1: + env("%d open items claim to be the release epic of %s: %s" % (len(admitted), title, " ".join("#%s" % r["number"] for r in admitted))) +rows = [r for r in all_rows if not is_release_epic(r)] verdict = "RED" if rows else "PASS" if json_out: with open(json_out, "w", encoding="utf-8") as f: json.dump({"milestone": title, "number": number, "open": open_n, "closed": closed_n, - "items": rows, "verdict": verdict}, f, indent=2, sort_keys=True) + "admitted": admitted, "items": rows, "verdict": verdict}, f, indent=2, sort_keys=True) f.write("\n") +for r in admitted: + print("ADMITTED #%s %s [%s] %s -- the release epic of this train, closed at 06x section 4 step 8" + % (r["number"], r["kind"], ",".join(r["labels"]), r["title"])) if not rows: - print("PASS milestone %s (#%s): 0 open, %d closed -- the cut may proceed" % (title, number, closed_n)) + print("PASS milestone %s (#%s): 0 open besides its release epic, %d closed -- the cut may proceed" % (title, number, closed_n)) sys.exit(0) for r in rows: @@ -197,14 +221,14 @@ st_ms() { printf '{"title":"%s","number":%s,"open_issues":%s,"closed_issues":%s}\n' "$1" "$2" "$3" "$4" } -# st_item NUMBER MILESTONE_NUMBER KIND STATE LABEL -- one items.jsonl line +# st_item NUMBER MILESTONE_NUMBER KIND STATE LABEL [TITLE] -- one items.jsonl line st_item() { pr="" if [ "$3" = pr ]; then pr=',"pull_request":{"url":"u"}' fi - printf '{"number":%s,"state":"%s","milestone":{"number":%s},"title":"item %s","labels":[{"name":"%s"}]%s}\n' \ - "$1" "$4" "$2" "$1" "$5" "$pr" + printf '{"number":%s,"state":"%s","milestone":{"number":%s},"title":"%s","labels":[{"name":"%s"}]%s}\n' \ + "$1" "$4" "$2" "${6:-item $1}" "$5" "$pr" } self_test() { @@ -213,7 +237,7 @@ self_test() { # S1 zero open, some closed -> PASS st_ms M 7 0 5 > "${fx}/milestones.jsonl" : > "${fx}/items.jsonl" - st_judge "$fx" S1 0 M "PASS milestone M (#7): 0 open, 5 closed" + st_judge "$fx" S1 0 M "PASS milestone M (#7): 0 open besides its release epic, 5 closed" # S2 one open issue -> RED, named st_ms M 7 1 5 > "${fx}/milestones.jsonl" @@ -276,6 +300,36 @@ self_test() { done st_judge "$fx" S12 1 M "#1000 issue" "#1149 issue" "150 open item(s)" + # S17 the train's own release epic is the one admitted open item, and it is named + st_ms M 7 1 5 > "${fx}/milestones.jsonl" + st_item 20 7 issue open epic "EPIC: release train M — schedule" > "${fx}/items.jsonl" + st_judge "$fx" S17 0 M "ADMITTED #20 issue [epic] EPIC: release train M" "0 open besides its release epic" + + # S18 an epic of something else (a topic epic) is an ordinary open item + st_item 21 7 issue open epic "EPIC: PP-TENSOR-001 — tensors" > "${fx}/items.jsonl" + st_judge "$fx" S18 1 M "#21 issue [epic] EPIC: PP-TENSOR-001" + + # S19 the release title without the epic label is not admitted + st_item 22 7 issue open bug "EPIC: release train M — schedule" > "${fx}/items.jsonl" + st_judge "$fx" S19 1 M "#22 issue [bug]" + + # S20 another train's epic is not this train's (prefix trap: M0 is not M) + st_item 23 7 issue open epic "EPIC: release train M0 — schedule" > "${fx}/items.jsonl" + st_judge "$fx" S20 1 M "#23 issue [epic] EPIC: release train M0" + + # S21 a pull request is never the release epic + st_item 24 7 pr open epic "EPIC: release train M — schedule" > "${fx}/items.jsonl" + st_judge "$fx" S21 1 M "#24 pr [epic]" + + # S22 two claimants are ambiguous and widen nothing + st_ms M 7 2 5 > "${fx}/milestones.jsonl" + { st_item 20 7 issue open epic "EPIC: release train M — a"; st_item 25 7 issue open epic "EPIC: release train M — b"; } > "${fx}/items.jsonl" + st_judge "$fx" S22 2 M "2 open items claim to be the release epic of M" + + # S23 the release epic beside a real open item: the item is RED, the epic is still named as admitted + { st_item 20 7 issue open epic "EPIC: release train M — a"; st_item 10 7 issue open P1; } > "${fx}/items.jsonl" + st_judge "$fx" S23 1 M "ADMITTED #20" "#10 issue [P1] item 10" "1 open item(s)" + # S16 --json records the verdict and the items st_ms M 7 1 5 > "${fx}/milestones.jsonl" st_item 10 7 issue open bug > "${fx}/items.jsonl" @@ -354,7 +408,7 @@ main() { exit $? fi case "$1" in - -h|--help) sed -n '2,33p' "$SELF_PATH"; exit 0 ;; + -h|--help) sed -n '2,/^$/p' "$SELF_PATH"; exit 0 ;; -*) usage ;; esac From fdbf19c7366c215099e1b2d9427d5cc9e63c25d2 Mon Sep 17 00:00:00 2001 From: Noah Gift <noah.gift@gmail.com> Date: Thu, 17 Sep 2026 16:14:39 +0200 Subject: [PATCH 6/6] =?UTF-8?q?PMAT-3445:=20quorum=20verdict=20=E2=80=94?= =?UTF-8?q?=203/3=20PASS=20at=20b5a09b0ba?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge-rail artifact from quorum-review.sh round 2 (gemini-3.1-pro-high, gemini-3.8-flash-high, gemini-3.7-flash-high; author family claude). Commit touches only the verdict so it speaks for its parent. Round 1 at dfe085294 is superseded: it missed the release-epic defect and two of its lanes read lane 1 first (receipt F-9, F-10). Pmat-Ticket: PMAT-3445 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- docs/audits/quorum-PMAT-3445.json | 150 ++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/audits/quorum-PMAT-3445.json diff --git a/docs/audits/quorum-PMAT-3445.json b/docs/audits/quorum-PMAT-3445.json new file mode 100644 index 0000000000..f2eed84748 --- /dev/null +++ b/docs/audits/quorum-PMAT-3445.json @@ -0,0 +1,150 @@ +{ + "ticket": "PMAT-3445", + "base": "origin/main", + "base_resolved": "origin/main", + "base_note": "no origin/origin/main exists; judged against the local ref", + "head": "b5a09b0ba0ce50f07b73f79019046eb975c87bcd", + "diff_sha256": "a3162818e5cd28656e5a9f2da79148fb3783ff0f43e48ad23e523d0f85c37b2a", + "width": 3, + "executor": "agy", + "prompt_mode": "inline", + "prompt_bytes": 68913, + "author": { + "model": "claude-opus-5", + "family": "claude", + "source": "flag" + }, + "agreed": true, + "lanes": [ + { + "lane": 1, + "status": "SUCCESS", + "verdict": "PASS", + "summary": "The diff implements exactly what PMAT-3445 requires: a script to verify that a release milestone has no open items (except the release epic) and integrates it into the build/release specifications and the release contract. The new `scripts/check_milestone_cut.sh` properly checks the open PRs/issues and handles the epic exception. The `docs/specifications/06x-release-schedule.md` and `APR-RELEASE-001-train-and-build-kaizen.md` have been properly updated to call this script at freeze and prior to the tag cut. The contract includes the new constraints and tests correctly. The self-test logic effectively tests the required edge cases as stated in the receipt. I found no evidence that the gate is weakened, tests assert the opposite, or claims are unsubstantiated. The implementation is well-tested and documented.", + "findings": [], + "raw_bytes": 2994, + "err_bytes": 0, + "envelope_status": "SUCCESS", + "verdict_source": "structured_output", + "grounding_check": "parity", + "model": "gemini-3.1-pro-high", + "model_measured": "gemini-3.1-pro-high", + "model_source": "measured", + "family": "gemini" + }, + { + "lane": 2, + "status": "SUCCESS", + "verdict": "PASS", + "summary": "Independent Review (Lane 2 of 3) for ticket PMAT-3445:\n\nVERDICT: PASS.\n\n1. Ticket Alignment:\nThe diff directly addresses PMAT-3445 (\"release train: a tag was cut while its milestone had open issues — 0.68.0 shipped with 2 open, one a user-faci\") by adding `scripts/check_milestone_cut.sh` as an automated gate checking that the milestone has no open issues or pull requests at the instant of the cut (except the train's own release epic, which is closed post-publish at step 8). The gate is integrated into `docs/specifications/06x-release-schedule.md` (§4 steps 1 and 4), `docs/specifications/APR-RELEASE-001-train-and-build-kaizen.md` (§4 T-0 and T-3), and `contracts/release-schedule-06x-v1.yaml` (equation `milestone_settled_at_cut`, invariant `RS0-INV-007`, falsification tests `FALSIFY-REL-06X-009/010`, and `qa_gate.checks`).\n\n2. Refutation Criteria Checks:\n- Test asserting opposite of ticket: None. All 25 self-test cases in `check_milestone_cut.sh` assert that unsettled open issues/PRs, count discrepancies, ambiguous epics, or missing milestones fail the cut (exit 1 or exit 2).\n- Gate weakened: None. Contract quality gate and release specifications were strengthened with mandatory pre-freeze and pre-tag checks.\n- Receipt claim not backed by diff: None. All files, specifications, equations, and test cases claimed in the receipt are present in the diff.\n- Unasked changes: None. All diff changes are strictly scoped to the ticket requirements and audit trails.\n\n3. Measured Verifications:\n- `bash scripts/check_milestone_cut.sh --self-test` (and bare run): exit 0, 25 cases passed.\n- All 5 mutations verified RED:\n (a) Judge always passes: exit 1 (S2, S3, S4, S12, S15, S16, S18, S19, S20, S21, S23 RED)\n (b) Count cross-check deleted: exit 1 (S6, S7 RED)\n (c) `--paginate` dropped: exit 1 (S15 RED)\n (d) Release-epic admission removed: exit 1 (S17, S22, S23 RED)\n (e) Admission widened to any epic: exit 1 (S18, S20, S21 RED)\n- `bashrs lint --no-ignore --level error`: exit 0 (0 issues found).\n- `pv validate contracts/release-schedule-06x-v1.yaml` & `pv lint`: exit 0, 0 errors, 0 warnings (PASS).\n- `cargo test -p aprender-core --test readme_contract`: exit 0 (15 passed).\n- `cargo test -p aprender-contracts --lib`: exit 0 (1538 passed).\n- `cargo deny check advisories`: exit 0 (`advisories ok`).\n- `cargo fmt --all -- --check`: exit 0.\n\nNo refuting findings found.", + "findings": [], + "raw_bytes": 6204, + "err_bytes": 0, + "envelope_status": "SUCCESS", + "verdict_source": "structured_output", + "grounding_check": "parity", + "model": "gemini-3.8-flash-high", + "model_measured": "gemini-3.8-flash-high", + "model_source": "measured", + "family": "gemini" + }, + { + "lane": 3, + "status": "SUCCESS", + "verdict": "PASS", + "summary": "Reviewed PMAT-3445 diff against ticket requirements and receipt. The diff introduces scripts/check_milestone_cut.sh to prevent cutting tags when a milestone contains open issues or pull requests, updates the release specifications (06x-release-schedule.md and APR-RELEASE-001) to make pre-tag checks binding, updates the release schedule contract (release-schedule-06x-v1.yaml), and records the audit plan, receipt, and estimates. All 25 self-test cases and 5 mutation test suites pass as claimed. No refuting flaws found. Verdict: PASS.", + "findings": [ + { + "claim": "check_milestone_cut.sh self-test passes all 25 test cases cleanly in both --self-test mode and bare invocation.", + "file": "scripts/check_milestone_cut.sh", + "grounding": "measured", + "line": 236 + }, + { + "claim": "Mutations (a) judge always passes, (b) count cross-check deleted, (c) unpaginated read, (d) release-epic admission removed, and (e) widened epic admission each turn the self-test RED on the exact expected cases.", + "file": "scripts/check_milestone_cut.sh", + "grounding": "measured", + "line": 116 + }, + { + "claim": "FALSIFY-REL-06X-010 passes: both release specifications (06x-release-schedule.md §4 and APR-RELEASE-001 §4) wire check_milestone_cut.sh into the freeze (T-0) and immediately before tag (T-3).", + "file": "contracts/release-schedule-06x-v1.yaml", + "grounding": "measured", + "line": 171 + }, + { + "claim": "The gate strictly admits only the train's own release epic (an issue with label `epic` and title prefixed by `EPIC: release train <M>`); any other open item or duplicate epic claimants cause exit 1 or exit 2 respectively.", + "file": "scripts/check_milestone_cut.sh", + "grounding": "cited", + "line": 117 + } + ], + "raw_bytes": 4720, + "err_bytes": 0, + "envelope_status": "SUCCESS", + "verdict_source": "structured_output", + "grounding_check": "parity", + "model": "gemini-3.7-flash-high", + "model_measured": "gemini-3.7-flash-high", + "model_source": "measured", + "family": "gemini" + } + ], + "dissent": [], + "dedup": [ + { + "file": "contracts/release-schedule-06x-v1.yaml", + "line": 171, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "FALSIFY-REL-06X-010 passes: both release specifications (06x-release-schedule.md §4 and APR-RELEASE-001 §4) wire check_milestone_cut.sh into the freeze (T-0) and immediately before tag (T-3)." + ] + }, + { + "file": "scripts/check_milestone_cut.sh", + "line": 116, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "Mutations (a) judge always passes, (b) count cross-check deleted, (c) unpaginated read, (d) release-epic admission removed, and (e) widened epic admission each turn the self-test RED on the exact expected cases." + ] + }, + { + "file": "scripts/check_milestone_cut.sh", + "line": 117, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "The gate strictly admits only the train's own release epic (an issue with label `epic` and title prefixed by `EPIC: release train <M>`); any other open item or duplicate epic claimants cause exit 1 or exit 2 respectively." + ] + }, + { + "file": "scripts/check_milestone_cut.sh", + "line": 236, + "lanes_agreeing": [ + 3 + ], + "claims": [ + "check_milestone_cut.sh self-test passes all 25 test cases cleanly in both --self-test mode and bare invocation." + ] + } + ], + "uncovered": [], + "coverage_source": "lanes", + "partial": false, + "partial_reasons": [], + "auto_merge": { + "checked": true, + "was_armed": false, + "disarmed": false, + "note": "auto-merge not armed" + }, + "lint": { + "ok": true, + "output": "receipt complete: kind=artifact lanes=3 author=claude-opus-5/claude" + } +}