PMAT-3459: the tag step cannot be reached without a clean milestone — gate inside cut_tag(), 3 mutants killed - #3617
Conversation
Closes the exit criterion #3459 is actually about. v0.68.1 was tagged 2026-09-17 15:06:29Z by an autopilot copy living OUTSIDE the repository, six minutes after #3455 merged the milestone gate; `grep -c check_milestone_cut autopilot.sh` was 0. The spec required the read and no code path that cuts a tag made it. THE GATE IS INSIDE THE FUNCTION THAT TAGS. cut_tag() calls scripts/check_milestone_cut.sh ahead of `git tag`, and there is no path through it that reaches `git tag` with the gate unsatisfied. Both non-zero codes are fail-closed and they are DIFFERENT failures, named separately: rc 0 -> MILESTONE-GATE <v> clean at the cut; the tag is cut rc 1 -> the milestone holds open item(s): no tag, no publish rc 2+ -> the gate could not judge: no tag. Unknown is not a pass. THE GUARD IS BEHAVIOURAL, NOT A grep. "The file mentions the gate" is satisfied by a comment. scripts/check_tag_step_gated.sh EXTRACTS cut_tag() from the autopilot, runs it against a stubbed gate once per outcome, and asserts on whether a GIT-TAG appears in the transcript. Its --self-test then builds three mutants and requires each to be RED, and the real subject to be GREEN so a red subject cannot masquerade as a killed mutant: mutant 1 gate call deleted (the #3459 defect, restored) -> RED, 2 failing rows mutant 2 gate verdict discarded with `|| true` -> RED mutant 3 cut_tag() removed entirely -> ENV rc=2, never a pass That covers #3459's three required mutations: gate call removed => RED, gate exit 1 => no tag and no publish, gate exit 2 => no tag. contracts/tag-step-milestone-gate-v1.yaml (kind: pattern) lands in the same PR, per the ticket. `pv validate` and `pv lint` both PASS, and every one of its six falsification tests was RUN, not assumed -- which is how FALSIFY-TSMG-006 was caught asserting `test -x` on a file the repo deliberately keeps non-executable (guard_tree.sh invokes its whole universe as `bash scripts/...`, so the executable bit is not the property). The test was wrong, not the file; corrected to `test -f`. WIRING, with no .github/workflows edit and no operator check-in: guard_tree.sh's universe is exactly `git ls-files 'scripts/check_*.sh'`, a flat glob, so the guard runs in the guard-tree job -- and `ci / gate` reads needs.guard-tree.result, so it is gated by the required check transitively. #3459 notes that wiring a self-test literally inside the `ci` job would be a workflow edit; this route does not need one. SEC011 ("missing validation for 'd' before rm -rf") was real in the new guard and is fixed with ONE validated rmtree() used by every call site, not suppressed and not repeated per site. Gates, fresh worktree off origin/main 00e5d2d -- all 12 rc=0: check_tag_step_gated.sh and --self-test · check_hardcoded_paths.sh --full-if-capable check_bashrs_gate.sh and --self-test · check_apr_bin_pinned.sh check_no_pipe_into_grep_q.sh · check_shell_lint_ratchet.sh · check_guards_are_wired.sh check_contract_enforcement.sh · check_contract_test_binding.sh check_sourced_libs_option_neutral.sh NOT in this PR, deliberately: V/T/MS/EPIC and AP are still 0.68.2's. Parameterising them is its own row -- mixing it here would make the gate change unreviewable, which is the lesson this ticket has been teaching all day. The hazard is stated in the PR body: the hardcoded-path ratchet does NOT match /mnt (#3592, proved with a committed two-path probe scoring +1 not +2), so AP=/mnt/... passes every gate we own and must be changed by hand with nothing to remind anyone. Refs #3080, #3445, #3455, #3592 Pmat-Ticket: PMAT-3459 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
§13.11 rung 1 — quorum shadow verdict Shadow mode: this records a verdict and merges nothing. A refusal |
Found while answering #3459's exit criterion 4. The self-test added in the previous commit was DISCOVERED BY NOTHING and therefore ran in no job at all: before: PASS scripts/check_tag_step_gated.sh [run] after: PASS scripts/check_tag_step_gated.sh [self-test] PASS scripts/check_tag_step_gated.sh [run] guard_tree.sh decides a guard "advertises --self-test" by looking for the literal substring `self-test` in that guard's OWN `--help` output (guard_tree.sh:47, advertises_self_test). check_tag_step_gated.sh had no --help handler, so it got one row instead of two and its three mutants were never executed by CI. The mutation proof existed and nothing ran it -- a facility with a self-test and no caller, in the commit that added the self-test. It is the same class this ticket keeps producing, and it was invisible to reading: the guard works, its self-test works, and the wiring between them did not exist. The fix is a --help handler printing the header comment, which is the idiom the rest of the tree uses (ci_run_explicit_test_commands.sh:147 and others). Verified by running guard_tree.sh --no-cargo, not by reading it: 66 checks, 0 failed, with both rows present. Refs #3080, #3459 Pmat-Ticket: PMAT-3459 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mutation proof on a real run — #3620 (draft, never merged)One line deleted from
Exactly the two rows of this guard and no other — the mutant broke nothing else in the tree. The mutant is more instructive than the design argument. With the call deleted, The general form: a substring test is satisfied by the documentation of the thing it looks for. The more carefully a function explains what it enforces, the more strings it leaves behind for a grep to find after the enforcement is deleted. That is why this guard runs |
…README regenerated Fixes the three REAL reds on this PR's first run (the rest were env: gx10 ENOSPC and an intel "dep info ... No such file" mid-build, #2822 class). 1. sigma gate (shard 3/3, lint_passes_on_real_contracts): two errors. - `proof_obligations[1].formal` carried an em-dash, which Σ does not declare. - formal_prose rose 1464 -> 1467: three `formal:` entries with no Σ symbol, against a shrink-only baseline. `prose: true` exempts the glyph check but NOT the ratchet (sigma_symbols.rs:107 counts before :110 checks the flag), so the fix is real formals, not a marker: ∀ rc ∈ {1, 2}: cut_tag(v, t, mc) with gate(v) = rc ⇒ ¬ tagged(t) ∧ gate(v) = 0 ⇒ tagged(t) ∀ m ∈ {gate_call_deleted, verdict_discarded, cut_tag_removed}: guard(m) ≠ 0 ∧ guard(subject) = 0 scripts/release/autopilot.sh ∈ tracked(main) ∧ cut_tag ∈ autopilot.sh The shell commands that were there belong in falsification_tests, where they already are. This is a better contract, not just a passing one. My local `pv lint` had reported "not armed: sigma" and I read that as clean. CI's test arms it. The CLAUDE.md pre-push checklist names `cargo test -p aprender-contracts --lib`, which reproduces CI exactly; I did not run it before the first push. Run now: 1666 passed, 0 failed. 2. the_tracked_repo_graph_is_fresh (shard 1/3): contracts/contracts.nt lacked the new contract's 5 triples. `pv extract contracts` with the pinned binary. 3. README claims must match measurement (guard-cargo): contracts/census.json said 1799, HEAD carries 1800. `pv census contracts --format json > contracts/census.json` (the Makefile's form -- bare `pv census` prints and writes nothing) and `scripts/readme_sync.sh --write`, CONTRACT_COUNT now 1800. All three derivatives produced with `. scripts/pv_bin.sh` (pv 0.68.2 from this tree's target dir), never a bare pv. Each of the three failing CI tests reproduced locally and re-run green. Refs #3080, #3459, #3569 Pmat-Ticket: PMAT-3459 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mutation proof — the required context, on the same run
Chain, all on head #3620 closed and its branch deleted; the run URLs above are the durable record. |
…ilestone-cut # Conflicts: # contracts/census.json
#3617 went CONFLICTING after #3614 landed: #3581 and this branch each regenerated contracts/census.json, contracts/contracts.nt and README.md for their own contract additions, and git saw two edits to the same generated bytes. Resolved the only correct way for a derived file: merge origin/main (taking either side on the conflict, since it is overwritten next), then REGENERATE all three from the merged tree with the pinned pv (scripts/pv_bin.sh, pv 0.68.2) -- never by hand. A hand-merged contracts.nt is a derived file no derivation produced. Only census.json hard-conflicted. contracts.nt and README.md would have auto-merged line-wise, which is the worse case: combined derived output that happens to apply cleanly. Regenerated anyway and compared: contracts.nt is byte-identical to what git produced (15605 lines both ways) -- verified, not assumed. census 1800 -> 1801 (main's one plus this branch's one); README CONTRACT_COUNT 1801. cargo test -p aprender-contracts --lib 1666 passed, 0 failed the_tracked_repo_graph_is_fresh ok check_readme_claims.sh rc=0 check_tag_step_gated.sh --self-test rc=0 check_pr_ont_delta.sh on the merged file set PASS Refs #3080, #3459, #3569 Pmat-Ticket: PMAT-3459 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Quorum routing (AD-04, Phase 1 step 4), stated by the author — Q1 FIRES and no quorum has run. Disarm pending a verdict. Files:
No receipt at |
|
quorum-review (AD-04): three PASS — agreed (auto_merge: checked=true was_armed=false disarmed=false) {
"ticket": "PMAT-3459",
"head": "b0e4be8e8072af119f936cdb903d37e1a78384c8",
"width": 3,
"executor": "agy",
"agreed": true,
"auto_merge": {
"checked": true,
"was_armed": false,
"disarmed": false,
"note": "auto-merge not armed"
},
"lanes": [
{
"lane": 1,
"verdict": "PASS",
"findings": 4
},
{
"lane": 2,
"verdict": "PASS",
"findings": 0
},
{
"lane": 3,
"verdict": "PASS",
"findings": 0
}
]
} |
docs/audits/quorum-PMAT-3459.json from quorum-review.sh --base origin/main --pr 3617 --author-model claude-opus-5, executor agy, width 3, judged at head b0e4be8: lane 1 gemini-3.1-pro-high PASS 4 findings (all confirmations, 2 cited / 2 asserted) lane 2 gemini-3.8-flash-high PASS lane 3 gemini-3.7-flash-high PASS dissent: none Independence inspected, not assumed: every lane file (lane-{1,2,3}.{json,err}) has zero references to a sibling lane, to $WORK or to the .lanes/ dir -- the recorded failure where a 3/3 was one vote read through $WORK. Three models, none in the author's family. This receipt was owed before arming and was not there: the PR was armed through arm_pr_automerge.sh, which asserts guard-tree on the head and by its own header says nothing about review. Pulled from the merge queue, judged, re-armed on this receipt. The ticket text the lanes judged against came from #3461's registration (PMAT-3459's fragment lives only on that unmerged branch); it was placed in the working tree for `pmat work status` and never staged, so the diff the lanes judged is base...HEAD unchanged -- the six files this PR carries. Pmat-Ticket: PMAT-3459 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AD-04 quorum: 3/3 PASS, three independent lanes
dissent: none. Independence inspected: zero references to a sibling lane, Ticket text for the lanes came from #3461's registration (PMAT-3459 is on that unmerged branch only), placed in the working tree for |
Cross-inspection of
|
| lane | conversation | status | verdict | findings | refs to sibling lanes/receipts | turns |
|---|---|---|---|---|---|---|
| lane 1 | 9079b207 |
SUCCESS | PASS | 4 (2 cited) | 0 | 1 turns / 243.522966875 s |
| lane 2 | 775c7e2e |
SUCCESS | PASS (structured_output) | 0 (0 cited) | 0 | 1 turns / 311.915992261 s |
| lane 3 | c81a9fcd |
SUCCESS | PASS | 0 (0 cited) | 0 | 1 turns / 204.140183204 s |
Distinct agy conversation ids: 3/3. No lane's response references another lane's file or any quorum-PMAT-*.json. Each verdict is grounded in its own findings (file:line). Read from /mnt/nvme-raid0/agent-wt/pmat3459-milestone/docs/audits/quorum-PMAT-3459.json.lanes on this box; the receipt on the branch records agreed: true, three PASS, models gemini-3.1-pro-high / 3.8-flash-high / 3.7-flash-high, author claude-opus-5 (flag).
Verdict line: 3/3 PASS, independent. Arming.
Table corrected 23:12Z: two cells first read "None" because my parser took response instead of structured_output; the receipt's verdict_source is structured_output for all three lanes and all three are PASS there. #3617 lane 2 opens with an interim sentence ("running in the background…") but its structured summary records the guard run (check_tag_step_gated.sh rc=0, --self-test rc=0, 3 mutants killed) — a completed review, not a premature PASS.
…ted, the three contract derivatives regenerated under the pinned pv Adopted from aprender-d4 (session unreachable; handoff on #3395). Conflicts were README.md and contracts/census.json (plus roadmap.yaml's aggregate): #3617 added contracts/tag-step-milestone-gate-v1.yaml to main (census 1800 -> 1801) after this branch was cut. Resolution: main's copies, then regenerated together with the pv built from this tree at HEAD (`. scripts/pv_bin.sh`, pv 0.68.2): `"$PV" census contracts --format json > contracts/census.json`, `"$PV" extract contracts`, `make readme-sync`; `make roadmap-aggregate` (75 fragments, aggregate --check idempotent). Mechanical, measured: the branch's own delta at its merge-base 237fbc3 was +24 contracts / +96 triples (1800 -> 1824, 15600 -> 15696); the merged tree is main + exactly that (1801 -> 1825, 15605 -> 15701). README CONTRACT_COUNT = 1825 in both blocks. The judged diff (docs/audits/quorum-PMAT-3401.json at 9f59beb) is unchanged in intent; the receipt stands. Gates: four roadmap guards PASS, check_readme_claims PASS, cargo test -p aprender-contracts --lib 1666 passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This is the exit criterion #3459 is actually about: the tag step calls
scripts/check_milestone_cut.shand cannot be reached without it.v0.68.1was tagged 2026-09-17T15:06:29Z by an autopilot copy living outside the repository — six minutes after #3455 merged the milestone gate.grep -c check_milestone_cut autopilot.shwas 0. The spec required the read; no code path that cuts a tag made it.The gate is inside the function that tags
cut_tag()calls the gate ahead ofgit tag. There is no path through it that reachesgit tagwith the gate unsatisfied, and the two non-zero codes are different failures, named separately:MILESTONE-GATE <v> clean at the cut— the tag is cutThe guard is behavioural, not a grep
"The file mentions the gate" is satisfied by a comment.
scripts/check_tag_step_gated.shextractscut_tag()from the autopilot and runs it against a stubbed gate, once per outcome, asserting on whether aGIT-TAGappears in the transcript.Its
--self-testthen builds mutants and requires each to be RED and the real subject to be GREEN, so a red subject cannot masquerade as a killed mutant:Mutant 1 is the #3459 defect restored — it reconstructs the tree that tagged v0.68.1 and requires the guard to refuse it. That is the difference between a guard and a decoration.
This covers the ticket's three required mutations: gate call removed ⇒ RED · gate exit 1 ⇒ no tag, no publish · gate exit 2 ⇒ no tag.
The contract
contracts/tag-step-milestone-gate-v1.yaml,kind: pattern, in the same PR as the ticket asks.pv validate→ 0 errors, 0 warnings, contract is valid;pv lint→ PASS, 8 armed.Every one of its six falsification tests was run, not assumed — which is how
FALSIFY-TSMG-006was caught assertingtest -xon a file the repo deliberately keeps non-executable (guard_tree.shinvokes its whole universe asbash scripts/..., so the executable bit is not the property). The test was wrong, not the file; corrected totest -f. A falsification test that fails for the wrong reason is worse than none.The self-test was running nowhere — found while answering criterion 4
The previous commit added a
--self-testwith three killed mutants. No job executed it. Measured withguard_tree.sh --no-cargo, not read:guard_tree.shdecides a guard advertises--self-testby looking for that literal substring in the guard's own--helpoutput (guard_tree.sh:47,advertises_self_test). This guard had no--helphandler, so it got one row and its three mutants were executed by nothing — a facility with a self-test and no caller, in the commit that added the self-test. Invisible to reading: the guard worked, the self-test worked, and the wiring between them did not exist. Fixed with the tree's own idiom (ci_run_explicit_test_commands.sh:147and others).guard_tree.sh --no-cargo: 66 checks, 0 failed, both rows present.Criterion 4 — what this satisfies, and the gap it does not close
The chain is now live and a broken self-test reddens a required check:
Worth recording, because both of us had it wrong: the required set is two mechanisms, not one. Classic branch protection lists
["ci / gate","workspace-test"]; the rulesets additionally requiregate. An earlier revision of this body claimedci / gatereadsneeds.guard-cargo.result— it does not.ci / gateis jobgate:at line 1536 ofpaiml/.github'ssovereign-ci.ymland aggregates only[test, lint, coverage, security, provenance]. The job that reads the guards is aprender's owngate.This satisfies criterion 4's effect but not its base-definedness, and that distinction is the point of the bullet.
ci.yml:66pinssovereign-ci.yml@70e51ec045d95983e9a3d02342d13283bf5f9dc1— sha-pinned, in another repo, immutable by the PR under review. Everything in aprender'sci.yml,guard-treeandgatealike, is head-defined: a PR can edit it. That is precisely why the PR-receipt check was moved out ofgate(PP-066 C0-5, PRQ-013, #2982):So moving this self-test into
gatewould re-introduce the property that job was deliberately cleaned of, and buy nothing —guard-treeis equally head-defined and already reddensgate. Closing the base-definedness gap needs an input hook inpaiml/.github'ssovereign-ci.yml, which has none today and is a cross-repo row. Recorded on #3459 rather than left in a PR body.Nothing under
.github/workflows/is touched by this PR.Gates — all 12 rc=0
Fresh worktree off
origin/main(00e5d2dde):check_tag_step_gated.sh+--self-test·check_hardcoded_paths.sh --full-if-capable·check_bashrs_gate.sh+--self-test·check_apr_bin_pinned.sh·check_no_pipe_into_grep_q.sh·check_shell_lint_ratchet.sh·check_guards_are_wired.sh·check_contract_enforcement.sh·check_contract_test_binding.sh·check_sourced_libs_option_neutral.shSEC011was real in the new guard and is fixed with one validatedrmtree()used by every call site — not suppressed, and not the check copy-pasted per site.⚠ What is NOT in this PR, and the hazard nothing catches
V/T/MS/EPICandAP=/mnt/nvme-raid0/agent-wt/rel-0682-autopilotare still 0.68.2's. Parameterising them is its own row — mixing it here would make the gate change unreviewable, which is the lesson this ticket has been teaching all day.The hardcoded-path ratchet does not match
/mnt(#3592, proved with a committed two-path probe scoring +1, not +2). SoAP=/mnt/...passes every gate we own, and it has to be changed by hand with nothing to remind anyone. This paragraph is the only guard that exists for it.keep-open: #3459 — this PR satisfies two of its four stated exit criteria: all three mutations RED, and the
kind: patterncontract in the same PR. Criterion 1 cannot be satisfied by any pull request — "Thev0.69.0tag is cut by the in-repo path" happens when the train runs. Criterion 4 (self-test insideci / gate) is not satisfied either: the issue measured on #3455 that aguard_tree.shself-test is not in that context, and satisfying it needs a.github/workflows/*edit, which is an operator check-in. Parameterisation is not an exit criterion — that was my own addition and it is withdrawn; it is #3618. #3080 is the parent epic; #3592 is its own row. Refs #3080, #3445, #3455, #3592, #3618.ont-delta: none — no ontology type, shape or known-red list touched. The changes to
contracts/census.json(1799→1800),contracts/contracts.nt(+5 triples) and READMECONTRACT_COUNTare the regenerated derivatives of adding onekind: patterncontract, produced with the pinnedpv(scripts/pv_bin.sh), not ontology edits.🤖 Generated with Claude Code