diff --git a/docs/plans/2026-06-13-equipment-tag-live-values-plan.md b/docs/plans/2026-06-13-equipment-tag-live-values-plan.md
new file mode 100644
index 00000000..be3a7ab4
--- /dev/null
+++ b/docs/plans/2026-06-13-equipment-tag-live-values-plan.md
@@ -0,0 +1,299 @@
+# Equipment-tag Live-Value Delivery — Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.
+
+**Goal:** Route a driver's published value (keyed by `FullName`) to its materialised equipment-tag OPC UA variable (keyed by a folder-scoped NodeId), so equipment tags stop showing `BadWaitingForInitialData`.
+
+**Architecture:** Mirror the proven `VirtualTagHostActor._nodeIdByVtag` pattern for **driver** values. Extract one shared `EquipmentNodeIds` NodeId-formula helper (kills the duplicated formula the router depends on), carry `DriverInstanceId` on the publish message, and have `DriverHostActor` build a `(DriverInstanceId, FullName) → NodeId[]` map each apply and resolve it in `ForwardToMux` before sending the existing `AttributeValueUpdate`. No `OpcUaPublishActor` change. Runtime-only — **no EF/Configuration change.**
+
+**Tech Stack:** .NET 10, Akka.NET (+ Akka.TestKit), OPC UA Foundation stack, xUnit + Shouldly. Design: `docs/plans/2026-06-13-equipment-tag-live-values-design.md` (master `7e9eb5d1`).
+
+**Branch:** `feat/equipment-tag-live-values` off master `7e9eb5d1`.
+
+---
+
+## Hard rules (every task)
+- Stage by path; never `git add .`. Never stage `sql_login.txt`, `src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/`, `pending.md`, or `current.md`.
+- Never echo the gateway API key / historian `SharedSecret` into a tracked file. No force-push, no `--no-verify`. No Configuration entity/EF migration change.
+
+## Dependency spine
+T0 → {T1, T3} (disjoint files, parallel). T2 needs T1. T4 needs T1 + T3. T5 (verify) needs all.
+
+| Task | Files | Class | Parallelizable with |
+|---|---|---|---|
+| T0 branch | git | trivial | — |
+| T1 EquipmentNodeIds helper | Commons (new) + Commons.Tests | small | T3 |
+| T2 repoint formula + parity test | Phase7Applier, VirtualTagHostActor, OpcUaServer.Tests | standard | T3 |
+| T3 AttributeValuePublished + DriverInstanceId | DriverInstanceActor | small | T1, T2 |
+| T4 DriverHostActor map + resolve + TestKit | DriverHostActor, Runtime.Tests | high-risk | — |
+| T5 verify + live /run | none | verification | — |
+
+---
+
+### Task 0: Create feature branch
+
+**Classification:** trivial
+**Estimated implement time:** ~1 min
+**Parallelizable with:** none
+
+**Files:** git only
+
+```bash
+git checkout master && git rev-parse HEAD # expect 7e9eb5d1...
+git checkout -b feat/equipment-tag-live-values
+```
+Confirm clean tree (ignore untracked `pending.md` / `current.md` / `sql_login.txt` / `pki/`).
+
+---
+
+### Task 1: Shared `EquipmentNodeIds` NodeId-formula helper
+
+**Classification:** small
+**Estimated implement time:** ~4 min
+**Parallelizable with:** Task 3
+
+**Files:**
+- Create: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs`
+- Test: `tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs`
+
+**Context:** The folder-scoped NodeId formula is currently copied in `Phase7Applier` and `VirtualTagHostActor` (each with a "MUST match" warning). This is the single source of truth both — and the new driver router — will use, guaranteeing the router's NodeId equals the materialised NodeId. Confirm the test project name first: `ls tests/Core | grep -i Commons` (it's `ZB.MOM.WW.OtOpcUa.Commons.Tests`).
+
+**Step 1: Write the failing tests**
+
+```csharp
+using Shouldly;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Commons.Tests.OpcUa;
+
+public class EquipmentNodeIdsTests
+{
+ [Fact]
+ public void Variable_with_no_folder_is_equipment_slash_name()
+ => EquipmentNodeIds.Variable("eq-1", "", "speed").ShouldBe("eq-1/speed");
+
+ [Fact]
+ public void Variable_with_null_folder_is_equipment_slash_name()
+ => EquipmentNodeIds.Variable("eq-1", null, "speed").ShouldBe("eq-1/speed");
+
+ [Fact]
+ public void Variable_with_folder_is_equipment_slash_folder_slash_name()
+ => EquipmentNodeIds.Variable("eq-1", "registers", "speed").ShouldBe("eq-1/registers/speed");
+
+ [Fact]
+ public void SubFolder_is_equipment_slash_folder()
+ => EquipmentNodeIds.SubFolder("eq-1", "registers").ShouldBe("eq-1/registers");
+}
+```
+
+**Step 2: Run — expect FAIL** (`EquipmentNodeIds` not defined).
+`dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests --filter "FullyQualifiedName~EquipmentNodeIds"`
+
+**Step 3: Implement**
+
+```csharp
+namespace ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
+
+///
+/// Single source of truth for equipment-namespace OPC UA NodeId strings. The variable NodeId is
+/// FOLDER-SCOPED ({parent}/{Name}), NOT the driver-side FullName — a driver wire ref is not
+/// unique across identical machines, so FullName-as-NodeId would collide in the sink. Used by the
+/// materialiser (Phase7Applier), the VirtualTag publish map, and the driver live-value router so all
+/// three agree on the exact NodeId a variable was placed at.
+///
+public static class EquipmentNodeIds
+{
+ /// The sub-folder NodeId under an equipment for a non-empty FolderPath: {equipmentId}/{folderPath}.
+ /// The owning equipment's NodeId.
+ /// The tag/vtag FolderPath (must be non-empty for this to be meaningful).
+ /// The sub-folder NodeId string.
+ public static string SubFolder(string equipmentId, string folderPath) => $"{equipmentId}/{folderPath}";
+
+ ///
+ /// The folder-scoped variable NodeId: {parent}/{name} where parent = equipmentId when
+ /// is null/empty, else .
+ ///
+ /// The owning equipment's NodeId.
+ /// The tag/vtag FolderPath, or null/empty for "directly under the equipment".
+ /// The tag/vtag Name (the leaf browse segment).
+ /// The folder-scoped variable NodeId string.
+ public static string Variable(string equipmentId, string? folderPath, string name)
+ {
+ var parent = string.IsNullOrWhiteSpace(folderPath) ? equipmentId : SubFolder(equipmentId, folderPath);
+ return $"{parent}/{name}";
+ }
+}
+```
+
+**Step 4: Run — expect PASS.**
+
+**Step 5: Commit**
+```bash
+git add src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/EquipmentNodeIds.cs tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/EquipmentNodeIdsTests.cs
+git commit -m "feat(commons): EquipmentNodeIds — single source of truth for folder-scoped equipment NodeIds"
+```
+
+---
+
+### Task 2: Repoint Phase7Applier + VirtualTagHostActor to `EquipmentNodeIds` (+ parity test)
+
+**Classification:** standard
+**Estimated implement time:** ~5 min
+**Parallelizable with:** Task 3
+**blockedBy:** Task 1
+
+**Files:**
+- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs` (`EquipmentSubFolderNodeId` :280; the inline `{parent}/{Name}` in `MaterialiseEquipmentTags` :189-191 and `MaterialiseEquipmentVirtualTags` :236-237)
+- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs` (`NodeIdFor` :149-153 + the `using`)
+- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/` (a small parity test asserting both produce `EquipmentNodeIds.Variable(...)`)
+
+**Context:** Replace the two copied formulas with the shared helper — **byte-identical output**. `Phase7Applier` is in OpcUaServer, `VirtualTagHostActor` in Runtime; both reference Commons. Read each before editing.
+
+**Step 1 (test first):** Add a parity/golden test (OpcUaServer.Tests) that pins the formula and proves both call sites match the helper — e.g. for an equipment tag with `FolderPath=null` the materialised NodeId equals `EquipmentNodeIds.Variable(eqId, null, name)`, and with a FolderPath it equals `EquipmentNodeIds.Variable(eqId, folder, name)`. (If an existing materialiser test already asserts concrete NodeIds like `eq-1/speed`, extend it; otherwise add one that drives `MaterialiseEquipmentTags` against a recording sink and asserts the variable NodeIds.) Run — should pass before AND after the repoint (the change is behaviour-preserving); its job is to lock the formula against future drift.
+
+**Step 2:** In `Phase7Applier`: replace `EquipmentSubFolderNodeId(eq, fp)` body (or its call sites) with `EquipmentNodeIds.SubFolder(eq, fp)`, and replace the inline `var nodeId = $"{parent}/{tag.Name}";` (tags :191 and vtags :237) with `var nodeId = EquipmentNodeIds.Variable(tag.EquipmentId, tag.FolderPath, tag.Name);` (and the vtag equivalent `v.EquipmentId, v.FolderPath, v.Name`). You may delete the private `EquipmentSubFolderNodeId` if all call sites move to the helper, or keep it as a thin forwarder — implementer's choice, but no behaviour change. Add `using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;`.
+
+**Step 3:** In `VirtualTagHostActor.NodeIdFor`, return `EquipmentNodeIds.Variable(p.EquipmentId, p.FolderPath, p.Name)`; add the `using`. Keep the "MUST match" doc-comment but update it to say the formula now lives in `EquipmentNodeIds`.
+
+**Step 4:** Build + test:
+```bash
+dotnet build src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer
+dotnet build src/Server/ZB.MOM.WW.OtOpcUa.Runtime
+dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests --filter "FullyQualifiedName~Phase7Applier"
+```
+Expect 0 errors; existing applier + vtag tests still green (byte-identical NodeIds).
+
+**Step 5: Commit** (both files + the test).
+
+---
+
+### Task 3: Carry `DriverInstanceId` on `AttributeValuePublished`
+
+**Classification:** small
+**Estimated implement time:** ~3 min
+**Parallelizable with:** Task 1, Task 2
+
+**Files:**
+- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs` (record :65; raise site :449)
+
+**Context:** The router needs to key on `(DriverInstanceId, FullName)`. The actor already holds `_driverInstanceId` (:77, set :169). Add it as the **first** parameter so the message self-identifies its source driver.
+
+**Step 1:** Change the record (`:65`):
+```csharp
+public sealed record AttributeValuePublished(string DriverInstanceId, string FullReference, object? Value, OpcUaQuality Quality, DateTime TimestampUtc);
+```
+
+**Step 2:** Update the raise site (`:449`):
+```csharp
+Context.Parent.Tell(new AttributeValuePublished(_driverInstanceId, msg.FullReference, msg.Snapshot.Value, quality, ts));
+```
+
+**Step 3:** Fix every other construction/consumer of `AttributeValuePublished` so it compiles — grep first:
+`grep -rn "AttributeValuePublished" src tests --include=*.cs | grep -v /obj/ | grep -v /bin/`
+The known consumer is `DriverHostActor.ForwardToMux` (Task 4 handles the resolution; here just make it compile — it can pass `msg` through to `_dependencyMux` unchanged). Update any test that constructs `AttributeValuePublished` to pass a `DriverInstanceId`.
+
+**Step 4:** Build Runtime: `dotnet build src/Server/ZB.MOM.WW.OtOpcUa.Runtime` → 0 errors. Run any DriverInstanceActor tests.
+
+**Step 5: Commit.**
+
+---
+
+### Task 4: `DriverHostActor` — driver-ref→NodeId map + `ForwardToMux` resolution
+
+**Classification:** high-risk
+**Estimated implement time:** ~5 min
+**Parallelizable with:** none
+**blockedBy:** Task 1, Task 3
+
+**Files:**
+- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs` (add `_nodeIdByDriverRef` field; build it in `PushDesiredSubscriptions` :558-607; resolve in `ForwardToMux` :379-392; `using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;`)
+- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/` (new Akka.TestKit test — find the existing DriverHostActor test file pattern first)
+
+**Context:** Mirror `VirtualTagHostActor`. Build the map every apply from `composition.EquipmentTags` (each carries `DriverInstanceId`, `FullName`, `EquipmentId`, `FolderPath`, `Name`) using `EquipmentNodeIds.Variable`. Value is a `List` for the 1→many case. Resolve in `ForwardToMux` and emit one `AttributeValueUpdate` per NodeId; on no-match, skip the OPC UA push (still forward to `_dependencyMux`) and debug-log.
+
+**Step 1: Write the failing tests (Akka.TestKit).** Read an existing DriverHostActor test to copy the harness (how it constructs the actor with `opcUaPublishActor` + `dependencyMux` probes and triggers an apply, or how `PushDesiredSubscriptions` is exercised). Then:
+- `Published_driver_value_routes_to_folder_scoped_nodeid`: after an apply whose composition has equipment tag `{EquipmentId="eq-1", DriverInstanceId="drv-1", FullName="40001", FolderPath=null, Name="speed"}`, send the host a `DriverInstanceActor.AttributeValuePublished("drv-1", "40001", 42.0, OpcUaQuality.Good, ts)`; the opcUaPublish probe receives `AttributeValueUpdate("eq-1/speed", 42.0, …)`.
+- `Same_ref_on_two_equipments_writes_both`: two equipment tags `(drv-1, "40001")` on `eq-1` and `eq-2`; one publish → probe receives BOTH `eq-1/speed` and `eq-2/speed`.
+- `Unmatched_ref_no_opcua_push_but_still_to_mux`: publish `(drv-1, "59999")` (not in composition) → opcUaPublish probe gets nothing (ExpectNoMsg), but the dependency-mux probe still receives the raw `AttributeValuePublished`.
+
+Run — expect FAIL.
+
+**Step 2: Implement.**
+- Field: `private readonly Dictionary<(string DriverInstanceId, string FullName), List> _nodeIdByDriverRef = new();`
+- In `PushDesiredSubscriptions`, right where it has the `composition` (alongside building `refsByDriver`), rebuild the map:
+```csharp
+_nodeIdByDriverRef.Clear();
+foreach (var t in composition.EquipmentTags)
+{
+ var key = (t.DriverInstanceId, t.FullName);
+ var nodeId = EquipmentNodeIds.Variable(t.EquipmentId, t.FolderPath, t.Name);
+ if (!_nodeIdByDriverRef.TryGetValue(key, out var list))
+ _nodeIdByDriverRef[key] = list = new List();
+ if (!list.Contains(nodeId)) list.Add(nodeId);
+}
+```
+- In `ForwardToMux`:
+```csharp
+private void ForwardToMux(DriverInstanceActor.AttributeValuePublished msg)
+{
+ _dependencyMux?.Tell(msg); // VirtualTag inputs — keyed by FullReference, unchanged
+
+ if (_opcUaPublishActor is null) return;
+ if (_nodeIdByDriverRef.TryGetValue((msg.DriverInstanceId, msg.FullReference), out var nodeIds))
+ {
+ foreach (var nodeId in nodeIds)
+ _opcUaPublishActor.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.AttributeValueUpdate(
+ nodeId, msg.Value, msg.Quality, msg.TimestampUtc));
+ }
+ else
+ {
+ _log.Debug("DriverHost {Node}: no equipment-tag NodeId for ({Driver},{Ref}) — value dropped",
+ _localNode, msg.DriverInstanceId, msg.FullReference);
+ }
+}
+```
+Update the stale `ForwardToMux` comment block (the "live values milestone… until then BadWaitingForInitialData" note) to describe the now-wired routing. Add the `using`.
+
+**Step 3:** Run the new tests — expect PASS.
+
+**Step 4:** Build + full Runtime suite:
+```bash
+dotnet build src/Server/ZB.MOM.WW.OtOpcUa.Runtime
+dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests --filter "FullyQualifiedName~DriverHost"
+```
+
+**Step 5: Commit** (DriverHostActor + the TestKit test).
+
+---
+
+### Task 5: Verify — build, test, agent-driven live `/run`
+
+**Classification:** verification
+**Estimated implement time:** ~5 min + live
+**Parallelizable with:** none
+**blockedBy:** all
+
+**Files:** none
+
+**Step 1:** Full solution build + the touched suites:
+```bash
+dotnet build ZB.MOM.WW.OtOpcUa.slnx
+dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests
+dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests
+dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests
+```
+Expect 0 errors; all green.
+
+**Step 2: Live docker-dev `/run` (agent drives; dev login DISABLED — no sign-in).** The routing is driver-agnostic; **Modbus** is the cleanest live proof (live sim at `10.100.0.35:5020`; Galaxy would need a real mxaccessgw).
+- Rebuild central nodes on this branch: `docker compose -f docker-dev/docker-compose.yml up -d --build migrator central-1 central-2` (no new migration here, but the rebuild ships the branch code).
+- In the AdminUI (`http://localhost:9200`): create a `ModbusTcp` driver under the `nw-uns` Equipment namespace (endpoint `10.100.0.35:5020`), author an equipment tag on an equipment bound to it (a valid Modbus FullName/register), and **deploy** (`POST http://localhost:9200/api/deployments`, header `X-Api-Key: docker-dev-deploy-key`, or the UI deploy).
+- Confirm the tag's OPC UA variable shows a **live value** (not `BadWaitingForInitialData`): `dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- read -u opc.tcp://localhost:4840 -n "ns=2;s=/"` (or `subscribe`). Browse first (`-r -d 4`) to find the NodeId.
+- Sanity: the central logs show no `WriteValue threw` / no-match storms.
+
+**Step 3:** On green, finish via `superpowers-extended-cc:finishing-a-development-branch` (intent: merge-to-master + push, confirm).
+
+---
+
+## Out of scope
+Native `IAlarmSource` alarms on the equipment path (Phase B) and the server-side `HistoryRead` backend (Phase C) — separate milestones in the design doc.
diff --git a/docs/plans/2026-06-13-equipment-tag-live-values-plan.md.tasks.json b/docs/plans/2026-06-13-equipment-tag-live-values-plan.md.tasks.json
new file mode 100644
index 00000000..0603bc38
--- /dev/null
+++ b/docs/plans/2026-06-13-equipment-tag-live-values-plan.md.tasks.json
@@ -0,0 +1,12 @@
+{
+ "planPath": "docs/plans/2026-06-13-equipment-tag-live-values-plan.md",
+ "tasks": [
+ {"id": 332, "subject": "Task 0: Create feature branch", "status": "pending"},
+ {"id": 333, "subject": "Task 1: EquipmentNodeIds shared helper", "status": "pending", "blockedBy": [332]},
+ {"id": 334, "subject": "Task 2: Repoint Phase7Applier + VirtualTagHostActor to EquipmentNodeIds", "status": "pending", "blockedBy": [333]},
+ {"id": 335, "subject": "Task 3: AttributeValuePublished gains DriverInstanceId", "status": "pending", "blockedBy": [332]},
+ {"id": 336, "subject": "Task 4: DriverHostActor map + ForwardToMux resolution", "status": "pending", "blockedBy": [333, 335]},
+ {"id": 337, "subject": "Task 5: Verify — build, test, live /run", "status": "pending", "blockedBy": [334, 336]}
+ ],
+ "lastUpdated": "2026-06-13"
+}