diff --git a/docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md b/docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md
new file mode 100644
index 0000000..3158953
--- /dev/null
+++ b/docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md
@@ -0,0 +1,716 @@
+# Akka Hosting Alignment — Gap Closeout Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use `superpowers-extended-cc:executing-plans` to implement this plan task-by-task.
+
+**Goal:** Close the four real/cosmetic gaps identified by the audit of `docs/plans/2026-05-26-akka-hosting-alignment-plan.md` so the v2 implementation matches the plan's literal contract (per-role appsettings overlays, explicit dual-endpoint visibility test, plan-prescribed filenames, removal of empty legacy directories).
+
+**Architecture:** Additive only. No production-runtime semantics change. One small extension to `OpcUaApplicationHost` so the OPC UA server can advertise peer URIs in `Server.ServerArray` — gated on a new option, defaults to old behavior. Everything else is JSON, test code, file moves, and `rm -rf` of stale bin/obj trees.
+
+**Tech Stack:** .NET 10, OPCFoundation .NET Standard SDK (`Opc.Ua.*`), xunit.v3, Shouldly, EF Core 10 (inherited; no schema changes).
+
+**Source plan:** `docs/plans/2026-05-26-akka-hosting-alignment-plan.md`. The audit findings closed by this plan map to Tasks 54, 59, 60, and the post-Task-56 cosmetic cleanup. **Read the source plan's "Conventions for every task" block — those rules still apply here.**
+
+**Branch:** `v2-gap-closeout` off `master`.
+
+---
+
+## Conventions for every task
+
+- **Branch:** Stay on `v2-gap-closeout`. Never commit to `master` while plan is running.
+- **Build command:** `dotnet build ZB.MOM.WW.OtOpcUa.slnx` — must be green before commit.
+- **Test command:** `dotnet test ZB.MOM.WW.OtOpcUa.slnx --no-build` — relevant new/changed tests must pass.
+- **Commit format:** Conventional Commits matching the source plan — `feat(host):`, `test(opcua):`, `chore(cleanup):`, `refactor(test):`, etc.
+- **Mac compatibility:** All code must build on macOS. The new dual-endpoint test boots two real OPC UA servers on loopback — works on macOS (no Windows-only APIs needed; PKI is created under a per-test temp dir).
+
+---
+
+## Task 0: Add three role-overlay appsettings files (Task 54 gap)
+
+**Classification:** small
+**Estimated implement time:** ~3 min
+**Parallelizable with:** Task 1, Task 5, Task 6
+
+**Files:**
+- Create: `/Users/dohertj2/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.admin.json`
+- Create: `/Users/dohertj2/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.driver.json`
+- Create: `/Users/dohertj2/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.admin-driver.json`
+
+**Background:**
+`Program.cs` line 33-35 loads `appsettings.{role-suffix}.json` where the suffix is the roles joined alphabetically with `'-'`. Today the loader passes `optional: true`, so the host boots without these files — but the source plan (Task 54) called them out as required scaffolding so operators have per-role tunable defaults.
+
+Suffix matrix:
+| `OTOPCUA_ROLES` env | Loaded file |
+|---|---|
+| `admin` | `appsettings.admin.json` |
+| `driver` | `appsettings.driver.json` |
+| `admin,driver` (any order) | `appsettings.admin-driver.json` (joined alphabetical) |
+
+**Step 1: Create `appsettings.admin.json`**
+
+Admin-only nodes don't bind drivers; tighten Serilog and disable the LDAP dev stub by default.
+
+```json
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft.AspNetCore": "Warning",
+ "Akka": "Information"
+ }
+ }
+ },
+ "Security": {
+ "Ldap": {
+ "DevStubMode": false
+ }
+ }
+}
+```
+
+**Step 2: Create `appsettings.driver.json`**
+
+Driver-only nodes have no Admin UI; raise OPC UA verbosity slightly so per-node diagnostics flow to logs.
+
+```json
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Opc.Ua": "Debug",
+ "Akka": "Information"
+ }
+ }
+ },
+ "Security": {
+ "Ldap": {
+ "DevStubMode": false
+ }
+ }
+}
+```
+
+**Step 3: Create `appsettings.admin-driver.json`**
+
+Combined-role nodes (the docker-dev compose default + the integration test harness) — turn on both surfaces with shared defaults.
+
+```json
+{
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft.AspNetCore": "Warning",
+ "Opc.Ua": "Information",
+ "Akka": "Information"
+ }
+ }
+ },
+ "Security": {
+ "Ldap": {
+ "DevStubMode": false
+ }
+ }
+}
+```
+
+**Step 4: Build green check**
+
+Run: `dotnet build ZB.MOM.WW.OtOpcUa.slnx`
+Expected: succeeds. (JSON files do not break the build; this is a smoke check that nothing else regressed.)
+
+**Step 5: Commit**
+
+```bash
+git add src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.admin.json \
+ src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.driver.json \
+ src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.admin-driver.json
+git commit -m "feat(host): add per-role appsettings overlays for admin/driver/admin-driver"
+```
+
+---
+
+## Task 1: Extend `OpcUaApplicationHost` with `PeerApplicationUris` + populate `Server.ServerArray`
+
+**Classification:** standard
+**Estimated implement time:** ~5 min
+**Parallelizable with:** Task 0, Task 5, Task 6
+
+**Files:**
+- Modify: `/Users/dohertj2/Desktop/OtOpcUa/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs` (add option + post-start population)
+- Test: `/Users/dohertj2/Desktop/OtOpcUa/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs`
+
+**Background:**
+The source plan's Task 60 promised a test where "real OPCFoundation client → both endpoints visible in ServerUriArray". That requires production code to populate the peer URIs onto each server's `Server.ServerArray` (NodeId i=2254) property. No such code exists in v2 today — this task adds it as an opt-in option so existing single-node tests keep their current behavior. Task 3 then writes the integration test that drives it across two servers.
+
+**Step 1: Write the failing unit test**
+
+Create `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs`:
+
+```csharp
+using System.IO;
+using System.Net.Sockets;
+using System.Net;
+using Microsoft.Extensions.Logging.Abstractions;
+using Opc.Ua;
+using Opc.Ua.Server;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.OpcUaServer;
+
+namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
+
+///
+/// Audit gap closeout — verifies
+/// is reflected in Server.ServerArray after start. Single-server in-process check; the
+/// cross-server visibility check lives in OtOpcUa.OpcUaServer.IntegrationTests.
+///
+public sealed class OpcUaApplicationHostServerArrayTests
+{
+ [Fact]
+ public async Task ServerArray_contains_local_uri_and_configured_peers_after_start()
+ {
+ var pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}");
+ try
+ {
+ var options = new OpcUaApplicationHostOptions
+ {
+ ApplicationName = "OtOpcUa.UnitTest",
+ ApplicationUri = "urn:OtOpcUa.UnitTest.NodeA",
+ OpcUaPort = AllocateFreePort(),
+ PublicHostname = "127.0.0.1",
+ PkiStoreRoot = pkiRoot,
+ PeerApplicationUris = new[] { "urn:OtOpcUa.UnitTest.NodeB" },
+ };
+
+ var server = new StandardServer();
+ await using var host = new OpcUaApplicationHost(options, NullLogger.Instance);
+ await host.StartAsync(server, CancellationToken.None);
+
+ var serverArray = server.CurrentInstance.ServerObject.ServerArray.Value;
+ serverArray.ShouldNotBeNull();
+ serverArray.ShouldContain("urn:OtOpcUa.UnitTest.NodeA");
+ serverArray.ShouldContain("urn:OtOpcUa.UnitTest.NodeB");
+ }
+ finally
+ {
+ if (Directory.Exists(pkiRoot)) Directory.Delete(pkiRoot, recursive: true);
+ }
+ }
+
+ private static int AllocateFreePort()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+}
+```
+
+**Step 2: Run the test — confirm it fails**
+
+Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests --filter "FullyQualifiedName~OpcUaApplicationHostServerArrayTests"`
+Expected: FAIL with `PeerApplicationUris` not found (compile error) — the option doesn't exist yet.
+
+**Step 3: Add the option**
+
+Edit `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs`. Add to `OpcUaApplicationHostOptions` (after `AutoAcceptUntrustedClientCertificates`, around line 65):
+
+```csharp
+///
+/// Peer server URIs published in Server.ServerArray after start, in addition to
+/// the local . Empty by default — set this on warm-redundancy
+/// deployments so OPC UA clients can discover the partner endpoint via the standard
+/// Server.ServerArray property (NodeId i=2254). Order does not matter; the local URI
+/// is always element 0.
+///
+public IList PeerApplicationUris { get; set; } = new List();
+```
+
+**Step 4: Populate `Server.ServerArray` after start**
+
+Edit `OpcUaApplicationHost.StartAsync` (around line 100-118). After the `_application.Start(server)` call and before the log line, insert:
+
+```csharp
+PopulateServerArray();
+```
+
+Then add the private method below `AttachUserAuthenticator`:
+
+```csharp
+///
+/// Writes the union of and
+/// to the OPC UA standard
+/// Server.ServerArray property (NodeId i=2254). Clients in a warm-redundancy
+/// deployment discover the partner endpoint by reading this property.
+///
+private void PopulateServerArray()
+{
+ var serverObject = _server?.CurrentInstance?.ServerObject;
+ if (serverObject is null) return;
+
+ var uris = new List { _options.ApplicationUri };
+ foreach (var peer in _options.PeerApplicationUris)
+ {
+ if (!string.IsNullOrWhiteSpace(peer) && !uris.Contains(peer))
+ uris.Add(peer);
+ }
+ serverObject.ServerArray.Value = uris.ToArray();
+}
+```
+
+**Step 5: Run the test — confirm it passes**
+
+Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests --filter "FullyQualifiedName~OpcUaApplicationHostServerArrayTests"`
+Expected: PASS. If `ServerObject.ServerArray.Value` is read-only (some SDK versions guard it), fall back to writing through `ServerArrayNode.Value` via the address-space accessor — but try the direct write first; the SDK exposes it as a settable BaseDataVariableState on `ServerObjectState`.
+
+**Step 6: Run full OpcUaServer.Tests suite to confirm no regression**
+
+Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests`
+Expected: all tests pass — `PopulateServerArray` is additive when `PeerApplicationUris` is empty (default), so existing tests don't change behavior.
+
+**Step 7: Commit**
+
+```bash
+git add src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs \
+ tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostServerArrayTests.cs
+git commit -m "feat(opcua): OpcUaApplicationHost publishes peer URIs in Server.ServerArray"
+```
+
+---
+
+## Task 2: Create `OtOpcUa.OpcUaServer.IntegrationTests` project
+
+**Classification:** small
+**Estimated implement time:** ~4 min
+**Parallelizable with:** Task 5, Task 6 (file moves elsewhere)
+**Depends on:** none (csproj is self-contained)
+
+**Files:**
+- Create: `/Users/dohertj2/Desktop/OtOpcUa/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj`
+- Modify: `/Users/dohertj2/Desktop/OtOpcUa/ZB.MOM.WW.OtOpcUa.slnx` (add the project)
+
+**Background:**
+The source plan's Task 60 named this exact project. Audit found ServiceLevel coverage relocated to other test projects but no `OpcUaServer.IntegrationTests` project exists. Creating the project skeleton in its own task keeps Task 3's commit focused on the test code.
+
+**Step 1: Create the csproj**
+
+Mirror the conventions in `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj`. The integration project needs the `Opc.Ua.Client` package (vs. only `Opc.Ua.Server` in the unit tests) — confirm the version against the existing client CLI's csproj: `src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/ZB.MOM.WW.OtOpcUa.Client.CLI.csproj`.
+
+```xml
+
+
+
+ false
+ true
+ ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests
+ true
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+```
+
+If `OPCFoundation.NetStandard.Opc.Ua.Client` isn't in `Directory.Packages.props`, add it there (mirror the existing `OPCFoundation.NetStandard.Opc.Ua.Server` version exactly).
+
+**Step 2: Add project to the solution**
+
+Run: `dotnet sln ZB.MOM.WW.OtOpcUa.slnx add tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj`
+Expected: "Project added to the solution."
+
+**Step 3: Build green check**
+
+Run: `dotnet build ZB.MOM.WW.OtOpcUa.slnx`
+Expected: builds. (Empty project, so no test discovery yet — `dotnet test` would say "no tests".)
+
+**Step 4: Commit**
+
+```bash
+git add tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ \
+ ZB.MOM.WW.OtOpcUa.slnx \
+ Directory.Packages.props # only if the Opc.Ua.Client version was added there
+git commit -m "test(opcua): scaffold OtOpcUa.OpcUaServer.IntegrationTests project"
+```
+
+---
+
+## Task 3: `DualEndpointTests` — real OPC UA client reads both URIs from `Server.ServerArray`
+
+**Classification:** standard
+**Estimated implement time:** ~5 min
+**Parallelizable with:** Task 5, Task 6
+**Depends on:** Task 1 (PeerApplicationUris wiring), Task 2 (IT project exists)
+
+**Files:**
+- Create: `/Users/dohertj2/Desktop/OtOpcUa/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs`
+
+**Background:**
+This is the explicit Task 60 deliverable: a real OPC UA client connects to one server and confirms it can discover the partner via `Server.ServerArray`. Single-server unit-side coverage exists in Task 1; this test exercises the wire path with both servers up.
+
+**Step 1: Write the test**
+
+```csharp
+using System.Net;
+using System.Net.Sockets;
+using Microsoft.Extensions.Logging.Abstractions;
+using Opc.Ua;
+using Opc.Ua.Client;
+using Opc.Ua.Configuration;
+using Opc.Ua.Server;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.OpcUaServer;
+
+namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
+
+///
+/// Source plan Task 60 — closes the audit gap. Boots two real
+/// instances on loopback, each configured with the other's ApplicationUri in
+/// . A real OPC UA client connects
+/// to Node A, reads Server.ServerArray, and asserts both URIs are visible — the
+/// warm-redundancy discovery contract clients depend on.
+///
+public sealed class DualEndpointTests
+{
+ private const string NodeAUri = "urn:OtOpcUa.DualEndpoint.NodeA";
+ private const string NodeBUri = "urn:OtOpcUa.DualEndpoint.NodeB";
+
+ [Fact]
+ public async Task Client_reads_both_ApplicationUris_from_NodeA_ServerArray()
+ {
+ var pkiRootA = Path.Combine(Path.GetTempPath(), $"otopcua-pki-a-{Guid.NewGuid():N}");
+ var pkiRootB = Path.Combine(Path.GetTempPath(), $"otopcua-pki-b-{Guid.NewGuid():N}");
+ var portA = AllocateFreePort();
+ var portB = AllocateFreePort();
+
+ try
+ {
+ await using var nodeA = await StartNodeAsync(NodeAUri, portA, pkiRootA, peers: new[] { NodeBUri });
+ await using var nodeB = await StartNodeAsync(NodeBUri, portB, pkiRootB, peers: new[] { NodeAUri });
+
+ var serverArray = await ReadServerArrayAsync($"opc.tcp://127.0.0.1:{portA}/OtOpcUa");
+ serverArray.ShouldContain(NodeAUri);
+ serverArray.ShouldContain(NodeBUri);
+ }
+ finally
+ {
+ if (Directory.Exists(pkiRootA)) Directory.Delete(pkiRootA, recursive: true);
+ if (Directory.Exists(pkiRootB)) Directory.Delete(pkiRootB, recursive: true);
+ }
+ }
+
+ private static async Task StartNodeAsync(
+ string applicationUri, int port, string pkiRoot, string[] peers)
+ {
+ var options = new OpcUaApplicationHostOptions
+ {
+ ApplicationName = applicationUri, // unique per node — SDK uses it for cert CN
+ ApplicationUri = applicationUri,
+ OpcUaPort = port,
+ PublicHostname = "127.0.0.1",
+ PkiStoreRoot = pkiRoot,
+ EnabledSecurityProfiles = new List { OpcUaSecurityProfile.None },
+ AutoAcceptUntrustedClientCertificates = true,
+ PeerApplicationUris = peers,
+ };
+ var server = new StandardServer();
+ var host = new OpcUaApplicationHost(options, NullLogger.Instance);
+ await host.StartAsync(server, CancellationToken.None);
+ return host;
+ }
+
+ private static async Task ReadServerArrayAsync(string endpointUrl)
+ {
+ var appConfig = new ApplicationConfiguration
+ {
+ ApplicationName = "OtOpcUa.DualEndpointClient",
+ ApplicationUri = $"urn:OtOpcUa.DualEndpointClient.{Guid.NewGuid():N}",
+ ApplicationType = ApplicationType.Client,
+ SecurityConfiguration = new SecurityConfiguration
+ {
+ ApplicationCertificate = new CertificateIdentifier(),
+ AutoAcceptUntrustedCertificates = true,
+ },
+ ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60_000 },
+ CertificateValidator = new CertificateValidator(),
+ };
+ await appConfig.Validate(ApplicationType.Client);
+ appConfig.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
+
+ var endpoint = CoreClientUtils.SelectEndpoint(appConfig, endpointUrl, useSecurity: false);
+ var endpointConfiguration = EndpointConfiguration.Create(appConfig);
+ var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfiguration);
+
+ using var session = await Session.Create(
+ appConfig, configuredEndpoint, updateBeforeConnect: false,
+ sessionName: "DualEndpointTests", sessionTimeout: 60_000,
+ identity: new UserIdentity(new AnonymousIdentityToken()),
+ preferredLocales: null);
+
+ var value = session.ReadValue(VariableIds.Server_ServerArray);
+ return (string[])value.Value;
+ }
+
+ private static int AllocateFreePort()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ var port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+}
+```
+
+**Step 2: Run the test**
+
+Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests`
+Expected: PASS. Wall-time ~3-5 s (two cert-creation cycles + session handshake).
+
+If the test hangs on the session handshake on first run, it's the SDK reading the trusted-cert store — bumping `AutoAcceptUntrustedClientCertificates = true` on both server hosts (already set above) should resolve it. If `CoreClientUtils.SelectEndpoint` throws because the SDK version uses a different overload, fall back to constructing the `EndpointDescription` directly with `EndpointUrl = endpointUrl, SecurityMode = MessageSecurityMode.None, SecurityPolicyUri = SecurityPolicies.None` and skipping `SelectEndpoint`.
+
+**Step 3: Commit**
+
+```bash
+git add tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs
+git commit -m "test(opcua): DualEndpointTests — real client reads peer URIs from Server.ServerArray"
+```
+
+---
+
+## Task 4: Wire `OtOpcUa.OpcUaServer.IntegrationTests` into v2-ci.yml
+
+**Classification:** small
+**Estimated implement time:** ~3 min
+**Parallelizable with:** Task 5, Task 6
+**Depends on:** Task 3 (project must exist + have a real test before CI runs it)
+
+**Files:**
+- Modify: `/Users/dohertj2/Desktop/OtOpcUa/.github/workflows/v2-ci.yml`
+
+**Step 1: Add the project to the `integration` job**
+
+Either extend the existing `integration` job to run a second `dotnet test` step, or convert it to a matrix. Prefer a matrix for symmetry with `unit-tests`:
+
+Open `.github/workflows/v2-ci.yml`, locate the `integration:` job. Replace it with:
+
+```yaml
+ integration:
+ needs: build
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ project:
+ - tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests
+ - tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+ - name: dotnet test ${{ matrix.project }}
+ run: dotnet test ${{ matrix.project }} --configuration Release --filter "Category!=E2E"
+```
+
+**Step 2: Build green check**
+
+Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests --configuration Release --filter "Category!=E2E"`
+Expected: matches the exact CI command — passes locally so CI will pass too.
+
+**Step 3: Commit**
+
+```bash
+git add .github/workflows/v2-ci.yml
+git commit -m "ci(v2): include OpcUaServer.IntegrationTests in integration matrix"
+```
+
+---
+
+## Task 5: Rename `FailoverScenarioTests` → `FailoverDuringDeployTests` (Task 59 cosmetic)
+
+**Classification:** trivial
+**Estimated implement time:** ~2 min
+**Parallelizable with:** Task 0, Task 1, Task 2, Task 6 (different files)
+
+**Files:**
+- Rename: `tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/FailoverScenarioTests.cs` → `FailoverDuringDeployTests.cs`
+- Modify: class name + namespace-internal references
+
+**Step 1: Rename the file and the class**
+
+```bash
+git mv tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/FailoverScenarioTests.cs \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/FailoverDuringDeployTests.cs
+```
+
+Then edit `FailoverDuringDeployTests.cs` and replace the single class declaration `public sealed class FailoverScenarioTests` with `public sealed class FailoverDuringDeployTests`. Use Edit, not sed — the file only declares this class once (`grep -c "FailoverScenario" .` ≤ 2).
+
+**Step 2: Sweep for any stale references**
+
+Run: `grep -rln "FailoverScenarioTests" .`
+Expected: zero matches after Step 1. If anything appears (e.g., a CI filter, a doc), fix the reference.
+
+**Step 3: Build + run test**
+
+Run: `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests --filter "FullyQualifiedName~FailoverDuringDeployTests"`
+Expected: same tests pass that previously passed under the old name.
+
+**Step 4: Commit**
+
+```bash
+git add tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/FailoverDuringDeployTests.cs
+git commit -m "refactor(test): rename FailoverScenarioTests → FailoverDuringDeployTests for plan parity"
+```
+
+---
+
+## Task 6: Delete empty bin/obj-only legacy directories
+
+**Classification:** trivial
+**Estimated implement time:** ~2 min
+**Parallelizable with:** Task 0, Task 1, Task 2, Task 5
+
+**Files:**
+- Delete: `src/Server/ZB.MOM.WW.OtOpcUa.Server/`
+- Delete: `src/Server/ZB.MOM.WW.OtOpcUa.Admin/`
+- Delete: `tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests/`
+- Delete: `tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests/`
+- Delete: `tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests/`
+
+**Background:**
+Source plan Task 56 deleted the projects from `ZB.MOM.WW.OtOpcUa.slnx` (confirmed by the audit) but left `bin/`+`obj/` shells on disk. These confuse new contributors and skew directory listings. None of them are referenced anywhere.
+
+**Step 1: Sanity-check that each directory is bin/obj-only**
+
+```bash
+for dir in \
+ src/Server/ZB.MOM.WW.OtOpcUa.Server \
+ src/Server/ZB.MOM.WW.OtOpcUa.Admin \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests; do
+ echo "--- $dir ---"
+ find "$dir" -maxdepth 2 -type f | grep -v "/bin/\|/obj/"
+done
+```
+
+Expected: every section is empty (no source files leak out). If any source file shows, STOP and surface it — don't delete blindly.
+
+**Step 2: Verify slnx doesn't reference them**
+
+Run: `grep -nE 'ZB\.MOM\.WW\.OtOpcUa\.(Server|Admin)(/|\.Tests|\.E2ETests)' ZB.MOM.WW.OtOpcUa.slnx`
+Expected: zero matches.
+
+**Step 3: Delete the directories**
+
+```bash
+rm -rf src/Server/ZB.MOM.WW.OtOpcUa.Server \
+ src/Server/ZB.MOM.WW.OtOpcUa.Admin \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Server.Tests \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests \
+ tests/Server/ZB.MOM.WW.OtOpcUa.Admin.E2ETests
+```
+
+**Step 4: Build green check**
+
+Run: `dotnet build ZB.MOM.WW.OtOpcUa.slnx`
+Expected: succeeds (these directories were already out of the solution).
+
+**Step 5: Commit**
+
+```bash
+git add -A
+git commit -m "chore(cleanup): remove stale bin/obj shells for deleted v1 Server/Admin projects"
+```
+
+---
+
+## Task 7: Final build + test green check
+
+**Classification:** trivial
+**Estimated implement time:** ~3 min
+**Parallelizable with:** none (verification, depends on all prior tasks)
+
+**Step 1: Restore + build**
+
+Run: `dotnet build ZB.MOM.WW.OtOpcUa.slnx`
+Expected: 0 errors, 0 warnings (TreatWarningsAsErrors is on across the solution).
+
+**Step 2: Run the full test suite**
+
+Run: `dotnet test ZB.MOM.WW.OtOpcUa.slnx --no-build`
+Expected: all tests green. Specifically confirm:
+- `OpcUaApplicationHostServerArrayTests` (Task 1) — pass
+- `DualEndpointTests` (Task 3) — pass
+- `FailoverDuringDeployTests` (Task 5) — same count of tests pass as before the rename
+
+**Step 3: Smoke check the audit assertions**
+
+Run:
+```bash
+ls src/Server/ZB.MOM.WW.OtOpcUa.Host/appsettings.*.json
+find tests/Server -iname "DualEndpointTests.cs" -o -iname "FailoverDuringDeployTests.cs"
+ls -la src/Server/ZB.MOM.WW.OtOpcUa.{Server,Admin} 2>/dev/null
+```
+
+Expected:
+- 4 appsettings files: `.json`, `.Development.json`, `.admin.json`, `.admin-driver.json`, `.driver.json`
+- Both renamed/new test files exist
+- The two `ls -la` calls return errors (directories gone)
+
+**Step 4: No commit unless cleanup turned up**
+
+If anything failed in Steps 1-3, fix it as a follow-up task — do not paper over with a `--no-verify` commit.
+
+---
+
+## Final verification
+
+After Task 7:
+
+1. `dotnet build ZB.MOM.WW.OtOpcUa.slnx` — green
+2. `dotnet test ZB.MOM.WW.OtOpcUa.slnx --no-build` — green (incl. 2 new tests)
+3. `git log --oneline master..HEAD` — exactly 6 commits, Conventional-Commits style
+4. Open PR `v2-gap-closeout` → `master` titled "v2: close audit gaps — appsettings overlays, DualEndpointTests, cleanup"
+
+---
+
+## Task index
+
+| # | Title | Class | Time | Parallel with |
+|---|---|---|---|---|
+| 0 | Per-role appsettings overlays | small | 3m | 1, 5, 6 |
+| 1 | OpcUaApplicationHost.PeerApplicationUris + ServerArray | standard | 5m | 0, 5, 6 |
+| 2 | OpcUaServer.IntegrationTests project skeleton | small | 4m | 5, 6 |
+| 3 | DualEndpointTests | standard | 5m | 5, 6 |
+| 4 | CI matrix entry for new IT project | small | 3m | 5, 6 |
+| 5 | Rename FailoverScenarioTests → FailoverDuringDeployTests | trivial | 2m | 0, 1, 2, 6 |
+| 6 | Delete stale bin/obj-only directories | trivial | 2m | 0, 1, 2, 5 |
+| 7 | Final build + test green check | trivial | 3m | none |
+
+**Total estimated subagent time:** ~27 min.
+
+**Dependency graph (non-parallel pairs):**
+- Task 3 depends on Task 1 (option must exist) and Task 2 (project must exist)
+- Task 4 depends on Task 3 (CI runs the project's tests)
+- Task 7 depends on all prior tasks
diff --git a/docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md.tasks.json b/docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md.tasks.json
new file mode 100644
index 0000000..2a6a911
--- /dev/null
+++ b/docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md.tasks.json
@@ -0,0 +1,14 @@
+{
+ "planPath": "docs/plans/2026-05-26-akka-hosting-alignment-gaps-closeout.md",
+ "tasks": [
+ {"id": 1, "subject": "Task 0: Per-role appsettings overlays", "status": "pending"},
+ {"id": 2, "subject": "Task 1: OpcUaApplicationHost.PeerApplicationUris + ServerArray population", "status": "pending"},
+ {"id": 3, "subject": "Task 2: OpcUaServer.IntegrationTests project skeleton", "status": "pending"},
+ {"id": 4, "subject": "Task 3: DualEndpointTests — real OPC UA client reads both URIs from Server.ServerArray", "status": "pending", "blockedBy": ["2", "3"]},
+ {"id": 5, "subject": "Task 4: Wire OpcUaServer.IntegrationTests into v2-ci.yml", "status": "pending", "blockedBy": ["4"]},
+ {"id": 6, "subject": "Task 5: Rename FailoverScenarioTests → FailoverDuringDeployTests", "status": "pending"},
+ {"id": 7, "subject": "Task 6: Delete empty bin/obj-only legacy directories", "status": "pending"},
+ {"id": 8, "subject": "Task 7: Final build + test green check", "status": "pending", "blockedBy": ["1", "2", "3", "4", "5", "6", "7"]}
+ ],
+ "lastUpdated": "2026-05-26T00:00:00Z"
+}