Skip to content

feat(step7): host qualification and the execution binding, kept apart - #356

Merged
PhysShell merged 23 commits into
claude/p022-s8-memory-metricfrom
claude/p022-hostqual
Sep 17, 2026
Merged

PhysShell merged 23 commits into
claude/p022-s8-memory-metricfrom
claude/p022-hostqual

Conversation

@PhysShell

@PhysShell PhysShell commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Phase 2 of the S8 repair chain. Stacked on #355 (the instrument's memory-metric repair), which it needs for the closed memory vocabulary.

Three artifacts, three questions, never merged:

question
host qualification does this environment satisfy the host predicate of a named, frozen T0?
session eligibility is this session, on that qualified host, able to start now?
session admissibility did the attempt that ran remain the one that was authorised?

and, separately, the execution binding: which Linux and Windows environments, which candidates and which instrument form this campaign. hostqual.py neither issues nor owns it — a utility that checks a CPU governor must not become the root of campaign identity. D7 later binds execution_binding_sha256 rather than restating the machines.

Qualification is versioned by T0

A qualification claims "this host satisfies T0-7", so it cannot float free of T0. --qualify requires the T0 path and commit and proves: the commit exists, blob_sha really is commit:path, the bytes hash to the recorded sha256, and the document declares FROZEN. A qualification against a NOT_FROZEN T0 is refused — today's reconnaissance cannot become campaign evidence by reuse — and the binding refuses a host qualified under a different T0 than it binds.

Every consumed artifact is proved before it is read

One boundary, one validator per artifact type: parse → prove kind/schema/shape → only then read semantics. Binding, qualification, environment manifest, preflight record, provisioning and session declaration all go through it.

Artifact validity is not predicate outcome. Malformed input — wrong kind, wrong schema, the string "false" where a boolean belongs, a VM omitting a VM-only field — is refused before any record exists. A declared boolean false is not malformed: it is a perfectly good declaration that this host does not qualify, and it reaches a real artifact. The exit codes say which happened:

0  valid artifact, positive outcome
1  valid artifact, NEGATIVE outcome — the record exists and names the failed check
2  malformed input or operational misuse — no record is produced

The envcapture schema string is duplicated rather than imported (neither tool imports the instrument or the capture tool), with a control proving the copy still equals what the producer emits.

Evidence classes stay apart

Declared — provisioning and the per-session operator declaration. Content-addressed, checked by shape and value, never called machine proof: a guest OS cannot establish that no neighbour arrived on the same hypervisor. 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, while a physical host answers those four with an explicit n/a: <reason>.

Machine-observedmanifest.provenance.ci == false (the field always exists, so demanding its absence could never be satisfied); performance governor on every applicable CPU plus an identified turbo mechanism; on Windows an accepted plan with processor state pinned at 100% on AC and DC, read by GUID and by position because every label on a non-English machine is localized; and the stratum's memory mechanism actually existing here.

Identity, candidate, quiesce

  • The qualification records a canonical hash of the whole identity block, and a session must reproduce it — a changed kernel, CPU count, RAM or toolchain can no longer walk past a matching fingerprint.
  • The candidate is hashed against the binding before the clock, and again after the session.
  • Quiesce is 120 s and is actually waited: 60 s quiet, then twelve 5 s intervals sampled from /proc/stat or GetSystemTimes, mean below 5 %, no interval above 20 %. A missing sample, a rewound counter or a zero denominator is NOT_ELIGIBLE rather than a skipped interval. A quiesce failure starts no clock, so it is not INVALID and costs no retry budget.
  • A postflight pass re-checks identity, power and candidate and requires closing-probe evidence. Preflight may not certify what a session did after it started.
  • A preflight and a postflight carrying two different execution_binding_sha256 values cannot meet in one session.

The binding proves its inputs, and --verify is total

The harness digest is recomputed from the instrument sources at the bound commit by the frozen formula, without importing the harness; because the workload manifest is one of those sources, proving the digest at a commit also proves the manifest there. --verify re-proves T0, instrument, manifest, both qualifications and both candidates — and there is no partial mode under that name: incomplete inputs are refused by the function and by the command line, so the string binding verified cannot appear over a skipped component.

Driven live, not only by fixture

The first live Windows run found a defect in this tool's own parser: it read the possible-range minimum as the current setting and reported 0% on a machine pinned at 100%. Fixed — the block ends with the two current indices, AC then DC.

Exploratory witness, not qualification evidence: on the development machine processor_min_dc = 5. Under the AC+DC rule that host does not qualify, which is the point of the rule — a machine can look perfect on its current power source and carry a different profile the moment the source changes.

Windows S606, honest declaration (this box is NOT dedicated):
  ci                     pass  manifest.provenance.ci == false
  power_policy           pass  active plan 8c5e7fda-…, processor state 100%
  required_memory_metric pass  max_process_peak_commit via win32 job object
  single_tenant          fail  the declaration is valid evidence that this host
                               does not qualify: dedicated_to_p022 is declared false
  => a qualification artifact IS written, qualified: false, exit 1
Linux (WSL):
  power_policy           fail  no cpufreq/scaling_governor on any CPU
  => artifact written, qualified: false

Both refusals are correct, and both leave evidence: neither environment is a measurement host, the tool says so for the right reason, and the attempt is recorded rather than discarded.

step 7 host qualification controls: 24 passed, 0 failed. Regression: envcapture 11/11, perf instrument 16/16, calibration freeze 7/7, training preregistration 9/9.

No real campaign qualification or binding has been created. T0 remains NOT_FROZEN; collection_authorized: false.

🤖 Generated with Claude Code

https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh

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
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ef8ef33c-90a3-4633-ab4a-90c550b9be21

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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
…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
claude and others added 17 commits September 17, 2026 07:46
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
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
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
@PhysShell
PhysShell merged commit 157f53f into claude/p022-s8-memory-metric Sep 17, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants