# Follow-up Closeout Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development
> to implement this plan task-by-task in this session.

**Goal:** Close the follow-ups recorded in `docs/plans/2026-08-17-deferred-closeout.md`
(as-built notes, "Follow-ups recorded, not started"): the feed-level alarm-truncation
signal on `StreamAlarms`/`AlarmFeedMessage`, the `ShowTagValues` redaction gaps
(alarms hub + `/browse` live values — TST-16 residual), the codegen guard's
one-directional Rust check (the recorded "no guard" note is stale — Check 3 exists),
the dashboard settings page's missing `GroupToTag`/`UntaggedSessionVisibility` rows,
the ApiKeysPage `DashboardTags` display/input gaps, and a bounded retry of the wnwrap
alarm probes using the secured-write verb the first attempt didn't use.

**Architecture:** Same two-phase posture as the prior three plans. Gateway-side work
builds and tests on macOS via `NonWindows.slnx`; windev (`ssh windev`, PowerShell, CI
clone `C:\build\mxaccessgw-ci`) runs the probe retry and final full-matrix verification.
**This plan touches `.proto` contracts** (Task 1) — contracts regeneration and the
five-client rollout (Task 2) follow the exact pattern Task 9 of the previous plan used.
The gateway-side truncation edge needs **no worker change**: `GatewayAlarmMonitor`
already receives `snapshot_truncated` on every reconcile; the feed frame is raised
gateway-side (per-reconcile fidelity, matching the operator-facing caveat's semantics).

**Tech stack:** .NET 10 gateway / protobuf contracts / five language clients / Blazor
Server dashboard / PowerShell codegen scripts / windev live rig.

**Branch:** `feat/followup-closeout` off `main` (`ab3ff16`).

---

## Ground rules for every implementer subagent

- NEVER run `git stash`, `git reset`, `git clean`, or `git checkout <sha/branch>`.
  Commit with explicit **pathspecs on the commit itself**:
  `git commit -m "..." -- <paths>` — never `git add -A`, `git commit -a`, or a bare
  `git commit` after add (a concurrent task's staged files would be swept in).
- Build/test mutual exclusion: before any `dotnet build`/`dotnet test`, acquire the lock via
  `mkdir /private/tmp/claude-501/-Users-dohertj2-Desktop-MxAccessGateway/f36938ae-bbca-4245-b5c9-fac512d69e22/scratchpad/buildlock`
  (retry with backoff while it fails); `rmdir` it on ALL exit paths, including failures.
- `TreatWarningsAsErrors=true`, `Nullable=enable` — new warnings break the build; fix, don't suppress.
- Follow `docs/style-guides/CSharpStyleGuide.md`: file-scoped namespaces, `sealed` by
  default, `Async` suffix, MXAccess-aligned names.
- Update affected docs in the same commit as the source change.
- MXAccess parity is the contract; never synthesize events. The feed truncation frame is a
  gateway-status frame (like `provider_status`), not a synthesized MXAccess event.
- Never log secrets, API keys, credentials, or tag values.
- The `Files:` block is the scope contract. If the task can't be done inside it, that's a
  plan defect — surface it, don't silently expand scope.
- On macOS build `NonWindows.slnx`; the x86 Worker and full `slnx` only build on windev.
- `git commit` hitting `index.lock` contention → wait 5 s and retry.

---

## Task 1: Feed-level alarm-truncation signal (proto + gateway)

**Classification:** high-risk
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 3, Task 4, Task 5, Task 6

**Files:**
- Modify: `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto`
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Alarms/GatewayAlarmMonitor.cs`
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Alarms/IGatewayAlarmService.cs` (doc comment only)
- Test: `src/ZB.MOM.WW.MxGateway.Tests/Gateway/Alarms/` (existing `GatewayAlarmMonitor` test class(es))
- Docs: `gateway.md`, `docs/Grpc.md`, `docs/DesignDecisions.md`
- Build output: `src/ZB.MOM.WW.MxGateway.Contracts/Generated/` (regenerated, committed)

Do NOT touch `docs/GatewayConfiguration.md` or `docs/GatewayDashboardDesign.md` (owned
by Task 3 this wave) or any `clients/**` path (Task 2).

**Spec.**

Proto (`mxaccess_gateway.proto`) — additive only:

1. New message next to `AlarmProviderStatus` (`:1025-1030`):

```proto
// Feed-level snapshot-completeness status. Emitted once on StreamAlarms open
// (after the initial provider_status frame, before the cached active_alarm frames)
// so late joiners learn the current verdict, and again on every change of the
// truncation verdict observed at reconcile. Mirrors the per-record
// ActiveAlarmSnapshot.from_truncated_snapshot caveat at feed level so live
// consumers (lmxopcua, ScadaBridge) can reason about completeness without
// polling QueryActiveAlarms. Additive in v1; absent frames mean "not truncated"
// only for streams opened against gateways that emit the frame at open.
message AlarmSnapshotStatus {
  // True while the monitor's cached active-alarm set derives from a truncated
  // (capped) worker fetch — the set may be missing alarms. Distinct from
  // provider degradation (AlarmProviderStatus.degraded).
  bool truncated = 1;
}
```

2. New oneof case in `AlarmFeedMessage` (`:1006-1023`, next free field 5):
   `AlarmSnapshotStatus snapshot_status = 5;`

Gateway (`GatewayAlarmMonitor.cs`):

- `ApplyReconcile` (`:632-694`) currently records `_snapshotTruncated = snapshotTruncated`
  at `:691` with a comment saying truncation "needs no special handling here". Change:
  detect the edge (old value ≠ new value) and, when it changes, broadcast
  `new AlarmFeedMessage { SnapshotStatus = new AlarmSnapshotStatus { Truncated = snapshotTruncated } }`
  via `BroadcastToAll` (`:720-732` — the provider-status precedent at `:531`). Match the
  existing locking discipline exactly: compute/record under `_sync` the way
  `ApplyProviderModeChangeAsync` (`:513-566`) does, broadcast the way it does. Update the
  now-stale comment at `:627-631`.
- Late-joiner priming in `StreamAsync` (`:748-800`): after the `provider_status` frame
  (`:782`) and before the cached `active_alarm` frames (`:786`), yield one
  `snapshot_status` frame carrying the current `_snapshotTruncated`, read under the same
  lock that snapshots `_alarms` (`:763-780`) so the flag and the alarm set are a
  consistent pair.
- `ClearCache` (`:742`) resets `_snapshotTruncated` to false — decide and document
  whether the reset emits a frame (it should: a monitor restart that drops a truncated
  verdict is a completeness change subscribers must see; emit via the same edge path).
- `MxAccessGatewayService.StreamAlarms` is a pass-through — no change.
- `IGatewayAlarmService.SnapshotTruncated` doc comment (`:41-59`): note the new
  feed-level frame so the two surfaces cross-reference.

Tests (extend the existing GatewayAlarmMonitor test class(es), matching their fake/
reconcile-driving idiom):
1. Reconcile flipping truncated false→true emits exactly one `snapshot_status` frame
   (truncated=true) to an attached subscriber; a second identical reconcile emits none.
2. true→false emits the clearing frame.
3. A subscriber attaching while truncated=true receives, in order: `provider_status`,
   `snapshot_status(truncated=true)`, cached `active_alarm`s, `snapshot_complete`.
4. A subscriber attaching while truncated=false receives `snapshot_status(truncated=false)`
   at open (the baseline frame is unconditional).

Docs, same commit:
- `gateway.md` §"truncated-snapshot visibility" (`:243-262`): add the feed-level frame,
  emission rules (open + edges), and ordering.
- `docs/Grpc.md` `:105-120` (provider_status oneof case rules): add the fifth case with
  its emission rules; `:95-99` StreamAlarms handler contract.
- `docs/DesignDecisions.md` `:206-230` ("truncation is reported per record…", dated
  2026-08-17): extend the entry — per-record stays for `QueryActiveAlarms` (bare stream,
  no envelope); the live feed now carries the set-level signal as a status frame.

**Step 1:** Write the four monitor tests first (they fail: no `SnapshotStatus` case).
Building the Tests project will fail to compile until the proto field exists — so add
the proto change, rebuild Contracts (regenerates `Generated/`), then confirm the tests
fail for behavioral reasons (no frame emitted), not compile errors.
**Step 2:** Implement the monitor changes; run
`dotnet test src/ZB.MOM.WW.MxGateway.Tests/... --filter "FullyQualifiedName~GatewayAlarmMonitor"` → all pass.
**Step 3:** Full macOS build (`NonWindows.slnx`) 0W/0E + run the alarm-area test filter.
**Step 4:** Update the three docs.
**Step 5:** Commit with pathspecs (proto, Generated, monitor, interface, tests, 3 docs).

---

## Task 2: Five-client rollout of `snapshot_status`

**Classification:** standard
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 7 (windev-only; disjoint files)
**Blocked by:** Task 1, Task 3, Task 4

**Files:**
- Modify: `clients/rust/protos/mxaccess_gateway.proto` (byte-identical refresh from Contracts)
- Regenerate: `clients/proto/descriptors/mxaccessgw-client-v1.protoset`
  (via `scripts/publish-client-proto-inputs.ps1`), Go + Python generated bindings
  (per-client `generate-proto.ps1`)
- Modify: `clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs:1552-1565`
  (feed renderer — add BOTH the missing `ProviderStatus` case and the new `SnapshotStatus` case)
- Modify: `clients/go/cmd/mxgw-go/main.go:1101-1114` (`formatAlarmFeedMessage` — same two cases)
- Modify: `clients/rust/crates/mxgw-cli/src/main.rs:2233-2264` and `:2266+`
  (`alarm_feed_message_summary` / `alarm_feed_message_to_json` — add `SnapshotStatus` arm)
- Modify: READMEs — `clients/dotnet/README.md` (~`:152-158`), `clients/python/README.md`
  (~`:118-123`), `clients/rust/README.md` (~`:126`), `clients/go/README.md` (~`:148-154`),
  `clients/java/README.md` (~`:120-126`): one paragraph each on the feed-level frame
- Modify: `docs/GatewayDashboardDesign.md:276` (AlarmsHub payload-case row: add the new case)

**Spec.** Follow the exact rollout Task 9 of the prior plan used (recorded at
`docs/plans/2026-08-17-deferred-closeout.md:300-318`): regenerate everything from
Contracts, refresh the Rust vendored proto byte-identically, then verify with
`pwsh scripts/check-codegen.ps1` (all four checks green — note Task 4 may have added a
reverse-direction sweep to Check 3 by the time this runs; it must pass too).
Python/Java CLIs render generic protobuf-JSON — README paragraph only, no code.
Java's `build/resources/**` proto copies are untracked build output — do not commit.

**Steps:** regenerate → renderer cases → build/test each touched client per the
CLAUDE.md verification matrix (dotnet client slnx + tests; `gofmt`+`go build`+`go test`;
`cargo fmt`+`check`+`test`+`clippy -D warnings`; python `pytest`; java `gradle test`) →
`pwsh scripts/check-codegen.ps1` green → READMEs + dashboard-design row → commit with pathspecs.

---

## Task 3: Complete `ShowTagValues` coverage — alarms hub + `/browse` (TST-16 residual)

**Classification:** high-risk (security posture)
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 1, Task 4, Task 5, Task 6

**Files:**
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/AlarmsHubPublisher.cs`
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardLiveDataService.cs`
- Modify (if value formatting sits there): `src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardTagValue.cs`
- Test: `src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/AlarmsHubPublisherTests.cs` (new)
- Test: existing `DashboardLiveDataService` test class (extend), else new file alongside
- Docs: `docs/GatewayConfiguration.md:195` (the residual sentence),
  `docs/GatewayDashboardDesign.md` (AlarmsHub row `:276` — redaction note; do NOT edit the
  payload-case list, Task 2 owns that), `archreview/remediation/00-tracking.md:231`
  (TST-16 residual row), `archreview/2026-07-12/60-testing-docs-gaps.md` (residual lines)

Do NOT touch `gateway.md` (owned by Task 1 this wave) or `SettingsPage.razor`/
`EffectiveDashboardConfiguration` (Task 5).

**Spec.**

Seam A — `AlarmsHubPublisher` (`:14-68`): today it forwards the raw `AlarmFeedMessage`
verbatim (`:40-43`), leaking `current_value`/`limit_value` from BOTH the `transition`
arm (proto `:882`, `:886`) and the `active_alarm` snapshot arm (proto `:931-932`) to any
`/hubs/alarms` browser client. Inject the dashboard options (mirror how
`DashboardEventBroadcaster.cs:53` captures `_showTagValues`); when `ShowTagValues` is
false, deep-`Clone()` the message and clear the four value fields before `SendAsync`.
**Never mutate the source message** — it is fanned out to gRPC `StreamAlarms` subscribers
and the AlarmsPage status loop (`DashboardEventBroadcaster.RedactValues` at `:247-259`
is the exact pattern, including the clone-only rule). When `ShowTagValues` is true,
forward as today. Frames with no value fields (`snapshot_complete`, `provider_status`,
and Task 1's `snapshot_status` if already merged — handle via default: clone only when
the arm carries values) pass through untouched.

Seam B — `/browse` live values: `BrowsePage.razor:133,:144,:147` renders
`value?.ValueText` unconditionally, fed by `DashboardLiveDataService.cs:99-101` →
`DashboardTagValue.ValueText` (`DashboardTagValue.cs:41-45`,
`DashboardMxValueFormatter.FormatValue`). Redact **at the service boundary** (per the
original TST-16 recommendation, `archreview/remediation/60-testing-docs-gaps.md:346-352`):
when `ShowTagValues` is false, `DashboardLiveDataService` produces `ValueText` as the
literal `"[redacted]"` instead of the formatted value; quality/timestamp columns stay.
No `BrowsePage.razor` change should be needed — if one turns out to be, that's a plan
defect to surface. Keep the redaction decision in one place; don't duplicate the check
in the page.

Tests:
- `AlarmsHubPublisherTests` (new): reuse the `CapturingHubContext` shape from
  `DashboardEventBroadcasterTests.cs:23` and the `DashboardSnapshotPublisherTests`
  BackgroundService-driving template (fake stream service, internal ctor if needed).
  Cases: ShowTagValues=false redacts both arms' value fields but keeps metadata;
  source message not mutated; ShowTagValues=true passes values through;
  valueless frames forwarded intact.
- `DashboardLiveDataService`: ShowTagValues=false → `ValueText == "[redacted]"`;
  true → formatted value.

Docs: rewrite `docs/GatewayConfiguration.md:195` — the flag now covers the events hub
mirror, the alarms hub, and `/browse`; state the "[redacted]" rendering. Mark the TST-16
residual rows closed in both archreview files (match their existing status wording).

**Steps:** tests first (fail) → implement both seams → targeted filters
(`~AlarmsHubPublisher`, `~DashboardLiveDataService`, plus `~DashboardEventBroadcaster`
regression) → macOS build 0W/0E → docs → commit with pathspecs.

---

## Task 4: Codegen guard — reverse-direction Check 3 + stale-note correction

**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 1, Task 3, Task 5, Task 6

**Files:**
- Modify: `scripts/check-codegen.ps1:75-95` (Check 3)
- Docs: `docs/GatewayTesting.md:687-698` (Check 3 description),
  `docs/ClientPackaging.md:199-213` (one-line note), `clients/rust/README.md:25-38`
  (refresh-rule note if wording changes)

**Spec.** The recorded follow-up ("no guard keeps `clients/rust/protos/` in sync") is
stale — Check 3 (`:75-95`) already SHA-256-compares vendored↔canonical and runs in CI
(`.gitea/workflows/ci.yml:76-78`). The real gap: `:79` iterates only the **vendored**
dir, so a newly added canonical proto with no vendored copy passes silently and would
break a standalone crate build later. Add the reverse sweep: iterate
`$canonicalProtoDir`; any canonical `*.proto` with no same-named vendored counterpart →
append to `$failures` with a copy-to instruction, same reporting style as `:81-89`.
Keep all-checks-always-run behavior. Do NOT edit the closeout plan's follow-ups block —
Task 9 owns that file.

**Steps:** edit script → verify locally: `pwsh scripts/check-codegen.ps1` runs Check 3
both directions green (macOS pwsh is fine for Check 3/hashing; if Checks 2/4 can't run
locally, run Check 3's logic standalone and say so) → prove the new failure mode by
temporarily copying a scratch proto into the canonical dir under the scratchpad — NOT
into the repo — or by dry-running the loop against a temp dir pair → docs → commit with
pathspecs.

---

## Task 5: Settings page — show `GroupToTag` and `UntaggedSessionVisibility`

**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 1, Task 3, Task 4, Task 6

**Files:**
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Configuration/EffectiveDashboardConfiguration.cs:3-10`
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Configuration/GatewayConfigurationProvider.cs:58-65`
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/SettingsPage.razor`
- Test: `src/ZB.MOM.WW.MxGateway.Tests/Configuration/GatewayConfigurationProviderTests.cs` (new)
- Test: page-render test alongside the existing dashboard render tests
  (`AlarmsPageTruncationBannerTests.cs` is the template)

**Spec.** Add `GroupToTag` (`IReadOnlyDictionary<string, string[]>` or matching shape)
and `UntaggedSessionVisibility` to the `EffectiveDashboardConfiguration` record; populate
in `GatewayConfigurationProvider` (`:58-65`) from `DashboardOptions.GroupToTag` (`:78`)
and `.UntaggedSessionVisibility` (`:85`). Neither is a secret — no masking. Rendering:
`GroupToTag` row next to "Dashboard role mapping" (`SettingsPage.razor:50-67`), same
`<ul><li><code>group</code> → tag1, tag2</li></ul>` idiom with the `(none configured)`
empty case; `UntaggedSessionVisibility` as a scalar row in the Dashboard cluster
(`:77-80`). Tests: provider projection test (new ground — assert both members copied,
including the empty-dictionary case); render test asserting a configured mapping and the
visibility value appear in the emitted markup (HtmlRenderer idiom, no bUnit).

**Steps:** tests first → implement → `--filter` the two new test classes → macOS build
0W/0E → commit with pathspecs. (No doc change: `docs/GatewayConfiguration.md` already
documents both options; the settings page is self-describing.)

---

## Task 6: ApiKeysPage — `DashboardTags` in ConstraintText + create-form input

**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 1, Task 3, Task 4, Task 5

**Files:**
- Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/ApiKeysPage.razor`
- Test: new render/behavior test alongside the dashboard render tests
- Docs: `docs/Authorization.md` (dashboard API-key management section, if it describes
  the create form's constraint fields)

**Spec.** Two defects, one page:

(a) `ConstraintText` (`:504-533`) never lists `DashboardTags`, so a tags-only key falls
through to `string.Join` of an empty list → `""` → `DashboardDisplay.Text` renders `-`,
while a truly unconstrained key says `unconstrained` — two spellings of one meaning,
except they're NOT the same meaning: the tags-only key IS constrained
(`ApiKeyConstraints.IsEmpty` counts tags, `ApiKeyConstraints.cs:62-71`). Fix by listing
it: `AddList(parts, "dashboard_tags", constraints.DashboardTags)` alongside the other
snake_case labels (~`:516`). `AddList` (`:535-541`) preserves order — keep that.

(b) The create form's Constraints section (`:97-133`) has no dashboard-tags input, so a
tags-only key can't be created from the dashboard at all. Add a textarea matching the
subtree/glob fields exactly: bind to a new `string DashboardTags` on `ApiKeyCreateModel`
(`:551-589`), clear it in `Reset()` (`:575-588`), split with the existing `ParseList`
(`:543-549`), and attach in `TryBuildCreateRequest` (`:415-455`) via object-initializer
on the `new ApiKeyConstraints(...)` at `:444-452` (init-only property):
`new ApiKeyConstraints(...) { DashboardTags = ParseList(CreateModel.DashboardTags) }`.
Help text: comma/newline-separated, matched case-insensitively against
`Dashboard:GroupToTag` grants; empty = untagged (visibility per
`UntaggedSessionVisibility`). Mirrors the CLI's `apikey create --dashboard-tags`.

Tests (HtmlRenderer page-render idiom): tags-only key renders `dashboard_tags: …` (not
`-`); unconstrained key still renders `unconstrained`; create-model round-trip —
`TryBuildCreateRequest` with a tags input yields constraints whose `DashboardTags`
matches (if the method is private, follow whatever access pattern the page's existing
tests use; an `internal`-for-testing hook is acceptable only if the repo already does
that elsewhere — otherwise drive through the rendered form or refactor minimally).

**Steps:** tests first → implement → `--filter` new test class → macOS build 0W/0E →
docs (only if `Authorization.md` enumerates the form fields) → commit with pathspecs.

---

## Task 7: Wnwrap alarm-probe retry — secured-write path (windev)

**Classification:** standard (investigation; may legitimately end blocked again)
**Estimated implement time:** ~10 min wall (windev round-trips)
**Parallelizable with:** Task 2
**Blocked by:** none (but runs in Wave 2 to keep windev free for it)

**Files:**
- Modify: `docs/AlarmProbeFindings.md` (append the attempt record + findings)
- Modify (only if findings answer the questions): comment-level updates in
  `src/ZB.MOM.WW.MxGateway.Worker/MxAccess/WnWrapAlarmConsumer.cs:425-435, :490-505, :655-670`
- NO probe code committed; NO `Skip=` flips committed; throwaway harness lives only in
  the windev CI clone and is deleted after.

**Spec.** The 2026-08-17 attempt failed because it used plain `Write` against classified
alarm UDAs (`SecurityError detail=1008`). The worker and live harness already implement
the right verb pair end-to-end (`WorkerLiveMxAccessSmokeTests.cs:445-583` —
`AuthenticateUser` then `WriteSecured`; on this rig `AuthenticateUser("Administrator","")`
is known to resolve to user id 1, `:649`). Retry on windev (`ssh windev`, CI clone):

1. Pull the branch; build what the probe needs (worker x86 + integration tests).
2. Drive `AuthenticateUser("Administrator","")` → `WriteSecured(true)` against
   `TestMachine_001.TestAlarm001` (env overrides
   `MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER`/`_PASSWORD` exist if a real Galaxy
   account is available — never echo credential values). A throwaway variant of
   `WnWrapConsumerProbeTests` (`Worker.Tests/Probes/`, `Skip=null` locally only,
   `GROUP` fixed to the findings run's `TestArea`, `MaxAlarmsPerFetch` droppable to 1–2)
   is the vehicle.
3. If the secured write lands: answer **Q1** — GUID stability across `UNACK→ACK`
   (`AlarmAckByGUID`) and across clear-then-re-raise; and **Q2** — `ALARM_RECORDS/@COUNT`
   total-active vs records-in-reply under `maxAlmCnt` 1–2 with all three TestMachine
   alarms active. Record both in `AlarmProbeFindings.md`; update the three
   `WnWrapAlarmConsumer` comment blocks to "observed" with the answer. **Do not change
   `IsTruncatedFetch` behavior in this task** — if Q2 says `@COUNT` is total-active,
   record that an exact-detection follow-up is now unblocked; the heuristic change is
   its own reviewed task later.
4. If still refused: append the attempt (verb used, identity resolved, status line —
   no secrets), and record which unblock paths remain (flip script re-enable /
   real Galaxy account / reclassification). That outcome completes this task.

**Steps:** windev session → probe → findings written → clean up throwaway files on
windev → commit (docs + any comment updates) with pathspecs from the Mac tree.

---

## Task 8: Windev full verification

**Classification:** small (no review — verification gate)
**Estimated implement time:** ~15 min wall
**Blocked by:** Task 1, Task 2, Task 3, Task 4, Task 5, Task 6, Task 7

Pull branch into `C:\build\mxaccessgw-ci`, then: full `slnx` build 0W/0E → worker x86
tests → gateway tests → live MXAccess smoke (`MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1`,
`~WorkerLiveMxAccessSmokeTests`) → `pwsh scripts/check-codegen.ps1` all green.
Known quirk: first build after pull may fail CS2001/CS0016 on stale Contracts obj —
clear obj/bin and rebuild, not a regression. Record all counts.

---

## Task 9: Bookkeeping — follow-ups closure + plan record

**Classification:** trivial
**Estimated implement time:** ~2 min
**Blocked by:** Task 8

**Files:**
- Modify: `docs/plans/2026-08-17-deferred-closeout.md` (follow-ups block `:419-434`):
  annotate each closed item with its closing commit; REWRITE the stale Rust-guard bullet
  to record that Check 3 already existed and only the reverse sweep was missing; leave
  anything genuinely still open (e.g. probe questions if Task 7 ended blocked;
  `IsTruncatedFetch` exactness if Q2 unblocked it) accurately stated.
- Modify: `docs/plans/2026-08-17-followup-closeout.md` (this file): as-built notes.
- Modify: `docs/plans/2026-08-17-followup-closeout.md.tasks.json`: statuses.

---

## Execution notes for the orchestrator

- Branch `feat/followup-closeout` off `main` before Task 1.
- Opus implementers per user instruction; reviewer chain per Classification
  (high-risk = spec-reviewer serial then code-reviewer; standard = parallel pair;
  small = code-reviewer only; trivial = none).
- Waves: **Wave 1:** 1, 3, 4, 5, 6 (files disjoint, including docs — ownership lines in
  each task are the contract) · **Wave 2:** 2 (after 1+3+4), 7 (windev) · **Wave 3:** 8 →
  final integration review → 9.
- Each implementer gets its full task text + the ground rules block.
- Doc-file ownership this wave matters more than usual: `gateway.md`→Task 1,
  `GatewayConfiguration.md`+`GatewayDashboardDesign.md`→Task 3 (Task 2 later adds the
  payload-case row), closeout-plan follow-ups→Task 9 only.
- Task 7/8 run against windev over `ssh windev` (PowerShell); psbridge is fallback.
- Do not merge to `main` without user instruction.

---

## As-built notes (execution record, 2026-08-18)

All 9 tasks completed on `feat/followup-closeout`; every classification-driven review
chain resolved **Approved**. The final integration review came back *Ready with
reservations* — all of its non-blocking findings were fixed in `c3c603f`, leaving no
open review item.

Verification: macOS `NonWindows.slnx` 0W/0E; gateway filtered suites green (dashboard
276/276, alarm suites green); `scripts/check-codegen.ps1` 4/4 on macOS. **The full
five-language client matrix ran locally for the first time** — dotnet 133 passed /1
skipped, Go clean, Rust 108, Python 168 passed /1 skipped, Java 131. Windev at `90331b6`:
full `slnx` 0W/0E (one transient MSB4166 node crash, clean on retry), worker x86 523
passed / 11 skipped plus **one pre-existing deterministic failure that reproduces on
`main`** (`WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`),
gateway 1151/1151 on rerun (the known windev load flake), live MXAccess smoke 8/8. The
comment- and prose-only commits landed after `90331b6` are unverified on windev by
design; they need only a cheap tip re-build there.

- **A session-limit outage interrupted four reviewers mid-run.** All four resumed
  cleanly on retry; no review was lost or silently truncated.
- **Task 2 surfaced two plan defects, both absorbed rather than deferred.** The spec
  claimed the Java CLI renders alarm frames as generic JSON; it is in fact an exhaustive
  `switch` that does not compile without an arm for the new case. The spec's work list
  also omitted the Java generated-bindings tree.
- **A reviewer's Minor-2 was refuted, and the doc defect behind it fixed instead.** The
  finding assumed alarm records coexist after a re-raise; fetch evidence from the probe
  rig shows the new GUID replaces the record. The prose that implied coexistence was
  corrected rather than the code.

Follow-ups recorded, not started — **all closed 2026-08-18 on `feat/followups-tickets`**
(plan `docs/plans/2026-08-18-followups-and-tickets.md`; per-bullet closing commits below):

- `WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`
  fails deterministically on windev **and on `main`** — pre-existing, needs its own
  investigation. *Closed (`7da52b6`, `462850a`, `aaeb86b`): test-harness defect — the fake
  runtime session stamped STA activity only at construction, so the watchdog correctly
  faulted `StaHung` pre-dispatch. Test-only fix; windev worker suite fully green (524/11
  skipped) for the first time.*
- `check-codegen.ps1` Check 4 is unrunnable on Windows: the `protoc-gen-go` version banner
  carries a `.exe` suffix that the exact-string compare in
  `clients/go/generate-proto.ps1:10,55` does not tolerate. *Closed (`c94c4d4`, plus
  `8ae0c2f` for a second Windows-only blocker found during verification: PS 5.1 strips
  embedded double quotes from the Python probe's `-c` argument). Check 4 verified 4/4 on
  windev under both PowerShell 5.1 and pwsh 7.*
- Windev has `protoc-gen-go-grpc` 1.6.1 against the repo's pinned 1.6.2. *Closed: 1.6.2
  installed on windev; `docs/ToolchainLinks.md` corrected from `@latest` to the pinned
  install commands (`6b5c737`).*
- `clients/java`'s `checkGeneratedClean` is dead under Gradle 9 (`Project.exec` was
  removed); it needs `ExecOperations` injection to work again. *Closed (`df45cb4`) via
  `ProviderFactory.exec`; verified on Gradle 9.5.1 (macOS) and 9.4.1 (windev), including
  a configuration-cache ordering proof.*
- `SettingsPage` renders every `EffectiveDashboardConfiguration` member except
  `RecentFaultLimit` / `RecentSessionLimit` (pre-existing, predates this branch).
  *Closed (`a390fe1`, test tightened in `fb68bdb`).*
- The ack-leg probe stays blocked; unblock paths are in `docs/AlarmProbeFindings.md`.
  *Closed as answered-why (`d1ae43d`, `bc22792`, `1605f54`): the ack is unavailable by
  configuration — the test attributes carry `MxSecurityOperate` and no `AlarmAckByName`
  overload can carry a credential (inferred, caveated). The GUID-across-ack question
  itself stays assumed; the remaining paths need a human at an interactive client.*
- The dashboard `AlarmsPage` truncation banner is still poll-driven — it could consume
  the new `snapshot_status` feed frame instead. *Closed (`7b6dfba`, `f57a6ae`): the
  page's status feed loop now consumes `snapshot_status`; the poll stays as reconcile
  baseline.*
- **A closed code-review finding regressed, or was never applied.** Server-012
  (`code-reviews/Server/findings.md:405-412`) is recorded *Resolved 2026-05-18* and claims
  it corrected two scope lists to the canonical `*:*` strings — the `CLAUDE.md`
  Build/Test/Run `apikey` sample and the `CLAUDE.md` Authentication-section scope list.
  Neither correction was present when this branch looked; both were re-fixed here, along
  with a third instance the finding never covered (`docs/Authentication.md`'s `ops.alice`
  example). The bookkeeping is the follow-up: other `Server-0xx` entries marked Resolved
  with documentation-only fixes should be spot-checked for the same pattern, since a
  finding that reads Resolved is not otherwise re-examined. *Closed (`d3ac527`): 20
  doc-only resolutions audited; two more regressions found and re-fixed (Server-040,
  Server-009); four moot (target files deleted); annotations recorded in findings.md.*

Explicitly decided, not an omission: **`../scadaproj/CLAUDE.md` needs no update.** The
umbrella index records the *set* of `.proto` files this repo owns, and that set is
unchanged — Task 1 added a message and a field inside an existing proto, not a new
contract file.
