Merge published upstream through fac0cf1652 without rewriting either history. Preserve the locally reviewed Windows command-client cleanup when resolving its equivalent upstream patch. All incoming frontend files retain their exact upstream content and original receipt. The supplied 6bf99407 preflight candidate remains unintegrated pending its own exact-source regression proof.
Contract-Neutral: Upstream convergence preserves published source identities and existing cleanup behavior; no new runtime or receipt semantics are authored by this merge.
Change-source: pulse-maintainer
An amber vSphere datastore or network gave no reason on its row: the
vCenter alarm behind it sat one click away in the drawer or one tab
away on the Overview. The alarm text itself led with boilerplate
("Network network-302 has VMware alarm ... (yellow)"), so the Overview
truncated the part that mattered.
A vCenter incident summary is now what vCenter flagged: the alarm's
own name, or "vCenter health is <colour>". Every surface that shows
it already names the resource and severity. Alert cards, notification
templates, and Patrol titles carry the resource name. Patrol's seed
context now names the resource on each alert line, and Patrol titles
key off the incident code instead of parsing the wording, while still
reading records persisted in the old form.
The alert card used to find vSphere by looking for "vmware" in the
message. It now routes on the incident provider, which also sends ESXi
host alarms to vSphere instead of to Machines, where hosts are not
listed. The card's own test becomes a registered alerts proof.
The Datastores and Networks tables gain the exception-first Health
column that TrueNAS storage uses. The cell moves into a shared
PlatformIssueReason, so the two platforms cannot drift; TrueNAS
renders the same. A row's dot and its reason now come from one
classification. Hosts shows only from a 72rem container, where it
reads instead of truncating, and the tablet band splits columns by
weight.
A composed stylesheet can differ from every earlier browser receipt. Show the final digest and bounded valid receipt origins so its owner can identify the exact missing pass without manually reconstructing source and parent identities. Invalid receipts never supply hints, and all existing admission rules stay enforced.
Contract-Neutral: Diagnostics only; browser receipt validation, coverage and admission rules are unchanged.
Change-source: pulse-maintainer
Close the connected exit-propagation gap in the CI service harness: a version or sc.exe query that prints plausible text but exits nonzero must not become accepted evidence. Check each captured native exit before inspecting the text, retaining the existing checked installer invocation.
The workflow and harness regressions now cover twelve native boundaries. Controlled PowerShell execution remains required by native Windows CI and explicitly unavailable in the offline Linux guest; production installation, service operations and customer acceptance are not inferred.
Contract-Neutral: CI proof verdict propagation only; production agent, installer, service lifecycle and permission contracts are unchanged.
Change-source: pulse-maintainer
PowerShell steps containing multiple native commands only expose the last exit to Actions. Check all agent build/version/self-test and lifecycle build/invocation exits immediately, keeping the original status in the error. A later success can no longer conceal an earlier failure or start lifecycle mutation after a failed build.
The regression validates all seven workflow boundaries, rejects each missing guard and tolerant steps, and runs the actual guards against controlled native exits when PowerShell is available. Windows CI requires that runtime regression; offline Linux explicitly retains the unavailable runtime as a skip.
Contract-Neutral: Native CI verdict propagation only; production agent identity, installer behaviour, service lifecycle and authority are unchanged.
Change-source: pulse-maintainer
Consume the full change list so an early dependency-path match cannot make printf receive SIGPIPE under pipefail and select inherited-advisory warning mode. Exercise the actual workflow classifier and real audit runner with bounded offline controls, including a critical finding; exact paths and existing audit policy stay unchanged.
Contract-Neutral: Repair the existing complete-list classification obligation only; strict dependency paths, inherited-warning policy, audit verdicts, permissions and publication contracts are unchanged.
Change-source: pulse-maintainer
Run the existing bounded strict audit before expensive local checks when frontend manifests or its audit runner change. Fail missing frontend dependencies rather than print a passing pre-push verdict after a skipped type-check. Preserve per-commit contract binding, CI gates and the inherited-advisory policy; execute real disposable Git and canned-provider orchestration controls without an external advisory query.
Contract-Neutral: Local developer preflight now reports incomplete checks honestly; no runtime, API, dependency graph or protected CI policy changes.
Change-source: pulse-maintainer
Retain the audit child's exit status alongside its captured report. Failed or stopped zero-count reports stay unavailable; positive findings still stop re-query and the existing unchanged-graph warning policy is preserved. Exercise failure, signal and real watchdog controls, and model npm's clean terminal status in the smoke provider.
Change-source: pulse-maintainer
Accept Delivery candidate 20261006T015404Z-delivery-trust with its exact six commits. Retain verified binary and VERSION backups on incomplete recovery, restore original paths and metadata, and avoid activating partial restores. Reviewed signed offline transaction and installer evidence source-vz0ldvxn; native systemd and released acceptance remain separate.
Change-source: pulse-maintainer
Accept exact product-intelligence candidate e70e2c6430. Existing guide and served mirror now distinguish container usage from Pulse RSS, preserve process identity and failed reads, and bound shared counters. Retained source-2h0n7shz and source-li4qy_25 cover 58 recipe/help and 37 documentation cases with complete output and completed guest stops; no runtime memory-repair claim.
Change-source: pulse-maintainer
The complete signed transaction, race controls, installer suite and vet passed, but the older shell smoke fake still refused stop and consequently could not reach its restore/start assertion. Allow only that successful stop boundary and retain the activation assertions. Production updater and its explicit stop-failure controls are unchanged.
Contract-Neutral: Existing failed-installer fixture correction only; production snapshot, rollback and activation contracts are unchanged.
Change-source: pulse-maintainer
Final combined-source proof source-hh5q6qzp exposed the new compiled native-agent frontend test outside the rootful source manifest. Add its exact source binding and explicit regression requirement, retaining complete compiled-test coverage and all production roots. Existing runtime, isolation and qualification authority are unchanged.
Change-source: pulse-maintainer
Keep the original archive-copy metadata semantics while checking every snapshot and restore. A content-only copy would lose extended attributes. Exercise that recovery and reject a non-regular restored destination before byte comparison or activation; qualify the restart backstop comments for incomplete recovery.
Change-source: pulse-maintainer
The first connected attempt failed before installer execution because the fixture expected .sshsig while ssh-keygen creates .sig. Require that real signed output and serve it under the simulated release URL. Production signature verification is unchanged; retain the original failed proof.
Contract-Neutral: Correct the test fixture signature filename only; production updater and trust contracts are unchanged.
Change-source: pulse-maintainer
A successful rename is not by itself the recovered-byte postcondition. Keep the snapshot and suppress activation when the recorded destination is still a link or no longer matches the saved bytes; connected no-op and changed-destination controls exercise that boundary.
Change-source: pulse-maintainer
Reject directory destinations before the same-directory file rename, use portable mode inspection in the connected transaction tests, and teach the existing service fakes that stopping and failed activation are separate operations. The recorded first candidate proof remains bound to its own source.
Contract-Neutral: Preserve the reviewed backup and rollback semantics; portable utility invocation and faithful test boundary only.
Change-source: pulse-maintainer
The conditional updater caller disables errexit, but binary and VERSION backups and restore copies were best-effort and their only backup was deleted even when recovery failed. Require a private verified snapshot before installer execution, consolidate the three rollback branches, stage exact original paths and absence, and retain the backup without automatic activation when recovery is incomplete. Exercise the complete signed offline path and prior-active-only service recovery.
Change-source: pulse-maintainer
Distinguish LXC/container memory charts from Pulse RSS, heap and virtual memory. Add a bounded five-counter sample with process identity readback, failure preservation and private-data cautions. Exercise the copied recipe with synthetic failures and an owned Linux child, preserving the existing CPU/write recipes.
Change-source: pulse-maintainer
Contract-Neutral: Documentation and executable recipe checks only; no runtime or public-contract delta.
The connected Docker-agent tests now import the API embed. Native jobs
previously ran their Go runtime suite before any frontend installation,
leaving that suite without its compile-time assets. Prepare the locked real
frontend build for every native runtime platform, reuse it for the Windows
command proof, and retain all existing runtime, installer and service
lifecycle checks.
The deployment documentation and ordered all-platform/stub-build regressions
describe the existing real-embed prerequisite. Source builds and Windows
cross-compilation remain distinct from native acceptance.
Contract-Neutral: CI asset preparation only; production agent identity, command authority, installer and service lifecycle contracts are unchanged.
Change-source: pulse-maintainer
A destination edit and a passing Test do not update the settings stored in an old queued delivery. Make the existing recovery help distinguish those paths, explain the passive Telegram header check, and keep the shipped guides in sync. Connect a real rejected HTTP delivery through edit, Test, restart, retained retry and new alert, including current disable, removal and pause controls.
Contract-Neutral: Documents and verifies existing destination-snapshot and retry semantics; no runtime, API, schema or delivery-policy change.
Change-source: pulse-maintainer
Retain the newer private-token and prompt-revocation guidance rather than restoring obsolete copy to satisfy stale assertions. Bind all nine added installer test sources in the rootful manifest and exercise their real reader hashes; the complete compiled-source coverage guard remains intact. No workflow, release policy or runtime authority changes.
Change-source: pulse-maintainer
The non-required **Benchmarks** check has failed on every non-superseded
PR since about 15:00 UTC on 5 October. Examples are #2521, #2520, #2524,
#2525, #2527, #2530 and #2532, each ending with `Paired base and
candidate benchmarks (pull requests)` timed out after 25 minutes. The
paired step runs 10 base and 10 candidate samples, so each second added
per sample costs 20 seconds of step time.
### What made it slower
Paired step duration across PR runs, with per-package seconds per sample
taken from the `ok` lines in the job logs:
| Period | Paired step | `internal/unifiedresources` |
`internal/monitoring` |
|---|---|---|---|
| 24 Sep – 5 Oct 08:00 | 16–18 min | 7.9 s | 4.5 s |
| after #2486 (08:11) | 20.5–20.9 min | 16.2 s | 5.0 s |
| after #2518 (15:01) | killed at 25 min | 38.2 s | 9.9 s |
- **#2518** added `BenchmarkRegistryProjectionSnapshot`. Its `cold` mode
rebuilt the registry between `b.StopTimer` and `b.StartTimer` on every
iteration. Go picks `b.N` from timed work only, so the empty-registry
cases ran hundreds of thousands of untimed ingests. Per sample,
`/0/cold/point-targets` took 10.3 s and `/0/cold/bulk-targets` 5.9 s at
a 100 ms benchtime. That is the cause of the timeouts.
- **#2486** added 46 owner-index and metadata sub-benchmarks, about +8 s
per sample. That is legitimate coverage. #2518's
`BenchmarkBroadcastMetricsSnapshot` and #2527 added about +6 s more.
- `-count`, `-benchtime` and the package list did not change.
### Fix
1. **Root cause.** `cold` now invalidates the registry's derived state
(canonical metadata, typed views, source-target index) instead of
rebuilding the registry. The refresh never reads its own output, so this
repeats a fresh ingest's first read: at 1000 VMs, 40011 vs 40014
allocs/op and about 5.09 MB/op, the same as before. Locally the 24-case
family now runs in 4.9 s; before, its 6 cold cases alone took 33.8 s.
2. **Scope.** Paired PR runs now sample only packages whose in-module
dependency closure (`go list -deps -test`) holds a changed file. A
`_test.go` file selects only its own package. Changes to
`go.mod`/`go.sum`, dirty or non-Git trees, or an unreadable graph select
every package. Applied to today's failures: #2521, #2524 and #2525
sample nothing; #2530 samples `internal/api`; #2532 samples api and
monitoring; #2520 samples 5 packages; #2527 samples 7. The metadata
records `selection=` and `packages=`.
3. **Budget.** A module or shared-model change still samples nearly
everything, about 24 minutes including the ~2.7 min warm-up. The step
timeout is now a measured 30 minutes (job stays at 35).
`PULSE_BENCH_BUDGET_SECONDS=1680` makes the script project its total
after each round. Once the projection reaches 28 minutes it stops and
prints each package's mean seconds per sample, which replaces an
unexplained timeout. `TestPairedBenchmarksStopInsideTheirBudget` keeps
budget < step timeout < job timeout.
4. **Store-backed chart benchmarks** (second commit). Go calls a
benchmark function again for each `b.N` it tries.
`HandleWorkloadCharts_StoreBacked` and
`HandleWorkloadsSummaryCharts_StoreBacked` each seeded 84,000 points
into a fresh SQLite store (about 0.6 s) to time a request of 0.2 ms or
less, two or three times per sample. In CI that was 3.5 s and 3.6 s of
internal/api's 11.5 s per sample. The timed loop now runs in a
`range=4h` sub-benchmark, so the parent seeds once per run. Data,
request and bytes/allocs per op are unchanged, and locally the pair
falls from about 6 s to 1–1.5 s. The names gain `/range=4h`, so this
PR's comparison reports them unmatched.
Unpaired branch runs still sample every package. With the cold fix,
their 15-minute step (13–14.2 min today) regains about 3 minutes of
headroom.
### Verification
- `scripts/tests/test-ci-benchmarks.sh` covers no-op, own-test-only,
shared-dependency, unreadable-graph, dirty-tree, module-file,
budget-exceeded, in-budget and invalid-budget cases. Five mutations of
the selection and budget logic are each caught.
- On the real package graph, selection maps `internal/models` to 7
packages, `internal/monitoring` to api+monitoring, an `internal/api`
embed to api, and frontend or docs paths to none.
- End to end with real Go on two clean clones (`origin/main` vs this
change), only `./internal/unifiedresources/` was sampled. Its per-sample
time went from 53.9 s to 26.7 s on a loaded Mac.
- `go test ./scripts/installtests -run
'TestPairedBenchmarksStopInsideTheirBudget|TestBenchmarkQualificationRetainsProvenance'`,
plus the script-reference and doc-drift suites.
This PR's own Benchmarks run should sample only
`internal/unifiedresources` and `internal/api`, because both changes are
those packages' own test files.
Retain exact proof-list equality while including the disk-History scope proof already registered in the assigned base. Require the connected pool test to exercise its host-availability check and both independent owning identities, not merely count resource rows.
Contract-Neutral: Update regression expectations and assertion coverage only; runtime, registry and subsystem contracts are unchanged.
Change-source: pulse-maintainer
Retain exact Delivery commits and current main repairs. Reviewed source-1dqqprfr verifies offline codec and Kubernetes races, runtime checks, vet and real server/agent builds with unchanged manifests. No frontend content is resolved or changed by this integration.
Change-source: pulse-maintainer
Since 15:00 UTC on 5 October every paired benchmark run that was not
superseded has hit its 25-minute step timeout. Each step runs ten base
and ten candidate samples, so per-sample growth is multiplied by twenty.
BenchmarkRegistryProjectionSnapshot (#2518) rebuilt a registry behind
StopTimer on every cold iteration. Go sizes b.N from timed work only, so
the empty-registry cases ran hundreds of thousands of untimed ingests
and took 16 s of a 100 ms benchtime. With #2486's new owner-index and
metadata benchmarks, internal/unifiedresources went from 8 s to 38 s per
sample. Cold now invalidates the registry's derived state instead. The
refresh never reads its own output, so this repeats a fresh ingest's
first read with the same allocations, and the family drops from 22 s to
about 5 s per sample.
Paired runs also measured every package even when both trees compiled
identical code, as on frontend-only changes. They now sample only
packages whose in-module dependency closure the change reaches. Module
files, dirty or non-Git trees and an unreadable graph still select
everything.
A module or shared-model change still samples nearly every package,
about 24 minutes with warm-up. The step therefore gets a measured 30
minutes, and the script stops at a 28-minute projected budget with
per-package costs instead of meeting a bare timeout.
Retain the reviewed four-module patch candidate, current disk History, isolated partial-PVE and Patrol changes. Validate this exact composition against the prepared dependency graph before offering source integration.
Change-source: pulse-maintainer
Promote v6.5.0-rc.1 to stable after its 24-hour soak. Only release metadata changes.
Contract-Neutral: Release-only v6.5.0 version, release notes and distribution metadata; no public install or runtime contract changes.
Change-source: pulse-maintainer
Explain exact rule scope, confirmation and uncertain-result readback in Patrol Activity. Keep the tested administrator-session workaround only for older builds without the controls; denied access is not a reason to use it. Bind help labels to the production controls and preserve the shipped mirror.
Contract-Neutral: Documentation and help regression coverage only; existing Patrol UI, API, suppression semantics, authentication, organisation scope and CSRF contracts are unchanged.
Change-source: pulse-maintainer
Remove log filters that hide reader failures and unrelated startup errors. Use current export labels and separate cached downloads from live diagnostics, preserving private evidence and the backup safety boundary.
Change-source: pulse-maintainer
Update the two fixed expected proof lists for the connected TrueNAS disk History tests already registered by the candidate. Preserve full-list equality, all existing proof paths and rejection controls; do not change guard implementation or runtime behaviour.
Contract-Neutral: Test expectations match existing registered proof paths; no guard, registry or runtime semantics change.
Change-source: pulse-maintainer
Preserve the held four-patch dependency repair and all current main monitoring and UI changes. The two earlier acquisition attempts failed before execution at capacity; this current-source composition still requires exact graph acquisition, fresh launch selection and runtime/build proof before integration.
Change-source: pulse-maintainer
Preserve current upstream guest-read, mock cancellation, tenant deletion and Patrol repairs while carrying the prepared partial-report candidate through final-source validation. No isolation or platform gate is relaxed.
Change-source: pulse-maintainer
Preserve the exact documentation and regression-test candidate; native browser follow-through and the missing UI remain owned separately.
Change-source: pulse-maintainer
Replace the rejected global host-network proposal with the unchanged private namespace on every platform, including native linux-386. Carry successful rows and omitted VMIDs through operation v2 without clearing degraded health or renewing omitted measurements; retain strict complete-only v1 compatibility. Restart the installer-owned helper only after isolated-unit and socket checks, and document the unresolved pmxcfs IPC limitation rather than claiming PVE recovery.
Change-source: pulse-maintainer
Explain the missing v6.4.5 undo control and the existing scoped API. Verify exact selection, confirmation, browser session and CSRF, failure handling and readback without copying credentials or deleting unrelated rules.
Contract-Neutral: Documentation and regression-proof coverage of existing Patrol suppression APIs only; no runtime, route, authority or subsystem contract changes.
Change-source: pulse-maintainer
## Problem
An agent installed by the cluster deploy flow starts with `--enroll` and
a one-time bootstrap token. Only the host module exchanged that token
for the runtime token. Everything else `main.go` builds at startup kept
the bootstrap token for the whole first process lifetime:
- the remote config client (initial fetch and the one-minute refresh)
- the updater
- the Docker / Podman and Kubernetes modules and their primary report
targets
The bootstrap token carries only the enrollment scope and the server
removes it on exchange, so each of those failed with 401 until the
service restarted. On a freshly deployed Proxmox node this showed up as
a remote config 401 every minute. Those modules also kept the
pre-enrollment agent ID instead of the server-assigned one.
## Fix
`main.go` now enrolls once, before it builds any authenticated client:
- `hostagent.Enroll` is a standalone entry point over the existing
enrollment loop. Payload, headers, retry and backoff, status handling,
and `runtime.token` / `agent-id` persistence are unchanged.
- It runs after self-test and agent-ID preparation and before the first
remote config fetch. Every module is then built from the runtime token
and the canonical agent ID.
- The ID is persisted to the configured agent-ID file, and the host
module's own enrollment is switched off so there is no second exchange.
- A persisted runtime token is reused without a request or host
discovery. A `403` keeps the configured token as a non-bootstrap
credential. A permanent rejection stops startup before any module is
built.
- Windows is gated off and keeps its previous behaviour. Startup there
runs before the service control dispatcher, and an enrollment retry
could outlast its 30-second deadline. No Windows installer passes
`--enroll`.
- Enrollment and reporting now derive hostname, OS and architecture
through one `resolveHostIdentity`, so the runtime token is bound to the
hostname the agent reports under.
## Tests
- `TestRunEnrollsBeforeBuildingAuthenticatedModules`: enrollment happens
before any request to Pulse. Remote config, the updater, and the host,
Docker and Kubernetes configs and their targets all carry the runtime
token and canonical ID, and the agent-ID file is written. With the new
step disabled, this test fails.
- `TestRunStopsBeforeModulesWhenEnrollmentIsRejected`,
`TestRunWithoutEnrollDoesNotExchangeTokens` and
`TestRunLeavesEnrollmentToTheHostModuleWhenGatedOff`.
- `TestEnroll*` cases for `Enroll`: payload and identity, the
monitoring-only command ceiling, reuse of a persisted token (including
when host discovery fails), the 403 keep, the 409 rejection, and a
missing token.
- `TestEnrollPresentsTheIdentityNewReports` checks that enrollment
presents the hostname, OS and architecture that `New` reports, with
Proxmox detection and the architecture fallback.
- `go test ./cmd/pulse-agent ./internal/hostagent
./internal/remoteconfig` passes. `go vet` is clean for linux, windows
and freebsd.
## Not in this change
- The health listener still starts after enrollment, so `/readyz` is
unreachable while enrollment retries on network errors. The installer
only warns in that case. With the health listener enabled, its
registration check now always sees the runtime token. With it disabled,
the process-only check can still race enrollment.
- Distinguishing a deploy binding mismatch from a non-bootstrap token,
which both return 403.
- Two older durability gaps that this change doesn't make worse. If
writing a custom `--agent-id-file` fails, the next start has no fallback
to the canonical ID saved in the state directory. If persisting
`runtime.token` fails, enrollment still succeeds, and the next restart
is left with the spent bootstrap token.
Update only compress and the Kubernetes trio from proposal 2317. Preserve other dependency and compiler pins and retained checksums. Require aligned patch floors, paired checksum records and exact-graph runtime proof before integration.
Contract-Neutral: Dependency patches and internal graph admission controls only; module names, public APIs, deployment interfaces, managed-runtime orchestration and acquisition security boundaries are unchanged.
Change-source: pulse-maintainer
Every subsystem must declare require_explicit_path_policy_coverage, so
build_verification_requirements never falls back to a rule's default
verification policy; an unmatched runtime file becomes a path-policy gap
instead. The default test_prefixes and exact_files were therefore never
consulted, yet 13 of 16 rules carried them (93 files and one prefix),
and authors kept adding proofs there as if they counted.
92 of the 93 files were already proof on some path policy, and
api-contracts' default prefix is on three of its own policies. The one
orphan, internal/api/router_bench_test.go, benchmarks the api-contracts
router rather than any performance-and-scalability runtime, so no policy
should take it; it stays an owned file only.
The registry drops the lists, registry_audit requires them empty and
points authors at the path policy instead, and the canonical protocol
says so. The protocol also stops promising same-subsystem tests "when
the registry explicitly allows them", which the audit no longer permits.
No verdict changes: build_verification_requirements returns identical
requirements for all 2746 owned runtime paths, and for an uncovered
path, under the old and new registry.
Document host-capacity normalisation, version-qualified rounding and the difference between collected readings and delivered alerts. Keep comparisons on ordinary workload observations rather than forced load, threshold changes or diagnostic reruns.
Contract-Neutral: Existing container CPU semantics and safety guidance only; no runtime, API, threshold, permission or lifecycle contract changes.
Change-source: pulse-maintainer
staged_verification_files_for_requirement() matches
allow_same_subsystem_tests against a subsystem's
owned_prefixes/owned_files, but both commit guards passed it the
impacted entry, which carried neither. Since the v6 import (778a2577b6)
a same-subsystem test never satisfied a requirement, while
subsystem_lookup.py (which passes the registry rule) and the guard's own
BLOCKED message ("same-subsystem test/spec files are also accepted")
said it would.
Honouring the 12 flags would have loosened the guard well past their
apparent intent. Every rule requires explicit path-policy coverage, so
the five rule-level flags are never consulted, and on the seven path
policies the flag reaches every test the subsystem owns: a
mutation-registry change would accept any internal/ai or AI Chat test
(~455) instead of its 4 listed proofs, and a frontend Actions change any
Go test under internal/unifiedresources. Six of the seven already list
their own directory in test_prefixes.
So the strictness stays and the tools now agree on it: the registry sets
the 12 flags to false (what has always been enforced), registry_audit
rejects true and points at test_prefixes/exact_files, and the impacted
entry carries the rule's ownership so the completion and commit-shape
guards read the flag the same way subsystem_lookup does.
No requirement accepts a proof it rejected before. subsystem_lookup
stops reporting matches the guard refused: a new internal/hostagent test
no longer claims three agent-lifecycle policies.
The completion guard's CI mode now compares contract text against the
evaluated commit (#2502), but it still decided which contracts a commit
owed from HEAD's registry and contract index. CI walks a branch with HEAD
at the tip (or at main after a merge), so a later commit could change an
earlier commit's verdict. If a later commit added a Shared Boundaries
reference to a file, an earlier commit that was complete when written got
blocked. If a later commit dropped ownership of a file, an earlier commit
that changed it without its contract passed. The pre-commit hook read
both from the commit's own index, so CI and the hook could still
disagree.
repo_file_io and subsystem_contracts can now read from a revision as well
as the working tree or index. A revision read uses only that commit's
tree and fails closed rather than falling back to the disk. In diff-base
mode the guard reads the registry and contract index from the evaluated
commit, as does the Prettier-only inference that --commit enables.
Replaying CI's per-commit loop over the last 120 commits on main gives
the same verdicts as before.