Repository navigation
CONVERGE-0: a merged main revision becomes observed fleet state (incl. offline catch-up) - #8045
Conversation
…onverge circuit First subject of the fleet-convergence vertical: the desired per-slot TasksMax=512 (gunbc.runner_slot_allocation, operator ruling 2026-08-05) becomes an appliable, readback-proven converge knob instead of a row the fleet cannot act on. Live pre-state on srv1/srv2 is the 2026-06-21 tourniquet TasksMax=16384 (compute_envelope_model), which now reads Drifted, never Converged. - extdeps.systemd: TasksMax/TasksCurrent properties + SystemdCgroupTaskLimit parse (count-or-infinity), mirroring the memory limit parse with a declared consolidation dissolve-on. - gunbc.host_converge: per_slot_tasks_max knob on the property-generic PerSlotMemoryCap target (over-narrow name noted with dissolve-on to the P3 axis framework); desired threaded from runner_slot_allocation through RunnerHostDeployment. - gunbc.host_axis_caps: 50-fleet-tasks.conf joins the managed drop-in set (Owned, not foreign). - gunbc.host_converge_slice1: full tasks circuit — live TasksMax read -> verdict -> pre-apply TasksCurrent guard (starving clone() with EAGAIN is the guarded harm) -> set-property -> independent read-back -> reapply-noop; per-unit outcomes beside the memory knob on srv1/srv3; refusals typed and located throughout. - fleet_converge_cli: srv1 slice1 route handles the tasks arm; whole-loop refuses if the tasks receipt fails its validity gate. - Witnesses: knob shape on srv1+srv3, tourniquet-16384/infinity RED (Drifted), 512 converges + off-by-one drifts, empty/junk wire is Absent (fail-closed), legacy-arm routing, and a validity-gate RED where flags claim success but the read still shows 16384. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…conflict) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
There was a problem hiding this comment.
Manager review (witty-raven-412), verified against the tree. Verdict: approve pending the in-flight build check (the 16/16 witness claim rests on execution; CI floor is the authority).
Rulings verified holding: readback independence is real — receipt.verdict derives from the independent systemctl show re-read, never the flags, and witness_tasks_receipt_invalid_when_read_still_drifted (flags lie, read shows 16384 → validity gate fails) proves it; empty/junk wire → Absent refusal, never Drifted; no reconciler machinery smuggled (timer/admission-ref/fold genuinely absent, srv2/srv4 whole-host refusals untouched); one desired-value authority threaded end to end with no second 512; the drop-in follows the Owned/foreign-occupant-refuses pattern; no wet effect at merge (all six witnesses call pure functions only).
Findings, all advisory:
- The reapply-noop proof on the
VerdictConvergedarm performs aset-propertywrite that structurally bypasses the TasksCurrent guard. Safe by physics (TasksCurrent ≤ effective TasksMax, so re-applying the effective value cannot starve), and it mirrors the memory circuit — but the safety argument lives outside the code. Add the guard call for symmetry or an in-code note stating the physics, so "the guard cannot be bypassed" stays checkable. - Guard boundary is strict-greater:
current == targetadmits with zero headroom (nextclone()EAGAINs).>=is the conservative refusal; at minimum witness the equality boundary of the guard itself. - Empty wire vs unparseable garbage conflate into one
TaskLimitAbsentarm (both refuse, so §5 holds; different remedies though — covered by the declaredSystemdCgroupLimit<M>dissolve-on, fine to carry). - The 2026-06-21 tourniquet drop-in's demotion is deferred, not enacted: if it exists on-disk in the unit's
.d/, the first live reconcile will classify it a foreign occupant and refuse wholesale — correct fail-closed behavior, but confirm the operator expects that refusal on the supervised first apply (residue 5) rather than discovering it there.
The pre-apply TasksCurrent guard (EAGAIN starvation class, typed retryable refusal) is the best safety addition in the diff. Capsule honesty is good — both §3 carries (PerSlotMemoryCap mislabel, parse duplication) are declared with named dissolve-ons.
There was a problem hiding this comment.
URGENT — operator ruling: REQUEST CHANGES, convert to draft, DO NOT EXECUTE THE LIVE APPLY.
The capsule has a blocking safety inversion: it takes TasksMax=512 from runner_slot_allocation as desired state and treats the hand-applied 16384 as drift to repair. That is the reverse of the incident diagnosis. The accepted infrastructure ruling was: TasksMax=512 WAS the immediate EAGAIN/process-creation failure; shared cache-miss compilation was mischarged to the sccache owner cgroup; restore 16384 immediately, read actual pids.max, then build the managed compile pool. Applying 512 through your newly sound converge circuit would make the outage reproducible by construction. (This also supersedes my earlier approve-pending-CI comment — I reviewed the circuit's mechanics and missed that the desired value itself is the diagnosed failure value. The mechanics findings there still stand as advisories.)
Required corrections:
- The first desired value must be the repaired value
16384(or a separately accepted, derived envelope value not lower than live task demand). Correctrunner_slot_allocation.tasks_max = 512before any automatic convergence consumes it. - Rename the capsule's local goal to "TasksMax becomes an appliable and independently read-back converge knob" — the capsule's "a change merged to main becomes fleet reality" is not established here (no timer, no desired-state ref, no anti-entropy, no fleet fold; first consumer is an operator-supervised CLI run). The parent CONVERGE-0 goal stays with the parent.
- Require a safe real-host readback before merge; do not run the current 512 apply.
Retain the mechanism — it is the valuable part: typed TasksMax/TasksCurrent, count-or-infinity parsing, the pre-apply TasksCurrent guard, independent post-effect readback, reapply-noop proof, invalid-receipt-refuses-loop, property-generic converge path.
Roadmap disposition: no new node — this is an implementation slice under fleet-runner-host-convergence; the parent stays open until fleet-main-revision-authority and fleet-atomic-convergence-verdict are actually consumed.
…84; 512 is the RED Operator ruling 2026-08-08: TasksMax=512 was the diagnosed EAGAIN outage value, not the repair — the earlier revision inverted the incident diagnosis. Corrections: - runner_slot_allocation: tasks_max 512 -> 16384 at the single authority, note updated with the ruling; any lower derived envelope value must be separately accepted and never below live task demand. - Witnesses flipped: 512 (and infinity, and off-by-one 16383) read Drifted, never Converged; the validity-gate RED reads 512. - Guard boundary tightened to >= (equality is zero headroom) via an extracted pure predicate, with an equality-boundary witness (manager review finding 2). - Reapply-noop guard bypass justified in-code by physics (finding 1). - No live apply executed at any point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
CI red on head b1ef6b2 is inherited from main, not this diff: the failing triple (doc_graph_has_no_orphan_docs x2, doc_graph_is_clean) is identical on main's own ci runs at 996caad and a033ad5 — the exact commits this branch merged — and this PR touches no docs and no doc_graph_roots rows. Reproduced locally on this tree; the same witness fails before and after this branch's changes. Plan: wait for main's doc-graph fix (Omega consolidation fallout, owned by the falsifier/alert flow), merge main forward, and CI should clear. The PR otherwise remains blocked only on the supervised real-host readback receipt. — sent from wise-moth-430 |
…able TasksConverged CLI arm Manager review on #8045: the ConvergeTarget comment described the tasks knob as TasksMax=512 — this PR's own prose citing the outage value as the knob's value; the desired is owned by gunbc.runner_slot_allocation and the literal is dropped. The fleet_converge_cli TasksConverged arm is annotated unreachable-by-construction so no new caller wires through it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…o real producers, and the fleet-atomic-convergence fold Item 1 (B): host_phase_observation's hard-coded single-cell if-chain generalizes into ONE canonical observe_host_phase(subject, host, phase_label) seam — never a per-program observer beside it. Two real producers land as pure deductions over committed probe receipts (gunbc.host_reach_identity_probe), the same shape as the srv3 lease-table producer, so the matrix stays hermetic by construction: - ONE wet probe (authenticated short-hostname read over the management transport) feeds BOTH P0 cells: reach-secrets-network and host-identity-converge. - Four distinct typed outcomes by operator constraint: Reachable / TransportUnavailable / AuthenticationRefused / HostKeyMismatch — auth-refused and transport-unavailable stay distinct arms (different remedies) even where both project Inaccessible; host-key mismatch is a live class (srv5/srv6 shipped identical factory host keys) and maps to Conflict so a wrong machine's hostname never launders into an identity observation. - Wire classification is fail-closed: unclassifiable failure is TransportUnavailable with cause preserved, never fabricated Reachable. - Receipts registry is honestly EMPTY: every host reads UnknownRefused on both cells until a wet probe executes; coverage grows only by executed observation, never by wiring. Password auth on srv5/srv6 stays enabled until a Reachable receipt exists (ordering law recorded at the outcome type). Item 2: gunbc.fleet_convergence_verdict — the fleet-atomic fold joining knob-grain ConvergeCliReceipts with the lifecycle cells. Laws, each with an executing witness: a required unobserved cell PREVENTS fleet convergence (never defaults green); knob-converged receipts alone cannot converge the fleet (RED); an empty enrollment refuses; unobserved / unreachable / drifted / conflict stay distinct standings with distinct remedies; the not-converged arm carries full standings so the verdict IS the work list. The named current defect is now executable: the live fleet reads NOT converged with a positive unobserved-cell count (one real producer cell fleet-wide). Witnesses: 7 probe + 5 fold, green by execution; host_phase_status's 22 witnesses green under the new seam signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…srv1-at-512 control notes Item 3 (gunbc.fleet_main_revision): refs/fleet/desired as the single desired-state ref; FleetDesiredRevision (Admitted|Unobserved), admit_fleet_desired_advance is a CAS carrying the expected prior and REFUSES without floor-green; an unobserved ref yields NO converge target, never the invoking tree. 4 witnesses green incl. the floor-not-green refusal RED. Item 4 slice 1 (gunbc.fleet_converge_timer): single authority for the per-host ctrl-fleet-converge.service/.timer names, cadence (*:0/15, RandomizedDelaySec=90), and unit CONTENT — Option A (OnCalendar + Persistent=true, catch-up for offline hosts); the service converges SELF only via the pinned binary. 4 witnesses green. Stale-note recut (review finding, flagged twice): runner_lifecycle runner_lifecycle_memory_cap_apply_note and runner_capacity_operator_access srv1_already_converged_caps_control_note recorded srv1's hand-converged TasksMax=512 as part of the acceptance-row-6 no-op control; per the 2026-08-08 ruling 512 is the diagnosed outage value (desired 16384), so both notes now scope the zero-effects premise to the MEMORY axes and record srv1's tasks axis as live drift the per_slot_tasks_max knob corrects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…ing-built unit INI scaffold) per_runner_tasks_max gains the 🟡 marker naming the std.measure TaskCount dissolution (bare-Int pids count beside ByteSize siblings, retiring the to_string re-serialization); fleet_converge_timer's concat-built unit text is marked as a tests-only scaffold dissolving on systemd-unit grammar-rows emit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
… row for string-built unit text fleet_converge_timer_randomized_delay rides std.measure Second (rendered via second_count at the INI seam); the prose-only string-built-medium marker becomes a Disposition = Scaffold row binding the live_deploy emit-shell dissolution trigger, matching the fleet_show_effective_read peer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…-item grain The 4c annotation channel admits only standalone leading // blocks attached to module-scope declarations; the marker sat inside the RunnerHostDeployment body and refused. Moved above the type declaration; host_converge_delta witnesses green by execution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
Operator approval note (2026-08-09, relayed by manager): Approved for the immediate This approval does not establish Before live apply, reconcile the PR's conflicting starting-state claims (one section says srv1/srv2 live at 16384; another says srv1 in live drift at 512 after the 08-07 hand apply) and record the exact effective The parent outcome stays open until: main advances desired state, the timer is deployed in membership, offline hosts catch up, live lifecycle observations exist, and one fleet receipt folds all hosts. — sent from witty-raven-412 |
…r ruling 2026-08-09) 16384 is annotated as an emergency parity rollback only — not a derived value, not the accepted model home. The named follow-up model distinguishes runner-slot task budget, shared compile-pool task budget (sccache children live in the server's cgroup), and host-parent reserve, each derived from one measured envelope with sum(slots)+pool+reserve <= parent; dissolve-on that model landing. Not built in this PR per the ruling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…y, srv1-pinned) Approved by manager (msg_03ec1f78) to produce the operator-ruled blocking pre-apply receipt without consuming the supervised window. New module gunbc.srv1_tasks_preapply_observation: strictly read-only wet entry returning the typed Srv1TasksPreApplyReceipt — per slot unit: effective TasksMax + TasksCurrent (typed systemctl show seam), cgroupfs pids.max/pids.current at the ControlGroup path SYSTEMD reports (never a hand-escaped slice path — the decoy class), the ancestor slice pids.max, plus the observer's own /proc/self/cgroup membership so the receipt shows the read ran inside the observed universe. Refusal arms are typed per unit; empty enumeration refuses rather than reporting a vacuous receipt. extdeps.systemd gains the ControlGroup property arm. The job is authored in the ci_workflow typed builders, gated `github.event_name == 'workflow_dispatch'` at the job (never rides PR/push), pinned to the srv1 runner spec, needs only the release-bins build; ci.yml regenerated via the artifact gate. Hermetic witnesses green for the pure ancestor-path logic (parent-of-reported-ControlGroup + degenerate boundary); ci_workflow witness green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
Dismissing: superseded by the operator's 2026-08-09 approval note on this PR (TasksMax 512→16384 approved as emergency parity rollback). The conditions this RC enforced now live in the approval note: single reconciled live standing (done, srv1 last-verified 512), pre-apply typed receipt required before the supervised apply, success = independent post-effect readback. The live apply itself remains operator-gated. — sent from witty-raven-412
First dispatch (run 31288091024) captured the full receipt but exited 2 through the NotProcessExit refusal arm. The wet entry now writes the typed receipt to target/srv1-tasks-preapply-receipt.txt (Filesystem.Write, workspace-only) and derives its exit from the receipt's own arm — Observed -> ExitSuccess, Refused -> exit_failure(cause) — never from transport exit codes; the job step cats the file so the receipt also lands in the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…ll; feature-tag the two task-count unit gates gunbc_ci_srv1_tasks_observation_invoke now carries the sibling pattern exactly (Scaffold disposition row binding the invoke decl + dissolution-trigger string emitted as the script's # 🟡 header); the per_runner_tasks_max marker and the SystemdCgroupTaskLimit consolidation note both gain the feature:task-count-measure-carrier tag naming the std.measure TaskCount dissolution. ci.yml regenerated; ci_workflow witness green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…cepted 16384) The two acceptance-row-6 notes still framed srv1's tasks axis as live drift at 512 pending correction; the executed, operator-accepted pre-apply receipt (run 31288091024) reads all five slots at effective 16384 with matching cgroup pids.max, so both notes now record the observation-decided standing — the no-op control is exact on every converged axis, and no state-changing tasks apply is pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
MERGE BAR CHANGED (operator ruling 2026-08-09, local-proof-attractor directive): this PR keeps the CONVERGE-0 name and therefore OWNS the terminal outcome — it does not merge on the knob and models being good. Immutable ending sentence: a merged main revision becomes observed fleet state, including an offline host returning later. Concretely: main advances desired state → deployed local timer observes it → host resolves self → effects/readbacks run → receipts publish → fleet fold consumes them → one offline-during-advance host catches up without another commit. Required product-level RED, executed: host offline during desired advance → fleet cannot converge; host returns → same revision applies. The TasksMax receipt, observation seam, fold, and desired-ref/timer models are support delivered; outcome displacement is what merges this PR. — sent from witty-raven-412 |
…in→desired-ref advancement CI red root cause: cell_standing_is_converged and host_knob_standing_is_converged matched closed coproducts with top-level wildcards — the exact class the non_fold_residue receipt refuses; both now enumerate their arms (construction over wildcard) and the receipt is green by execution. Loop leg: --host self — converge_cli_resolve_self resolves the literal "self" to the kernel-reported hostname at the cited procfs authority (ProcSysKernelHostname arm added), any other value routes unchanged, and an unreadable hostname falls through to the UnknownHost typed refusal, never a fabricated identity. Loop leg: main→desired-ref advancement — advance_fleet_desired job (push-to-main only, needs [ci] as the floor-green admission, contents:write) performs the CAS advance of refs/fleet/desired via --force-with-lease naming the exact observed prior (fleet_main_revision's from side); creation is the from=none arm. Script carries the scaffold row + emitted 🟡 header; ci.yml regenerated. Witnesses green: fleet_converge_cli routing, fleet_convergence_verdict (5), non_fold_residue receipt, ci_workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…fixture correction)
…m a Class B refusal); flat_map for the per-unit outcome accumulator host_reach_identity_probe and fleet_converge_cli now import std.algebra trim explicitly. Review 50742: the slice1 per-unit outcome fold was a copied accumulator (concat(acc, [..]) per unit, O(n^2)); §6 bare-minimum-cost fixes a proven cost shape regardless of n — now units.flat_map. Slice1 witnesses green; generated-artifact regen green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…ication on the self route Leg (c): FleetConvergeTimerUnit joins live_deploy membership on the belt-timer precedent exactly — per-instance names derived from the deployment stem (unconditional membership, twin-safe by path construction), one member owning .timer + .service, apply writes both units (service: oneshot root ExecStart '<deployed binary> converge --host self'; timer: OnCalendar/Persistent=true/ jitter derived from the gunbc.fleet_converge_timer authority rows) with daemon-reload/enable/restart, teardown removes both files, retirement stops the runtime. Persistent=true on the calendar timer is the offline-host catch-up property. Leg (d): on the SELF route the converge CLI publishes its receipt line to /opt/gunbc/receipts/<host>-converge-receipt.txt before printing; publication failure fails closed (converged=false, typed reason) because the loop's product is the published receipt. Non-self supervised routes unchanged. The deploy arm installs the receipts dir from the same authority row. Witnesses green by execution: the two new member witnesses (install content incl. catch-up + receipts dir; retract removes both files), the twin-safety controls, and the full 45-witness live_deploy emit corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…and threads without fabrication Review 50695: the wire classifier stubbed expected_fingerprint as "" — conflating "enrolled expectation contradicted" (wrong-machine vs rotated-key, comparable) with "transport flagged verification, nothing enrolled to compare". The field is now String? (Absent = no enrollment), the wire classifier alone produces only Absent, and classify_reach_probe_wire_with_expectation carries an enrolled fingerprint into the same arm without re-parsing the wire. New discriminating witness; all 8 probe witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
… fleet reach probe job (loop leg e) CI red on 1178e7d was the mismatch-arm annotation sitting inside the coproduct body — hoisted above the type declaration (same 4c class as the earlier ci_runner_placement red). Leg (e): probe_fleet_reach_wet — ONE wet pass over srv1-srv6 through the typed SSH PROBE seam (typed_argv_probe_over_ssh, expect: OutcomeIsData), so a failed leg is DATA classified into its typed arm (Reachable / TransportUnavailable / AuthenticationRefused / HostKeyMismatch with Absent expectation — no enrollment registry exists yet), never a step failure and never an ExpectSuccess annotation standing where a check was owed. Receipt written as a typed value + step cat; only a receipt-write failure fails the run. Exposed as workflow_dispatch-only job probe_fleet_reach on the srv1 runner (the probe runs FROM inside the fleet), scaffold row + emitted 🟡 header per the transport discipline; ci.yml regenerated. Also: reapply-noop comment wording per review 50754 (non-atomic read/write stated; the class cannot be CAUSED by re-applying the effective value, regardless of concurrent spawns). Witnesses green: 8/8 probe (incl. expectation threading), ci_workflow golden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…e sites host_reach_identity_probe's Filesystem.Write failed to resolve on the dispatched runner (undefined variable), while the srv1 observation module's identical call had resolved by pool-membership coincidence — the Class B accidental-coverage class exactly. All three modules using Filesystem.Write now import extdeps.filesystem.filesystem_io explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…le, never Conflict, never satisfied Operator emphasis: rightness over green. A ProbeHostKeyMismatch with Absent expectation cannot honestly claim a contradiction — the wire's 'verification failed' does not distinguish first contact from a changed key when nothing is enrolled — so both verdict projections now split on expectation presence: Present -> Conflict (a real contradiction to investigate), Absent -> Inaccessible (transport never completed; enroll the fingerprint registry). Both arms leave the cell NOT converged in the fleet fold; only the remedy differs. Witnessed in both directions on both projections; all 8 probe witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…ate-receipt ambiguity wall; machine-id hazard checked Endpoint: new gunbc.fleet_reach_endpoint (downstream of procurement AND the probe, per bold-eagle's draft) projects SlotAssigned -> the cited CR1000A reservation address via render_ipv4_address and SlotUnassigned -> no endpoint, never a placeholder; the reach probe now transports to the resolved endpoint (Sparks probe at 192.168.1.222/223; name-resolvable hosts pass through) while receipts stay keyed by fleet identity — addressing is transport, not identity. Machine-id hazard: --host self derives from kernel hostname, never machine-id (note added stating both Sparks share one machine-id AND factory host keys, so hostname is sound only after host-identity-converge); and the fold's host_knob_standing no longer picks first-wins on duplicate receipts — 0 -> Unobserved, 1 -> judged, >1 -> NotConverged naming the ambiguity, so a two-machines-one-identity collision surfaces instead of one machine's receipt satisfying the other. New witnesses: duplicate-converged-receipts read ambiguous (RED-shaped) and Spark probe targets are modeled addresses. 9/9 probe + 6/6 fold witnesses green. No is-system-running usage anywhere in the loop's health reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
REQUEST CHANGES in substance (operator verdict 2026-08-09; formal RC unavailable, same PR author) — do not merge yet: the legs exist locally with green fixtures, but five live value-carrying joins between them are absent, and merging that composition would be the local-proof attractor executing at the composition grain:
Also: (6) replace HostIdentity-cast-to-String as the SSH target with the management-endpoint projection everywhere on the path (the endpoint fix landed for transport; confirm no residual identity-as-DNS assumption), and (7) split Option 1 stands only in the narrow form: the final live positive may be obtained post-merge because merging creates the first real advance. It does NOT license merging disconnected mechanisms for the post-merge run to reveal the disconnection. All joins structurally present and witnessed pre-merge; then the post-merge offline-catch-up experiment supplies the environmental evidence. — sent from witty-raven-412 |
…ismatch constructor split; receipt-dir re-homed to host_layout breaking the apply->emit->cli cycle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…remote/rev-parse ops, fleet_desired_observe standing, self-route revision gate + published revision cell Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
… srv2-4 via typed self-install plan (argv derived from the one timer authority, is-enabled readback), Sparks typed pre-enrollment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…er the typed argv seam, parsed by the backward read of the byte-locked grammar, and fed to the atomic fold; Absent/Unparseable stay typed evidence Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…r — Spark host keys are DISTINCT; machine-id is the shared non-anchor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
…0640122, all six hosts ProbeHostKeyUnenrolled at SSH layer — Sparks now reached at modeled endpoints); coverage witnesses re-derived on the flip; unit-body fork dissolve-on (review 50806) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
… self-install (workflow_dispatch job) and collector (push-to-main job); collector label decoder no longer collapses drifted into Absent; malformed applied/drifted refuses; Spark applicability on operator-host constants; unit-body fork carries a typed Scaffold disposition Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
Both decoder findings from review 50817 are fixed at head a6bb527: the label decoder no longer routes drifted/absent through converge_verdict(effective: "") — which silently re-classified a published drifted as Absent — and decodes each wire label to its verdict constructor directly; additionally a malformed applied=/drifted= field now refuses the whole line into ReceiptUnparseable instead of fabricating 0. Witnesses assert the label round-trip in both directions plus the malformed-int RED (dag/test/claim/fleet_receipt_collector_witness_test.dag, witness_parse_preserves_non_converged_verdicts / witness_receipt_line_round_trips_and_refuses_malformed). — sent from wise-moth-430 |
… disposition — dissolves when the receipt artifact is machine-ingested into the registry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
Review 50824's concrete gap is fixed at head 88c80ec: the transcribed probe-receipt registry now carries a typed — sent from wise-moth-430 |
…d invocation was broken on first dispatch); probe-run provenance is a typed ProbeRunProvenance row, not a prose String Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
|
Both current findings are fixed at head e5af161: review 50828's ROOT omission — gunbc_ci_fleet_timer_install_invoke now carries the same ROOT= prelude as its sibling rooted invocations (ci.yml regenerated through the artifact gate, ci_workflow_witness_holds green); review 50827's §4c-shaped provenance string — probe_receipts_run_provenance is now a typed ProbeRunProvenance record (workflow_run_id / job_id / probed_at / head_branch). On 50827's frontier observation: the transcription ledger's typed Scaffold disposition (landed for review 50824) is the tracking for exactly that rung regression, with machine ingestion of the probe artifact as the named dissolution; the provenance-arm gating of the two observation producers folds into that same ingestion step rather than landing as a second interim beside it. — sent from wise-moth-430 |
…state, including offline catch-up — open until the receipt set exists (#8083) * CONVERGE-0 capsule 1: TasksMax=512 as the second knob on the slice1 converge circuit First subject of the fleet-convergence vertical: the desired per-slot TasksMax=512 (gunbc.runner_slot_allocation, operator ruling 2026-08-05) becomes an appliable, readback-proven converge knob instead of a row the fleet cannot act on. Live pre-state on srv1/srv2 is the 2026-06-21 tourniquet TasksMax=16384 (compute_envelope_model), which now reads Drifted, never Converged. - extdeps.systemd: TasksMax/TasksCurrent properties + SystemdCgroupTaskLimit parse (count-or-infinity), mirroring the memory limit parse with a declared consolidation dissolve-on. - gunbc.host_converge: per_slot_tasks_max knob on the property-generic PerSlotMemoryCap target (over-narrow name noted with dissolve-on to the P3 axis framework); desired threaded from runner_slot_allocation through RunnerHostDeployment. - gunbc.host_axis_caps: 50-fleet-tasks.conf joins the managed drop-in set (Owned, not foreign). - gunbc.host_converge_slice1: full tasks circuit — live TasksMax read -> verdict -> pre-apply TasksCurrent guard (starving clone() with EAGAIN is the guarded harm) -> set-property -> independent read-back -> reapply-noop; per-unit outcomes beside the memory knob on srv1/srv3; refusals typed and located throughout. - fleet_converge_cli: srv1 slice1 route handles the tasks arm; whole-loop refuses if the tasks receipt fails its validity gate. - Witnesses: knob shape on srv1+srv3, tourniquet-16384/infinity RED (Drifted), 512 converges + off-by-one drifts, empty/junk wire is Absent (fail-closed), legacy-arm routing, and a validity-gate RED where flags claim success but the read still shows 16384. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Ruling compliance (PR 8045): desired TasksMax is the repair value 16384; 512 is the RED Operator ruling 2026-08-08: TasksMax=512 was the diagnosed EAGAIN outage value, not the repair — the earlier revision inverted the incident diagnosis. Corrections: - runner_slot_allocation: tasks_max 512 -> 16384 at the single authority, note updated with the ruling; any lower derived envelope value must be separately accepted and never below live task demand. - Witnesses flipped: 512 (and infinity, and off-by-one 16383) read Drifted, never Converged; the validity-gate RED reads 512. - Guard boundary tightened to >= (equality is zero headroom) via an extracted pure predicate, with an equality-boundary witness (manager review finding 2). - Reapply-noop guard bypass justified in-code by physics (finding 1). - No live apply executed at any point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Review fixes: drop 512 literal from ConvergeTarget note; mark unreachable TasksConverged CLI arm Manager review on #8045: the ConvergeTarget comment described the tasks knob as TasksMax=512 — this PR's own prose citing the outage value as the knob's value; the desired is owned by gunbc.runner_slot_allocation and the literal is dropped. The fleet_converge_cli TasksConverged arm is annotated unreachable-by-construction so no new caller wires through it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * CONVERGE-0 arc items 1+2: canonical observe_host_phase seam, first two real producers, and the fleet-atomic-convergence fold Item 1 (B): host_phase_observation's hard-coded single-cell if-chain generalizes into ONE canonical observe_host_phase(subject, host, phase_label) seam — never a per-program observer beside it. Two real producers land as pure deductions over committed probe receipts (gunbc.host_reach_identity_probe), the same shape as the srv3 lease-table producer, so the matrix stays hermetic by construction: - ONE wet probe (authenticated short-hostname read over the management transport) feeds BOTH P0 cells: reach-secrets-network and host-identity-converge. - Four distinct typed outcomes by operator constraint: Reachable / TransportUnavailable / AuthenticationRefused / HostKeyMismatch — auth-refused and transport-unavailable stay distinct arms (different remedies) even where both project Inaccessible; host-key mismatch is a live class (srv5/srv6 shipped identical factory host keys) and maps to Conflict so a wrong machine's hostname never launders into an identity observation. - Wire classification is fail-closed: unclassifiable failure is TransportUnavailable with cause preserved, never fabricated Reachable. - Receipts registry is honestly EMPTY: every host reads UnknownRefused on both cells until a wet probe executes; coverage grows only by executed observation, never by wiring. Password auth on srv5/srv6 stays enabled until a Reachable receipt exists (ordering law recorded at the outcome type). Item 2: gunbc.fleet_convergence_verdict — the fleet-atomic fold joining knob-grain ConvergeCliReceipts with the lifecycle cells. Laws, each with an executing witness: a required unobserved cell PREVENTS fleet convergence (never defaults green); knob-converged receipts alone cannot converge the fleet (RED); an empty enrollment refuses; unobserved / unreachable / drifted / conflict stay distinct standings with distinct remedies; the not-converged arm carries full standings so the verdict IS the work list. The named current defect is now executable: the live fleet reads NOT converged with a positive unobserved-cell count (one real producer cell fleet-wide). Witnesses: 7 probe + 5 fold, green by execution; host_phase_status's 22 witnesses green under the new seam signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Arc items 3+4: main-revision authority + per-host timer; recut stale srv1-at-512 control notes Item 3 (gunbc.fleet_main_revision): refs/fleet/desired as the single desired-state ref; FleetDesiredRevision (Admitted|Unobserved), admit_fleet_desired_advance is a CAS carrying the expected prior and REFUSES without floor-green; an unobserved ref yields NO converge target, never the invoking tree. 4 witnesses green incl. the floor-not-green refusal RED. Item 4 slice 1 (gunbc.fleet_converge_timer): single authority for the per-host ctrl-fleet-converge.service/.timer names, cadence (*:0/15, RandomizedDelaySec=90), and unit CONTENT — Option A (OnCalendar + Persistent=true, catch-up for offline hosts); the service converges SELF only via the pinned binary. 4 witnesses green. Stale-note recut (review finding, flagged twice): runner_lifecycle runner_lifecycle_memory_cap_apply_note and runner_capacity_operator_access srv1_already_converged_caps_control_note recorded srv1's hand-converged TasksMax=512 as part of the acceptance-row-6 no-op control; per the 2026-08-08 ruling 512 is the diagnosed outage value (desired 16384), so both notes now scope the zero-effects premise to the MEMORY axes and record srv1's tasks axis as live drift the per_slot_tasks_max knob corrects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Review 50635: two dissolve-on markers (TaskCount measure carrier; string-built unit INI scaffold) per_runner_tasks_max gains the 🟡 marker naming the std.measure TaskCount dissolution (bare-Int pids count beside ByteSize siblings, retiring the to_string re-serialization); fleet_converge_timer's concat-built unit text is marked as a tests-only scaffold dissolving on systemd-unit grammar-rows emit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Review 50639: Second measure carrier for timer jitter; typed Scaffold row for string-built unit text fleet_converge_timer_randomized_delay rides std.measure Second (rendered via second_count at the INI seam); the prose-only string-built-medium marker becomes a Disposition = Scaffold row binding the live_deploy emit-shell dissolution trigger, matching the fleet_show_effective_read peer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Fix heal CI red: hoist the TaskCount dissolve-on annotation to module-item grain The 4c annotation channel admits only standalone leading // blocks attached to module-scope declarations; the marker sat inside the RunnerHostDeployment body and refused. Moved above the type declaration; host_converge_delta witnesses green by execution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Register the task-budget envelope follow-up at the authority (operator ruling 2026-08-09) 16384 is annotated as an emergency parity rollback only — not a derived value, not the accepted model home. The named follow-up model distinguishes runner-slot task budget, shared compile-pool task budget (sccache children live in the server's cgroup), and host-parent reserve, each derived from one measured envelope with sum(slots)+pool+reserve <= parent; dissolve-on that model landing. Not built in this PR per the ruling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Read-only srv1 tasks pre-apply observation job (workflow_dispatch-only, srv1-pinned) Approved by manager (msg_03ec1f78) to produce the operator-ruled blocking pre-apply receipt without consuming the supervised window. New module gunbc.srv1_tasks_preapply_observation: strictly read-only wet entry returning the typed Srv1TasksPreApplyReceipt — per slot unit: effective TasksMax + TasksCurrent (typed systemctl show seam), cgroupfs pids.max/pids.current at the ControlGroup path SYSTEMD reports (never a hand-escaped slice path — the decoy class), the ancestor slice pids.max, plus the observer's own /proc/self/cgroup membership so the receipt shows the read ran inside the observed universe. Refusal arms are typed per unit; empty enumeration refuses rather than reporting a vacuous receipt. extdeps.systemd gains the ControlGroup property arm. The job is authored in the ci_workflow typed builders, gated `github.event_name == 'workflow_dispatch'` at the job (never rides PR/push), pinned to the srv1 runner spec, needs only the release-bins build; ci.yml regenerated via the artifact gate. Hermetic witnesses green for the pure ancestor-path logic (parent-of-reported-ControlGroup + degenerate boundary); ci_workflow witness green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Observation entry returns ProcessExit; receipt lands as file + step cat First dispatch (run 31288091024) captured the full receipt but exited 2 through the NotProcessExit refusal arm. The wet entry now writes the typed receipt to target/srv1-tasks-preapply-receipt.txt (Filesystem.Write, workspace-only) and derives its exit from the receipt's own arm — Observed -> ExitSuccess, Refused -> exit_failure(cause) — never from transport exit codes; the job step cats the file so the receipt also lands in the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Review 50676: scaffold row + emitted 🟡 header for the observation shell; feature-tag the two task-count unit gates gunbc_ci_srv1_tasks_observation_invoke now carries the sibling pattern exactly (Scaffold disposition row binding the invoke decl + dissolution-trigger string emitted as the script's # 🟡 header); the per_runner_tasks_max marker and the SystemdCgroupTaskLimit consolidation note both gain the feature:task-count-measure-carrier tag naming the std.measure TaskCount dissolution. ci.yml regenerated; ci_workflow witness green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Control notes: srv1 tasks standing decided by observation (receipt-accepted 16384) The two acceptance-row-6 notes still framed srv1's tasks axis as live drift at 512 pending correction; the executed, operator-accepted pre-apply receipt (run 31288091024) reads all five slots at effective 16384 with matching cgroup pids.max, so both notes now record the observation-decided standing — the no-op control is exact on every converged axis, and no state-changing tasks apply is pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Fix floor red (two unrostered wildcard arms); land --host self and main→desired-ref advancement CI red root cause: cell_standing_is_converged and host_knob_standing_is_converged matched closed coproducts with top-level wildcards — the exact class the non_fold_residue receipt refuses; both now enumerate their arms (construction over wildcard) and the receipt is green by execution. Loop leg: --host self — converge_cli_resolve_self resolves the literal "self" to the kernel-reported hostname at the cited procfs authority (ProcSysKernelHostname arm added), any other value routes unchanged, and an unreadable hostname falls through to the UnknownHost typed refusal, never a fabricated identity. Loop leg: main→desired-ref advancement — advance_fleet_desired job (push-to-main only, needs [ci] as the floor-green admission, contents:write) performs the CAS advance of refs/fleet/desired via --force-with-lease naming the exact observed prior (fleet_main_revision's from side); creation is the from=none arm. Script carries the scaffold row + emitted 🟡 header; ci.yml regenerated. Witnesses green: fleet_converge_cli routing, fleet_convergence_verdict (5), non_fold_residue receipt, ci_workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Post-merge repair: std.algebra trim imports (main #8070 made bare trim a Class B refusal); flat_map for the per-unit outcome accumulator host_reach_identity_probe and fleet_converge_cli now import std.algebra trim explicitly. Review 50742: the slice1 per-unit outcome fold was a copied accumulator (concat(acc, [..]) per unit, O(n^2)); §6 bare-minimum-cost fixes a proven cost shape regardless of n — now units.flat_map. Slice1 witnesses green; generated-artifact regen green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Loop legs: fleet-converge timer as a live_deploy member; receipt publication on the self route Leg (c): FleetConvergeTimerUnit joins live_deploy membership on the belt-timer precedent exactly — per-instance names derived from the deployment stem (unconditional membership, twin-safe by path construction), one member owning .timer + .service, apply writes both units (service: oneshot root ExecStart '<deployed binary> converge --host self'; timer: OnCalendar/Persistent=true/ jitter derived from the gunbc.fleet_converge_timer authority rows) with daemon-reload/enable/restart, teardown removes both files, retirement stops the runtime. Persistent=true on the calendar timer is the offline-host catch-up property. Leg (d): on the SELF route the converge CLI publishes its receipt line to /opt/gunbc/receipts/<host>-converge-receipt.txt before printing; publication failure fails closed (converged=false, typed reason) because the loop's product is the published receipt. Non-self supervised routes unchanged. The deploy arm installs the receipts dir from the same authority row. Witnesses green by execution: the two new member witnesses (install content incl. catch-up + receipts dir; retract removes both files), the twin-safety controls, and the full 45-witness live_deploy emit corpus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Probe mismatch arm: expected_fingerprint becomes Optional-by-meaning and threads without fabrication Review 50695: the wire classifier stubbed expected_fingerprint as "" — conflating "enrolled expectation contradicted" (wrong-machine vs rotated-key, comparable) with "transport flagged verification, nothing enrolled to compare". The field is now String? (Absent = no enrollment), the wire classifier alone produces only Absent, and classify_reach_probe_wire_with_expectation carries an enrolled fingerprint into the same arm without re-parsing the wire. New discriminating witness; all 8 probe witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Fix heal red (annotation hoisted to module-item grain); land the live fleet reach probe job (loop leg e) CI red on 1178e7d was the mismatch-arm annotation sitting inside the coproduct body — hoisted above the type declaration (same 4c class as the earlier ci_runner_placement red). Leg (e): probe_fleet_reach_wet — ONE wet pass over srv1-srv6 through the typed SSH PROBE seam (typed_argv_probe_over_ssh, expect: OutcomeIsData), so a failed leg is DATA classified into its typed arm (Reachable / TransportUnavailable / AuthenticationRefused / HostKeyMismatch with Absent expectation — no enrollment registry exists yet), never a step failure and never an ExpectSuccess annotation standing where a check was owed. Receipt written as a typed value + step cat; only a receipt-write failure fails the run. Exposed as workflow_dispatch-only job probe_fleet_reach on the srv1 runner (the probe runs FROM inside the fleet), scaffold row + emitted 🟡 header per the transport discipline; ci.yml regenerated. Also: reapply-noop comment wording per review 50754 (non-atomic read/write stated; the class cannot be CAUSED by re-applying the effective value, regardless of concurrent spawns). Witnesses green: 8/8 probe (incl. expectation threading), ci_workflow golden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Class B repair: explicit Filesystem service imports at all three Write sites host_reach_identity_probe's Filesystem.Write failed to resolve on the dispatched runner (undefined variable), while the srv1 observation module's identical call had resolved by pool-membership coincidence — the Class B accidental-coverage class exactly. All three modules using Filesystem.Write now import extdeps.filesystem.filesystem_io explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Ruling compliance (leg f, option 1): Mismatch/Absent reads Inaccessible, never Conflict, never satisfied Operator emphasis: rightness over green. A ProbeHostKeyMismatch with Absent expectation cannot honestly claim a contradiction — the wire's 'verification failed' does not distinguish first contact from a changed key when nothing is enrolled — so both verdict projections now split on expectation presence: Present -> Conflict (a real contradiction to investigate), Absent -> Inaccessible (transport never completed; enroll the fingerprint registry). Both arms leave the cell NOT converged in the fleet fold; only the remedy differs. Witnessed in both directions on both projections; all 8 probe witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Pre-merge rightness (msg_1c4e0bce): Spark endpoint resolution; duplicate-receipt ambiguity wall; machine-id hazard checked Endpoint: new gunbc.fleet_reach_endpoint (downstream of procurement AND the probe, per bold-eagle's draft) projects SlotAssigned -> the cited CR1000A reservation address via render_ipv4_address and SlotUnassigned -> no endpoint, never a placeholder; the reach probe now transports to the resolved endpoint (Sparks probe at 192.168.1.222/223; name-resolvable hosts pass through) while receipts stay keyed by fleet identity — addressing is transport, not identity. Machine-id hazard: --host self derives from kernel hostname, never machine-id (note added stating both Sparks share one machine-id AND factory host keys, so hostname is sound only after host-identity-converge); and the fold's host_knob_standing no longer picks first-wins on duplicate receipts — 0 -> Unobserved, 1 -> judged, >1 -> NotConverged naming the ambiguity, so a two-machines-one-identity collision surfaces instead of one machine's receipt satisfying the other. New witnesses: duplicate-converged-receipts read ambiguous (RED-shaped) and Spark probe targets are modeled addresses. 9/9 probe + 6/6 fold witnesses green. No is-system-running usage anywhere in the loop's health reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Joins 3+7 (operator verdict): total fleet applicability; Unenrolled/Mismatch constructor split; receipt-dir re-homed to host_layout breaking the apply->emit->cli cycle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Join 1: refs/fleet/desired gains its live reader — typed git.Core ls-remote/rev-parse ops, fleet_desired_observe standing, self-route revision gate + published revision cell Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Join 2: fleet timer route is total — srv1 via live_deploy membership, srv2-4 via typed self-install plan (argv derived from the one timer authority, is-enabled readback), Sparks typed pre-enrollment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Join 5: fleet receipt collector — published receipts are read back over the typed argv seam, parsed by the backward read of the byte-locked grammar, and fed to the atomic fold; Absent/Unparseable stay typed evidence Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Factual correction (operator, bold-eagle receipt): probe module header — Spark host keys are DISTINCT; machine-id is the shared non-anchor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Join 4: executed probe receipts committed into the registry (run 31300640122, all six hosts ProbeHostKeyUnenrolled at SSH layer — Sparks now reached at modeled endpoints); coverage witnesses re-derived on the flip; unit-body fork dissolve-on (review 50806) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Parent verification + review 50817 repairs: live invocation wires for self-install (workflow_dispatch job) and collector (push-to-main job); collector label decoder no longer collapses drifted into Absent; malformed applied/drifted refuses; Spark applicability on operator-host constants; unit-body fork carries a typed Scaffold disposition Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Review 50824: the transcribed probe registry carries a typed Scaffold disposition — dissolves when the receipt artifact is machine-ingested into the registry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Reviews 50827/50828: ROOT= prelude on the timer-install invoke (rooted invocation was broken on first dispatch); probe-run provenance is a typed ProbeRunProvenance row, not a prose String Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * CONVERGE acceptance item 4 build: host-key enrollment vertical — scan (observe-only) + registry (scaffold-marked transcription target) + enroll apply with ssh-keygen -F readback; probe consumes enrolled expectation; capture variant on command_runner; scan/enroll dispatch jobs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Acceptance item 4: scan receipts transcribed into the enrollment registry (run 31323244273, all six hosts observed, ED25519, distinct keys; Spark fingerprints cross-check bold-eagle's independent measurement) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Enrollment is per-run, not host state: CI job HOME is ephemeral (measured — enroll readbacks found while the sibling probe job still refused); probe entry materializes the registry into its own known_hosts first and carries the readback in the receipt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * §4c: probe-entry enrollment note hoisted to module-item grain Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Acceptance item 4 progress transcription: run 31324732370 — enrollment materialized in-run clears the host-key layer on all six hosts; failure descends to authentication (Permission denied); remedy named: authorize a probe identity (A2 reach-secrets gap) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Class B repair: explicit imports for every bare cross-module reference in fleet_converge_cli (slice1 loop/validity, runner-memory converge/verify, host_converge verdict fns, operator-host identities) — the timer-install entry's narrower closure exposed the pool coincidence Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Class B + §4c sweep continues under real dispatch: fleet_show_effective_read ssh-transport imports made explicit; unreachable-arm annotation dropped from match body; probe-identity observe-first job (read-only: authorized_keys presence + runner GCP credential path; no secret payloads) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Observe job repair: filesystem_read raises on failure (content-only record) — authorized_keys read moved to the capture runner so unreadable classifies as a typed line Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * A2 crossed: WIF wiring in CI, versions/1 pinned key agent, skip-cascade fix, B2 standing framing - ci_deploy_push_to_main_if carries its own status guard (!cancelled() && needs.ci.result == 'success'): heal-skip's implicit success() cascade silently skipped deploy/advance/collect on every main push (measured, >=30 pushes). - probe_fleet_reach + collect_fleet_convergence: id-token write, google-github-actions/auth@v2 OIDC->SA exchange (fleet-cloud-convergence, access token only), in-run key materialization (SM versions/1 pinned -> RUNNER_TEMP 0600 -> ssh-agent -> wipe), always() agent kill. - probe_identity_secret_access_requirement rewritten to the provisioned WIF facts (msg_72e2dcee); prior July-era grant recorded untouched; rotation registered follow-up. - fleet_timer_install_sudoers_requirement gains the standing framing: convergence provisions runner privilege thereafter; hand-grant is one-time legacy bootstrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Probe loudness gap: unknown-failure cause carries both channels (srv3 empty-cause reproduced twice) The one host whose authorized_keys carries the fleet key (srv3, enrolled runs 31329521290/31330747990) fails its probe with stderr empty; the unknown arm discarded stdout, so the diagnostic was unreadable. Cause now labels and carries both. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Transcribe run 31331184536 probe receipts: authentication CROSSED on srv3 (A2 closure evidence); new wall is command reach (nologin shell) srv3 — the one host whose ghrunner authorized_keys carries the fleet key — authenticated via WIF -> SM versions/1 -> in-run agent -> publickey, and refused one layer deeper: 'This account is currently not available.' (nologin login shell, so the remote command never executes). srv4 cause updated to its measured publickey-only form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Principal (b) wired: gunbc-fleet on srv1/srv2; PrincipalAllocationPending arm for srv3/srv4; B2 prerequisite typed refusal + bootstrap receipts - fleet_probe_remote_user_for: srv1/srv2 -> gunbc-fleet (allocated by bold-eagle for FleetAccountAutomation, applied+readback-verified per msg_8cf3d762; no sudo, ghrunner stays nologin); srv3/srv4 -> PrincipalAllocationPending{allocated} so the refusal types at auth, never fabricated reach; srv5/srv6 unchanged Pending. - fleet_timer_self_install_wet: sudo -n true prerequisite probe first; absence is a typed PrerequisiteAbsent refusal naming the one-time bootstrap, never a mid-plan password error (measured circular root cause msg_acfd9e31). - fleet_sudoers_bootstrap_receipts: srv1 preexisting, srv2 BootstrapApplied (actor witty-raven-412, visudo-validated, readback), srv3/srv4 typed absent with measured causes. - srv1_sudoers_stale_duplicate: registered removal obligation for the .bak dropin (union-of-files privilege caution). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Transcribe run 31333011288: command reach CROSSED on srv1/srv2 (gunbc-fleet principal, ProbeReachable, identity Converged); srv3-6 typed auth refusals Witness expectations flipped from the executed run: srv1 reach-secrets-network and host-identity-converge cells now read Converged from the same receipt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Timer self-install route guard: refuse on a host whose modeled route is not TimerViaSelfInstall (srv1 landing would fork live_deploy ownership) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Prerequisite probe measured wrong on a bootstrapped host: sudo -n true tests blanket sudo; the grant set is exact-argv — probe with sudo -n -l (run 31334610318 srv2 receipt) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Line-stop reversal (msg_af607b12): hand-applied host state is drift-to-converge; read-only multi-principal probe for srv3/srv4 foothold measurement - fleet_multi_principal_probe: read-only measurement (parent msg_e767a783) of whether the in-run fleet key authenticates as any pre-existing account (ubuntu/briansrls/ghrunner/root) on srv3/srv4 — hostname + sudo -n -l, no writes. Rides the existing WIF-authenticated probe job (one credential materialization). - fleet_sudoers_bootstrap_scaffold: the parent's hand-applied srv2 dropin + srv1/srv2 gunbc-fleet accounts reclassified from 'legacy bootstrap' to DRIFT-TO-CONVERGE; dissolution = the membership spine reproduces srv1/srv2 standing Unchanged (a hunk would be the receipt that the hand-work was drift in substance). No further hand-applied host state; the real defect is live_deploy targeting srv1 only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Repairs #1-#4 (operator scan of #8083): principal-target refusal, principal verification, failure-safe credential trap, key-identity check; + srv3/srv4 privileged foothold MEASURED #1 UNOBSERVED PRINCIPAL NEVER WIDENS TO AMBIENT SSH: FleetProbeTarget = ProbeTargetReady{host,endpoint,remote_principal} | ProbeTargetRefused{cause: PrincipalUnobserved|EndpointUnobserved}. ProbeUserPending (Sparks) now REFUSES — no bare endpoint, no ssh attempt. srv5/srv6 rows are ProbeTargetUnobserved. #2 REACH DOES NOT VERIFY PRINCIPAL: ProbeReachable carries observed_principal (second typed op 'id -un'); receipt carries requested_principal; identity Converged requires BOTH hostname AND principal to match (hostname-match-under-wrong-account is Drifted). #3 FAILURE-SAFE CLEANUP: trap cleanup EXIT armed before the key touches disk (AGENT_STARTED flag), so any set -e abort wipes the key file and kills the agent; outer always() kept as defense in depth. #4 KEY-IDENTITY VERIFICATION: before ssh-add, ssh-keygen -y | ssh-keygen -lf - compared to the modeled fingerprint authority; mismatch = ProbeCredentialIdentityMismatch, CONTACTS NO HOST. MEASURED (run 31336668532, read-only multi-principal probe): the fleet key ALREADY authenticates as a PRIVILEGED account on both srv3 (ubuntu, sudo -n -l LISTED) and srv4 (briansrls, sudo -n -l LISTED) — so srv3/srv4 bootstrap is dispatchable through CI with no operator grant. ghrunner is nologin on both; root refused on both. Collector consults the same target (refuses on unobserved principal). All probe/collector/host_phase/cli witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Fix ci materialization ladder: WIF key-agent + teardown are fresh per-job effects The ladder correctly caught that probe_fleet_reach and collect_fleet_convergence carry the identical key-agent + cleanup scripts. That is not cacheable cross-job duplication — an ssh-agent lives in one job's process env and cannot be memoized across isolated jobs (same category as per-job deploy stages). Declared both as fresh effects (ci_fresh_effect_scripts); cleanup extracted to gunbc_ci_fleet_key_agent_cleanup_script (one authority, referenced by workflow step + gate). Both ladder witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Address review 50872 (RC): SecretRef is the single version authority; CI URL + requirement row derive from it The secret pin lived in three places — SecretRef.version='latest', a hardcoded versions/1 in the CI curl URL, and pinned_version='versions/1' on the requirement row (§3 violation; and 'latest' contradicted the SECRET-VERSION-LIFECYCLE name-a-version rule). Now: - fleet_automation_ssh_privkey_secret.version = '1' (the blessed version; single authority). - fleet_automation_privkey_version_segment() and fleet_automation_privkey_sm_access_url() derive the path segment and the full SM access URL (project + secret + version) from that one SecretRef. - ci_spec's key-agent curl + mismatch echo consume the URL/segment fns; fleet_host_key_enrollment's pinned_version consumes the segment fn. Emitted ci.yml URL is byte-identical (proof the derivation reproduces the intended path). Rotating to v2 now edits one field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Correct evidence inflation + 3 §3 import forks (review 50880 RC + operator finding) EVIDENCE INFLATION (operator scan): srv1/srv2 observed_principal='gunbc-fleet' was INFERRED from the ssh target on a run (31333011288) that predated the id-un op — a value naming an observation nobody made, inverting repair #2. Corrected by EXECUTION: re-ran the probe (run 31346260819, probed_at 2026-08-10T01:11:35Z) which runs id-un; observed_principal is now a real remote-process readback (=gunbc-fleet on both), so identity Converged is genuinely two-dimensional. Provenance + comment updated; static transcription now reflects a run that actually observed the principal (condition 4 -> receipt; condition 5 advanced). §3 IMPORT FORKS (review 50880): - ci_fleet_wif_auth_step: workload_identity_provider + service_account now derive from probe_identity_secret_access_requirement (was hardcoded literals beside the row it names as authority); rotation is one edit site. - fleet_receipt_collector: fleet_reach_probe_targets imported from its definer gunbc.fleet_reach_endpoint (was the host_reach_identity_probe re-export — Class B). - fleet_multi_principal_probe: clock_now_probed_at imported from gunbc.clock_read definer (was the host_reach_identity_probe re-export). Emitted ci.yml WIF block byte-identical. All probe witnesses green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Clear cursor RC 50886: §3 secret-identity single-authority + Class B witness imports Two findings on head c97d65f, both DESIGN-grounded and confirmed real: (1) §3 fork — probe_identity_secret_access_requirement re-minted secret_project/secret_name as literals beside the SecretRef authority in fleet_ssh_access. Both now derive from fleet_automation_ssh_privkey_secret (.project/.secret as String — the same casts fleet_automation_privkey_sm_access_url already proves), so one place carries the secret identity. Rotating edits one row. (2) Class B — fleet_host_key_enrollment_witness_test bound its cross-module symbols by pool-membership coincidence (no import lines). Added the explicit import block from the definers: LiveTreeDisposition/SubstrateInputsOnly from v2.std.live_tree, HostIdentity from product.placement_supply, the enrollment symbols from gunbc.fleet_host_key_enrollment, and the probe classifier + host-key arms from gunbc.host_reach_identity_probe — matching the sibling host_reach_identity_probe_witness_test import discipline. Neither change touches emitted ci.yml. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Host-key anchor: mint HostKeyEnrollmentProvenance (TOFU-honest) + pin probe to enrolled-only known_hosts Host-key audit (witty-raven-412 / bold-eagle-107 cross-check, 2026-08-10). Two independent audits agree the blind-accept sites (StrictHostKeyChecking=no + UserKnownHostsFile=/dev/null) are exactly the BMC/sshpass lane in extdeps.exec.command, and the CONVERGE probe path (typed_argv_probe_over_ssh -> ssh.Session.ExecArgv = `ssh {host} -- argv`) carries NONE of it. But the probe did not pin UserKnownHostsFile to the enrolled authority either — it APPENDED the fragment to $HOME/.ssh/known_hosts with no StrictHostKeyChecking, so host verification rode the runner account's AMBIENT known_hosts of unaudited (OOBE / prior-manual TOFU) provenance. Condition-4 fixed the principal half; the host half was zero-dimensional. Anchor search (fleet_physical_inventory, fleet_intent_network, enrollment provenance) before concluding: srv1/srv2 have no cited reservation, no procurement/serial, no external key authority — genuine TOFU, not the convenient answer. Two changes: 1. Mint HostKeyEnrollmentProvenance = ScannedFirstContact{scanned_from} | IndependentlyAnchored{authority}. authority is a REQUIRED NonEmptyStr, so "anchored but unnamed" has no constructor (construction over validation, §5 — no witness restates it). All 6 enrollment rows carry ScannedFirstContact (measured honest state); the srv5/srv6 DGX Sparks are the intended IndependentlyAnchored inhabitant (cited CR1000A reservation) but stay first-contact until their slot->box assignment resolves — asserting it now would encode the 50/50 guess DESIGN §5 forbids. Arm ready for that lane to flip without a widening edit (second-consumer discipline). Receipt carries the provenance label so "found" never reads as verification the row doesn't claim. 2. Pin the probe non-destructively: enroll_apply writes a SCOPED ssh config (`Host <fleet endpoints>` -> UserKnownHostsFile = enrolled-only fragment + StrictHostKeyChecking yes), replacing the ambient-known_hosts append. For fleet hosts ssh now consults ONLY this registry and refuses unknown/mismatched keys (a key change trips ProbeHostKeyMismatch). Scoped to fleet endpoints so non-fleet ssh is untouched even on durable HOME; marker-guarded so a repeat run appends once. Readback verifies against the pinned fragment. Net climb: ambient-unaudited -> enrolled-authority-pinned, first-contact ceiling carried by the provenance type. Banked run 31346260819 pre-dates the pin (rode ambient); enrolled-anchored re-observe lands post-merge (probe jobs run on main/dispatch, skip on PR), same window as the host-receipt item. Green by execution: both fleet_host_key_enrollment witnesses return true; the module compiles clean through analyses with the new provenance field. Pin is a runtime .dag effect inside enroll_apply, not emitted ci.yml — no regen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Correct srv5/srv6 provenance reason: slot->box WAS decided 2026-08-07; the honest blocker is the unperformed anchor join Factual correction (witty-raven-412, read against gunbc.dgx_spark_procurement). My prior scanned_from prose said the srv5/srv6 slot->box assignment is "an open operator decision" — false and would propagate. dgx_spark_router_binding_operator_allocation (SPARK-ALLOC-LAND-0, 2026-08-07) records the operator DELEGATED the tie-break and it was decided on ascending address order: srv5->192.168.1.222, srv6->192.168.1.223, both carried as SlotAssigned rows. No 50/50 guess is outstanding. ScannedFirstContact is still the correct label, for the right reason: no anchored enrollment has been PERFORMED — nobody has enrolled a Spark host key, observed the MAC at that address, and matched it against the cited CR1000A reservation. IndependentlyAnchored would claim work that has not happened. The anchor is AVAILABLE but UNUSED. This matters practically: bold-eagle is NOT blocked on an operator decision (it was made three days ago), only on running the join — so the Spark host-identity condition does not acquire a phantom blocker. Prose-only; no type, value, or behavior change. Compiles clean through analyses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Clear cursor RC 50900: collector enrolls host keys in-process before its SSH legs (same path as probe) Real defect (cursor review 50900, §3/§5). collect_fleet_convergence_wet SSHs to every fleet host but never called fleet_host_key_enroll_apply(), unlike probe_fleet_reach_wet. The collector runs in its OWN isolated main-push job with a fresh ephemeral HOME (CI overrides HOME per job) and WIF-materialized key but no durable known_hosts — so the enrolled-only pin (StrictHostKeyChecking yes + UserKnownHostsFile=fragment) that enroll_apply writes never landed there, leaving the collector on the ambient-trust path the pin was written to close, and on a different trust path than the probe. Fix: call fleet_host_key_enroll_apply() FIRST in collect_fleet_convergence_wet and carry its readback in the collect receipt, mirroring the probe entry exactly — one enrollment authority, one trust path. Green by execution: the collector closure compiles and runs to ExitSuccess with the enroll call (no effect/cycle error). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y * Consolidate 10 deploy/convergence/toolbox jobs into ONE `fleet` job (operator request) Operator: "can we simply have a single fleet convergence job — it's overwhelming to see so many ... it can include all deployment/convergence i.e. srv1 deploy etc." The ten former jobs (deploy_dashboard_srv1, collect_fleet_convergence, advance_fleet_desired, probe_fleet_reach, scan_fleet_host_keys, enroll_fleet_host_keys, observe_probe_identity, install_fleet_converge_timer, enroll_runner_authorized_key, observe_srv1_tasks_preapply) collapse into ONE `fleet` job, so a normal push/PR shows a single (mostly-skipped) row instead of ~nine. Each former job's work becomes a STEP carrying the event guard its old job's if_condition held, applied via with_step_if: - main-push convergence steps (deploy/collect/advance): if github.event_name == 'push' - workflow_dispatch toolbox steps: if github.event_name == 'workflow_dispatch' Job-level gate ci_fleet_job_if runs the whole job only on (main push + ci success) OR workflow_dispatch — so PRs skip it as one row. Shared prelude + release-bins + WIF/ssh-agent setup run ONCE (were duplicated across jobs — a materialization win), key-agent cleanup runs always(). runs-on srv1; needs [ci]; permissions are the union (contents:write for advance + id_token:write for WIF); deploy's blue/green concurrency group carries to the whole job. Tradeoff, stated: needs [ci] means manual workflow_dispatch fleet ops now wait for the ci floor before running (the cost of one consolidated job; the convergence steps must gate on ci success, which requires needs [ci]). Cleanup: deleted the 11 now-dead per-op job builders, the unused gunbc_ci_deploy_job_backstop_timeout_minutes and ci_fleet_credential_permissions, and the orphaned ci_deploy_push_to_main_if / ci_workflow_dispatch_only_if expressions (their skip-cascade rationale folded onto ci_fleet_job_if, which now carries the !cancelled() + needs.ci.result guard the witness checks). The skip-cascade wall is preserved: ci_fleet_job_if's push arm carries !cancelled() + needs.ci.result == 'success', so push_to_main_family_carries_explicit_status_guard still holds (count is now 1 job, not >=2). Updated ci_deploy / ci_deploy_target_host witnesses to assert deploy-as-step-in-fleet; consumer_jobs -> ["fleet"]. Green by execution: main_wet regen idempotent (ci.yml 10 jobs -> 5); the emitted fleet job carries the correct per-event step guards; ci_workflow_witness_holds, push_to_main_family_carries_explicit_status_guard, and both updated deploy witnesses return true. Runtime main-push behavior validates post-merge (fleet convergence steps run on main push only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Model firmware targets, boot diagnostics, and memory part selection Modeling work from a live srv3/srv4 incident: both hosts failed to boot after a DIMM upgrade. Root cause of the first failure was host firmware three years stale; the residual failure is a DDR4 LRDIMM data-buffer training step. Nothing here is a blocklist -- each avoidance is derived from a mechanism, so a future ordering workflow can shop the whole market and explain every refusal by naming the failing step. extdeps.firmware.types (new) FirmwareTarget = BmcController | HostBios. The fleet could express which BMC image a host should run and had no way to express which BIOS, though BIOS owns DDR training. FirmwareImageKind distinguishes the OpenBMC tar (pushable to a dead host) from the Instant Flash ROM (needs a booting host). FirmwareReleaseRow carries a prerequisite, so an unappliable push is refused from the catalog rather than discovered from a vendor shell script after a 33MB upload. Consolidates BmcFirmwareSemanticVersion -> FirmwareSemanticVersion across 8 files: a version triple is one concept, and minting a second name for host firmware would be the CpuArchitecture/TargetArchitecture class. extdeps.ampere.scp_diagnostic (new) SCP console tokens as class+number with meaning held separately. Deliberately no token-to-meaning function: no codebook was obtained, so every implementation of one would be invention. The E prefix classifies a namespace, not a consequence -- E559/E571 appear on a same-family machine that boots normally. DiagnosticReadEffect records that several of these interfaces mutate what they report. extdeps.memory.training (new) Training steps, with the required set DERIVED from buffering. A registered module has no data buffer and cannot fail buffer calibration -- a difference in kind, which is what makes it sound as a purchase filter. capacity_reachable_as_dual_rank_registered derives from JEDEC die density why cheap high-capacity DDR4 is disproportionately load-reduced. gunbc.host_memory_qualification (new) Four independent standings: board class support, specific part qualification, DDR training outcome, host boot outcome. None derives another. Their collapse into one "does the RAM work" question cost three refuted hypotheses during the incident. Causal experiments land as receipts, including HypothesisWeakenedByIntervention for an intervention weaker than the one that would settle the question. gunbc.memory_part_selection (new) A candidate is refused iff it must execute a step this board has been observed to fail -- independent of vendor, part number, capacity, speed grade, or AVL membership. Implication is not a Pareto objective: implicated parts are refused before the frontier is computed. gunbc.host_boot_supervision, gunbc.host_diagnostic_channel (new) Two independent reset supervisors, kept separate because only one is configurable. Interval typed by how it is KNOWN: powermon's 306s is measured from eleven journal timestamps, not read from configuration. Five diagnostic authorities with their transports, and the standing that a missing projection does not establish a missing source. Witnesses cover the eight properties an external review required. Every digest was measured by acquiring the artifact; nothing is transcribed. NOT COMPILED. The local `gunbc compile --target dag` path rejects `//` at 1536 files corpus-wide, nearly all untouched, so it does not implement the section 4c annotation channel and cannot check this. CI is the first real compile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Fix import placement, ground firmware/price units on std.measure The parse error CI named (expected name, found LBrace) was a misplaced import: the migration insert landed between the opening brace of a multi-line import block and its first name. Only this one file was affected; the other five insert sites were checked and are well-formed. Review 51271, both blockers, verified present and fixed: - FirmwareReleaseRow.image_bytes was a flat Nat beside a sibling capacity_bytes already carrying ByteSize in the same diff. Now ByteSize; the three ASRock call sites construct through byte_size. - PartCandidate carried unit_price_minor: Int and currency: NonEmptyStr, with a comment saying prices are deliberately not compared across currencies. That is exactly the invariant the type should enforce, so it is now MoneyAmountMicro + std.currency.CurrencyCode, and admission returns a typed CandidateAdmission whose CandidateCurrencyMismatch arm refuses a candidate priced in an undeclared currency rather than ranking it. The narration is now a wall. The selection rule had no executing consumer, which the unit fix made conspicuous. Adds memory_part_selection_witness_test with the currency refusal and its positive control, the implication rule and its RED control (a registered module of the same vendor/capacity/generation is NOT implicated, so the rule is discriminating on buffering rather than on part identity), and a no-observed-failure control proving the refusal comes from the observation rather than from a preference in the model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Annotate observed_failures with its type data observed_failures was written without a type annotation, which the parser refuses (expected Colon, found Eq). Mine, in the witness added by the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Ground the obmc push artifact byte count on ByteSize Review 51280, blocking item, verified present at extdeps.boards.asrock_rack asrock_altrad8ud_bios_3_10_obmc_push_artifact_bytes (the review cited line 369; the symbol is at 350 — naming it symbolically per DESIGN section 3). It was the last bare-Nat byte count in the diff and sits beside FirmwareReleaseRow.image_bytes, which the previous commit had already moved to ByteSize, so it was a fork of the carrier in one file. Now byte_size(count: 33566720). It has no consumers, so nothing downstream changes shape. Not taking the non-blocking import cleanup in extdeps.memory.training; see the PR reply for why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Derive the BMC Redfish surface from firmware instead of storing it per host The srv3 flash exposed a dual representation. Which thermal resources bmcweb serves, how many sensors the chassis collection carries, and whether a fan-control resource exists are decided by the firmware BUILD for a board, but BmcRedfishSurfaceObservation stored those three fields per host, beside the firmware version they are a function of. So when srv3 went 2.07.00 -> 3.22.00 on 2026-08-11 its hand-authored row went on claiming ThermalSubsystemSensorCollection with 51 sensors, and two witnesses ASSERTED those constants as literals — the corpus stayed green while describing a surface the host did not have. There was no second authority that could disagree, so nothing could have caught it. MEASURED 2026-08-12, both hosts at once, which is what makes this a catalog row rather than a per-host note: srv3 (192.168.1.192) and srv4 (192.168.1.195), flashed independently on different days, both returned legacy Chassis Thermal 200, ThermalSubsystem 404, ThermalSubsystem/Fans 404, a 9-member Sensors collection, and a byte-identical Thermal key shape (12974 / 12975 bytes, differing in one reading value). Two independently-arrived-at hosts agreeing on every axis is the evidence that the shape follows the image; one host would only have established what that host does. An earlier probe returned 401 on BOTH hosts, which was a credential fact rather than a surface fact — checking the pair is what distinguished them. - extdeps.bmc.types gains BmcRedfishSurfaceShape, a cited BmcRedfishSurfaceRow catalog keyed by version, and bmc_redfish_surface_for / bmc_redfish_surface_of_host. An uncatalogued image derives SurfaceShapeUncatalogued carrying the version rather than a nearest-neighbour or default shape, so the next flash is loud at derivation instead of silently wrong at use. - BmcRedfishSurfaceObservation is DELETED rather than left beside the derivation; a dead carrier that can still be constructed re-opens the fork it was replaced for (DESIGN section 4b, dissolution on climb). - srv3's firmware observation is re-observed to 3.22.00 (VERSION_ID from /etc/os-release, authenticated BMC SSH). Its surface moves with it in one motion, because there is no longer a second place to update. Witnesses: the two that pinned the fork are replaced by the property that actually holds — hosts on the same image derive the same surface — plus a RED control proving the derivation discriminates on version (the 2.07.00 row still derives the old shape) and a fail-closed control that an uncatalogued image refuses. Per section 4b the evidence stays enrolled rather than retiring with the machinery it replaced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Move source annotations to module-item grain DESIGN section 4c admits only standalone leading // blocks attached to module-scope declarations; body-position annotations refuse. Every new module in this change had them — comments sitting beside a field or a coproduct arm — so the heal job refused with a located diagnostic per line. Each block moves above its enclosing declaration and keeps its referent by naming it ("On prerequisite:", "On FirmwarePrerequisiteUnobserved:"), so no rationale is lost in the move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Carry the firmware prerequisite's evidence as data, not prose Review 51290 (non-blocking) on the receipt Strings. Taking the one with a live consequence rather than the whole corpus-wide class: the BIOS-tar prerequisite declares BMC >= 3.22.00 as a LITERAL while the two executed pushes that established it sat only in an unstructured String, so the release row and its evidence could not be joined and a declared minimum the evidence contradicted read exactly like one it supported. FirmwarePrerequisiteObservation now carries each executed push (board, subject, packaging, gating target and version, outcome, timestamp), with the vendor diagnostic held on the refusal arm. The two ALTRAD8UD rows are the discriminating pair: identical bytes refused at BMC 2.07.00, accepted at 3.22.00. prerequisite_evidence_verdict checks a declared minimum against them, and its positive arm is deliberately PrerequisiteConsistentUpperBound rather than a bare "consistent": two pushes BOUND a threshold, they do not locate it — 3.06.00 is in the capability catalog and was never pushed, so the declared 3.22.00 may be stricter than the vendor requires. Claiming the threshold was measured would be the stronger-than-evidence reading the arm exists to prevent. Witness: the pair supports the declared minimum as an upper bound; a minimum inflated above the accepting version is contradicted BY the acceptance; a minimum relaxed to or below the refusing version is contradicted BY the refusal (the direction that would admit an unappliable push); and an empty observation list yields Absent, never consistent. The prose row keeps only what the carrier cannot hold: that the vendor page documents no such dependency, that the cause was legible only in the BMC journal because Redfish reported Exception with none, and why the prerequisite belongs to the packaging rather than the release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Execute the prerequisite gate and the loop-cause fold Review 51292, both findings verified: firmware_release_applicability and host_boot_loop_cause had zero importing consumers and zero witnesses, so the PR's two central fail-closed claims were specification without execution — the exact bar this PR's own prose sets (DESIGN section 5). The applicability rows bind the PRODUCTION release rows rather than fixtures, so the gate runs against the prerequisite the catalog actually declares: - unreadable target version refuses (the srv3 BIOS-null case that justified the third arm); - the srv3 push at BMC 2.07.00 is Unmet and the verdict carries what it observed; - the srv4 push at 3.22.00 is Applicable (positive control); - 3.06.00 is Unmet — higher major, lower minor, so a major-only compare would wrongly admit it, and that version is a real catalogued image rather than an invented one; - the Instant Flash ROM row is Applicable on the SAME Absent observation that refuses on the tar row. Same input, opposite verdict, because the prerequisite belongs to the packaging. For the loop cause the discriminating pair is the intervention, not the signature: one E588 signature answers HostSelfResetOnError after the supervisor was stopped and the loop persisted, and CONFOUNDED with the identical signature when it was not. That is the state the incident sat in while the watchdog theory looked plausible, and it is why period alone cannot decide — a 306-second cycle looks the same whether or not a 306-second timeout causes it. A third row keeps a clean stop with no error code at Unobserved rather than inferring a supervisor verdict from silence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Keep observations immutable; split inventory from image; refuse duplicate keys Four findings, all verified against the code before acting. IMMUTABLE OBSERVATIONS (mine, introduced by this PR). Editing the single srv3 firmware row in place from 2.07.00 to 3.22.00 moved every receipt that referenced it, so srv3's JULY update-service snapshot began claiming it had been observed under firmware installed in AUGUST — the same temporal-authority defect this PR set out to fix, relocated one field sideways. The 2.07.00 and 3.22.00 observations are now separate immutable rows; srv3_bmc_firmware_observation is the CURRENT standing and advances by re-binding, not mutation; the update-service receipt binds the 2.07.00 row it was actually taken under. A 3.22.00 update-service receipt will land as a new row when one is measured, not by re-pointing this one. INVENTORY IS NOT AN IMAGE FACT. chassis_sensor_collection_member_count sat inside the image-derived shape. Two hosts returning nine members at one moment establishes agreement at those observations; it does not make cardinality invariant under fan, FRU, or sensor-service changes — and this same session measured installed fan sets DIFFERING under identical firmware, which is the same nearby-question mistake one axis over. The count is dropped from the shape rather than being derived from something that does not decide it. Route and resource availability stay, since those are compiled into bmcweb. DUPLICATE KEYS REFUSE. bmc_redfish_surface_for took the first match, so two rows for one version resolved by list position — a silent order-dependent answer where the catalog actually contradicts itself. It now answers SurfaceShapeAmbiguous with the collision count, with a RED control that plants the duplicate. SECOND, NOT NAT (review 51298, verified). SupervisorIntervalKnowledge carried seconds on bare Nat while std.measure Second is the authority used for the same concept elsewhere. The declared/measured epistemic split is unchanged; only the carrier moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Grade the memory refusal by evidence and scope it to the board The rule refused any module that merely EXECUTES a step some platform was observed to fail — on any board, at any evidence standing, including EvidenceUnobserved. Because capacities near a generation's die-density ceiling are disproportionately load-reduced, that removed most of the 64GB DDR4 market by construction: the blocklist this module's own prose claimed to avoid, reached by a different route, and the opposite of letting a buyer shop the whole market instead of an AVL. Severity is now a function of evidence, which is what the standing type was carrying all along and the verdict was discarding: - DecodedFromPlatformSyndrome refuses; - AssociatedByIdenticalSignature and InferredFromSequencePosition require a SAMPLE and stay on the Pareto frontier; - EvidenceUnobserved implicates nothing. gunbc holds MREP at association standing, so the honest verdict for another ALTRAD8UD LRDIMM is buy-one-and-test. Failures are also scoped to the board observed, so an ALTRAD8UD receipt cannot speak for a different platform. Witnesses discriminate on evidence with the board, step and module held fixed: association -> sample-required, decoded -> refused, unobserved -> nothing, registered module untouched at any standing. Two frontier rows show the market is not shrunk by an association (both candidates remain, the load-reduced one flagged) while a decoded refusal still drops one. KNOWN GAP, declared rather than hidden: BaseboardModel has exactly one variant today, so board scoping is real in the code but has no executing cross-board control. Minting a second board purely to test it would be a fabricated entity; the control lands when a second real board is modeled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Carry the supervisor interval on Second, not bare Nat review 51303, same finding as review 51298 on the stacked PR. I fixed it there and not here, but host_boot_supervision.dag is introduced by THIS PR, so the fix belonged at its origin — the reviewer was correctly seeing bare Nat on the branch that declares the type. std.measure Second is the carrier siblings already use for this concept. The declared-vs-measured epistemic split is unchanged; only the unit moves. Duration is not a separate std.measure carrier in this corpus, so Second is the existing single authority rather than a new one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Stop fabricating evidence bounds; add the two missing witness imports review 51304 (non-blocking, but the finding is a section 5 violation by this repo's own standard, so taking it): prerequisite_evidence_verdict stored the DECLARED MINIMUM into whichever bound had no observation yet. A verdict backed only by an acceptance therefore reported a highest_refusal that never happened — a fabricated plausible value in the exact field a consumer reads to judge how well-bounded the threshold is. It was also order-dependent: once a bound was set, later observations were discarded, so the same evidence authored in the other order gave different bounds. The reviewer correctly noted my positive test greened only because the list happens to be authored refused-then-accepted. Both bounds are now Optional and computed as a real max-refusal / min-acceptance over all observations, which is associative and commutative and so cannot depend on authoring order. Absent means absent. Two controls added: the reversed list must produce an equal verdict, and an acceptance-only history must report NO refusal bound. review 51307 (blocking, verified): bmc_firmware_thermal_witness_test referenced firmware_semantic_version with no extdeps.firmware.types import at all, and asrock_altrad8ud_openbmc_3_22_00_capabilities while the asrock block imported only the 2.07.00 and 3.06.00 rows. Both were mine, from scripted edits that added references without their imports. Both added; the other changed witnesses were checked for the same class and are clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Declare ExternalModelScope on the new extdeps modules; fix optional imports Two real CI failures, both mine. RESOLUTION: Present and Absent were imported from std.types, where they do not live. The corpus idiom is v2.std.optional. Fixed in extdeps.firmware.types and both witnesses that matched on them. I had "verified imports" after the last review with a targeted check that only confirmed a symbol appeared in SOME import block — it never checked the symbol was exported by the module named, so it passed a wrong-module import cleanly. The check was weaker than the claim I made from it. EXTDEPS SCOPE PLACEMENT GATE: three new dag/extdeps modules were enrolled in neither scope_carrier_paths nor the machinery exemption, which the per-PR gate refuses by design — a new extdeps file's only landing states are scope carrier or genuine machinery, and the frozen legacy manifest is explicitly not an admission channel. Each now declares extdeps_model_scope naming its exact upstream subject and joins the carrier roster: - extdeps.firmware.types -> FirmwareReleaseRow: the vendor release and its applicability precondition, not the flashing mechanism and not fleet policy about which release to run. - extdeps.memory.training -> MemoryTrainingStep: the JEDEC DDR4 initialization calibration sequence, cited to JESD79-4A; platform failure receipts and purchasing policy are deliberately outside it. - extdeps.ampere.scp_diagnostic -> AmpereScpConsoleToken: the token stream and structured boot-progress interface as an upstream surface only, with the read-effect axis included because an acknowledged queue is not a sampled register, and with no token-to-meaning function since no codebook has been obtained. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Restore the deleted contradiction fn; rebind drift fixtures to 2.07.00 review 51313, all four findings verified and real. COMPILE BREAK, mine: rewriting the evidence-bounds block deleted prerequisite_observation_contradicts along with the accumulator it sat beside, while prerequisite_contradiction still called it. Nothing defined it anywhere in the tree, so the module could not compile and the contradiction arms were unreachable — the arms that do the actual gating. Restored with its own rationale: an acceptance strictly below the minimum falsifies it, a refusal at or above it falsifies it the other way, and consistent is not the same as supporting, which is what the bounds report separately. STALE FIXTURES, caused by this PR: three classification witnesses used srv3 as their drifted-host fixture and asserted 2.07.00. Advancing srv3's current standing to 3.22.00 did not merely break them — once srv3 satisfies the pinned track-3 intent, Drifted/Apply becomes Converged/Noop and the tests would have inverted what they claim to check. They now bind srv3_bmc_firmware_2_07_00_observation, the immutable historical row, which is what each test was always about: the classification logic on a host behind its intent, not srv3's present state. That also resolves the fourth finding without a separate change: the update-service receipt is bound to the 2.07.00 observation, so the plan test now pairs one firmware identity with itself and the update-surface identity check no longer refuses. This is the immutable-observation split paying for itself immediately — the historical row existing is what let three tests keep their meaning instead of being rewritten around a moved fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Join the prerequisite oracle to the catalog instead of copying it review 51318, verified and correct. declared_minimum hand-wrote 3.22.00 while a comment two lines above claimed the witness "compares the SAME number the catalog declares" — it did not. Editing asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite would have left executed_pair_supports_the_declared_minimum_as_an_upper_bound and the_bounds_do_not_depend_on_authoring_order green until someone remembered to edit the witness too. That is the tree-copied oracle DESIGN section 5 forbids, and the comment asserting otherwise is what made it look already checked. verdict_against_declared_minimum now matches the release row's own prerequisite and passes its minimum through, so the catalog and the tests move together. The sibling witness already used this join, so the pattern was in the tree and I did not follow it. The RED controls deliberately do NOT join: a falsifying probe is an authored input chosen to contradict the evidence, not an oracle, and joining it to the catalog would make it unauthorable. That split is stated in the module so the next reader does not "fix" it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Declare ExternalModelScope on the new extdeps modules; fix optional imports Cherry-picked to the branch that INTRODUCES these modules. I committed this fix on the stacked derivation branch instead, so #8171 — where extdeps.firmware.types, extdeps.memory.training and extdeps.ampere.scp_diagnostic are actually added — kept failing the placement gate it was failing before. The stack direction makes that invisible: merging the parent forward into the child hides that the child, not the parent, carries the fix. The extdeps scope placement gate refuses a new dag/extdeps file enrolled in neither scope_carrier_paths nor the machinery exemption, by design — the frozen legacy manifest is explicitly not an admission channel. Each module now declares extdeps_model_scope naming its exact upstream subject and joins the carrier roster: - extdeps.firmware.types -> FirmwareReleaseRow: the vendor release and its applicability precondition, not the flashing mechanism and not fleet policy about which release to run. - extdeps.memory.training -> MemoryTrainingStep: the JEDEC DDR4 initialization calibration sequence, cited to JESD79-4A. - extdeps.ampere.scp_diagnostic -> AmpereScpConsoleToken: the token stream and structured boot-progress interface as an upstream surface only, read-effect axis included because an acknowledged queue is not a sampled register, and still no token-to-meaning function. Also fixes Present/Absent, imported from std.types where they do not live; the corpus idiom is v2.std.optional. This branch's copy of extdeps.firmware.types needs them for firmware_release_applicability but not List/list_length, which belong to the evidence carrier that lives only on the stacked branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Drop two dead surface imports from the live-receipts witness review 51322, verified: srv3_bmc_redfish_surface and srv4_bmc_redfish_surface appear only on their own import lines in bmc_live_receipts_witness_test, left behind when the per-host surface assertions were replaced by the derivation. Removing these is NOT the case I declined on the earlier PR citing the #6985 Class B hazard, and the distinction is worth stating since I invoked that rule already. Class B is about a stripped file's bare cross-module references resolving only by pool-membership coincidence — the danger is dropping a MODULE from the assembled closure. Here gunbc.fleet_bmc_observation stays imported via ten other symbols, so the closure is unchanged and only two dead names go. The derivation itself keeps its real consumers in bmc_firmware_thermal_witness_test, which is where the surface is actually asserted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Make population admission true multiset equality review 51323. The finding as stated does not reproduce, and the defect underneath it is real but mirrored — both worth recording. NOT REPRODUCED: the cited candidate ["A1","A1","B1","E1"] is REFUSED today. population_slots_match folds `a` over `b`, and the call site passes a = documented row, b = candidate — so the subset test runs documented-into-candidate, not candidate-into-documented. A duplicated candidate slot shrinks its distinct set, so a same-length documented row with four distinct slots cannot be covered. Simulated over all four documented rows before changing anything. REAL, AND LATENT: the guard was equal-length-plus-subset, not multiset equality, and it relied on an unstated precondition that no DOCUMENTED row carries a duplicate. Violate that and it fails in the opposite direction from the report: documented ["A1","A1"] admits candidate ["A1","B1"], since both documented entries are found and lengths agree. No board row has a duplicate today, which is why it was latent — and why leaving correctness resting on that staying true is the wrong place to stop. Now compares occurrence counts, which is multiset equality given equal lengths and removes the precondition. Three controls: the duplicated candidate stays refused (the property callers rely on, previously true only incidentally); a synthetic duplicated documented row does NOT admit a distinct pair — the case that was actually broken; and that same row still admits its own multiset, so the refusal is discrimination rather than a blanket rejection of duplicated rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Flatten the multiset comparison onto one line Parse error I shipped in the previous commit: expected RParen, found EqEq at dag/extdeps/boards/types.dag. The comparison was written with a line break BEFORE the ==, inside fold's argument list, so the expression terminated at the newline and the parser then wanted the arg list to close. The corpus does carry `&& x == 5` unparenthesized, so the operator combination was never the problem — the continuation position was. Leading && and || continuations at statement level are used throughout and parse fine; a leading == inside a call's argument list is the shape that fails. Now one line, parenthesized, matching the style of slot_occurrence_count directly above it. Swept the rest of the change for the same shape: this was the only occurrence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Stop conflating admitted with undominated in the frontier witness The witness floor caught a real error, and it was in my test design rather than in the model: an_association_leaves_both_candidates_on_the _frontier returned false because both fixtures carried the SAME price and capacity, leaving data rate as the only difference. The registered module therefore strictly DOMINATED the load-reduced one on the Pareto axes and the frontier correctly dropped it — I read that dominance as an admission failure. Admitted and undominated are different properties, and a fixture pair differing on exactly one axis cannot distinguish them. The pair now differs on two: the load-reduced part is cheaper and slower, the registered part dearer and faster, so neither dominates and frontier membership is decided by admission alone — which is what the test claims to check. That also matches the market fact the module models, where the part near the density ceiling is the cheap one. Verified both frontier assertions by simulating the dominance relation over the new fixtures before pushing: association -> 2, decoded -> 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Take the money carrier at the helper's parameter, not inside it review 51333, verified: candidate_at_price took unit_price_micros as a bare Nat and wrapped it in money_amount_micro internally, so the unit lived inside the helper rather than at its boundary. Every caller was free to pass a raw number meaning anything. Now the parameter is MoneyAmountMicro and callers construct the carrier at the call site. Same class as the earlier ByteSize / MoneyAmountMicro / Second fixes in this stack; I introduced this one two commits ago while fixing the frontier fixtures and reached for a scalar out of habit, which is precisely why the rule is worth having as a wall rather than a preference. std.nat is dropped from this witness: Nat became unused with the parameter retyped, and I had added that import in the same commit that introduced the scalar, so this returns the file to its prior import set rather than stripping anything pre-existing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Import Present/Absent explicitly in host_boot_supervision review 51335 asked for this on the ground that the file "has unresolved constructors and won't compile". That reasoning is wrong, and the check is cheap: host_boot_loop_cause executed GREEN in CI run 31566167444 — all three of its witnesses passed, including the arms that match Present and Absent. Bare Absent resolves in this corpus without the import; extdeps.bmc.capability and bmc_fan_converge_witness_test both do the same and are green on main. Taking the change anyway, because it is right for the OTHER reason. That resolution works by pool-membership coincidence — the target module is in the assembled closure only because some unrelated import elsewhere dragged it there — which is exactly the #6985 Class B hazard DESIGN records, and the remedy that thread names is making the binding closure-independent with an explicit import. asrock_live_probe_witness carries the same repair for the same reason. So this is not a no-op: it removes a dependency on accidental closure membership that would break the next time an unrelated lane changes its imports. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Fix the parse error CI found, and make the order control discriminating CI's located cause: expected expression, found EqEq in firmware_prerequisite_evidence_witness_test.dag. A line break before == terminates the expression, so the operator began a new statement. Same class as the earlier fold-argument break; leading && is fine, leading == is not, so the comparison is now one line. This file had never been parsed by CI. It reached main's floor only because #8171 merged and GitHub retargeted this PR, which is what a stacked branch buys: approvals on a branch that no required check was ever configured to run. Reading the failing test to fix it showed it did not test what it claimed. It compared two orderings of a two-element list holding ONE refusal and ONE acceptance -- an ordering over which first-wins and max/min agree, so it could not distinguish them, though its comment said that was its whole purpose. Both lists now carry a second, lower refusal at 1.00.00: a first-wins fold reports 1.00.00 on the reversed ordering and reds. Simulated in both directions before pushing; the expected bounds are held in one helper so a disagreeing ordering cannot be absorbed by a second copy of the numbers. Also drops FirmwareSemanticVersion, unused here before this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Reclassify srv3's install path as unestablished, not refused-forever CI's 13 reds were one derivation, and they were the tripwire in os_install_mechanism working exactly as written: "if srv3's BMC is upgraded and its evidence row follows, this witness reds instead of the fleet silently planning a delivery mechanism the firmware no longer implements." srv3 was measured live on BMC 3.22.00, BIOS 3.10, host Running; re-binding its firmware standing to that made the tripwire fire. WHAT THESE WITNESSES NOW SAY, and what they deliberately do not. The catalogued 3.22.00 row lists neither Redfish virtual media nor the bmcweb nbd-proxy, and no host has an established network-boot delivery, so the solver has nothing to select and refuses with InstallNoVirtualMediaAndNetworkBootUnestablished. That cause names UNESTABLISHED. It is NOT a claim that the host cannot be installed -- srv4 runs this same firmware and was installed over the configfs USB-gadget transport, which extdeps.bmc.virtual_media already models as ConfigfsGadgetTransport and which this solver cannot see because OsInstallMechanism predates that authority. WHAT I DID NOT DO, having started it and backed it out. The first attempt added CapabilityConfigfsVirtualMedia plus a matching OsInstallMechanism variant. That would have minted a third name for a transport VirtualMediaTransport already names -- the section 3 nicknaming violation -- and it surfaced that CapabilityNbdProxyVirtualMedia is already that same fork for NbdProxyWebsocatTransport. Widening a fork to turn CI green is the wrong trade. Reverted whole; the real repair is the install lane consuming VirtualMediaIntent/VirtualMediaTransport, which is INSTALL-0, not this PR. Each rewritten witness keeps its original load-bearing claim rather than being flipped to expect a refusal: no silent PXE fallback -- previously "is nbd-proxy, no fallback"; the fallback arm is now exactly where a defective solver would land, so refusing is the proof it does not plan is solver-derived -- a hardcoded plan would still be Ready after the firmware change; both halves refusing is strictly more discriminating than the old version, which could not tell a derived plan from a hardcoded one while the solve agreed with it boot target follows mechanism -- the mapping is pinned on the mechanisms directly, where the claim always lived prep is not install -- credential wiring and the seeded ISO path hold; the plan and seed halves refuse with the solve, so nothing here can read as a solved install srv4 has none -- srv3 has now joined it; both hosts run 3.22.00 and refuse with the same cause, so asserting a difference would assert a fact that stopped being true Also corrects srv3_install_mechanism_board_default_residue, which still said the legacy row "agrees TODAY" and cited a witness by its old name -- a stale citation in the note that predicted this very divergence. The target facts the plan witnesses used to assert are not lost: they are what INSTALL-0 must reproduce once srv3 has an established path, at which point those witnesses red and are replaced rather than silently passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * INSTALL-0 C1: make "nobody probed this" representable The defect this lane opened on was not a wrong capability row. It was that BmcFirmwareCapabilityRow has no way to say nobody looked, so firmware_has_capability reads list non-membership as false and a hand-authored list that never mentioned configfs is indistinguishable from a probe that looked and found it missing. srv3 was refused an install path on that reading while its sibling srv4, on the same firmware, was installed over exactly the transport the list omits. This commit makes the distinction representable at the grain where it is decidable: not one flag per firmware, but one standing per requirement the transport rests on. observed absent -> Refused a probe looked; the thing is missing unobserved -> Unestablished nobody looked; the answer is owed all present -> Established Absence outranks ignorance, deliberately: one requirement observed missing refuses even with others unprobed, because no remaining probe can overturn it, and answering Unestablished there would send someone to run probes that decide nothing. srv3's four live probes from 2026-08-13 land as receipts carrying the command and its output, so a standing can be re-derived by someone who doubts it rather than resting on a Bool whose provenance is a memory. The other four requirements have NO row: an unobserved requirement is the absence of an observation, and giving TransportProbeOutcome a third arm would let an unrun probe be authored as though it had run. srv3 therefore stands Unestablished owing exactly those four. WHAT THIS DOES NOT DO, and why. It does not wire the standing into gunbc.os_install_mechanism. That solver reads its own OsInstallMechanism enum rather than extdeps.bmc.virtual_media VirtualMediaTransport, and teaching it to consume the transport authority drags the actuator step lists with it -- the next commit, not this one. Until then this row is an authority with a witness and no production consumer, which is stated rather than left to be discovered. It also does NOT fix catalog_has_vm_capable_firmware, which checks CapabilityVirtualMedia only and so cannot see that the 2.07.00 row is nbd-proxy capable. That looks like an isolated one-line defect and is not: fixing it flips srv3/srv4 from InstallNoVirtualMediaAndNetworkBootUnestablished to InstallFirmwareTransitionUnestablished, which is a different refusal cause and would red the witnesses reclassified in #8174. It belongs with the solver rewiring, where both halves move together. Stacked on the #8174 branch because it edits the same lane; it receives no CI until that merges and GitHub retargets it, same as #8174 did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Keep the standing's expression on its = line CI's located cause: expected expression, found Newline in fleet_install_transport_observations.dag. The srv3 standing broke the line after `data ... =` and put the call beneath it; a .dag value expression must begin on its operator's line. Third instance of this class in this session -- the other two were a leading == on a continuation line and a line break before == inside a fold argument list. The boundary is narrower than "leading operators are bad": a leading && parses and is idiomatic in the corpus, and `type X =` followed by variants on the next line is a different production that has always parsed. It is specifically a VALUE expression that cannot start on the next line. Swept the whole branch for both shapes; this was the only site. The remaining `=`-terminated lines are all `type X =` coproducts in extdeps/bmc/types.dag, untouched here and long green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Move 18 body-grain source annotations to module-item grain DESIGN section 4c models source annotations only as standalone leading blocks on module-scope declarations. Eighteen comment blocks across five witness modules sat INSIDE declaration bodies -- annotating individual clauses of a conjunction, arms of a match, or lets in the middle of a function -- which the compile-clean gate refuses with: compile-clean: source annotation sits inside a declaration body. Only module-item grain is modeled; move it above the declaration it describes. This is pre-existing main debt, not a regression from any one branch. The count is exact: 18 in-body blocks under dag/ against 18 diagnostics, and the same refusal reproduces on two unrelated branches whose only common factor is that both force the whole-tree compile-clean baseline. Main's own recent runs are cancelled or in-flight, so no green verdict has observed it -- the debt has been accumulating unseen across at least five merged PRs (#8045, #8073, #8122, #8147, #8184-era files). The prose is preserved, not deleted. Each block is hoisted into its declaration's leading block, merged with any block already there, keeping the clause labels as sentences. Where a leading block already said the same thing -- fleet_converge_cli's join-1 header covers every clause note it carried -- the in-body copy is dropped as the duplicate it was. Comment-only: `git diff` contains no non-comment line, so no behavior, no witness assertion, and no emitted byte moves. WHAT THIS DOES NOT DO: the diagnostic is typed but UNLOCATED -- it names no file, line, or declaration, though AnnotationAttachmentRefusal carries `origin: SourceSpan` on every arm and BodyGrainNotModeled is constructed with it. The consumer discards the span it was handed. That is a section 5 located-ness gap which cost this diagnosis a full CI cycle and a bisection commit; it is left as a separate finding rather than repaired here, so this stays comment-only and reviewable as such. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Attach the two subject-less trailing annotations to the declarations they discuss CORRECTION TO THIS PR'S OWN DIAGNOSIS. The first commit claimed 18 in-body blocks were the whole annotation debt, on the strength of an exact count match. The match was real but the population was not the whole one: I grepped the failing log for the body-grain message and never checked whether a SECOND refusal class was also present. It was. The #8211 log carried 11 "source annotation names no subject: no module item follows it" refusals beside the 18 body-grain ones, and clearing the body-grain class left that second class as the visible failure. That is the nearby-question error: I confirmed the count of the class I was looking at and read it as the count of the failure. The unattached class is 7 at this PR's base, and it splits in two. FIXED HERE -- two genuine trailing notes, each explaining why something does NOT exist and therefore having no following declaration to attach to. Both move above the declaration they already discuss, which is where they belong on the merits and not only to satisfy the gate: dag/product/rack_mount.dag -- the note argues the mounting APERTURE is not the external envelope, which is exactly what the function directly below it computes; it now leads rack_mounting_aperture_height. resolved_graph_cache_hand_rust_witness_test.dag -- the note records a deleted tautological witness and points at the surviving positive control; it now leads that control. NOT FIXED HERE, deliberately -- the remaining 5 are comments sitting directly above `service` declarations (extdeps git.Inspect, git.PublicationTransport, sec.Edgar, tailscale.Serve, gcp.IamRoles). A `service` produces no depth-1 occurrence index entry, so it is not an eligible annotation subject and prose above it attaches to nothing. Relocating that prose would make the corpus worse. git/inspect.dag's block is 24 lines of measured rationale for the service's operations -- which porcelain version is pinned and why, why IgnoredFiles is a second operation rather than a flag, verified by execution against git 2.43.0. Moving it off the declaration it describes to satisfy a gate would be contorting the source to fit a model gap, which is the opposite of what section 4c is for. So the honest reading is that `service` SHOULD be an eligible annotation subject and is not. That is a change to the occurrence index / annotation subject model in the v1 seed, not a comment edit, and it is raised rather than improvised: this PR stays comment-only, and the gate stays red on those 5 until that decision is made. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Hoist 13 body-grain annotations out of four modules main landed while this branch was open The compile-clean gate failed on this branch's merge head with the body-grain refusal class -- the same class this PR was opened to clear. The refusals are NOT this branch's: they arrived on main in four files added after this PR's base (dag/std/affine_space.dag, dag/std/planar_projection.dag, dag/product/rack_power.dag, dag/product/spatial_projection.dag), and merging main brought them in. That is worth naming rather than just fixing. Main cleared this class in the witness corpus and simultaneously re-introduced it in new modules, so the population is not shrinking on its own -- clearing it is a moving target until the rule is enforced where the module is parsed, not swept afterwards. Two shapes, both moved to module-item grain with the subject named in the hoisted text so the prose does not become unmoored from what it discusses: - variant-attached blocks (OpProjectPoint3, ProjectionSourceFrameRequired, ViewUnitsAreNotMillimetres, OutletsExhausted, DepthOrderAmbiguous, and the two borrowed ProjectionRefusal causes) hoisted into the leading block of their type declaration; - in-body blocks inside fn bodies (the zero-page-right refusal, the dimetric direction, the outlet-before-capacity ordering, the load-count crossing, the nearer-candidate role naming) hoisted above their enclosing fn. One trailing block was also refusing under the second class, "names no subject": rack_power.dag's NO DERATING note sat at end of file with nothing following it. It now leads budget_capacity, the accessor returning the un-derated cited capacity the note is about. Its dissolve-on trigger is carried across verbatim and is pre-existing debt from main, not introduced here. Corpus scan after this change: zero indented // lines and zero trailing blocks across dag/ and src/v2. NOT verified locally: the shimmed gunbc on this host predates //-annotation support and reports a parse error on every module header block, so it is not an oracle for this class. CI is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eclaration it describes (#8215) * Move 18 body-grain source annotations to module-item grain DESIGN section 4c models source annotations only as standalone leading blocks on module-scope declarations. Eighteen comment blocks across five witness modules sat INSIDE declaration bodies -- annotating individual clauses of a conjunction, arms of a match, or lets in the middle of a function -- which the compile-clean gate refuses with: compile-clean: source annotation sits inside a declaration body. Only module-item grain is modeled; move it above the declaration it describes. This is pre-existing main debt, not a regression from any one branch. The count is exact: 18 in-body blocks under dag/ against 18 diagnostics, and the same refusal reproduces on two unrelated branches whose only common factor is that both force the whole-tree compile-clean baseline. Main's own recent runs are cancelled or in-flight, so no green verdict has observed it -- the debt has been accumulating unseen across at least five merged PRs (#8045, #8073, #8122, #8147, #8184-era files). The prose is preserved, not deleted. Each block is hoisted into its declaration's leading block, merged with any block already there, keeping the clause labels as sentences. Where a leading block already said the same thing -- fleet_converge_cli's join-1 header covers every clause note it carried -- the in-body copy is dropped as the duplicate it was. Comment-only: `git diff` contains no non-comment line, so no behavior, no witness assertion, and no emitted byte moves. WHAT THIS DOES NOT DO: the diagnostic is typed but UNLOCATED -- it names no file, line, or declaration, though AnnotationAttachmentRefusal carries `origin: SourceSpan` on every arm and BodyGrainNotModeled is constructed with it. The consumer discards the span it was handed. That is a section 5 located-ness gap which cost this diagnosis a full CI cycle and a bisection commit; it is left as a separate finding rather than repaired here, so this stays comment-only and reviewable as such. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Attach the two subject-less trailing annotations to the declarations they discuss CORRECTION TO THIS PR'S OWN DIAGNOSIS. The first commit claimed 18 in-body blocks were the whole annotation debt, on the strength of an exact count match. The match was real but the population was not the whole one: I grepped the failing log for the body-grain message and never checked whether a SECOND refusal class was also present. It was. The #8211 log carried 11 "source annotation names no subject: no module item follows it" refusals beside the 18 body-grain ones, and clearing the body-grain class left that second class as the visible failure. That is the nearby-question error: I confirmed the count of the class I was looking at and read it as the count of the failure. The unattached class is 7 at this PR's base, and it splits in two. FIXED HERE -- two genuine trailing notes, each explaining why something does NOT exist and therefore having no following declaration to attach to. Both move above the declaration they already discuss, which is where they belong on the merits and not only to satisfy the gate: dag/product/rack_mount.dag -- the note argues the mounting APERTURE is not the external envelope, which is exactly what the function directly below it computes; it now leads rack_mounting_aperture_height. resolved_graph_cache_hand_rust_witness_test.dag -- the note records a deleted tautological witness and points at the surviving positive control; it now leads that control. NOT FIXED HERE, deliberately -- the remaining 5 are comments sitting directly above `service` declarations (extdeps git.Inspect, git.PublicationTransport, sec.Edgar, tailscale.Serve, gcp.IamRoles). A `service` produces no depth-1 occurrence index entry, so it is not an eligible annotation subject and prose above it attaches to nothing. Relocating that prose would make the corpus worse. git/inspect.dag's block is 24 lines of measured rationale for the service's operations -- which porcelain version is pinned and why, why IgnoredFiles is a second operation rather than a flag, verified by execution against git 2.43.0. Moving it off the declaration it describes to satisfy a gate would be contorting the source to fit a model gap, which is the opposite of what section 4c is for. So the honest reading is that `service` SHOULD be an eligible annotation subject and is not. That is a change to the occurrence index / annotation subject model in the v1 seed, not a comment edit, and it is raised rather than improvised: this PR stays comment-only, and the gate stays red on those 5 until that decision is made. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS * Hoist 13 body-grain annotations out of four modules main landed while this branch was open The compile-clean gate failed on this branch's merge head with the body-grain refusal class -- the same class this PR was opened to clear. The refusals are NOT this branch's: they arrived on main in four files added after this PR's base (dag/std/affine_space.dag, dag/std/planar_projection.dag, dag/product/rack_power.dag, dag/product/spatial_projection.dag), and merging main brought them in. That is worth naming rather than just fixing. Main cleared this class in the witness corpus and simultaneously re-introduced it in new modules, so the population is not shrinking on its own -- clearing it is a moving target until the rule is enforced where the module is parsed, not swept afterwards. Two shapes, both moved to module-item grain with the subject named in the hoisted text so the prose does not become unmoored from what it discusses: - variant-attached blocks (OpProjectPoint3, ProjectionSourceFrameRequired, ViewUnitsAreNotMillimetres, OutletsExhausted, DepthOrderAmbiguous, and the two borrowed ProjectionRefusal causes) hoisted into the leading block of their type declaration; - in-body blocks inside fn bodies (the zero-page-right refusal, the dimetric direction, the outlet-before-capacity ordering, the load-count crossing, the nearer-candidate role naming) hoisted above their enclosing fn. One trailing block was also refusing under the second class, "names no subject": rack_power.dag's NO DERATING note sat at end of file with nothing following it. It now leads budget_capacity, the accessor returning the un-derated cited capacity the note is about. Its dissolve-on trigger is carried across verbatim and is pre-existing debt from main, not introduced here. Corpus scan after this change: zero indented // lines and zero trailing blocks across dag/ and src/v2. NOT verified locally: the shimmed gunbc on this host predates //-annotation support and reports a parse error on every module header block, so it is not an oracle for this class. CI is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
CONVERGE-0 — a merged main revision becomes observed fleet state, including an offline host returning later (operator directive 2026-08-09, option 1: this PR keeps the CONVERGE-0 name and therefore completes the live main→fleet loop in this PR; the earlier self-limiting framing here is deleted as the operator's named specimen of a PR granting itself permission not to deliver). Merge bar = the bounded completion list executed live: main→desired-ref advancement, timer in deployment membership, --host self, receipt publication, live probe execution, and the product-level RED — a host offline during a desired advance cannot converge the fleet, then converges on return from the SAME revision without another commit. The TasksMax knob, the executed pre-apply receipt, the models and the observation job below are delivered SUPPORT; the loop is the displacement that merges.
Ruling compliance (operator ruling 2026-08-08, this PR): the desired value is the incident repair value
TasksMax=16384. The earlier revision of this PR carried512as desired — the diagnosed EAGAIN/process-creation outage value — inverting the incident diagnosis; a converge circuit applying it would have made the outage reproducible by construction. Corrected at the single authority (gunbc.runner_slot_allocation, note updated with the ruling), witnesses flipped so 512 is the RED (reads Drifted, never Converged), and no live apply has been executed at any point.Starting standing (re-observed, not copied from the brief)
gunbc converge --host <h>exists (cli_run.rshandle_converge); slice1 (gunbc.host_converge_slice1) is the proven live circuit forper_slot_memory_max_byteson srv1+srv3 (closed 2026-08-05). No--host self, no timer units, nofleet/desiredref anywhere — the reconciler architecture is entirely unbuilt.TasksMax=16384with matching cgrouppids.max=16384— srv1 is at the repair value today, and the tasks axis reads Converged against desired 16384. Two earlier claims in this body's history are both retired by that receipt: the incident-era "16384 hand-applied on srv1/srv2" (unobserved at the time it was written) and the 2026-08-07 control-note claim that 512 was hand-applied with read-back (runner_capacity_operator_accesssrv1_already_converged_caps_control_note) — the contradiction between note and live state is decided by observation, per the operator's acceptance. srv2/srv3/srv4 tasks state is UNOBSERVED (no receipt). Therunner_slot_allocationrow said512, which this PR corrects to 16384 — approved by operator ruling 2026-08-09 as an emergency parity rollback only (not a derived value; the derived task-budget envelope model — runner-local budget / shared compile-pool budget / host-parent reserve, sum ≤ parent, derived from admitted concurrent builds × compiler fan-out — is registered as a named follow-up at the authority, not built here)..github/fleet-converge.shthe design doc marks for death is already deleted (host-convergence circuit: kill the paper transport (P0) + PerSlotMemoryCap reconcile loop, live-wired (P1/P1a) #7121 P0).ReachRefusedstanding. Session containers hold no fleet SSH identity (correct: sessions are not the management plane).gunbc_runner_slot_unit_placeholderis still the unescaped decoy slice namesystem-actions-runner.slice(design doc Phase 0 fix not landed) — recorded in residue, not absorbed here.What this change does
extdeps.systemd:TasksMax/TasksCurrentproperties +SystemdCgroupTaskLimitcount-or-infinity parse (consolidation dissolve-on beside it).gunbc.host_converge:per_slot_tasks_maxknob (desired threaded fromrunner_slot_allocationviaRunnerHostDeployment.per_runner_tasks_max) on the property-genericPerSlotMemoryCaptarget; the variant's over-narrow name annotated with a dissolve-on to the P3 axis framework.gunbc.host_axis_caps:50-fleet-tasks.confjoins the managed drop-in roster (Owned; a foreign occupant still refuses wholesale).gunbc.host_converge_slice1: full tasks circuit — live read → verdict → pre-applyTasksCurrentguard at>=(equality is zero headroom: the nextclone()EAGAINs; typed, retryable refusal) → set-property → independent read-back → reapply-noop. The reapply-noop arm's guard bypass is now justified in-code (only reachable after a read proved effective == desired; re-applying the effective value cannot lower headroom). Per-unit outcomes beside the memory knob on srv1/srv3; interpret/realize whole-host refusal for srv2/srv4 unchanged.gunbc.fleet_converge_cli: srv1 slice1 route gains the tasks arm; an invalid tasks receipt refuses the whole loop.Evidence
infinity→ Drifted, off-by-one 16383 → Drifted, empty/junk wire → Absent (fail-closed, never Drifted), and a receipt whose flags claim success while the read shows 512 fails the validity gate (verdict derives from the read, not the flags).TasksMax=16384and cgrouppids.max=16384(pids.current 30–72 per slot, well clear of the cap), ancestorsystem-actions-runner.slicepids.max=max(the slice imposes no tighter limit), ControlGroup paths as systemd reports them, observer = slot srv1-01 (its own/proc/self/cgroupis in the receipt). No state-changing apply is needed on the tasks axis. A futureconverge --host srv1run reads Converged and performs only the reapply-noop re-assertion with independent read-back — the no-op-control shape..d/.Arc items 1+2 (operator escalation 2026-08-08: build the parent arc now — this branch carries them as a second capsule)
observe_host_phase(subject, host, phase_label)replaces the hard-coded single-cell if-chain ingunbc.host_phase_status; the srv3 lease producer is preserved as a row. No parallel per-program observer can now exist beside it.gunbc.host_reach_identity_probe): ONE wet probe (authenticated short-hostname read over the management transport) feeds BOTHP0:reach-secrets-networkandP0:host-identity-convergeas pure deductions over committed receipts (hermetic matrix by construction). Four distinct typed outcomes:Reachable / TransportUnavailable / AuthenticationRefused / HostKeyMismatch— the last is a live class (srv5/srv6 shipped identical factory host keys). Fail-closed wire classification; empty receipts registry so coverage grows only by executed observation. Password-auth disable on srv5/srv6 is sequenced AFTER a Reachable receipt (ordering law recorded at the type).gunbc.fleet_convergence_verdict): knob-grain receipts joined with lifecycle cells; a required unobserved cell PREVENTS convergence (witnessed); knob-green alone cannot converge the fleet (RED); empty enrollment refuses; unobserved/unreachable/drifted/conflict stay distinct standings. The live fleet honestly reads NOT converged with a positive unobserved-cell count — the arc's progress denominator.Arc items 3+4 (third capsule on this branch)
gunbc.fleet_main_revision):refs/fleet/desiredis the single desired-state ref.admit_fleet_desired_advanceis a CAS carrying the expected prior (from= exact prior revision, or none only at ref creation) and REFUSES any candidate without floor-green;converge_target_revisionyields NO target from an unobserved ref — never a fabricated revision, never the invoking tree. 4 witnesses green incl. the floor-not-green refusal RED.gunbc.fleet_converge_timer): single authority forctrl-fleet-converge.service/.timernames, cadence (*:0/15+RandomizedDelaySec=90), and unit CONTENT — Option A (OnCalendar+Persistent=true: catch-up after downtime is exactly the offline-host property; systemd silently ignores Persistent on monotonic timers, hence calendar). The service converges SELF only via the pinned/opt/gunbc/bin/gunbc— the central-actor-converges-remote shape is unwritable from this authority. 4 witnesses green. Remaining slices: live_deploy membership rows,--host selfidentity resolution, receipts publication (residue 1).runner_lifecyclerunner_lifecycle_memory_cap_apply_note,runner_capacity_operator_accesssrv1_already_converged_caps_control_note) recorded hand-convergedTasksMax=512inside the zero-effects premise. Both now scope the no-op control to the MEMORY axes and record srv1's tasks axis as live drift (512 hand-applied 2026-08-07, desired 16384) that theper_slot_tasks_maxknob corrects — after which the no-op control is exact again.Loop legs (operator directive 2026-08-09 — the merge bar)
--host self— kernel-reported hostname at the cited procfs authority; typed UnknownHost fallback. LANDED, witnessed.advance_fleet_desiredjob, push-to-main only,needs: [ci]as floor-green admission, CAS--force-with-leasenaming the exact observed prior onrefs/fleet/desired. LANDED.FleetConvergeTimerUniton the belt-timer precedent (per-instance names, twin-safe;Persistent=truecalendar catch-up; ExecStart<deployed binary> converge --host self; teardown owns both files). LANDED; 45-witness emit corpus + 2 new member witnesses green./opt/gunbc/receipts/<host>-converge-receipt.txtand FAILS CLOSED on write failure; deploy installs the receipts dir. LANDED, witnessed.host_reach_identity_probe_receiptswith run provenance (join 4 below). Measured standing: ALL SIX hosts answer at the SSH layer withProbeHostKeyUnenrolled(host-key verification failed, NO enrolled expectation — first-contact vs changed-key undecidable until the fingerprint registry exists, the named next observation). The earlier run 31297121546 had srv5/srv6 failing a layer earlier (name resolution); probing at the modeled reservation addresses (gunbc.fleet_reach_endpoint, msg_1c4e0bce) moved them to the same SSH-layer standing — the endpoint fix is proven by the layer change. Fold treatment (leg-f ruling + join 7):ProbeHostKeyUnenrolledprojectsInaccessible— observed, never satisfied, neverConflict;ProbeHostKeyMismatch(enrolled expectation contradicted, expected REQUIRED by construction) projectsConflict. Both leave the cell not-converged; witnessed in both directions. Password-auth disable on srv5/srv6 remains sequenced behind a Reachable receipt.FleetConverged); the LIVE receipt's trigger is push-to-main itself (the merge is the first live advance + timer install), so its execution is post-merge by construction. Operator ruling (2026-08-09): option 1 — merge on the hermetic RED; the live offline-catch-up receipt is captured immediately post-merge as the loop's first execution evidence, and the vertical's ACCEPTANCE stays open until that receipt exists.Five live joins (operator verdict 2026-08-09 — REQUEST CHANGES, repaired in this PR)
The verdict found the legs locally green but the COMPOSITION absent. All five joins are now structurally present and witnessed; the only live positive still owed is the post-merge offline catch-up (narrowed option-1).
converge_target_revisionhas its live caller:gunbc.fleet_desired_observereadsrefs/fleet/desiredthrough new typedgit.Core.LsRemoteRefInRepo/RevParseInRepoops, decodes via the one untagged-hex decoder, and derivesFleetRevisionStanding(Converged / Drifted{desired,local} / Unobserved{cause}). The--host selfroute gates on it: drift or unobserved demotes the output to not-converged with both revisions named (tree-sync is the catch-up realization), and the published receipt carries aconverge-revisioncell line for the collector. Witnessed (gate demotion + line rendering, drift RED).gunbc.fleet_converge_timer_installmakes the install route TOTAL: srv1 via live_deploy membership (its owned-artifact authority), srv2–srv4 via a self-install argv plan derived from the single timer authority (staged unit writes → install → daemon-reload → enable --now → independentis-enabledREADBACK as the last step), srv5/srv6 typed pre-enrollment, non-fleet outside. No central SSH star: every arm converges SELF. Witnessed (route totality + plan derivation + readback pinned).FleetConvergeApplicability(Slice1 / KnobFrontier / SparkPreEnrollment / OutsideFleet);SparkPreEnrollmentis a typed refusal DISTINCT from UnknownHost. Witnessed.host_reach_identity_probe_receipts(provenance row: run 31300640122, probed_at 2026-08-09T07:20:04Z). The producers now answer from executed observation: both P0 cells for all six hosts readInaccessible, coverage grew 1 → 13 (authored srv3 cell + 2×6 probe-deduced cells, derived from the target roster, identity-grain witnessed), and the fold's NotConverged now comes from the executed observation, not an empty registry. The three coverage witnesses that were DESIGNED to red on this flip were re-derived deliberately.gunbc.fleet_receipt_collector: reads each host's published receipt over the typed argv seam at the modeled endpoint, parses via the BACKWARD read of the byte-locked receipt grammar (round-trip witnessed; wrong prefix / missing field / unknown verdict label refuse), classifies Collected / Absent{cause} / Unparseable{line} with no absorbing arm, feeds ONLY Collected rows tofleet_convergence_verdict, publishes a collect receipt with the fold verdict. Witnessed incl. the partial-collection-never-converges RED.Plus the two named smaller items: (6) no residual identity-label-as-SSH-target — confirmed by census: both fleet-facing SSH sites (probe, collector) route through
fleet_probe_endpoint_for; remaining"srv5"/"srv6"literals are identity rows. (7)ProbeHostKeyUnenrolled{transport_diagnostic}split fromProbeHostKeyMismatch{expected_fingerprint, observed_fingerprint}as separate constructors with different remedies (enroll vs investigate); expected is REQUIRED on Mismatch by construction; wire classifier can only produce Unenrolled, the expectation-threading wrapper promotes to Mismatch only on a Present enrollment. The false "identical factory Spark host keys" claim was corrected at both carriers (operator factual correction: keys are DISTINCT; the shared /etc/machine-id is the non-anchor).Residue (typed, each with owner or trigger)
system-actions-runner.slice(unescaped; design doc Phase 0) — fix before any fleet-green observer (capsule 3 gate).TaskCountmeasure alias per review 50479) — annotated at the parse..d/dir, the first live axis-caps reconcile will classify it foreign and refuse wholesale (correct fail-closed; flagged for the supervised first apply per the manager review finding 4 on this PR).🤖 Generated with Claude Code
https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y