diff --git a/spikes/README.md b/spikes/README.md
new file mode 100644
index 0000000..b76c6cc
--- /dev/null
+++ b/spikes/README.md
@@ -0,0 +1,77 @@
+# M1 spikes (docs/WINDOWS_PORT.md §13)
+
+Programs that answer questions the port is currently *guessing* at. They are checked in and
+runnable on purpose: a spike whose answer nobody can reproduce six months later is a rumour.
+
+They are deliberately **not** in `windows/Nucleic.sln`. The solution's broker contract tests must
+keep running on any machine against `FakeWslc`; these need the `Microsoft.WSL.Containers` preview
+NuGet and a real WSL stack, so they are built by path.
+
+---
+
+## `WslcApiDump` — what does the wslc API actually look like?
+
+**The problem it solves.** Items 5, 6 and 7 — the C# broker, the Swift wslc client, and the
+control plane — were all built against Microsoft's *documentation*, on machines with no WSL.
+`NucleicBroker/Wslc/WslcFacade.cs` is therefore a careful transcription that has never been
+compiled against the real assembly, and `-p:UseWslc=true` is the only thing standing between it
+and the build. Every line in it might be right. We have no idea which.
+
+A typed spike cannot tell us: if `Session.CreateOrOpen` is really `Session.Open`, the spike fails
+to **compile**, which teaches us one name and stops. So this tool is written entirely in
+reflection. It cannot fail to build, and a single run prints the complete list of what is wrong.
+
+```powershell
+cd windows/spikes/WslcApiDump
+dotnet run # dump the API + check every facade assumption
+dotnet run -- --probe # + call the two read-only statics (service version, missing components)
+dotnet run -- --session # + create a real session and print its live property VALUES
+```
+
+It writes the full public object model to `wslc-api-dump.txt` (`--out` to relocate) and prints an
+`ok` / `MISSING` line per assumption, each with *why that member matters* and a nearest-name hint.
+It exits 0 even when assumptions fail — a mismatch is the product, not an error. Only a genuinely
+broken run (the assembly won't load) exits non-zero.
+
+**What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING.
+
+### Why `--session` earns its risk
+
+`--session` creates a real wslc session named `nucleic-spike` under
+`%LOCALAPPDATA%\Nucleic\spike\wslc` and leaves it running. It exists for one question that type
+metadata cannot answer: **where does the WSL-facing host gateway address come from?** §5 makes
+gateway TCP the primary control-plane transport, `WslcContainerEngine.ensureRunning` returns that
+address to the Swift side, and `control-bridge.js` dials it. The facade guesses
+`Session.HostGatewayAddress`. Dumping the live *values* of every session property lets us
+recognise a gateway IP whatever it is called — and if nothing on the session looks like one,
+that is itself the finding, and §5's hvsocket fallback gets promoted from upside to dependency.
+
+Leaving the session running is also the cheap version of a §2.3 question: broker supervision
+assumes wslc state is **service-backed**, so that a crashed `nucleic-brokerd` can reattach and
+re-enumerate rather than orphaning containers. If `wslc session ls` still shows `nucleic-spike`
+after this process exits, that assumption holds. Tear it down with `wslc` when you're done.
+
+### The three answers that change the design
+
+Most mismatches are a one-line edit in `WslcFacade.cs` — that is exactly what the `IWslc` seam is
+for, and nothing above it should move. These three are different:
+
+| Finding | Consequence |
+| --- | --- |
+| No create-or-attach on `Session` | Broker-crash reattach (§2.3) has no mechanism; supervision needs redesigning before item 11. |
+| No gateway address on a live `Session` | §5's primary transport loses its source; promote the AF_HYPERV/AF_VSOCK fallback and spike that instead. |
+| No uid on `ProcessSettings` | Recoverable, and already planned for: exec wraps argv in `setpriv`/`su agent -c`. Interceptors and nash don't care about the numeric uid (§3.2). |
+
+---
+
+## Not yet written
+
+- **`WslcSpike`** — the typed happy path: session → GHCR pull of `naros-agent` → container with an
+ NTFS `ContainerVolume` → `exec git status` in the bind-mounted worktree → stdio round-trip →
+ SIGTERM, plus the 9P latency numbers §15 wants (`git status` and `npm install` on a real repo,
+ mounted vs. in-VM). Deliberately held back until `WslcApiDump` has run: written now, against
+ guessed names, it would not compile, and fixing it blind is the mistake this whole approach
+ exists to avoid.
+- **`HvSocketSpike`** — M1 (b): AF_HYPERV host listener ↔ AF_VSOCK dial from inside a wslc
+ container, plus gateway-TCP reachability and default-firewall behaviour in NAT and mirrored
+ modes. Only worth building once a container can be started at all.
diff --git a/spikes/WslcApiDump/FacadeAssumptions.cs b/spikes/WslcApiDump/FacadeAssumptions.cs
new file mode 100644
index 0000000..462aba7
--- /dev/null
+++ b/spikes/WslcApiDump/FacadeAssumptions.cs
@@ -0,0 +1,105 @@
+namespace WslcApiDump;
+
+///
+/// Every member NucleicBroker/Wslc/WslcFacade.cs calls on the preview
+/// Microsoft.WSL.Containers API, as plain strings.
+///
+/// The facade was written from Microsoft's documentation without a machine to run it on
+/// (docs/WINDOWS_PORT.md §1.5), so each line here is a *claim* — and this spike's job is to say,
+/// for each one, whether the shipped assembly agrees. Strings rather than typed references is the
+/// entire trick: a typed spike that names Session.CreateOrOpen fails to COMPILE if that
+/// method has a different name, which teaches us nothing. Reflection turns every wrong guess into
+/// a printed line instead of a build error, so one run produces the complete worklist.
+///
+/// Keep in lockstep with WslcFacade.cs. A member that stops being used should leave this list;
+/// a newly-used member should join it, so the next run of this tool still covers the real surface.
+///
+internal static class FacadeAssumptions
+{
+ internal sealed record Assumption(string Type, string Member, Kind MemberKind, string Why);
+
+ internal enum Kind { Method, Property, Event, Constructor, EnumValue, Type }
+
+ internal static readonly Assumption[] All =
+ [
+ // ---- Service entry point: onboarding (§8 step 2) ----
+ new("WslcService", "GetServiceVersion", Kind.Method,
+ "hello capabilities — hostd degrades across preview→GA churn on this string"),
+ new("WslcService", "GetMissingComponents", Kind.Method,
+ "components.missing RPC; drives the guided-install onboarding page"),
+ new("WslcService", "InstallComponentsAsync", Kind.Method,
+ "components.install RPC"),
+ new("ComponentFlags", "None", Kind.EnumValue,
+ "the 'nothing missing' sentinel the facade filters on"),
+
+ // ---- Session: one per channel, hosts every container (§3.2) ----
+ new("SessionSettings", ".ctor", Kind.Constructor,
+ "SessionSettings(name, dataDir) — the two-arg shape the facade assumes"),
+ new("SessionSettings", "CpuCount", Kind.Property, "session.ensure cpu"),
+ new("SessionSettings", "MemoryMB", Kind.Property,
+ "session.ensure memoryMB; §3.2 notes a resize needs a sandbox restart"),
+ new("Session", "CreateOrOpen", Kind.Method,
+ "THE create-or-attach primitive. If this is absent, broker reattach after a crash "
+ + "(§2.3) has no mechanism and the whole supervision design changes"),
+ new("Session", "Terminate", Kind.Method, "session.terminate RPC"),
+ new("Session", "SessionTerminationHandler", Kind.Property,
+ "session.down notification → hostd's reconcile sweep"),
+ new("Session", "HostGatewayAddress", Kind.Property,
+ "THE control-plane address (§5). ensureRunning returns it to the Swift engine and "
+ + "control-bridge.js dials it. If this member does not exist, §5's primary transport "
+ + "needs another source (query the vNIC) or the hvsocket fallback gets promoted"),
+
+ // ---- Images ----
+ new("Session", "PullImageAsync", Kind.Method, "image.pull RPC (naros-agent from GHCR)"),
+ new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(reference)"),
+ new("PullImageOptions", "Credentials", Kind.Property, "GHCR auth (registryAuth)"),
+ new("PullImageOptions", "Progress", Kind.Event,
+ "image.pullProgress → the existing controlDownloadProgress UI surface"),
+ new("RegistryCredentials", ".ctor", Kind.Constructor, "RegistryCredentials(user, password)"),
+ new("Session", "GetImages", Kind.Method, "image.list / image.inspect"),
+ new("Session", "DeleteImage", Kind.Method, "image.delete"),
+
+ // ---- Containers ----
+ new("Session", "CreateContainer", Kind.Method, "container.create"),
+ new("Session", "GetContainers", Kind.Method, "container.list; reattach re-enumeration"),
+ new("ContainerSettings", ".ctor", Kind.Constructor, "ContainerSettings(image)"),
+ new("ContainerSettings", "Name", Kind.Property, "channel-suffixed container naming"),
+ new("ContainerSettings", "Hostname", Kind.Property, "container.create hostname"),
+ new("ContainerSettings", "NetworkingMode", Kind.Property,
+ "NAT vs mirrored (§5 item 4) — determines how the guest reaches the host"),
+ new("ContainerSettings", "Volumes", Kind.Property, "the NTFS worktree bind mount (D8)"),
+ new("ContainerSettings", "InitProcess", Kind.Property, "naros-init as PID 1 (docs/NAROS.md)"),
+ new("ContainerVolume", ".ctor", Kind.Constructor,
+ "ContainerVolume(hostPath, guestPath, readOnly) — the 3-arg shape"),
+ new("ContainerNetworkingMode", "", Kind.Type, "enum parsed from the RPC's networkingMode"),
+ new("Container", "Start", Kind.Method, "container.start"),
+ new("Container", "Stop", Kind.Method, "container.stop(signal, grace)"),
+ new("Container", "Delete", Kind.Method, "container.delete"),
+ new("Container", "State", Kind.Property, "container.state → running/stopped/absent"),
+ new("Container", "GetStatistics", Kind.Method,
+ "container.stats → ContainerResourceSample; the Swift engine folds deltas from it"),
+ new("Container", "RunProcess", Kind.Method, "proc.exec — the agent's own exec path"),
+ new("ContainerState", "Running", Kind.EnumValue, "the one state the facade tests by name"),
+ new("DeleteContainerFlags", "Force", Kind.EnumValue, "container.delete force"),
+
+ // ---- Processes: the agent stdio path (§3.2 WslcProcessHandle) ----
+ new("ProcessSettings", "CmdLine", Kind.Property, "argv"),
+ new("ProcessSettings", "Environment", Kind.Property, "env"),
+ new("ProcessSettings", "WorkingDirectory", Kind.Property, "cwd"),
+ new("ProcessSettings", "OutputMode", Kind.Property, "event-mode stdio, not polling"),
+ new("ProcessSettings", "Terminal", Kind.Property, "tty for the Terminal panel (§7)"),
+ new("ProcessSettings", "UserId", Kind.Property,
+ "runAsUID 501 (§3.2). If absent, exec falls back to a setpriv/su wrapper argv — "
+ + "interceptors and nash don't care about the numeric uid, so this is recoverable"),
+ new("ProcessSettings", "GroupId", Kind.Property, "runAsGID"),
+ new("ProcessOutputMode", "Event", Kind.EnumValue,
+ "the mode that makes stdio push-based; polling would change the whole broker design"),
+ new("Signal", "", Kind.Type, "the enum Stop/Signal take; the RPC carries POSIX ints"),
+ new("Process", "OutputReceived", Kind.Event, "→ proc.stdout / proc.stderr notifications"),
+ new("Process", "Exited", Kind.Event, "→ proc.exit; must never overtake output (OutboundWriter)"),
+ new("Process", "WriteStdin", Kind.Method, "proc.stdin (NDJSON to the agent)"),
+ new("Process", "CloseStdin", Kind.Method, "proc.closeStdin"),
+ new("Process", "Signal", Kind.Method, "proc.signal — the agent's real POSIX Stop path"),
+ new("Process", "ResizeTerminal", Kind.Method, "proc.resize (tty mode)"),
+ ];
+}
diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs
new file mode 100644
index 0000000..1ec99bc
--- /dev/null
+++ b/spikes/WslcApiDump/Program.cs
@@ -0,0 +1,323 @@
+using System.Reflection;
+using System.Text;
+
+namespace WslcApiDump;
+
+///
+/// M1 spike (a), phase 1 (docs/WINDOWS_PORT.md §13): what does Microsoft.WSL.Containers
+/// ACTUALLY look like, and where is WslcFacade.cs wrong?
+///
+/// Everything the Windows container subsystem rests on — items 5, 6 and 7 — was written against
+/// documentation, on a machine with no WSL. This program is the cheapest possible way to convert
+/// that pile of assumptions into a worklist, and it is written entirely in reflection so that it
+/// cannot fail to build no matter how wrong the assumptions turn out to be.
+///
+/// It never mutates anything unless asked: the default run only reads type metadata. `--probe`
+/// additionally calls the two safe statics (service version, missing components). `--session`
+/// goes one step further and creates a real session, because the single most important unknown —
+/// which property carries the WSL gateway address (§5) — can only be answered by looking at a
+/// live one.
+///
+internal static class Program
+{
+ private const string AssemblyName = "Microsoft.WSL.Containers";
+
+ private static int Main(string[] args)
+ {
+ var probe = args.Contains("--probe") || args.Contains("--session");
+ var session = args.Contains("--session");
+ var outPath = ArgValue(args, "--out") ?? "wslc-api-dump.txt";
+
+ Assembly assembly;
+ try
+ {
+ assembly = Assembly.Load(new AssemblyName(AssemblyName));
+ }
+ catch (Exception e)
+ {
+ Console.Error.WriteLine($"could not load {AssemblyName}: {e.Message}");
+ Console.Error.WriteLine(
+ "Is the preview NuGet restored? `dotnet restore windows/spikes/WslcApiDump`.");
+ return 2;
+ }
+
+ var types = assembly.GetExportedTypes().OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray();
+ Console.WriteLine($"{AssemblyName} {assembly.GetName().Version} — {types.Length} public types");
+ Console.WriteLine();
+
+ var report = new StringBuilder();
+ report.AppendLine($"# {AssemblyName} {assembly.GetName().Version}");
+ report.AppendLine($"# location: {assembly.Location}");
+ report.AppendLine();
+ foreach (var type in types) DescribeType(type, report);
+ File.WriteAllText(outPath, report.ToString());
+ Console.WriteLine($"full API dump → {Path.GetFullPath(outPath)}");
+ Console.WriteLine();
+
+ var missing = CheckAssumptions(types);
+
+ if (probe) ProbeStatics(types);
+ if (session) ProbeSession(types, ArgValue(args, "--session-name") ?? "nucleic-spike");
+
+ Console.WriteLine();
+ Console.WriteLine(missing == 0
+ ? "RESULT: every WslcFacade assumption is present. Fix nothing; write the typed spike."
+ : $"RESULT: {missing} assumption(s) wrong — each one is a line to change in "
+ + "windows/NucleicBroker/Wslc/WslcFacade.cs, and nowhere else (that is what IWslc is for).");
+ // Exit 0 either way: a mismatch is this tool's PRODUCT, not its failure. Only a genuinely
+ // broken run (assembly missing) is non-zero, so a wrapper script can tell them apart.
+ return 0;
+ }
+
+ // MARK: - Type dump
+
+ private static void DescribeType(Type type, StringBuilder report)
+ {
+ var kind = type.IsEnum ? "enum"
+ : type.IsInterface ? "interface"
+ : type.IsValueType ? "struct"
+ : "class";
+ report.AppendLine($"{kind} {type.FullName}"
+ + (type.BaseType is { } b && b != typeof(object) ? $" : {b.Name}" : ""));
+
+ if (type.IsEnum)
+ {
+ foreach (var name in Enum.GetNames(type)) report.AppendLine($" .{name}");
+ report.AppendLine();
+ return;
+ }
+
+ foreach (var ctor in type.GetConstructors())
+ report.AppendLine($" .ctor({Parameters(ctor)})");
+ foreach (var property in type.GetProperties(Public).OrderBy(p => p.Name, StringComparer.Ordinal))
+ report.AppendLine(
+ $" {Short(property.PropertyType)} {property.Name} "
+ + $"{{ {(property.CanRead ? "get; " : "")}{(property.CanWrite ? "set; " : "")}}}");
+ foreach (var evt in type.GetEvents(Public).OrderBy(e => e.Name, StringComparer.Ordinal))
+ report.AppendLine($" event {Short(evt.EventHandlerType)} {evt.Name}");
+ foreach (var method in type.GetMethods(Public)
+ .Where(m => !m.IsSpecialName)
+ .OrderBy(m => m.Name, StringComparer.Ordinal))
+ report.AppendLine($" {Short(method.ReturnType)} {method.Name}({Parameters(method)})");
+ report.AppendLine();
+ }
+
+ private const BindingFlags Public =
+ BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
+
+ private static string Parameters(MethodBase m) =>
+ string.Join(", ", m.GetParameters().Select(p => $"{Short(p.ParameterType)} {p.Name}"));
+
+ private static string Short(Type? t)
+ {
+ if (t is null) return "void";
+ if (!t.IsGenericType) return t.Name;
+ var name = t.Name[..t.Name.IndexOf('`')];
+ return $"{name}<{string.Join(", ", t.GetGenericArguments().Select(Short))}>";
+ }
+
+ // MARK: - Assumption check (the actual product)
+
+ private static int CheckAssumptions(Type[] types)
+ {
+ Console.WriteLine("WslcFacade.cs assumptions:");
+ Console.WriteLine();
+ var wrong = 0;
+ foreach (var group in FacadeAssumptions.All.GroupBy(a => a.Type))
+ {
+ var type = types.FirstOrDefault(t => t.Name == group.Key);
+ if (type is null)
+ {
+ // Summarise rather than repeat: if a whole type is renamed or namespaced away,
+ // one line naming it beats a paragraph per member. The per-member "why" only
+ // earns its space when the type exists and a single member is wrong.
+ wrong += group.Count();
+ Console.WriteLine($" MISSING TYPE {group.Key} "
+ + $"({group.Count()} member(s): {string.Join(", ", group.Select(a => a.Member))})");
+ var near = Nearest(group.Key, types.Select(t => t.Name));
+ Console.WriteLine(near.Length > 0
+ ? $" nearest types: {string.Join(", ", near)}"
+ : " no similarly-named type — check the dump file's namespaces");
+ continue;
+ }
+
+ foreach (var assumption in group)
+ {
+ if (assumption.MemberKind is FacadeAssumptions.Kind.Type || Has(type, assumption))
+ {
+ Console.WriteLine($" ok {type.Name}.{assumption.Member}");
+ continue;
+ }
+ wrong++;
+ Console.WriteLine($" MISSING {type.Name}.{assumption.Member} "
+ + $"({assumption.MemberKind.ToString().ToLowerInvariant()})");
+ Console.WriteLine($" why: {assumption.Why}");
+ var near = Nearest(assumption.Member, MemberNames(type));
+ if (near.Length > 0)
+ Console.WriteLine($" nearest: {string.Join(", ", near)}");
+ }
+ }
+ return wrong;
+ }
+
+ private static bool Has(Type type, FacadeAssumptions.Assumption a) => a.MemberKind switch
+ {
+ FacadeAssumptions.Kind.Constructor => type.GetConstructors().Length > 0,
+ FacadeAssumptions.Kind.EnumValue => type.IsEnum && Enum.GetNames(type).Contains(a.Member),
+ FacadeAssumptions.Kind.Event => type.GetEvent(a.Member) is not null,
+ FacadeAssumptions.Kind.Property =>
+ type.GetProperty(a.Member) is not null || type.GetField(a.Member) is not null,
+ FacadeAssumptions.Kind.Method => type.GetMethods(Public).Any(m => m.Name == a.Member),
+ _ => true,
+ };
+
+ private static IEnumerable MemberNames(Type type) =>
+ type.IsEnum
+ ? Enum.GetNames(type)
+ : type.GetMembers(Public).Where(m => !m.Name.StartsWith('.')).Select(m => m.Name).Distinct();
+
+ /// Cheap "did they just rename it" hint: shared prefix or containment, no edit
+ /// distance. A three-name shortlist is enough to spot HostGateway vs
+ /// HostGatewayAddress, which is the realistic failure mode.
+ private static string[] Nearest(string wanted, IEnumerable candidates)
+ {
+ var needle = wanted.TrimStart('.');
+ if (needle.Length == 0) return [];
+ return candidates
+ .Where(c => c.Contains(needle, StringComparison.OrdinalIgnoreCase)
+ || needle.Contains(c, StringComparison.OrdinalIgnoreCase)
+ || SharedPrefix(c, needle) >= 4)
+ .Distinct()
+ .Take(3)
+ .ToArray();
+ }
+
+ private static int SharedPrefix(string a, string b)
+ {
+ var n = 0;
+ while (n < a.Length && n < b.Length && char.ToLowerInvariant(a[n]) == char.ToLowerInvariant(b[n])) n++;
+ return n;
+ }
+
+ // MARK: - Live probes
+
+ /// The two statics that are safe to call on any machine: they only read state.
+ private static void ProbeStatics(Type[] types)
+ {
+ Console.WriteLine();
+ Console.WriteLine("live probe (read-only):");
+ var service = types.FirstOrDefault(t => t.Name == "WslcService");
+ if (service is null) { Console.WriteLine(" no WslcService type — skipping"); return; }
+
+ foreach (var name in new[] { "GetServiceVersion", "GetMissingComponents" })
+ {
+ var method = service.GetMethods(Public)
+ .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == 0);
+ if (method is null) { Console.WriteLine($" {name}: absent"); continue; }
+ try
+ {
+ Console.WriteLine($" {name}() = {Render(method.Invoke(null, null))}");
+ }
+ catch (TargetInvocationException e)
+ {
+ // The interesting failure: WSL not installed. That IS the onboarding condition
+ // §8 step 2 exists to handle, so report it plainly rather than as a crash.
+ Console.WriteLine($" {name}() threw {e.InnerException?.GetType().Name}: "
+ + $"{e.InnerException?.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Create a real session and print every readable property of it.
+ ///
+ /// This is here for one question: §5's control plane needs the WSL-facing host address, the
+ /// facade guesses it is Session.HostGatewayAddress, and no amount of type-metadata
+ /// reading tells us whether that property holds what we need. Dumping the live VALUES of a
+ /// session lets us recognise a gateway IP when we see one, whatever it happens to be called.
+ ///
+ /// Leaves the session running on purpose — `wslc session ls` should show it, and whether it
+ /// survives this process exiting is itself one of the §2.3 questions (service-backed state is
+ /// what makes broker-crash reattach possible). Tear it down with `wslc` when done.
+ ///
+ private static void ProbeSession(Type[] types, string name)
+ {
+ Console.WriteLine();
+ Console.WriteLine($"live probe (creates session '{name}'):");
+ var settingsType = types.FirstOrDefault(t => t.Name == "SessionSettings");
+ var sessionType = types.FirstOrDefault(t => t.Name == "Session");
+ if (settingsType is null || sessionType is null)
+ {
+ Console.WriteLine(" SessionSettings/Session absent — skipping");
+ return;
+ }
+
+ var dataDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "Nucleic", "spike", "wslc");
+ Directory.CreateDirectory(dataDir);
+
+ object? settings;
+ try
+ {
+ settings = Activator.CreateInstance(settingsType, name, dataDir);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($" SessionSettings(name, dataDir) rejected: {e.InnerException?.Message ?? e.Message}");
+ Console.WriteLine(" → constructor shape differs; see the .ctor lines in the dump file");
+ return;
+ }
+
+ var factory = sessionType.GetMethods(Public)
+ .FirstOrDefault(m => m.IsStatic && m.GetParameters().Length == 1
+ && m.GetParameters()[0].ParameterType == settingsType);
+ if (factory is null)
+ {
+ Console.WriteLine(" no static Session factory taking SessionSettings — see the dump");
+ return;
+ }
+ Console.WriteLine($" using {sessionType.Name}.{factory.Name}(SessionSettings)");
+
+ object? live;
+ try
+ {
+ live = factory.Invoke(null, [settings]);
+ }
+ catch (TargetInvocationException e)
+ {
+ Console.WriteLine($" session create threw {e.InnerException?.GetType().Name}: "
+ + $"{e.InnerException?.Message}");
+ return;
+ }
+ if (live is null) { Console.WriteLine(" factory returned null"); return; }
+
+ Console.WriteLine(" live session properties (look for the gateway address — §5):");
+ foreach (var property in sessionType.GetProperties(Public)
+ .Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
+ .OrderBy(p => p.Name, StringComparer.Ordinal))
+ {
+ string rendered;
+ try { rendered = Render(property.GetValue(live)); }
+ catch (Exception e) { rendered = $""; }
+ Console.WriteLine($" {property.Name} = {rendered}");
+ }
+ Console.WriteLine(" (session left running — `wslc session ls`; whether it outlives this "
+ + "process is the §2.3 reattach question)");
+ }
+
+ private static string Render(object? value) => value switch
+ {
+ null => "null",
+ string s => $"\"{s}\"",
+ System.Collections.IEnumerable e and not string =>
+ "[" + string.Join(", ", e.Cast