diff --git a/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json b/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json
index 741e7914..0758de6c 100644
--- a/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json
+++ b/docs/plans/2026-07-15-universal-discovery-browser-implementation.md.tasks.json
@@ -1,12 +1,12 @@
{
"planPath": "docs/plans/2026-07-15-universal-discovery-browser-implementation.md",
"tasks": [
- {"id": 0, "subject": "Task 0: Branch setup (feat/universal-discovery-browser)", "status": "pending"},
- {"id": 1, "subject": "Task 1: ITagDiscovery.SupportsOnlineDiscovery default member", "status": "pending", "blockedBy": [0], "parallelWith": [2, 13]},
- {"id": 2, "subject": "Task 2: Commons project references (Core.Abstractions)", "status": "pending", "blockedBy": [0], "parallelWith": [1, 13]},
- {"id": 3, "subject": "Task 3: AbCip opt-in SupportsOnlineDiscovery=true", "status": "pending", "blockedBy": [1], "parallelWith": [4, 5]},
- {"id": 4, "subject": "Task 4: TwinCAT opt-in SupportsOnlineDiscovery=true", "status": "pending", "blockedBy": [1], "parallelWith": [3, 5]},
- {"id": 5, "subject": "Task 5: FOCAS opt-in SupportsOnlineDiscovery=true", "status": "pending", "blockedBy": [1], "parallelWith": [3, 4]},
+ {"id": 0, "subject": "Task 0: Branch setup (feat/universal-discovery-browser)", "status": "completed"},
+ {"id": 1, "subject": "Task 1: ITagDiscovery.SupportsOnlineDiscovery default member", "status": "completed", "blockedBy": [0], "parallelWith": [2, 13]},
+ {"id": 2, "subject": "Task 2: Commons project references (Core.Abstractions)", "status": "completed", "blockedBy": [0], "parallelWith": [1, 13]},
+ {"id": 3, "subject": "Task 3: AbCip opt-in SupportsOnlineDiscovery=true", "status": "completed", "blockedBy": [1], "parallelWith": [4, 5]},
+ {"id": 4, "subject": "Task 4: TwinCAT opt-in SupportsOnlineDiscovery=true", "status": "completed", "blockedBy": [1], "parallelWith": [3, 5]},
+ {"id": 5, "subject": "Task 5: FOCAS opt-in SupportsOnlineDiscovery=true", "status": "completed", "blockedBy": [1], "parallelWith": [3, 4]},
{"id": 6, "subject": "Task 6: CapturingAddressSpaceBuilder + captured-tree model", "status": "pending", "blockedBy": [1, 2]},
{"id": 7, "subject": "Task 7: CapturedTreeBrowseSession", "status": "pending", "blockedBy": [6]},
{"id": 8, "subject": "Task 8: IUniversalDriverBrowser + DiscoveryDriverBrowser core open path", "status": "pending", "blockedBy": [7]},
@@ -14,7 +14,7 @@
{"id": 10, "subject": "Task 10: Capture coalescing + MaxConcurrentCaptures cap", "status": "pending", "blockedBy": [9], "parallelWith": [12]},
{"id": 11, "subject": "Task 11: BrowserSessionService universal fallback + CanBrowse", "status": "pending", "blockedBy": [8], "parallelWith": [9]},
{"id": 12, "subject": "Task 12: DI registration in AddAdminUI", "status": "pending", "blockedBy": [11], "parallelWith": [10]},
- {"id": 13, "subject": "Task 13: TagModal -> editor parameter plumbing (DriverType + GetDriverConfigJson)", "status": "pending", "blockedBy": [0], "parallelWith": [1, 2]},
+ {"id": 13, "subject": "Task 13: TagModal -> editor parameter plumbing (DriverType + GetDriverConfigJson)", "status": "completed", "blockedBy": [0], "parallelWith": [1, 2]},
{"id": 14, "subject": "Task 14: AbCip picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 3], "parallelWith": [15, 16]},
{"id": 15, "subject": "Task 15: TwinCAT picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 4], "parallelWith": [14, 16]},
{"id": 16, "subject": "Task 16: FOCAS picker body universal browse mode", "status": "pending", "blockedBy": [10, 12, 13, 5], "parallelWith": [14, 15]},
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
index ca7e045f..8b2cd78f 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs
@@ -999,6 +999,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
///
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
+ /// Controller enumeration is real (CIP Symbol Object walk) — config-gated on
+ /// EnableControllerBrowse, which the universal browser's PatchForBrowse guarantees at open time.
+ public bool SupportsOnlineDiscovery => true;
+
///
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
index 9fca75f6..9df8851c 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs
@@ -427,6 +427,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// node set is non-empty and stable.
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable;
+ /// The FixedTree device folder is real enumeration — config-gated on FixedTree.Enabled,
+ /// which the universal browser's PatchForBrowse turns on. FOCAS is the UntilStable driver: the
+ /// FixedTree cache fills a couple of seconds post-connect, so the browser's settle loop re-captures
+ /// until the node set is non-empty and stable.
+ public bool SupportsOnlineDiscovery => true;
+
///
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
index 7420e6a7..1460d0dc 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs
@@ -380,6 +380,10 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery
// discovery pass is sufficient.
public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once;
+ /// ADS symbol upload is real device enumeration — config-gated on
+ /// EnableControllerBrowse, which the universal browser's PatchForBrowse guarantees at open time.
+ public bool SupportsOnlineDiscovery => true;
+
///
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs
new file mode 100644
index 00000000..e832f236
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDiscoveryGateTests.cs
@@ -0,0 +1,19 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class AbCipDiscoveryGateTests
+{
+ /// AbCip opts into the universal discovery browser — its CIP Symbol Object walk is real
+ /// device enumeration (config-gated on EnableControllerBrowse, guaranteed by PatchForBrowse at open).
+ [Fact]
+ public void SupportsOnlineDiscovery_IsTrue()
+ {
+ var drv = new AbCipDriver(new AbCipDriverOptions(), "drv-1");
+ ((ITagDiscovery)drv).SupportsOnlineDiscovery.ShouldBeTrue();
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs
new file mode 100644
index 00000000..a920d596
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests/FocasDiscoveryGateTests.cs
@@ -0,0 +1,20 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class FocasDiscoveryGateTests
+{
+ /// FOCAS opts into the universal discovery browser — its FixedTree device folder is real
+ /// enumeration (config-gated on FixedTree.Enabled, turned on by PatchForBrowse). FOCAS is the
+ /// UntilStable driver: the browser's settle loop re-captures until the FixedTree cache fills.
+ [Fact]
+ public void SupportsOnlineDiscovery_IsTrue()
+ {
+ var drv = new FocasDriver(new FocasDriverOptions(), "drv-1");
+ ((ITagDiscovery)drv).SupportsOnlineDiscovery.ShouldBeTrue();
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs
new file mode 100644
index 00000000..97da1f2c
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests/TwinCATDiscoveryGateTests.cs
@@ -0,0 +1,19 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class TwinCATDiscoveryGateTests
+{
+ /// TwinCAT opts into the universal discovery browser — its ADS symbol upload is real
+ /// device enumeration (config-gated on EnableControllerBrowse, guaranteed by PatchForBrowse at open).
+ [Fact]
+ public void SupportsOnlineDiscovery_IsTrue()
+ {
+ var drv = new TwinCATDriver(new TwinCATDriverOptions(), "drv-1");
+ ((ITagDiscovery)drv).SupportsOnlineDiscovery.ShouldBeTrue();
+ }
+}