# MTConnect write-back Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development > (opus implementers per user instruction). **Goal:** Implement `IWritable` on `MTConnectDriver` via the Agent's HTTP PUT/POST surface (opt-in at agent `AllowPut` AND driver `write.enabled`), and reconcile the stale MQTT write-through/browse-gap records. **Architecture:** See `docs/plans/2026-08-05-mtconnect-writeback-design.md` (in this branch). Write = observation injection into the agent buffer, never machine command. Batch-safe `WriteAsync` mirroring `MqttDriver.cs:670-745`; new seam method on `IMTConnectAgentClient`; discovery stamps `Operate` only for leaves the driver would accept. **Tech Stack:** .NET 10, xUnit v3 + Shouldly, existing `CannedAgentClient` / stub `HttpMessageHandler` seams. No new packages. **Worktree:** `/Users/dohertj2/Desktop/OtOpcUa/.claude/worktrees/mqtt-mtconnect-write` (branch `feature/mqtt-mtconnect-write`). All paths below relative to it. --- ### Task 0: Write options block (config + parse) **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** Task 1 **Files:** - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectDriverOptions.cs` - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs` (config DTO region ~:3257-3345 + `ParseOptions` ~:2222) - Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectDriverOptionsTests.cs`, `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectFactoryTests.cs` TDD: failing test first — `{"write":{"enabled":true}}` parses to `Options.Write.Enabled == true`; absent block defaults to `false`; malformed value behaves like the sibling `Probe`/`Reconnect` blocks (match their existing tolerance/failure semantics exactly — read them first). Add `MTConnectWriteOptions` (sealed record, `Enabled`, default false) on `MTConnectDriverOptions` following the `Probe`/`Reconnect` pattern, a nullable `WriteDto` on the config DTO, wire through `ParseOptions`. Commit `feat(mtconnect): parse the write.enabled config block`. ### Task 1: Agent-client write seam **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 0 **Files:** - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/IMTConnectAgentClient.cs` - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectAgentClient.cs` - Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectAgentClientWriteTests.cs` (new) Add to the seam: ```csharp Task WriteObservationAsync( string deviceName, string dataItemId, string value, CancellationToken cancellationToken); ``` with `MTConnectWriteOutcome` a small readonly record: `Accepted` bool + `AgentError` string? (agent-produced MTConnectError text) — transport failures THROW (HttpRequestException etc.), the driver maps them; agent refusals return `Accepted=false` with the text. Implementation in `MTConnectAgentClient` over `_http` (the bounded-timeout unary client): `HttpMethod.Post` to `{base}/{deviceName}?{Uri.EscapeDataString(dataItemId)}={Uri.EscapeDataString(value)}` built with the same left-part logic as `BuildRequestUri` (:382-403). A 2xx whose body parses as an `MTConnectError` document is a refusal (reuse the LocalName-only XML helpers in `MTConnectXml.cs`); non-2xx is a refusal carrying the status; empty 2xx is accepted. TDD via the internal `HttpMessageHandler` ctor seam (`MTConnectAgentClient.cs:107`), the same pattern `MTConnectDriverProbeTests.cs:22-28` uses: assert verb, exact URI (device path + escaped query), accepted on empty 200, refusal on MTConnectError-in-200 (agent text surfaced), refusal on 404, throw on handler-thrown transport error. Commit `feat(mtconnect): agent-client observation write (HTTP POST, AllowPut surface)`. ### Task 2: `MTConnectDriver` implements `IWritable` + discovery stamping **Classification:** high-risk **Estimated implement time:** ~5 min **Parallelizable with:** none **Files:** - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriver.cs` (class decl :63, remarks :46-53, WriteAsync new, `TagBinding` device-name map ~:3170-3213, `DiscoverAsync` stamping :1030-1046) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectDriverFactoryExtensions.cs` (remark :33-39) - Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Contracts/MTConnectTagDefinition.cs` (remark :10-14) - Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/CannedAgentClient.cs` (record writes, scripted outcomes) - Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests/MTConnectWriteTests.cs` (new), `MTConnectDiscoverTests.cs` (stamping cases) Per design §2.3. Status-code constants: follow MQTT's named-const pattern (`MqttDriver.cs:76-89`) — `StatusBadNodeIdUnknown 0x80340000`, `StatusBadNotWritable 0x803B0000`, `StatusBadTypeMismatch 0x80740000`, `StatusBadCommunicationError 0x80050000`, plus `BadNotConnected` matching `ReadAsync`'s existing constant. Refusal order per slot: disabled → BadNotWritable; no client → BadNotConnected; unresolved RawPath → BadNodeIdUnknown; CONDITION category → BadNotWritable; null/unrepresentable value → BadTypeMismatch; then the seam call — `Accepted=false` or thrown transport error → BadCommunicationError (log agent text at Warning, never into `LastError`). `OperationCanceledException` fills remaining slots with BadCommunicationError and returns. Never throw. Success = `WriteResult(0u)`. Device-name resolution: extend `TagBinding` (or a parallel FrozenDictionary built in `BuildBinding` ~:2887-2980) with `dataItemId → owning deviceName` from the probe model; fall back to `options.DeviceName`; unresolvable device → BadNodeIdUnknown. Values format via `Convert.ToString(value, CultureInfo.InvariantCulture)`. Discovery: `CanWrite(dataItem)` = `options.Write.Enabled && !IsCondition(category)` → `SecurityClassification.Operate` else `ViewOnly`; update the :1040-1042 comment. **Verify** how materialization derives `CurrentWrite` (search for how MQTT's Operate-stamped nodes get the write bit — SecurityClass + capability) and state the finding in the commit message; if a server-side gate keys on the driver capability alone, note that existing ViewOnly-committed raw tags keep their authored SecurityClass. TDD: every refusal leg observed red first where feasible; the batch-shape test (one bad slot doesn't fail the batch) mirrors `MqttDriverWriteTests.cs:74`. The publish itself is proven by Task 3 live legs, not by asserting a fake was called beyond the seam-call recording (same philosophy as `MqttDriverWriteTests.cs:11-15`). Commit `feat(mtconnect): IWritable via agent observation writes (#468-era deferral closed)`. ### Task 3: Integration fixture + write legs **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 4 **Files:** - Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/Docker/agent.cfg` (`AllowPut = true` + comment rewrite) - Modify: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs` (remove the "(b) Writes" not-covered remark :64-65; add write legs) Legs (env-gated by the existing `MTCONNECT_AGENT_ENDPOINT` fixture, `Assert.Skip` clean): 1. Driver with `write.enabled` writes a distinct value to a writable EVENT data item (pick one from `Devices.xml` the SHDR adapter does not continuously overwrite — read `adapter.py` to choose; if every item is pumped, add a static EVENT item to `Devices.xml`) → poll `/current` until the observation carries the value (bounded wait, `StreamWait` pattern). 2. A write naming a nonexistent dataItemId → `BadCommunicationError` (agent refusal mapping). 3. `write.enabled` absent → same write returns `BadNotWritable` without touching the agent. Commit `test(mtconnect): live write legs against the AllowPut fixture`. ### Task 4: Docs reconciliation (MTConnect + stale MQTT records) **Classification:** small **Estimated implement time:** ~5 min **Parallelizable with:** Task 3 **Files:** - Modify: `docs/drivers/MTConnect.md` (capability :71-77, Deferred :295-301, new §Write-back) - Modify: `docs/plans/2026-07-15-mtconnect-driver-design.md` (§3.6 superseded note) - Modify: `docs/drivers/Mqtt.md` (delete stale :550-556 and :586-587) - Modify: `docs/plans/2026-07-24-driver-expansion-tracking.md` (P2 gap bullets :239-263 → closed #509/#510/#513/#514; §Next actions :288-289 rewritten) - Modify: `deferment.md` (:126 MTConnect row, :132-133 four-driver IWritable line → Sql + Calculation only) Per design §1 + §3. The Write-back section MUST carry the §2.2 semantics warning (observation injection, adapter overwrites, two-layer opt-in, `AllowPutFrom` pointer). Commit `docs: MTConnect write-back recorded; stale MQTT write-through/browse-gap records reconciled`. ### Task 5: Full-suite verification **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** none `dotnet build ZB.MOM.WW.OtOpcUa.slnx` (0 errors) + full MTConnect unit suite + AdminUI suite + `dotnet test --filter "FullyQualifiedName~Mqtt"` (docs-only but cheap insurance). Integration suite compiles; live legs skip offline. Fix fallout, commit if changes. ### Task 6: Live gate (main session, headless — NO chrome) **Classification:** standard **Estimated implement time:** ~10 min (main session, after merge) Main session, not a subagent: bring up the MTConnect fixture (`ssh dohertj2@10.100.0.35` compose in `/opt/otopcua-mtconnect/` — `lmxopcua-fix sync` equivalent via rsync first, since agent.cfg changed), run the Task 3 legs live (`dotnet test --filter` the new write tests with `MTCONNECT_AGENT_ENDPOINT` set), then the docker-dev rig leg: author + deploy an MTConnect driver with `write.enabled`, `Client.CLI write` a value, read it back via Client.CLI and `/current`. Record results in the design doc §Outcome.