Skip to content

CONVERGE-0: a merged main revision becomes observed fleet state (incl. offline catch-up) - #8045

Merged
briansrls merged 34 commits into
mainfrom
session/wise-moth-430
Aug 9, 2026
Merged

briansrls merged 34 commits into
mainfrom
session/wise-moth-430

Conversation

@briansrls

@briansrls briansrls commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

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 carried 512 as 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 2026-08-08, receipts below
Goal:                TasksMax becomes an appliable, independently read-back converge knob (parent vertical goal stays with CONVERGE-0)
First live consumer: operator-supervised `gunbc converge --host srv1|srv3` (slice1 route)
Actuator + readback: typed systemctl set-property TasksMax; independent systemctl show TasksMax re-read + reapply-noop proof (never the apply's own output)
Acceptance:          knob derives from runner_slot_allocation (16384); outage value 512 reads Drifted (RED enrolled); pre-apply TasksCurrent guard refuses at >= (zero headroom refuses); safe real-host readback before merge (see below)
Predecessor:         TasksMax as an unactionable desired row; the inverted 512 desired row (corrected here at its authority)
Agreement changes:   operator ruling on this PR — desired value 512→16384, goal renamed, real-host readback required before merge
Ending standing:     tasks knob in fleet_converge_policy for all hosts; slice1 applies it on srv1/srv3; srv2/srv4 remain whole-host typed refusals (unchanged)
Residue:             see below

Starting standing (re-observed, not copied from the brief)

  • gunbc converge --host <h> exists (cli_run.rs handle_converge); slice1 (gunbc.host_converge_slice1) is the proven live circuit for per_slot_memory_max_bytes on srv1+srv3 (closed 2026-08-05). No --host self, no timer units, no fleet/desired ref anywhere — the reconciler architecture is entirely unbuilt.
  • TasksMax was not a ConvergeKnob — no path from any ruling to any host. ONE live standing, decided by executed observation (workflow_dispatch run 31288091024, receipt below): srv1's five slots read effective TasksMax=16384 with matching cgroup pids.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_access srv1_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). The runner_slot_allocation row said 512, 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).
  • The star script .github/fleet-converge.sh the 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).
  • srv4 answered SSH at the TCP layer from this session (auth-denied, not unreachable) — a delta vs the brief's ReachRefused standing. Session containers hold no fleet SSH identity (correct: sessions are not the management plane).
  • gunbc_runner_slot_unit_placeholder is still the unescaped decoy slice name system-actions-runner.slice (design doc Phase 0 fix not landed) — recorded in residue, not absorbed here.

What this change does

  • extdeps.systemd: TasksMax/TasksCurrent properties + SystemdCgroupTaskLimit count-or-infinity parse (consolidation dissolve-on beside it).
  • gunbc.host_converge: per_slot_tasks_max knob (desired threaded from runner_slot_allocation via RunnerHostDeployment.per_runner_tasks_max) on the property-generic PerSlotMemoryCap target; the variant's over-narrow name annotated with a dissolve-on to the P3 axis framework.
  • gunbc.host_axis_caps: 50-fleet-tasks.conf joins the managed drop-in roster (Owned; a foreign occupant still refuses wholesale).
  • gunbc.host_converge_slice1: full tasks circuit — live read → verdict → pre-apply TasksCurrent guard at >= (equality is zero headroom: the next clone() 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

  • Slice1 witness entry green by execution on the merged tree (incl. main's membership_reconcile rename and width-increase cross-check change), with RED controls: outage 512 → Drifted, 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).
  • Neighboring entries green: host_converge_delta, fleet_converge_cli, host_axis_caps, fleet_converge_apply, srv3_runner_memory_converge, fleet_show_effective_read.
  • PRE-APPLY RECEIPT — EXECUTED AND ACCEPTED (workflow_dispatch run 31288091024, job observe_srv1_tasks_preapply on srv1, 2026-08-09T01:25Z; operator acceptance 2026-08-09). The ruled one-exact-receipt, produced by the read-only observation job on this branch and re-verified green on run 31288643367 (receipt lands as a typed file + step log): all five srv1 slots at effective TasksMax=16384 and cgroup pids.max=16384 (pids.current 30–72 per slot, well clear of the cap), ancestor system-actions-runner.slice pids.max=max (the slice imposes no tighter limit), ControlGroup paths as systemd reports them, observer = slot srv1-01 (its own /proc/self/cgroup is in the receipt). No state-changing apply is needed on the tasks axis. A future converge --host srv1 run reads Converged and performs only the reapply-noop re-assertion with independent read-back — the no-op-control shape.
  • Real-host readback requirement: DISCHARGED by the receipt above. The former blocking text here expected a pending observation with a possible 512-drift outcome; that branch is closed by execution. What remains standing from the ruling: the success criterion for ANY future apply on this circuit is the independent post-effect readback, never the systemctl exit code; and the foreign-occupant wholesale refusal remains the fail-closed outcome if a foreign drop-in occupies the unit .d/.

Arc items 1+2 (operator escalation 2026-08-08: build the parent arc now — this branch carries them as a second capsule)

  • Canonical observation seam: observe_host_phase(subject, host, phase_label) replaces the hard-coded single-cell if-chain in gunbc.host_phase_status; the srv3 lease producer is preserved as a row. No parallel per-program observer can now exist beside it.
  • First two real producers (gunbc.host_reach_identity_probe): ONE wet probe (authenticated short-hostname read over the management transport) feeds BOTH P0:reach-secrets-network and P0:host-identity-converge as 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).
  • Fleet-atomic convergence fold (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.
  • Evidence: 7 probe + 5 fold witnesses green by execution; host_phase_status's 22 witnesses green under the new seam.

Arc items 3+4 (third capsule on this branch)

  • Main-revision authority (gunbc.fleet_main_revision): refs/fleet/desired is the single desired-state ref. admit_fleet_desired_advance is 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_revision yields NO target from an unobserved ref — never a fabricated revision, never the invoking tree. 4 witnesses green incl. the floor-not-green refusal RED.
  • Timer / anti-entropy, slice 1 (gunbc.fleet_converge_timer): single authority for ctrl-fleet-converge.service/.timer names, 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 self identity resolution, receipts publication (residue 1).
  • Stale-note recut (review finding, flagged twice): the two srv1 acceptance-row-6 control notes (runner_lifecycle runner_lifecycle_memory_cap_apply_note, runner_capacity_operator_access srv1_already_converged_caps_control_note) recorded hand-converged TasksMax=512 inside 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 the per_slot_tasks_max knob corrects — after which the no-op control is exact again.

Loop legs (operator directive 2026-08-09 — the merge bar)

  • (a) --host self — kernel-reported hostname at the cited procfs authority; typed UnknownHost fallback. LANDED, witnessed.
  • (b) main→desired advancement — advance_fleet_desired job, push-to-main only, needs: [ci] as floor-green admission, CAS --force-with-lease naming the exact observed prior on refs/fleet/desired. LANDED.
  • (c) timer in deployment membership — FleetConvergeTimerUnit on the belt-timer precedent (per-instance names, twin-safe; Persistent=true calendar catch-up; ExecStart <deployed binary> converge --host self; teardown owns both files). LANDED; 45-witness emit corpus + 2 new member witnesses green.
  • (d) receipt publication — the self route writes /opt/gunbc/receipts/<host>-converge-receipt.txt and FAILS CLOSED on write failure; deploy installs the receipts dir. LANDED, witnessed.
  • (e) live probe execution — EXECUTED TWICE; the current standing is run 31300640122 (probe_fleet_reach, ExitSuccess, 2026-08-09T07:20:04Z), whose typed receipts are COMMITTED into host_reach_identity_probe_receipts with run provenance (join 4 below). Measured standing: ALL SIX hosts answer at the SSH layer with ProbeHostKeyUnenrolled (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): ProbeHostKeyUnenrolled projects Inaccessible — observed, never satisfied, never Conflict; ProbeHostKeyMismatch (enrolled expectation contradicted, expected REQUIRED by construction) projects Conflict. Both leave the cell not-converged; witnessed in both directions. Password-auth disable on srv5/srv6 remains sequenced behind a Reachable receipt.
  • (f) offline catch-up receipt — hermetic RED enrolled and green (an unobserved/unreachable required cell PREVENTS 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).

  1. Desired-ref live read — converge_target_revision has its live caller: gunbc.fleet_desired_observe reads refs/fleet/desired through new typed git.Core.LsRemoteRefInRepo/RevParseInRepo ops, decodes via the one untagged-hex decoder, and derives FleetRevisionStanding (Converged / Drifted{desired,local} / Unobserved{cause}). The --host self route 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 a converge-revision cell line for the collector. Witnessed (gate demotion + line rendering, drift RED).
  2. Timer beyond srv1 — gunbc.fleet_converge_timer_install makes 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 → independent is-enabled READBACK 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).
  3. Applicability total — FleetConvergeApplicability (Slice1 / KnobFrontier / SparkPreEnrollment / OutsideFleet); SparkPreEnrollment is a typed refusal DISTINCT from UnknownHost. Witnessed.
  4. Probe artifact consumed — the executed run's receipts are COMMITTED into 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 read Inaccessible, 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.
  5. Receipt collector — 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 to fleet_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 from ProbeHostKeyMismatch{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)

  1. Reconciler architecture — the fleet fold landed here (arc item 2); remaining: main-revision authority (desired from merged main, arc item 3), timer/anti-entropy (arc item 4), wet probe realization + first receipts, receipts publication. Owner: this session.
  2. Decoy slice name system-actions-runner.slice (unescaped; design doc Phase 0) — fix before any fleet-green observer (capsule 3 gate).
  3. PerSlotMemoryCap → PerSlotUnitProperty rename — dissolve-on P3 axis framework (annotated at the declaration).
  4. SystemdCgroupLimit parse consolidation (also home for the empty-vs-garbage wire split and a TaskCount measure alias per review 50479) — annotated at the parse.
  5. 2026-06-21 hand drop-in demotion — if a hand-authored TasksMax drop-in exists in the unit .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

…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
@gunbai-bot gunbai-bot Bot changed the title converge-0 CONVERGE-0 capsule 1: TasksMax=512 as the second knob on the slice1 converge circuit Aug 8, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 8, 2026 04:14
…conflict)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168R31q15ZsPJyik18Roz3y
@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

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.

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The reapply-noop proof on the VerdictConverged arm performs a set-property write 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.
  2. Guard boundary is strict-greater: current == target admits with zero headroom (next clone() EAGAINs). >= is the conservative refusal; at minimum witness the equality boundary of the guard itself.
  3. Empty wire vs unparseable garbage conflate into one TaskLimitAbsent arm (both refuse, so §5 holds; different remedies though — covered by the declared SystemdCgroupLimit<M> dissolve-on, fine to carry).
  4. 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.

gunbai-bot[bot]
gunbai-bot Bot previously requested changes Aug 8, 2026

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The first desired value must be the repaired value 16384 (or a separately accepted, derived envelope value not lower than live task demand). Correct runner_slot_allocation.tasks_max = 512 before any automatic convergence consumes it.
  2. 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.
  3. 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.

@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 8, 2026 05:59
…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
@gunbai-bot

gunbai-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

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

@gunbai-bot gunbai-bot Bot changed the title CONVERGE-0 capsule 1: TasksMax=512 as the second knob on the slice1 converge circuit CONVERGE-0 capsule 1: TasksMax becomes an appliable, independently read-back converge knob (desired = repair value 16384) Aug 8, 2026
Brian Searls and others added 2 commits August 8, 2026 17:28
…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
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 8, 2026 19:10
Brian Searls and others added 6 commits August 8, 2026 23:34
…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
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Operator approval note (2026-08-09, relayed by manager):

Approved for the immediate TasksMax 512 → 16384 correction. 512 is the diagnosed outage value; restoring 16384 is a safe emergency rollback to the previously working envelope.

This approval does not establish 16384 as a derived terminal value, and does not accept RunnerSlotDesired.tasks_max as the final model. Shared compiler tasks, runner-local tasks, host packing, compiler thread fan-out, jobserver admission, and the host parent reserve are still independently authored or unmodeled. The follow-up must replace this scalar with runner-slot, compile-pool, and host-parent task budgets derived from one measured envelope.

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 TasksMax, pids.max, pids.current, and relevant ancestor limit. Success is the independent post-effect readback, not the systemctl exit code.

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

Brian Searls and others added 2 commits August 9, 2026 00:55
…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
@gunbai-bot
gunbai-bot Bot dismissed their stale review August 9, 2026 01:25

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

Brian Searls and others added 3 commits August 9, 2026 01:37
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
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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
Brian Searls and others added 8 commits August 9, 2026 04:36
…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
@gunbai-bot gunbai-bot Bot changed the title CONVERGE-0 capsule 1: TasksMax becomes an appliable, independently read-back converge knob (desired = repair value 16384) CONVERGE-0: a merged main revision becomes observed fleet state (incl. offline catch-up) Aug 9, 2026
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Main advances the ref, but no host reads it. The timer runs the already-installed binary (ExecStart=<deployed gunbc> converge --host self) with no fetch of refs/fleet/desired, no revision comparison, no realization lookup — converge_target_revision appears only in its definition and fixture. An old binary cannot discover a new desired model because a ref advanced. Required: timer observes desired ref → compares with installed realization → obtains the exact realization → runs it → receipt binds desired revision + artifact identity + host identity + standing.
  2. The timer is wired through deployment_spec_srv1() only. No path installs it on srv2–srv6, so the first main push installs anti-entropy on one host, not the fleet.
  3. --host self resolves a string, not an implementation. srv2/srv4 retain whole-host refusals; srv5/srv6 have no arm. Define "applicable" explicitly and give every applicable enrolled host a real self route — a fleet outcome cannot quietly define four of six hosts as inapplicable.
  4. The live probe artifact is not consumed. host_reach_identity_probe_receipts remains []; the fold reports NotConverged because the registry is empty, not because of the executed observation. Required join: wet artifact → typed parse → subject/attempt verification → receipt population → phase observation → fold. No hand-copying.
  5. Local converge receipts are written but never collected. fleet_convergence_verdict consumes an in-memory list; no collector reads receipts from enrolled hosts, verifies revision/freshness, and supplies the fold. Publication without consumption is specification-without-execution at the exact seam this PR exists to close.

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 ProbeHostKeyUnenrolled{transport_diagnostic} from ProbeHostKeyMismatch{expected, observed} — no-enrolled-key cannot establish a mismatch, and the two remedies differ.

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

Brian Searls and others added 7 commits August 9, 2026 06:48
…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
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review 50824's concrete gap is fixed at head 88c80ec: the transcribed probe-receipt registry now carries a typed Disposition = Scaffold row (probe_receipt_ledger_transcription_scaffold, bound to host_reach_identity_probe_receipts) like the sibling interim debts, with the dissolution named: it dissolves when the wet probe's receipt artifact is machine-ingested into the registry (typed parse → committed rows), at which point a stale hand row becomes unwritable. On the wider finding: the hand transcription itself is the accepted interim for join 4 — the frontier note names executed provenance as the next rung, the row carries the exact run provenance (run 31300640122), and the parent lane's seam verification explicitly accepted the transcription with machine ingestion as the trigger. The registry is deliberately hermetic input to the phase matrix (the wet path writes the artifact; committed rows move only with review), so drift between live SSH reality and the committed rows surfaces as a diff in the next transcription rather than silently — and the scaffold row now counts that debt until ingestion lands.

— 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
@gunbai-bot

gunbai-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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

@briansrls
briansrls merged commit 147ebbc into main Aug 9, 2026
19 of 20 checks passed
@briansrls
briansrls deleted the session/wise-moth-430 branch August 9, 2026 15:03
briansrls added a commit that referenced this pull request Aug 10, 2026
…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>
briansrls added a commit that referenced this pull request Aug 13, 2026
* 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>
briansrls pushed a commit that referenced this pull request Aug 13, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant