Operator made the Odin muninn-bounded-realtime-send donor a campaign. FEC moves
to a CultLib cultnet-rs media_fec codec (reed-solomon-erasure) used by both
Muninn and Ratatoskr; bounded whole-frame sender; one latency owner. Q1-Q8 open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
restart-muninn.ps1, its cmd alias, deploy-raven-muninn-binary.ps1 and
repair-raven-muninn-task-actions.ps1 started and replaced serve on Raven by
hand through a scheduled task and a VBS launcher. Idunn's host actuator is
the only thing that starts serve there now, so they leave rather than stay
as a second launch path. Docs say what replaced them and what remains.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Odin admits a signer's presence only above the sequence it last stored,
across the signer's restarts. The sequence is now the observation clock,
which a fresh process cannot restart below.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A switched-off gamepad made --health fail and so kept the Idunn presence
at warming forever; the daemon was serving the whole time. Presence now
reads the telemetry surface the tick rewrites; source freshness stays in
--health where an operator asked for it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The committed workspace patch pointed CultLib at ../CultLib, which only a
workstation with that checkout beside it could satisfy; on Raven the
build died looking for it. odin-core now comes from Odin 3e96c6c, whose
CultLib pin is c2a9a6e like ours, so the diamond closes without a patch.
A developer who wants a local CultLib puts the patch in .cargo/config.toml.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When Idunn's host actuator starts serve, it hands over the runtime bundle,
a one-shot activation credential and the host's provider identity. From
those, serve publishes a dual-signed gamecult.runtime_presence_health.v2
record to Odin every two seconds: warming until its own health check
passes, active after. Odin's correlation of that record is what admits
the incarnation and keeps it alive. An operator-started serve has no
bundle and publishes nothing.
deployment/idunn/raven-muninn.toml is the target declaration: one cargo
build step on the host, the daemon and the loopback script as artifacts,
and the launch contract that today's restart script assembles by hand,
with Raven's paths as argument bindings. Cargo.lock is committed so the
build is --locked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Windows hands a priority class down to a child only when it is below
normal, so the mux child's above-normal never reached ffmpeg; on a live
stream with the host at 99.8% CPU the encoder ran at normal, 790 video
chunks per 10 s went missing and the mux loop sat a second behind. Raising
the processes in place took that to 116 lost and a 60-chunk backlog with
zero late frames, and the viewer reported every frame arriving. The mux
child and video encoder now run at high, the audio pipeline at
above-normal, from spawn.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
serve is the body that must outlive a racing store write or an absent
Odin. It exited on the first "failed to atomically replace" and left its
activation child streaming with nobody answering requests; OBS's own
request sat at pending while it watched the orphan. The tick body now
reports and the loop continues on the next interval. CultLib pin c2a9a6e
carries the store-side retry for the same race.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The "kept existing capture stream" branch wrote the running command back
with the current time on every tick, which made a running command the
newest forever: no request sent tonight could outrank the July start until
this was cut. The spawn transition keeps the command's stamp too. The
teardown map records the audio result: 860 frames and 2,756 audio packets
in 29 s, nothing expired, on a producer host at 93% CPU.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The command engine orders commands by updated_at, and both terminal
writers, superseding and reaping, re-stamped the dead record with the
current time. A superseded command therefore outranked the command that
had superseded it on the very next tick and respawned; tonight a July
start did that to the stop that ended it. The stamp is the command's
ordinal now: superseding and reaping change state and detail only, with
the exit time carried in the detail. A newer command that was already
handled still shadows an older pending one, which the earlier cut had
broken and the existing test caught.
CultLib pin 47b9fc4: readers no longer stop at acknowledgement-only
packets, which was what starved the video leg once reliable audio shared
the session.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5 ms resend was a July LAN value for the old dialled client. A receiver
that polls every 2 ms answers in 3-6 ms, so nearly every audio fragment
was resent before its acknowledgement arrived and the producer's send
loop drowned in retransmits: 7 video frames in 9 s with audio on, 266
without. Audio expires at the assembly deadline regardless.
The activation child now raises itself to above-normal priority, which
its encoder and capture children inherit. Measured on Raven at 93% total
CPU with a game running: at normal priority the mux loop starved and
video backed up 1.5 s before expiring wholesale.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reaping a killed child rewrites its command with a fresh stamp. Letting
that stamp compete for "latest" let a July command supersede the request
that had just stopped it and respawn itself with the sources of its day,
which is how the first start request tonight ran with a Focusrite that
no longer exists. Terminal commands are out of the running; a test pins
both halves.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The command engine orders commands by comparing updated_at strings. A
consumer's RFC 3339 stamp sorts below every `unix-` stamp Muninn writes,
so the first stop request sent tonight lost to a start command stored in
July and Raven kept encoding to nobody. Translated commands now carry
Muninn's own stamp; the test pins the ordering.
Idunn daemon health on the Raven launch is optional now and omitted by
default: no Idunn manages this host, and the publisher only filled
yggdrasil's firewall log.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The loopback capture script the restart ships was still only in Odin's
tree; it lives here now. Both scripts wrote sftp batches that Win32-OpenSSH
rejects today: backslashes in quoted local paths are read as escapes, and
"C:/..." remote paths resolve under the login home rather than the drive.
Local paths go in with slashes and remote Windows paths as "/C:/...".
The alias-carried SSH user is honoured by passing -SshUser "".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The check asserted ffmpeg video-proof constants in restart-muninn.ps1 that
were removed with the VideoProof task; the encoder profile lives in the
daemon and its tests. It blocked every deploy for a machine that is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The activation child binds one CultNet RUDP hub on --media-rudp-bind and
serve advertises it as the stream's media_endpoint; receivers dial it and
are known by their connect payload. Video rides the media channel lossy
under parity, audio the new audio channel reliably with the assembly
deadline as expiry, both on one session per receiver. Repairs go back to
the receiver that asked; a receiver silent for 6 s is dropped.
Gone: --obs-target-host/--obs-port (refused with the reason), the second
audio connection, the Odin provider-catalog lookup of an OBS receiver, the
receiver advertisement request-stream used to publish, and the cultmesh://
target requirement on activate. The consumer's host no longer needs a
firewall rule; Raven opens one UDP port, and restart-muninn.ps1 adds it
beside the HID controller rule.
CultLib pin c84cb2e.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Muninn now publishes gamecult.media_stream_advertisement for its A/V
stream beside the Muninn-specific OBS catalog, and each serve tick pulls
gamecult.media_stream_request documents addressed to this host from Odin.
A pending request becomes the capture command the existing tick already
runs — same command id, so the outcome is traceable — and the command's
state is written back onto the request under its stream:receiver key, so
a consumer watches one document. A request naming a codec this producer
cannot encode is answered failed with the supported codecs named, and
never becomes a command.
The translation is a pure function with tests. The node registers the
two CultLib records beside Odin's document set.
cargo test -p muninn-daemon -> 152 passed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
build_receiver_feedback moved to CultLib (34c8ea7): a record both ends
construct belongs beside the record. The sender's repair-cache tests
keep constructing feedback the way a receiver would, now through the
same code the receiver uses.
cargo test -p muninn-daemon -> 148 passed, no warnings
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Frame assembly, the assembly set, reassembly, expired-frame feedback and
their tests were kept here after the September dead-code cut as the seed
for Ratatoskr's reassembly. That seed has been planted (Ratatoskr
2618184, crates/ratatoskr-core/src/video.rs), so the producer stops
carrying code only a consumer runs. 705 lines. What stays is the producer
half: packetize, XOR stripe parity, and build_receiver_feedback, which the
sender's repair-cache tests use to construct what a receiver would say.
Also removes adts_frame, a test helper orphaned by the AAC cut.
cargo test -p muninn-daemon -> 151 passed, no warnings
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three repos, one contract none of them owns, and one pin standing between Muninn
and a clean build. Names what is blocked, who owns unblocking it, what is next
and which two breaks are deliberate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
05e0925 carries the media contract, wire envelope and channel delivery. The
diamond with odin-core is unchanged and still waits on Odin bumping its own
CultLib pin; the local uncommitted patch that works around it now points at the
main CultLib checkout rather than a throwaway worktree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
odin-core was a path dependency on `../odin-core`, a sibling that does not exist
in this repository — a leftover from when these crates lived inside Odin. It now
resolves from Odin's upstream at 3a6368f. CultLib moves from c13b6ba to 96138ff,
which is where the media contract, wire envelope and channel delivery live.
That surfaces a diamond rather than fixing one. odin-core pins CultLib at
c13b6ba; this crate now needs 96138ff; Cargo resolves two cultcache-rs copies
and the DatabaseEntry from one is not the DatabaseEntry from the other:
git+...CultLib.git?rev=96138ff#cultcache-rs@0.1.0
git+...CultLib.git?rev=c13b6ba#cultcache-rs@0.1.0
Muninn cannot resolve this alone. It uses 23 symbols from odin-core, and while
16 are its own records that belong here — capture stream, command boundary, HID
controller state, the five Move records, OBS catalog, Quest access, telemetry
surface, transport profile — the remaining 7 are genuinely Odin's: provider
discovery, Eve advertisement and surface records, Idunn daemon health. Moving
the 16 shrinks the coupling; it does not remove it.
The sequence that unblocks a clean build is therefore:
1. CultLib claude/cultmesh-media-stream-contract merges to main
2. Odin bumps its CultLib pin to that main
3. this builds standalone
Steps 1 and 2 are not this repository's to take. Until then a local
[patch."https://github.com/GameCult/CultLib.git"] unifying both consumers on one
CultLib checkout builds fine, and is deliberately not committed — it points at
working trees that exist on one machine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Muninn's media records now come from CultLib rather than odin-core, and its
envelope, record keys, validators and chunk key are gone in favour of the shared
ones. 608 lines removed against 149 added, nearly all of it duplication that
existed because there was nowhere else to put it.
What went, and why it was there:
The four *WirePayload tuple structs restated each record's entire field list
positionally, kept in step by hand, for the sole purpose of applying
serde_bytes to the payload — a workaround for the DatabaseEntry derive ignoring
serde attributes. The records carry `bytes` now, so the derive emits bin itself.
CultLib has a test comparing both encodings byte for byte before the deletion.
encode/decode_media_wire_record, the four record-key builders, the four record
validators, VideoChunkKey and the feedback-key normaliser were all contract
living inside one producer. A consumer needed every one of them and had to
reimplement or approximate them, which is precisely how the C++ receiver drifted.
Muninn keeps what is genuinely its own: it names itself as producer, once, in
MUNINN_MEDIA_PRODUCER, and passes that through MediaWireProvenance. The envelope
no longer hardcodes "muninn-media:" and "muninn.media" into documents any other
producer would also have had to emit.
One test changed meaning rather than being deleted. A hex fixture captured from
the deployed C++ OBS bridge carries muninn.media_receiver_feedback.v1, which the
rename retires. It is now
`the_previous_generation_bridge_is_deliberately_no_longer_understood` and asserts
the refusal, naming what it refused. That break is real and accepted: the bridge
is being replaced by Ratatoskr, and accepting both schema names would preserve
the ownership confusion the rename exists to remove. If that test starts passing,
someone has put the old id back.
Requires CultLib 96138ff on claude/cultmesh-media-stream-contract. The pin in
Cargo.toml still points at c13b6ba and must move before this builds clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The receiving end now has a repo. The wire envelope does not: MuninnMediaWireRecord
and encode_media_wire_record are still here, both ends need them, and until they
reach CultLib beside the gamecult.media_* records Ratatoskr cannot decode a real
stream.
Also corrects this session's claim that no audio decoder existed. There is one --
an ffmpeg child with a hardcoded -f aac. Opus is that becoming codec-driven, and
it measures 16.8x against PCM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The map argued congestion collapse: a reliable channel retransmitting under
loss until it starved itself. The measurement refutes the mechanism and keeps
the conclusion. Reliable loses 31% of a clean 12 Mbps offer with no loss and no
congestion present, and barely moves as loss rises.
Also corrects the claim that June tuning was fitted against the VPS path. The
July acceptance ledger records those runs as direct LAN with no WireGuard route;
the hairpin arrived with the relocation, afterwards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Video now opts into an unreliable media channel and audio states Reliable
explicitly, rather than both inheriting a hardcoded default.
`rudp_media_transport_options_follow_low_latency_profile` has asserted
Unreliable since it was written and has never passed. It was not aspirational:
CultNet hardcoded the media channel Reliable in both the profile builder and the
send path, so Muninn had no way to express the intent. Muninn signalled it the
only way available, by passing no reliable expiry — meaning "unreliable, so
expiry is moot" — and got reliable delivery instead.
Measured on Raven through `cultnet-impair`, 848-byte packets at 12 Mbps, which
is Muninn's real shape:
profile mode delivered
clean unreliable 100.00%
clean reliable 69.10%
loss 1% unreliable 98.95%
loss 1% reliable 72.00%
loss 3% unreliable 96.73%
loss 3% reliable 73.30%
The reliable channel loses 31% of a clean 12 Mbps offer, and its delivery barely
moves as loss rises, so loss was never the binding constraint — it cannot carry
this rate at all. The unreliable channel holds every packet on a clean link and
then degrades by roughly the loss injected, which is what a media lane should
do. Reverse traffic differs by three orders of magnitude: 43 bytes of
acknowledgement against ~135,000.
An earlier draft of this message argued the defect was retransmission amplifying
load under loss. The measurement does not support that mechanism. The conclusion
survives it; the reasoning did not.
Audio passes Reliable explicitly and does not change. That pairing is still
wrong — 3 Mbps of uncompressed PCM does not belong on a reliable channel — but
that is the Opus change, and inheriting the right value silently is what
produced this bug.
Requires CultLib's `claude/media-channel-delivery`. The dependency pin still
points at c13b6ba and must be moved before this builds from a clean checkout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Switch Muninn RUDP audio to PCM packets` (2a4f08d, 2026-06-23) moved audio onto
raw PCM and left the ADTS machinery compiled in behind it. Nothing has called it
since: AudioAdtsStreamSendConfig, AudioAdtsStreamSendState and its impl,
CompleteAdtsFrames and complete_adts_frames formed a closed cluster reachable
only from its own five tests.
295 lines, verified unreachable by the compiler with tests compiled, and with
the platform trap checked — none of it sits behind a cfg, unlike the bluetoothctl
helpers that only look dead because their #[cfg(unix)] callers vanish on Windows.
Deliberately narrower than the dead-code warnings suggest. The rest of what the
compiler calls dead in media_packetizer.rs is the receiver half: frame assembly,
chunk expiry, receiver feedback. That is not abandoned work. It is the only
stand-in the sender's feedback path has for the C++ receiver in Mimir — the
tests for MuninnRudpReceiverFeedbackStats and RecentVideoChunkRepairCache build
their fixtures with build_receiver_feedback — and it is the reference shape that
the hand-rolled C++ implementation drifted away from. Deleting it would blind
the path we are debugging and discard the seed of the Rust receiver that
replaces the drift.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Muninn's three crates carried 268 commits inside Odin, from "Add lazy Muninn
telemetry organ" on 2026-06-08 to the Idunn extraction merge on 2026-09-05.
That history is the evidence trail for everything the rebuild has to answer,
so it moves intact rather than being flattened into an initial commit.
Adds only what a standalone repository needs: a workspace manifest, a README
stating the authority Muninn actually holds, and the working teardown map.
Nothing is cut here and nothing builds yet. muninn-daemon still depends on
odin-core by path for twenty record types, which is one of the inversions
being removed. Deleting the old-generation deployment scripts is a separate
intent and waits on the Idunn recipe that replaces them, because three of
them are still the Restart actuators for live Idunn target records.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Idunn lived as a crate inside the repository of a service it manages, which
inverted the authority it exists to hold. It is now GameCult/Idunn with its
full 136-commit history; this merge removes the old body from Odin.
Cut: crates/idunn-daemon and vendor/cultmesh-rs. Odin keeps its own Idunn
projection (crates/odin-core/src/idunn.rs), its RUDP client, and its recipe
under deployment/idunn -- those are Odin's view of a managed relationship,
not Idunn's authority.
Conflicts resolved:
- crates/idunn-daemon/src/main.rs, scripts/linux/idunn-yggdrasil: deleted,
per the extraction. Both live in GameCult/Idunn.
- docs/architecture.md: took the branch. Main still described Gjallar as
Nightwing-resident under E:\Projects\Gjallar; it runs on Yggdrasil from
/srv/gjallar/current, confirmed against the host.
- src/odin/documents.cjs: union. All three hunks were pure branch-side
additions of the ghostlight.schema_catalog type; markers stripped rather
than taking --theirs, to preserve auto-merged HEAD changes elsewhere in
the file. node --check passes.
Verified: odin-core 22/22 on Windows; odin-daemon type-checks clean for
x86_64-unknown-linux-gnu, which is its real target -- it is unconditionally
Unix (systemd socket activation via File::from_raw_fd) and does not build
on Windows by design.
main is tagged pre-idunn-extraction-main: the last tree containing the
swarm keepalive supervision and the July Bifrost Persona feedback target,
neither of which exists on the branch or in GameCult/Idunn. See the merge
notes for why that matters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Idunn now lives at https://github.com/GameCult/Idunn with its full 136-commit
history. Odin no longer contains the deployment authority that manages it.
Also removes vendor/cultmesh-rs, the private fork that had diverged from
CultLib in both directions. Its unique capability -- the RUDP document server
and the message_id correlation rework -- was reconciled into CultLib first, so
this deletes a duplicate rather than a capability. The four crates that
consumed it by path (muninn-daemon, odin-core, odin-daemon, sleipnir-daemon)
now consume CultLib at c13b6ba like everything else.
What deliberately stays in Odin: crates/odin-core/src/idunn.rs, which is Odin's
own projection of Idunn state for aggregation and is Odin's to own;
src/odin/idunn-rudp.cjs, its client for talking to Idunn; and
deployment/idunn/recipe.toml, which despite the path is Odin's own deployment
recipe expressed in Idunn's format -- it builds odin-daemon.
odin-core: 22 passed, 0 failed, unchanged across the cut. odin-daemon still
fails to compile on Windows for the pre-existing Unix-only std::os::fd reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IdunnDaemonHealthTrustBindingRecord and its schema constant moved to
cultnet-rs (GameCult/CultLib@c13b6ba). odin-core now imports them from
cultnet_rs instead of defining them, and idunn-daemon/provisioning.rs
pulls all six provider-health symbols from cultnet_rs directly, merged
into its existing cultnet_rs use block. Dropped the odin-core path
dependency from idunn-daemon/Cargo.toml — nothing in the crate reaches
odin_core:: anymore. Bumped the CultLib git rev across all six
consuming manifests to the new commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves all six CultLib pins from cbde940 to ae0ac0d, which carries the
reconciled cultmesh-rs. The delta is three commits and near-nothing in the
packages Odin consumes: a license line, two lines in cultnet-rs replication,
and one additive pub fn.
The dev-dependency is a real pre-existing defect, not fallout from the bump.
odin-core's tests use rmp_serde in crates/odin-core/src/idunn.rs but never
declared it, so the lib-test target has never compiled. Verified pre-existing:
9 errors at the old pin, 8 at the new. With the dep declared, odin-core tests
compile and 22 pass -- including idunn_records_round_trip_through_cultmesh,
which matters for the extraction this branch is preparing.
Note for anyone checking this on Windows: odin-daemon does not build here and
that is expected, not a regression. It imports std::os::fd and from_raw_fd,
which are Unix-only. Identical failure at both pins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The drift was 94 hunks across 4 files, not 94 files as I described it earlier.
muninn-daemon carries almost all of it; sleipnir-daemon needed four lines.
Checked rather than assumed. Ignoring whitespace, the only remaining deltas are
trailing commas rustfmt adds when it wraps an argument list, and braces removed
from single-expression closure bodies. Both are semantics-preserving.
sleipnir-daemon's change is whitespace and nothing else.
Everything except the Unix-only odin-daemon builds; muninn and sleipnir pass 162
tests, with the same pre-existing media-channel failure.
vendor/cultmesh-rs is its own workspace so `cargo fmt` does not reach it, but it
reports no drift of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CultNet submodule and the in-tree CultCache copy are gone. Both now come
from CultLib, and vendor/cultmesh-rs is the only fork left -- it too consumes
CultLib, so exactly one version of each crate spans the graph. That mattered:
mixing sources would have put two cultnet-rs crates in one build, and the types
would not have unified.
The in-flight CultMesh RUDP document server comes with it because it is a
prerequisite, not a coincidence. The previously committed CultMesh calls
assume_connected(), a CultNet method Odin's fork carried and CultLib does not,
so the old code cannot build against CultLib at all. The newer code no longer
calls it. Verified by reverting the file and watching it fail.
Three API differences needed adapting: CultLib's transport options carry a
reconnect_policy the fork lacked (7 sites), its create_ack builds the packet
straight from ack state and so cannot fail, and its transport profile owns each
channel's reliable expiry.
Everything but odin-daemon builds; odin-daemon is Unix-only and does not compile
on this machine either way. muninn and sleipnir pass 162 tests. The one failure,
rudp_media_transport_options_follow_low_latency_profile, expects an Unreliable
media channel while every CultNet lineage including both revisions Odin had
pinned declares it Reliable, so it failed before this change too.
The in-flight idunn-daemon work is deliberately left uncommitted; the committed
tree was verified to build without it. Also left alone: the untracked
odin-daemon crate and deployment recipe, and the root manifest, whose now-dead
vendor excludes are inert. That recipe still requires the vendor/cultnet-rs
gitlink and will refuse admission until that line is dropped -- the fix is in
the working tree, waiting on that directory being tracked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit fixed the target catalog and the operator alarm, but the
actuators the catalog invokes carried the same E: paths. Idunn would have
resolved every target and then failed on the first restart or deploy it ran,
for exactly the reason the alarm failed.
Rebased 41 references across 27 scripts:
- .cmd bodies invoke their sibling .ps1 through %~dp0 instead of an absolute
drive path, and restart-weksa.cmd reaches its sibling repo the same way.
- .ps1 parameter defaults and assignments derive from $PSScriptRoot: one level
up for paths inside Odin, two for sibling repos under the projects root.
Left deliberately alone: $RemoteViliRoot and $StateRoot in restart-vili.ps1.
Both are consumed inside the EncodedCommand block that restart-vili ssh's to
raven, so they describe raven's filesystem, not starfire's. Whether raven also
moved off E: is an operator question, not something to infer from here.
Verified: all 46 scripts under scripts/ parse clean, and the derivation
resolves to F:\Projects\Odin and F:\Projects on this host.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>