Skip to content

Runner micro-VM: a built image is not-pinnable, and the road to running jobs in the VM - #10628

Merged
briansrls merged 4 commits into
mainfrom
session/nimble-swift-27-vm-migration
Sep 6, 2026
Merged

briansrls merged 4 commits into
mainfrom
session/nimble-swift-27-vm-migration

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

What landed before this

#10589 got a micro-VM booting in CI on srv2: boot=printed-runner-version 2.336.0, the guest powering itself off, and no VMM surviving (run 33997260934, cleanup verified on the host). That is slice 1's acceptance criterion from the brief.

What this PR carries

One defect found by reading that run's output rather than the diff. The guest image build printed, beside the image it had just produced:

guest-image=unpinned-first-contact sha256=8fcf... (author this as ArtifactDigestPinned with FirstObservedOnFetch provenance)

Following that advice pins a digest that legitimately changes whenever the tree does, and demands byte-reproducible mke2fs output to stay green — which the row declaring the artifact forbids three lines away.

One arm was answering two questions. ArtifactDigestUnpinned covered both fetched, author a pin from this (advice correct) and built here, no pin can ever exist (advice actively wrong). Now split:

  • ArtifactDigestNotPinnable on the integrity side
  • ArtifactBuildReceipt on the verdict side, rendering build-receipt sha256=... (this build's output; not a pin, and not to be authored as one)

The coproduct is closed, so the new arm forced every consumer to answer for it rather than defaulting.

The control has an authorable red

the_built_image_reports_a_receipt_and_never_advises_authoring_a_pin goes red on both halves if runner_guest_built_image_artifact is given the Unpinned arm instead, because that arm carries the pin advice. It asserts the arm and that the rendered text never names ArtifactDigestPinned — asserting only that the text differs would pass on any rewording.

Worth recording: three reviews approved this module while the receipt was advising a reader to do what the module forbids. Execution caught it; review did not.

What still blocks running jobs in the VM

The booted VM cannot run a job. Being explicit, because "it boots" reads as closer than it is:

  • No network. GuestNetworkAbsent — no tap, no egress. It cannot reach GitHub.
  • No credential. The JIT drive is a placeholder pointed at the rootfs.
  • The agent is a probe. ExecStart prints the runner version and powers off.

Remaining, in order: tap networking with host NAT and nftables refusing tap-to-tap; JIT minting (App installation token -> generate-jitconfig -> read-only drive); a guest agent that mounts the drive and runs the listener ephemerally; then one real required-witnesses-build job with verified teardown.

Two items needing a decision, both now on the critical path:

  1. EnsureSupplementaryGroupMembership is authorized but never applied. runner_host_privileged_operations feeds only the sudoers renderer; nothing executes the op. /dev/kvm on srv2 is currently writable because it was done by hand. Networking adds several privileged ops per host, so without an applier the manual debt compounds per host per op.

  2. sccache is an isolation hole — RETRACTED, and the retraction is the durable fact. This claim was wrong and was carried on this PR while it was open. Measured on srv2 afterwards: the endpoint is SCCACHE_SERVER_UDS=/var/lib/ctrl/sccache-ci/server.sock — a Unix domain socket, nothing listening on TCP, and guests receive block drives only. There is no transport by which a guest can reach the host sccache daemon. Model the runner microVM in .dag: the prerequisite is a refusal, and it belongs to the topology rather than to virtualization #9578's observation was about a topology where the invoking process could reach the host daemon; carrying its conclusion across the VM boundary was unjustified, and I did that without checking whether it survived the change.

    The real consequence is cost, not isolation: a 10 GiB working cache and the MAKEFLAGS jobserver FIFO are both filesystem objects and both unreachable, so a migrated job compiles cold and its parallelism no longer participates in host-side coordination. Coordination belongs to the cgroup envelope (Slice=, already part of the attempt binding), not to smuggling the FIFO into the VM.

    Anyone reading this PR as a handoff should NOT treat sccache as a blocker for the canary. Ship it cold, with the absence explicit rather than accidental, and measure.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VQ1HbKBP5SH24N2zxExYmN

Brian Searls and others added 4 commits September 5, 2026 23:59
fleet-converge run 33996923229 built the guest image and its receipt printed

  guest-image=unpinned-first-contact sha256=8fcf... (author this as
  ArtifactDigestPinned with FirstObservedOnFetch provenance)

beside the image THAT BUILD had just produced. Following that advice pins a
digest which legitimately changes whenever the tree does, and would demand
byte-reproducible mke2fs output to stay green -- the module forbids exactly
that, three lines away, in the row declaring the artifact.

One arm was answering two questions. ArtifactDigestUnpinned covered both
"fetched, and a pin should be authored from this" (advice correct) and "built
here, and no pin can ever be authored" (advice actively wrong). They are now
distinct: ArtifactDigestNotPinnable on the integrity side, ArtifactBuildReceipt
on the verdict side, rendering "build-receipt sha256=... (this build's output;
not a pin, and not to be authored as one)". Because the coproduct is closed and
elimination exhaustive, the new arm forced every consumer to answer for it
rather than falling through a default.

The control the_built_image_reports_a_receipt_and_never_advises_authoring_a_pin
has an AUTHORABLE red: give runner_guest_built_image_artifact the Unpinned arm
instead and it goes red on both halves, because that arm is the one carrying the
pin advice. It asserts the arm AND that the rendered text never names
ArtifactDigestPinned -- asserting only that the text differs would pass on any
rewording.

Worth recording how this survived: three reviews approved this module while the
receipt was advising a reader to do what the module forbids. It took a real
run's output to see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ1HbKBP5SH24N2zxExYmN
…y applied

TWO THINGS THE OPERATOR'S QUESTION FOUND.

1. THE PRIVILEGED OPERATIONS WERE AUTHORIZED AND NEVER APPLIED.
executor_privileged_operation_argv had NO production consumer -- only witness
tests. runner_host_privileged_operations fed only the sudoers renderer, which
grants PERMISSION to run each argv, and runner_microvm_host_ready explicitly
disclaims the KVM grant as belonging to the sudoers roster. Granted by one
module, disclaimed by another, executed by neither. The consequence was a hand
step per host per operation: srv2's /dev/kvm is writable because an operator
typed the modeled argv by hand, verbatim the line already rendered into
provisioning/srv2/gunbc-ghrunner.sudoers.

gunbc.executor_privileged_apply closes it, and executes THE SAME argv function
the sudoers line is rendered from. A separate command builder would let a grant
and its execution drift into two spellings of one operation -- the §3 fork the
operation type exists to prevent -- so "what is permitted" and "what is executed"
are one fact by construction. Elevation is sudo -n, not -E: -n REFUSES when a
command is not covered by a NOPASSWD grant, where -E can prompt and would hang a
converge on a password nobody will type. Apply stops at the first failure and
has no tolerable-failure arm.

2. THE NETWORK SLICE MODELED THE PROCEDURE INSTEAD OF THE FACT.
The first draft added five privileged operations and two binary grants (ip, nft)
because that is how an operator creates a tap by hand. srv2 was then MEASURED
running systemd-networkd enabled and active. A tap is a .netdev file, its address
a .network file, the firewall /etc/nftables.conf -- all FILE INSTALLS, which this
fleet already grants. The slice collapsed to two operations (InstallFile,
SystemdReloadUnit) on binaries already in the roster: the authorization surface
SHRANK rather than grew, and the tap became state that survives reboot rather
than a command someone ran.

The observe-then-act standing the first draft needed is DELETED rather than
ported: it existed because `ip tuntap add` fails on an existing device, and no
operation here has that shape.

MEASURING THE HOST ALSO PREVENTED AN OUTAGE. srv2 runs an active dockerd beside
table ip filter / ip nat / ip mangle. In nftables every table hooking a point
runs and ANY drop wins, so `policy drop` on our forward chain -- the obvious
spelling of "isolate the guests" -- would have dropped every packet dockerd
forwards. Policy is accept with an explicit tap-to-tap drop, and a witness goes
red if anyone flips it back. Isolation's FIRST wall is arithmetic anyway: a /30
per slot leaves a guest no route to another guest at all.

Nine witnesses, each with an authorable red. The three newest cover what the
rewrite left uncovered -- User= (without it every launch needs privilege),
ConfigureWithoutCarrier (without it the host has no route to an unbooted guest,
presenting as a guest bug), and distinct unit paths per slot (without which the
second slot silently overwrites the first).

systemctl reload earns its own row: it runs ExecReload and the process never
dies, where restart would drop every link networkd manages including the host's
own uplink -- indistinguishable, from here, from the host going away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ1HbKBP5SH24N2zxExYmN
…sses

TWO REVIEW FORKS (review 61223), BOTH DISSOLVED BY DERIVATION.

netmask "255.255.255.252" and prefix_length 30 were two spellings of one fact.
Only one would move on a re-prefix: the host's .network file would carry the new
CIDR while the guest's kernel ip= line kept the old mask, and the guest would
boot with a subnet that disagrees with its own gateway. Nothing refuses that --
the guest simply cannot reach the host. The mask now derives from the prefix;
the kernel's ip= takes a dotted quad rather than a prefix, so the conversion has
to happen somewhere, and doing it from the single prefix row keeps it one fact.

supernet "172.30.0.0/16" re-minted what octet1/octet2 already say, and the
annotation directly above it DESCRIBED the derivation while the row spelled it
out. That one is worse: moving octet2 relocates every slot's network while the
NAT rule keeps matching the old /16, so guest traffic silently stops being
masqueraded and every per-slot row still reads correctly.

ONE VALUE CANNOT PROVE A DERIVATION. Asserting only /30 -> 255.255.255.252
passes for a function that ignores its argument and returns a constant -- which
is exactly the row being replaced. The control asserts four prefixes (/30, /24,
/16, /31); no constant satisfies all four. A second control pins the invariant
the fork actually broke: the guest's dotted quad and the host's prefix must
describe ONE subnet, since if they diverge both rows still read correctly alone.

THE CI FAILURE WAS MINE AND MY GATE COULD NOT SEE IT.

`slot_index: i as Nat` in two witness helpers COMPILED CLEAN in every targeted
compile and then errored at claim execution -- "cannot cast Int to Nat" -- taking
six witnesses red on the required floor at fa4f529. A bare literal already
inhabits Nat, so the cast was never load-bearing; what it did was defer a type
error past the only instrument that would not catch it.

The root cause is not the cast. It is that this lane's gate all session was
`gunbc compile`, which type-checks and never RUNS a claim, so every witness
written here had its correctness attested by an instrument structurally incapable
of failing on an execution error. claim_executor is now built locally. A local
run of the witnesses lane was attempted and ABANDONED, not passed: it reached
discovery-authority at 720s and began thrashing (majflt 49 -> 150783,
stall_majflt_per_min 23983), matching the memory-pressure signature this repo
carries a typed refusal for. That run establishes nothing in either direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ1HbKBP5SH24N2zxExYmN
…o Int

THE CI RED WAS `slot.slot_index as Int` -- CASTING A VALUE THAT IS ALREADY Int.

My previous fix was wrong and I reported it as root-caused. CI's first message
said "cannot cast Int to Nat", I found an `as Nat` in a witness helper, fixed
that, and pushed without running the claims. The real error is "cannot cast Int
to Int" in runner_microvm_network, four occurrences. The discriminating evidence
I did not have: the five ATTEMPT witnesses passed while the eight NETWORK
witnesses errored, and both files used the identical helper -- so the helper was
never the cause.

The root cause is not the cast. This lane's gate was `gunbc compile`, which
type-checks and never RUNS a claim, so every witness written here was attested by
an instrument structurally incapable of failing on an execution error.
claim_executor is now built and run locally: planned=3533 executed=3533
claims_failed=0, 20 of this lane's witnesses passing. That is the evidence for
this commit; compile-clean is not.

REVIEW 61259, BOTH FINDINGS, FIXED BY CONSTRUCTION.

nft_chain_wire hardcoded `type filter` for every chain, including the postrouting
chain whose only rule is a masquerade. nftables permits NAT statements ONLY in a
chain of type nat and rejects the WHOLE ruleset otherwise, so the generated file
could not load on any host -- taking the tap-to-tap drop down with the NAT rule
that was wrong. The chain type is now modeled and paired with its hook.

The witnesses could not have caught this: they asserted the rendered TEXT
contained the right words, and a ruleset nftables rejects contains them equally
well. A string assertion over a config language cannot separate a valid ruleset
from an invalid one.

runner_microvm_attempt_dir joined an unrestricted NonEmptyStr under the attempt
root, so "../shared" escaped it and "a/../b" aliased another attempt's storage --
the storage isolation this module's own header promises, defeatable through the
identity it is keyed on. Every path derives from that value, so one traversing id
moves the rootfs, JIT drive and config together and cleanup would prove the WRONG
subtree empty.

AttemptSegment is sole_constructor and REFUSES rather than sanitizes: a rewritten
id would silently disagree with the id every receipt names. MicroVmAttempt is
sealed too -- a record that merely HOLDS a validated segment can be assembled
beside the validator by anyone holding one, so the seal would guarantee only that
SOME segment was checked, not that THIS attempt's was. The pair is the unit that
must be constructible in one place.

The seal proved itself immediately: the test helper's fallback tried to build an
AttemptSegment and was refused at compile. Tests now go through microvm_attempt,
so they would break if the refusal were deleted rather than passing over a path
production cannot reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ1HbKBP5SH24N2zxExYmN
@briansrls
briansrls merged commit d9c0240 into main Sep 6, 2026
4 checks passed
@briansrls
briansrls deleted the session/nimble-swift-27-vm-migration branch September 6, 2026 09:14
@briansrls
briansrls restored the session/nimble-swift-27-vm-migration branch September 6, 2026 09:18
gunbai-bot Bot pushed a commit that referenced this pull request Sep 7, 2026
… was itself a 3c dangling consumer

I declined to guess why apply_executor_privileged_operations_wet has no
callers, and said so rather than approximating. The answer is one commit and it
is not "unfinished wiring" either.

#10628 introduced gunbc.executor_privileged_apply WHOLE, expressly to close the
authorized-but-never-applied gap. Its own account: executor_privileged_operation_argv
had no production consumer, runner_host_privileged_operations fed only the
sudoers renderer, and this module "closes it, and executes THE SAME argv
function". Then it landed without the caller it was written for.

That is DESIGN section 3c precisely, down to the tell that section lists in its
own words -- consumption described in the PR body but absent from the diff. So
the repair meant to end this class was itself an instance of a neighbouring one.

The two readings are the same missing edge, but only the second explains
itself: "one wiring remains" says how much code, while "a section 3c dangling
consumer" says WHY it is missing and therefore why waiting will not produce it.
A reader with only the first may assume someone is mid-way through the wiring.

Verified rather than inherited: section 3c exists in the current DESIGN.md and
its text names this tell -- my in-context copy predated the section, so citing
it from memory would have been a positional guess at a document that had moved.

"Executed by neither" stays unchanged, and it is still the sentence that keeps
the rung honest.

Compile on this base: 26 blocking errors, none in any file this touches;
identical to the count before this edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01632CTZ8diiLNBQaXobNWKQ
briansrls pushed a commit that referenced this pull request Sep 7, 2026
…specimens of it (#10703)

* File the class a hand-converged host capability belongs to, and both specimens of it

A host holds a capability no modeled path put there, so every later green on
that host is uninterpretable: a receipt naming it cannot distinguish the
modeled path working from a hand-run answering. The harm is not the capability,
which usually works. It is that the ambiguity is permanent and travels with
every future citation of that host.

THE STRUCTURAL CAUSE IS A CAPABILITY AUTHORIZED BY ONE MODULE, DISCLAIMED BY
ANOTHER, AND EXECUTED BY NEITHER. runner_host_grants emits
EnsureSupplementaryGroupMembership and runner_host_sudoers_content renders
permission to run its argv; runner_microvm_host_ready explicitly disclaims
applying it and names the result "a hole an operator falls into once per host,
by hand". A modeled capability with no executor is a manual step, and the row
granting it makes that manual step look converged -- DESIGN section 6's
out-of-band actuation wearing convergence vocabulary.

Two specimens, both real.

srv2 KVM, 2026-09-05: an operator ran the authorized argv by hand, so srv2 has
read kvm=writable ever since while every sibling reads present-not-writable.
The annotation recording the earlier measurement went stale at that moment and
COULD NOT GO RED, because DESIGN section 4c says no Accepted program reads an
annotation. It was found only because someone asked whether the grant was
converged.

srv1 Firecracker, 2026-09-06, this row's occasion: I ran the modeled
runner_microvm_host_converge_wet by hand on srv1, installing Firecracker
v1.16.1-aarch64 into the executor-owned root -- 30 paths under
~/.local/share/gunbc-firecracker, the root directory itself created by that run,
so nothing pre-existed. Digest-verified, idempotent, spending no host grant, and
still wrong: srv1 is the canary of record and its capability would not have come
from the path that owns it.

DECLARING THE DEVIATION WAS CONSIDERED AND REJECTED AS THE WEAKER ARM. Declaring
makes the ambiguity legible but leaves every later srv1 receipt carrying a
provenance caveat forever; removing it makes the question stop existing.
Convergence is only meaningful if it is the only way state arrives. The install
was reverted the same day, the enumerated tree removed with nothing adjacent
touched, and the honest post-revert state is that srv1 cannot run a guest --
which is what makes the convergence gap visible rather than papered over.

The row states the repair in preference order: bake the capability into a host
image so the question cannot be asked, or close it in fleet converge. Never an
applier bolted beside the disclaiming module, and never the sudoers argv by
hand, which is how the srv2 specimen was produced.

Distinguishing facts against the neighbours it is easy to misfile as are carried
in the row: execution_provenance_loss concerns an observation that may not have
run, where here it runs and reports faithfully; head_landed_by_hand concerns an
artifact landing uncertified, where this is durable host state with unrecorded
origin.

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

* Locate the class on the ladder, since a row that does not is the stall it describes

Review found the filing carried invalid state, harm, distinguishing facts,
cause, specimens and a ruling, but never named rung found at, ceiling with
reason, or a next-rung trigger. DESIGN section 4b requires all three, and
"REPAIR, IN PREFERENCE ORDER" is a product preference rather than a derived
ceiling and a capability-shaped trigger. A class that is not located on the
ladder never ranks for climbing, which is the untracked stall the same section
forbids -- so the omission was the failure this row exists to file, committed
in the filing itself.

RUNG FOUND AT is BELOW THE FLOOR, which is not a rung: section 4b puts silent
wrongness outside the ladder and forbids it outright. Nothing executed
distinguishes a converged capability from a hand-placed one --
runner_microvm_host_ready observes WHETHER a host holds a capability and never
WHAT PLACED IT, so both produce byte-identical standing. srv2 is the measured
proof: writability persisted through every later standing read, the annotation
recording the earlier measurement went stale and could not go red, and it
surfaced only when a human asked. Provenance has no mechanism at all, not a
weak one.

CEILING is derived and DIFFERENT FOR TWO HOST SHAPES, because one number would
be dishonest. A host whose runtime root is rebuilt from a repository-built image
each boot reaches STRUCTURALLY IMPOSSIBLE -- no persistent root survives to hold
a capability the image did not place, so the invalid state has no constructor. A
mutable persistent host reaches MECHANICALLY PREVENTABLE and no higher, since an
operator with sudo can always place state. Section 4b(1) makes a class's rung
the minimum across in-scope paths, so while any mutable host is in the
population the attainable ceiling is mechanically preventable.

NEXT-RUNG TRIGGER names capabilities, at the grain of the loss. The loss is
plural -- every host, every capability -- so a trigger naming one host or one
grant would be satisfied while the class stayed dead. To mechanically
preventable: a host-standing observation reporting, per capability, the identity
of the converge run that placed it, such that an unattributable capability is a
typed refusal rather than a ready standing. A receipt that merely RECORDS
provenance without refusing does not reach it -- srv2 was already recorded in
prose and still could not go red. To structurally impossible: a runtime root
reconstructed from a repository-built image on every boot. Naming a specific
host-image pull request would name an artifact; what discharges the row is the
property that a rebooted host cannot carry unattributed capability, whichever
change delivers it.

docs/design-failure-modes.md regenerated from the authority.

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

* chore: regenerate drifted generated artifacts (ci auto-heal)

Ledger-Repair-Judged: docs/design-failure-modes.md
Ledger-Rows-Repaired: docs/design-failure-modes.md capability_arrives_outside_the_converged_path
Ledger-Repair-Judged: docs/design-rung-drops.md

* Say that the applier is built and unreachable, since "missing" sends the next author to fork it

Review 61842 verified independently that apply_executor_privileged_operations_wet
still has no callers, which is what made me look at it. It exists. This row's
first draft implied the executing half had to be written, and that reading is
wrong in the expensive direction.

gunbc.executor_privileged_apply is already correct where it matters: it
elevates with sudo -n, so a command not covered by a NOPASSWD grant REFUSES
instead of hanging on a prompt no automated converge will answer, and it renders
its argv through the SAME executor_privileged_operation_argv the sudoers line is
rendered from -- so what is permitted and what is executed are one fact by
construction rather than by review. It has zero callers anywhere in the corpus,
test modules included; the only other occurrence is a decl_ref citation.

So DESIGN section 4b(2)'s three-way split lands on its strongest arm. Not
"cannot climb further", not "can climb after one grounding", but CAN CLIMB NOW
WITH THE MECHANISM ALREADY WRITTEN -- the remaining work is one wiring into the
converge path.

Recording this is load-bearing rather than tidy. The opposite reading sends the
next author to build a second applier beside the first, which is precisely the
argv fork that module's own annotation says it exists to prevent: a sudoers line
authorizing something nobody runs, beside a command nobody authorized.

"Executed by neither" stays accurate and is unchanged -- nothing calls it. What
changes is the repair's size and shape.

Compile on this merged base: 26 blocking errors, none in any file this touches;
the count rose from 22 because main moved under the branch, not because of this
edit. The projection is left to heal, per the merge driver's instruction.

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

* Name why the applier has no caller: the repair that closed this class was itself a 3c dangling consumer

I declined to guess why apply_executor_privileged_operations_wet has no
callers, and said so rather than approximating. The answer is one commit and it
is not "unfinished wiring" either.

#10628 introduced gunbc.executor_privileged_apply WHOLE, expressly to close the
authorized-but-never-applied gap. Its own account: executor_privileged_operation_argv
had no production consumer, runner_host_privileged_operations fed only the
sudoers renderer, and this module "closes it, and executes THE SAME argv
function". Then it landed without the caller it was written for.

That is DESIGN section 3c precisely, down to the tell that section lists in its
own words -- consumption described in the PR body but absent from the diff. So
the repair meant to end this class was itself an instance of a neighbouring one.

The two readings are the same missing edge, but only the second explains
itself: "one wiring remains" says how much code, while "a section 3c dangling
consumer" says WHY it is missing and therefore why waiting will not produce it.
A reader with only the first may assume someone is mid-way through the wiring.

Verified rather than inherited: section 3c exists in the current DESIGN.md and
its text names this tell -- my in-context copy predated the section, so citing
it from memory would have been a positional guess at a document that had moved.

"Executed by neither" stays unchanged, and it is still the sentence that keeps
the rung honest.

Compile on this base: 26 blocking errors, none in any file this touches;
identical to the count before this edit.

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

* chore: regenerate drifted generated artifacts (ci auto-heal)

Ledger-Repair-Judged: docs/design-failure-modes.md
Ledger-Rows-Repaired: docs/design-failure-modes.md capability_arrives_outside_the_converged_path
Ledger-Repair-Judged: docs/design-rung-drops.md

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <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