Conversation
`peak_rss_bytes` was false on Windows. A witness settles it rather than a manual page: a child that commits 256 MiB and never touches a page is reported as 262.5 MiB by the job object's PeakProcessMemoryUsed, while on Linux nothing becomes resident. The job object counts COMMITTED memory — job and process memory limits are defined on committed virtual memory — and `ru_maxrss` counts RESIDENT pages. One field name claimed both. The same witness refuted the other half of the suspicion. `wait4` on the immediate child does NOT stop at the shell: shell -> heavy child and shell -> shell -> heavy child both return the descendant's 260 MiB against 13 MiB for a light control, so the POSIX path already measures the tree it should. So the quantity now travels with the number. Each sample and each cell carries `memory_metric`, drawn from a closed set — `max_process_peak_resident` on the wait4 and `/usr/bin/time -v` paths, `max_process_peak_commit` on the Windows job object — and a value produced under any other kind raises rather than being recorded. No downstream reader infers semantics from `sys.platform`, and no alias lets Windows commit go on being read as RSS. Renamed with it, because the old nouns lied in the same way: `peak_rss` -> `peak_memory`, `rss_mechanism` -> `memory_mechanism`, `rss_unavailable_reason` -> `memory_unavailable_reason`, `raw_peak_rss_bytes` -> `raw_peak_memory_bytes`, `RssProbe` -> `MemoryProbe`. Deliberately NOT done: Windows resident-set sampling. `PeakWorkingSetSize` is the resident analogue, but obtaining it for a tree of processes that come and go needs handle tracking or polling, and this instrument does not sample. Two operating systems answering different, honest questions beats a profiler added so they can pronounce the same noun. This moves `measurement_harness_digest` 562a7f7232da -> 104c384d01bf. The bindings that name the old digest are re-bound in the next commit; nothing here tries to preserve it. perf instrument controls 16/16, round 7 apparatus controls 10/10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The memory-semantics repair moved `measurement_harness_digest`
562a7f7232dad2f4c79c6adfe0e1e7e25680b4b6f3444d54824bf0405e3c14b3 before
104c384d01bf6060bdec1e7c916053ddb04b97fcbd0b39f8a4fc57b8f139672f after
and three controls said so, each by name: `freeze-harness-untouched`,
`training-prereg-bindings` and `envcapture-frozen-untouched`. That is the
provenance system delivering its inconvenience on purpose, so nothing here tries
to preserve the old digest.
Re-bound: the digest in the policy freeze, in the ratified design constants, and
in the training preregistration's bindings — and with it
`design_constants_blob_sha1`, which moved because the design-constants artifact
itself was re-bound. A chain of bindings re-bound in the order the chain runs.
Not touched: the committed evidence of runs that actually happened. The sizing,
calibration and round-7 datasets were produced by the old instrument and record
what it produced; rewriting them would be forging a record, not re-accepting a
binding. They carry `peak_rss` because that is the field the instrument had when
they were taken.
This is a re-acceptance of the step-4/5/6 bindings, not a code change; it is a
separate commit so it can be reviewed as one. Controls after re-binding:
calibration constants 4/4, calibration freeze 7/7, calibration policy 10/10,
perf instrument 16/16, round 7 apparatus 10/10, step 7 environment capture
11/11, training preregistration 9/9.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The section was called "Peak RSS" and described two mechanisms as though they produced one number. They do not: POSIX `ru_maxrss` is a peak resident set over the waited-for descendant chain, and the Windows job object's `PeakProcessMemoryUsed` is peak committed memory over the processes of the job. The witness that settles it is recorded here rather than left in a session log — 256 MiB committed and never touched reads as 262.5 MiB on Windows and as nothing resident on Linux — as is the half of the suspicion the same witness refuted: `wait4` does not stop at a wrapping shell. Recorded with it: the strata answer different honest questions and may not be pooled across platforms; Windows resident sampling was considered and refused because this instrument does not sample; and the digest moved 562a7f7232da -> 104c384d01bf, with steps 4/5/6 re-bound in their own commit and the committed evidence of past runs deliberately left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Two artifacts, because they answer two questions. Qualification: does this one environment satisfy the T0-7 predicate. Execution binding: which Linux and Windows environments, which candidates and which instrument form THIS campaign. A utility that checks a CPU governor must not become the root of campaign identity, so `hostqual.py` neither issues nor owns the binding. `hostqual.py` splits evidence into the two classes that are not the same thing. DECLARED provisioning — dedication, absence of other users, hypervisor configuration — is content-addressed, shape-checked and never called machine proof: a guest OS cannot establish what the hypervisor is doing. MACHINE-OBSERVED is what a checker actually asserts: `manifest.provenance.ci == false` (the field is always present, so a predicate demanding its absence could never be met), `performance` governor on every applicable CPU plus an identified turbo mechanism, the Windows active plan with AC processor state pinned at 100/100, and the stratum's memory mechanism actually existing on the host. Quiesce is implemented exactly, not approximately: twelve 5 s intervals over the final minute, `/proc/stat` on Linux and `GetSystemTimes` on Windows, mean below 5 % and no interval above 20 %. A missing sample, a counter that went backwards or a zero denominator is NOT_ELIGIBLE rather than a skipped interval — an unreadable machine is not a quiet machine. A quiesce failure starts no clock, so it is not INVALID and costs no retry budget. A qualification is not a certificate of perpetual quiet. Session eligibility is proved again per session, against a fresh manifest, and the record names the execution binding, the qualification and that manifest — so a qualified host that this campaign never bound cannot be substituted into the middle of it. `execbinding.py` carries references and identity, never copies: duplicating the governor or the provisioning blob would create two copies of one fact, and two copies drift. It requires both strata, refuses an unqualified or mislabelled host, refuses a binding whose strata share one memory metric, and refuses to overwrite an existing binding — a rebuild before the first clock is legitimate but never silent. Both platform branches were driven live, not only by fixture. On Windows the first live run found a defect in this tool's own powercfg parse: it read the range's minimum as the current setting and reported 0 % on a machine pinned at 100 %. The block ends with the two current indices, AC then DC, and everything before them describes the possible range. Fixed here, with the labels never parsed — on this Russian Windows every one of them is localized. On Linux the tool correctly refuses WSL, where no cpufreq governor exists. step 7 host qualification controls: 10 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The mini-review found a hole in the middle of a chain that was otherwise content-addressed end to end: a qualification said "this host satisfies T0-7" without naming WHICH T0. SHAs everywhere and the causation missing anyway. Q1 — `--qualify` now requires the T0 path and commit, proves the commit exists, that `blob_sha` really is `commit:path`, that the bytes hash to the recorded sha256, and that the document declares FROZEN. A qualification against a NOT_FROZEN T0 is refused, so today's reconnaissance cannot become campaign evidence by reuse. `execbinding` refuses a host qualified under a different T0 than the campaign binds. Q2 — `--environment-id` is gone. The id is the manifest's own observed value and the provisioning declaration must agree with it: one identity, not three strings that agree while everyone behaves. Q3 — provisioning is checked by VALUE, not only by shape. `dedicated_to_p022` and `no_concurrent_user_workload` must be true, `hosted_ci_runner` false, `is_vm` a real boolean; a VM must promise fixed vCPU, fixed RAM, no live migration and no dynamic memory, with `n/a` unavailable to it, and a physical host must answer those four with an explicit `n/a: <reason>` rather than bare booleans. A VM declaring `fixed_vcpu: false` could previously qualify. Q4 — per-session operator facts left the host record. "No campaign workload", "no interactive user", "no prohibited background job" are properties of a moment, and now live in a session declaration bound by sha256 into eligibility. Q5 — the 120 s window is waited, not asserted. The quiet minute really elapses before the twelve 5 s intervals; a control drives an injected clock and requires 60 + 60. A constant nobody waits for is documentation. Q6 — identity is compared as a whole. The qualification records a canonical hash of the manifest's identity block and the session must reproduce it, so a changed kernel, CPU count, RAM or toolchain can no longer walk past a matching fingerprint. Q7/Q8 — the candidate is hashed against the binding before the clock, and a new `--session-postflight` pass re-checks identity, power and candidate afterwards and requires closing-probe evidence. Preflight may not certify what a session did after it started. Q9 — power is a structured snapshot, compared by equality through the session. Owner ruling applied: Windows requires 100% on AC *and* DC, so a machine cannot be compliant while plugged in and change policy when the power source does. Q10/Q11 — the binding proves its inputs instead of trusting strings. The harness digest is recomputed from the instrument sources at the bound commit by the frozen formula without importing the harness; the workload manifest comes from that commit's git object; `--verify` re-proves T0, instrument, manifest, both qualifications and both candidates. Q12 — a preflight and a postflight carrying two different `execution_binding_sha256` values cannot meet in one session. Q13 — a provisioning template lands in `scripts/step7/examples/`, outside any evidence directory, marked EXAMPLE / NOT EVIDENCE, with no real identity in it, and a control keeps it valid under the rules it teaches. step 7 host qualification controls: 18 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…e campaign Two findings from the second Phase-2 review, both reproduced before repair. S-A — machine-produced artifacts were consumed as ordinary dicts. A document whose kind was "NOT A BINDING AT ALL" was accepted as the execution binding, and an arbitrary object carrying `identity` and `provenance` was accepted as an environment manifest: the probe returned an ELIGIBLE session from two handwritten files. The asymmetry was exactly backwards — the two DECLARED artifacts were validated by kind and schema, and the three machine ones were not. There is now one boundary. `load_artifact()` parses, proves the artifact by its own validator, and only then hands it over; no consumer reads a field before the type is established, and a refusal raises rather than becoming "not eligible" — an input that is not what it claims is an operator error, not a session that failed a predicate, so it exits 2. The envcapture schema string is duplicated rather than imported, with `hostqual-producer-schema` proving the copy still equals what the capture tool emits. One validator per artifact type, not five call sites: `validate_qualification` now lives in `execbinding` — the module that binds campaigns is the lower one, so `hostqual` reuses it without an import cycle. That immediately paid: the required A4 attack found that binding never checked a qualification's SCHEMA, only its kind. A wrong-schema qualification could be bound. It cannot now. S-B — `--verify` narrowed silently to whatever it was handed. Called without the strata it skipped the qualification and candidate checks and still printed "binding verified". Owner ruling applied: no partial mode under that name. The function refuses incomplete input maps — not only argparse, because the next caller may be a script — and the command line refuses before running anything. The string "binding verified" is now unreachable unless the full set was attempted and passed. P-A — the control inventory is checked by a control. The docstring list and the executed set must be the same set, because this class of defect has now been found twice and a third discovery wearing a new hat is not a surprise worth paying for. step 7 host qualification controls: 22 passed, 0 failed. Regression: envcapture 11/11, perf instrument 16/16, calibration freeze 7/7, training prereg 9/9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
S-C, found by the review of the previous repair — and introduced by it. Closing
the artifact-boundary hole, I folded value judgements into the shape validator,
so `dedicated_to_p022: false` was refused exactly like a forged document. The
honest case that used to produce a record with `qualified: false` and
`single_tenant: fail` produced nothing at all: one line on stderr and no
artifact.
That erases negative attempts, which is how a laboratory ends up with machines
that pass on the first try because the other tries were never artifacts.
The boundary now separates the two questions it had merged:
artifact validity is this the artifact it claims to be — kind, schema,
types, applicability shape. Malformed is refused before
any record exists.
predicate outcome do the declared values satisfy the predicate. Every
failure reaches a real record.
So the string "false" is malformed, a missing key is malformed, `is_vm` answered
with "n/a" is malformed, a VM omitting a VM-only field is malformed, and a
physical host answering those with bare booleans is malformed — while the
boolean `false`, anywhere it is allowed, is a valid declaration that this host
does not qualify.
Exit codes now say which class occurred, and are written down rather than
implied:
0 valid artifact, positive outcome
1 valid artifact, NEGATIVE outcome — the record exists and says why
2 malformed input or operational misuse — no record is produced
The same split applies to the session declaration: an operator who truthfully
records that a prohibited job is running gets `eligible: false` with the reason,
not an error message and no evidence.
Three controls added, driven through the command line so the exit codes are
part of the proof: an honest provisioning negative, an honest VM negative and an
honest session negative each leave an artifact naming the failed check, while
`"false"` as a string leaves none and exits 2.
step 7 host qualification controls: 24 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
PhysShell
pushed a commit
that referenced
this pull request
Sep 16, 2026
…gates Phase 3, in one commit, on the four findings the freeze review left plus the platform split that #355 made necessary. S1 — the gate population belongs to T0, not to a function. The gates are computed over exactly the decisive workloads of the D7-bound manifest that the `launcher-e2e` rung admits — all thirteen at the current manifest — and calibration workloads never enter a cutover gate. A missing decisive workload, a missing engine side, an unexpected extra cell or a calibration cell inside the set each make the attempt INVALID: a missing cell is not a smaller denominator. The instrument's applicability rule implements that sentence; it is not the source of it, because two readers must not be able to answer differently about which cells a gate covers. S2 — the numeric domain is checked BEFORE the two-dimensional rule, not inside it. Every quantity a gate consumes must be defined, real and finite, with medians strictly positive; None, NaN or an infinity in R or either coordinate invalidates the attempt. Checked first because the rule's OR would otherwise let `relative = NaN, absolute = 10 ms` pass on the strength of the other coordinate. A comparison against an undefined number is not one that succeeded. S3 — a primary cell holds exactly N samples, each finite, positive and carrying its stratum's declared metric kind. One null, one missing sample or one unexpected metric kind invalidates the session. A median over the surviving subset is forbidden by name: the instrument's summariser drops absent memory samples silently, so without this rule a cell that lost half its measurements reports a confident median of the other half. S8 — since the memory quantity is platform-local, gate identity carries its stratum: eight primary gates, four per stratum, with `max_process_peak_resident` on Linux and `max_process_peak_commit` on Windows. `resident == commit` is asserted nowhere, the strata are never pooled, and the two memory budget families carry identical numbers while remaining two budgets — they agree today because both came from one product budget before any data existed, and either may move without the other. S7 — the replacement conditions are closed at six, with no "other environmental reason" escape hatch, and the retry budget belongs to the STRATUM. Replacement does not reset it; a valid outcome-bearing session closes the stratum and is retained forever; re-measurement requires a previous INVALID and remaining budget. Every replacement records both binding identities and the budget left, so a reader can see what it did and did not buy. Verified by enumeration rather than by reading: the eight-gate roll-up is total and single-valued over all 6561 combinations, 108 boundary points across the three budget families yield exactly one verdict each, and the domain check rejects NaN and both infinities before the rule sees them. Stale vocabulary swept from the normative text: no generic "peak RSS" gate, no "four primary gates", no cross-platform R. The old names survive only where they are explicitly named obsolete. Status is unchanged and deliberately so: NOT_FROZEN, collection_authorized false. This closes the content; the freeze is its own review and its own commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…flag bite Two freeze-blockers from the full T0 review, both of the same family: a chain that reads correctly and does not actually hold. F1 — the accepted D7 payload binds the reference, the harness digest and version, and the workload manifest. It does NOT bind the execution binding: `execution_binding_sha256` is absent from `D7_PAYLOAD_BINDING_KEYS`, the string `execution_binding` does not occur in the instrument at all, and the gate only checks that the listed keys are present — an extra key is tolerated and never verified. Meanwhile this tool's own docstring promised D7 would bind it. One accepted component was advertising a join another accepted component cannot see. A campaign link now makes the join by bytes: it names the execution binding, the D7 payload's sha256, its blob at a named commit, and the attestation's sha256. Preflight and postflight both re-prove it — a link naming a different binding, a freeze edited after the campaign was linked to it, or a campaign swapped between preflight and postflight each refuse — and the link is recorded in both session records. Admissibility under T0 is the existence of the postflight record, so evidence produced outside a campaign cannot acquire one. What this deliberately does not do: make the instrument's firewall aware of the campaign. `IdentityGate` still arms from the payload and attestation alone, so a decisive clock can physically run with no binding in existence; what cannot happen is that such a run becomes admissible. Teaching the firewall would edit `perf_baseline.py`, move `measurement_harness_digest` and reopen steps 4/5/6 — a price not paid for a property obtainable by gating admissibility instead of execution. F4 — `collection_authorized` was decoration. Both T0 readers accepted FROZEN regardless of the flag, and the fixtures had quietly encoded the intended rule all along. The two fields are now read as one authority state on both paths: FROZEN+true may proceed to the remaining gates, while FROZEN+false, NOT_FROZEN+false and NOT_FROZEN+true each refuse — the third by name, because an authorisation without a fixed protocol is a contradiction and honouring the flag over the contract is how a tool starts arguing with its own rules. A fixture defect surfaced on the way: two campaigns built from identical content in the same second produced the same commit sha, so the "different campaign" attack was proving that a link equals itself. Campaigns are now distinct documents. step 7 host qualification controls: 26 passed, 0 failed. Regression: envcapture 11/11, perf instrument 16/16, calibration freeze 7/7, training prereg 9/9. No instrument change; the harness digest does not move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The frozen T0 names an accepted harness identity that, on its own branch, does
not exist yet: `104c384d01bf` arrives with the memory-semantics repair, which is
a different pull request. That is fine while the merge order holds — and an
order held by agreement is one stray click from becoming an archaeological
artifact, with a document saying FROZEN in a tree where its referent is absent.
So the order is checked instead of promised, and checked against the TARGET
TREE rather than against pull-request numbers: a number proves someone pressed a
button and says nothing about what the merged tree contains. The gate refuses
unless, at the commit a merge would produce:
- T0 declares FROZEN *and* collection_authorized: true;
- the harness digest recomputed from that tree's own instrument sources, by
the frozen formula, equals the one T0 names;
- the policy freeze, the design constants and the training preregistration all
bind that same digest — present is not the same as re-bound;
- the capture, qualification and binding tools exist and actually enforce the
campaign link and the authority state;
- the step-7 note still revokes the automatic collection authority, so hosts
plus a binding cannot again be enough to start a clock.
The expected digest is read out of the frozen T0 and recomputed from the tree; a
control proves no digest literal lives in the gate's own source, because a gate
trusting its own constant would be checking itself.
Exercised against real merges, not only fixtures. The full chain
(#355 -> #356 -> #353 -> #354) is allowed. #354 alone is refused on three
predicates at once — the instrument hashes to 562a7f7232da where T0 names
104c384d01bf, the bindings are stale, the machinery is absent. #355 plus #354
without the machinery is refused on one. T0-0 turns out to need no predicate of
its own: #353 is an ancestor of the freeze commit, so it cannot be skipped.
merge gate controls: 7 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The first revision proved words. `step7_machinery_present` asked whether the two tool files existed and whether certain identifiers appeared in them, which a file containing `def check_campaign_link(): pass` satisfies — and the fixture that was supposed to exclude that world was exactly that file. A check for the name of a mechanism is not a check of the mechanism. `step7_machinery_enforces` now extracts the target tree's own hostqual and execbinding, runs them in a subprocess against a synthetic campaign, and requires all four attacks to be refused: a link naming another execution binding, a freeze edited after the link was made, FROZEN with collection_authorized false (on both readers), and a campaign swapped between preflight and postflight. A tool that accepts any of them fails the gate, and the refusal says which attack got through. `steps_4_5_6_rebound` searched each artifact's text for the digest. The right digest sitting in any field — a comment, a history entry, a field nobody binds — was read as a binding. It now walks the exact path each artifact actually binds at: measurement_harness_digest in the policy freeze, bound_measurement_harness_digest in the design constants, bindings.measurement_harness_digest in the training preregistration. Anywhere else is not a binding, and the refusal names the path and both digests. Fixtures ship the real tools; the two attack controls mutate one enforcement point each and require the gate to notice. Twelve controls, four of them new: a permissive link check is refused, an authority check that always passes is refused, a stale binding with the right digest in a decoy field is refused, and correct exact fields are allowed. Against the real merges: the full chain is allowed (rc 0); #354 without #355 and #356 is refused on three predicates (rc 1); #354 with the instrument but without the step-7 tools is refused on one (rc 1). 30d2f32 is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…thing Two holes in the proof, not in the mechanism. The authority table exercised three of the four states. R15 defines all four, and `NOT_FROZEN+false` was the one nobody checked — so a future tree in which a reader quietly authorised it would have passed the gate. It is in the table now, and `DEAD_AUTHORITY` has a twin that breaks `execbinding.t0_at` instead of `hostqual.bind_t0`: the gate's message claims enforcement on both readers, and a control that only breaks the first left half that claim resting on nothing. Both mutations must now be caught naming all three forbidden states. The campaign-swap witness asserted only that a swapped campaign is inadmissible. A `session_admissibility` returning `admissible: False` unconditionally would have satisfied it — the witness would have read "nothing is admissible" as "the swap was caught". It now requires the unchanged campaign to survive preflight to postflight first, and requires the swapped one to be refused by a reason naming the campaign link, so a broken closing probe cannot stand in for the continuity check. Two mutations prove each half bites: removing only the preflight/current campaign-link comparison while leaving `check_campaign_link` intact, and a postflight that refuses everything. A mutation that changes no bytes now raises rather than passing as a second positive control nobody reads as one. 15 controls, 0 failed. The probes are unchanged: full chain rc 0, #354 alone rc 1 on three predicates, #354 without the tools rc 1 on one. 30d2f32 is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
#357 built a gate that refuses the wrong merge. Nothing ran it. A weapon in a safe is not a guard at the door, so this is the wiring: a workflow that runs it against the commit a merge would produce, under two names a ruleset can require. Wiring it exposed two things the gate could not answer on its own. **Applicability.** A required check runs on every pull request in the repository. `mergegate.py` refuses a tree with no T0 in it — correctly, as a question about a contract that must be there — so as a required check today it would refuse every pull request, including this one. The wrapper settles it first: no frozen T0 at the merge commit means no merge can make the contract reachable, the check passes and says so. The moment a tree carries T0 every predicate applies in full. **Co-change.** The workflow that runs on a pull request is the one on that pull request. A branch could otherwise carry the freeze and a weakened gate together and be judged by the gate it brought with it. So the gate's own files — `mergegate.py`, this wrapper, the workflow — may not change in the same merge that introduces or changes the frozen contract. Repairing the gate on its own stays ordinary work; that distinction is a control, not a promise. The applicability decision lives in the tool that owns `T0_PATH`, never a second copy in YAML, and a control fails if the workflow ever grows one. Another fails if the contexts the workflow declares stop matching the ones a ruleset is told to require — a required check nothing reports waits forever and reads as protection. Exit codes are the step-7 three: 0 proceed or not applicable, 1 refused, 2 the question could not be asked. 11 wiring controls, 0 failed; the 15 gate controls unchanged. Both suites also run green on Linux, which is where the job will run. Against real commits: this branch's head is not applicable, the full chain is allowed, #354 alone is refused on three predicates. This is half the wiring. Until a ruleset requires these two contexts, the job reports and nothing is prevented. #354 stays a draft until it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
ruff allows a sys.path insertion before an import, so the E402 suppression was covering nothing and RUF100 said so. Caught by running the CI-pinned ruff (0.15.8) rather than the one that happened to be installed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
`ruff check .` flagged one over-long line in scripts/perf_baseline.py. Rewrapping it is not a cosmetic edit: the file is an INSTRUMENT SOURCE, its bytes are hashed into the measurement harness digest, and the frozen #263 preregistration names that digest — 104c384d01bf — with steps 4, 5 and 6 bound to the same value. Measured rather than assumed: the rewrap moved it to 533a921baf13. A merge carrying that would have put the frozen contract in a tree that does not contain the identity it names, which is the one thing scripts/step7/mergegate.py exists to refuse. The linter would have been satisfied and the contract broken. So the line stays and pyproject.toml records why, scoped to E501 in that one file, with the condition for lifting it: a re-binding of steps 4/5/6. perf instrument controls: 16 passed, 0 failed. ruff: all checks passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
Sixteen ruff findings, all introduced by this PR and none of them caught here before, because `ruff check .` was never run against this branch. `main` is clean, so every one of them is mine. Behaviour-preserving throughout: `datetime.UTC` for the deprecated alias, list unpacking for two concatenations, three long lines wrapped, and six unpacked names the controls never read prefixed with an underscore. The one that was almost a real defect is B023: three fixture paths were closed over by a lambda inside a loop. It never bit, because the lambda is called in the same iteration that builds it — but that is a property of today's control body, not of the code, and a later `refuses(...)` that defers the call would have made every attack in that loop test the last fixture three times. The names are bound as defaults now, so the control cannot start lying quietly. step-7 qualification 26/26, envcapture 11/11, perf instrument 16/16, ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
Three ruff findings, all mine: an unsorted import block, one over-long line inside the witness source, and an unpacked name a control never reads. The long line is inside WITNESS_SOURCE, so wrapping it edits the script the gate runs against the target tree's tools. Hoisting the identity into a local changes nothing it does, and the controls and probes were re-run to say so rather than assumed: 15 merge gate controls pass, host qualification 26/26, and the three real merge probes still come back 0 / 1 / 1 with the same predicates. This is a lint pass only. No predicate, no attack and no message moved, and 30d2f32 is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
If `--base` named a commit the clone does not carry, `blob_sha` returned None for every gate file on that side, the co-change rule read all of them as rewritten, and the merge was REFUSED — for a fact about the checkout's depth rather than about the merge. The workflow sets fetch-depth 0, so this would not have fired today; it would have fired the first time someone ran the wrapper by hand, or the first time a shallow clone was used to save a minute, and the refusal would have been about the gate being rewritten. A false refusal teaches people that the gate is noise, which is how a required check dies. The base is now validated like the merge commit, and an unreachable one is exit 2 with a message that names the reason. A control builds exactly that case and requires 2, refusing to accept 1. 12 wiring controls, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
tests/run_tests.py imports every test_*.py and calls `run()` on it. The new suite called its entry point `main()`, so the runner reported it as a module with no run() and ended the whole test job — after my own controls had printed 15 green lines, which is exactly the shape of a failure that looks like someone else's. Found by reading CI rather than by running the file directly: `python tests/test_step7_mergegate_ci.py` passes either way, and that is precisely why the local run said nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…r exists
Renaming RssProbe to MemoryProbe and the `rss` field to `memory` was the point
of this PR — the number now carries the name of what it measures. What the PR
did not do is follow the name out of the module. Three call sites still built
`pb.Harness(..., rss=pb.RssProbe())`, which is a TypeError and an AttributeError
the moment either is reached:
scripts/round7/runner.py:369, :536
scripts/round6/metrology.py:73
mypy --strict caught the two in round7 because that file is in its `files` list.
It said nothing about round6/metrology.py, which is not — that one was found by
grepping for the old names, and it is the more interesting of the two facts: the
type checker's silence there was never evidence.
No behaviour changes; the constructor and the field are spelled the way
perf_baseline.py now spells them. scripts/perf_baseline.py is untouched, so the
harness digest is still 104c384d01bf.
mypy --strict clean on Linux, which is the platform CI checks: on Windows the
same run reports `os.wait4` missing and several ignores unused, and those
findings must not be "fixed" — they are the POSIX branch being read on a host
that has no POSIX.
round 7 apparatus 10/10, perf instrument 16/16.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The previous commit gave this module a run() for tests/run_tests.py to call — onto a name a local helper already had. The helper won, the entry point was shadowed, and eight controls started invoking themselves recursively instead of the wrapper. Caught immediately by ruff (F811) and by the suite dropping to 4 passed, 8 failed, which is what a repository's own checks are for. The helper is `invoke` now and says in its docstring why it is not `run`. 12 wiring controls, 0 failed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
ci(step7): the merge gate becomes a check, not a script in a drawer
feat(step7): the merge order becomes a predicate on the target tree
feat(step7): host qualification and the execution binding, kept apart
PhysShell
merged commit Sep 17, 2026
2b2314e
into
claude/p-022-analysis-wiring-mwhqlw
63 checks passed
19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
S8 from the hostile freeze review of #354. Instrument semantics repair — no policy, no threshold, no authorization.
What a witness established
peak_rss_byteswas false on Windows, and the witness settles it rather than a manual page.The job object counts committed memory — job and process memory limits are defined on committed virtual memory — while
ru_maxrsscounts resident pages. One field name claimed both.The same witness refuted the other half of the suspicion:
wait4does not stop at a wrapping shell. The POSIX path already measures the tree it should.The repair
The quantity now travels with the number. Every sample and every cell carries
memory_metricfrom a closed set:max_process_peak_resident— POSIXwait4, and the/usr/bin/time -vfallbackmax_process_peak_commit— the Windows job objectA value produced under any other kind raises
InstrumentErrorrather than being recorded. No downstream reader infers semantics fromsys.platform, and no alias lets Windows commit go on being read as RSS.Renamed because the old nouns lied the same way:
peak_rss→peak_memory,rss_mechanism→memory_mechanism,rss_unavailable_reason→memory_unavailable_reason,raw_peak_rss_bytes→raw_peak_memory_bytes,RssProbe→MemoryProbe.Deliberately not done: Windows resident-set sampling.
PeakWorkingSetSizeis the resident analogue, but obtaining it for a tree of processes that come and go needs handle tracking or polling. This instrument does not sample, and will not perturb the elapsed-time interval so two operating systems can pronounce the same noun.The digest moved, on purpose
Three controls said so by name —
freeze-harness-untouched,training-prereg-bindings,envcapture-frozen-untouched— which is the provenance system delivering its inconvenience on purpose. Steps 4/5/6 are re-bound in their own commit (includingdesign_constants_blob_sha1, which moved because the design-constants artifact was itself re-bound).Committed evidence of runs that actually happened is not rewritten. The sizing, calibration and round-7 datasets record what the old instrument produced and still carry
peak_rss; rewriting them would forge a record rather than re-accept a binding.Controls
calibration constants 4/4 · calibration freeze 7/7 · calibration policy 10/10 · perf instrument 16/16 · round 7 apparatus 10/10 · step 7 environment capture 11/11 · training preregistration 9/9 · evidence coverage 12/12.
The repository runner also reports 17
FAIL[...]lines fromvariant-corpus,variant-ledgerandartifact-golden. Those are pre-existing on this Windows checkout — the same 7 + 10 appear at the base commit6194d7e— and are byte-exact fixtures against a CRLF working tree, unrelated to this change.🤖 Generated with Claude Code
https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh