# Follow-ups and Tickets Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development > to execute this plan task-by-task in this session. **Goal:** Close every follow-up recorded in `docs/plans/2026-08-17-followup-closeout.md` (as-built notes, "Follow-ups recorded, not started") plus the two ticket-worthy windev findings — leaving no recorded open item anywhere. **Architecture:** No new components. Ten independent closures: two toolchain/script fixes (Check 4 Windows pin, Gradle 9 `checkGeneratedClean`), two dashboard display gaps (Settings recent-limit rows, push-driven Alarms truncation banner), two documentation audits (Server-0xx resolution regression sweep, `Authentication.md` runnable-examples pass), one worker-test investigation (the deterministic `WorkerPipeSessionTests` failure on windev), one bounded rig probe (ack-leg unblock paths), a windev verification pass (including the `protoc-gen-go-grpc` 1.6.2 pin install), and bookkeeping. **Tech Stack:** PowerShell (codegen scripts), Gradle/Groovy (Java client), Blazor server-side Razor + xUnit/bUnit-style render tests (dashboard), .NET Framework 4.8 x86 xUnit (worker tests, windev-only), wnwrap probe harness (windev rig), Markdown. **Branch:** `feat/followups-tickets` off `main` (`45058d5`). --- ## Ground rules for every implementer subagent - **Git discipline (Mac tree):** NEVER `git stash`, `git reset`, `git clean`, or `git checkout `. Commit with **pathspecs on the commit**: `git commit -m "..." -- `. If `index.lock` blocks you, wait 5 s and retry. - **Build/test lock:** before any `dotnet build`/`dotnet test`/`cargo`/`gradle`/`go test` on the Mac, acquire the lock: `mkdir /private/tmp/claude-501/-Users-dohertj2-Desktop-MxAccessGateway/f36938ae-bbca-4245-b5c9-fac512d69e22/scratchpad/buildlock` (retry with backoff until it succeeds); `rmdir` it on ALL exit paths, including failure. - **Quality gates:** `TreatWarningsAsErrors=true`, `Nullable=enable`. Follow `docs/style-guides/CSharpStyleGuide.md` (file-scoped namespaces, `sealed` by default, `Async` suffix). Update affected docs in the same commit as source. - **MXAccess parity:** never synthesize events; the dashboard/gateway forwards only what the worker/monitor emits. The `snapshot_status` frame is a gateway-status frame (like `provider_status`), NOT a synthesized MXAccess event. - **Never log or echo secrets, API keys, credentials, or tag values.** - **Scope contract:** the task's `Files:` block is the `files_to_edit` contract. If the task needs other files, that is a plan defect — surface it in your report, don't silently expand scope. (Exception: Tasks 7 and 8 are investigations; their Files list is starting points, and they must report every file they end up touching.) - **Windev access (Tasks 7, 8, 9 only):** `ssh windev` lands in PowerShell. The CI clone is `C:\build\mxaccessgw-ci`. Quirks: the first `slnx` build after a pull often fails on stale Contracts obj artifacts (CS2001/CS0016) — clear the Contracts `obj`/`bin` and rebuild, it is not a regression; the gateway suite is load-sensitive — rerun a filtered subset before believing a flake. To get branch code there: `git -C C:\build\mxaccessgw-ci fetch origin && git -C C:\build\mxaccessgw-ci checkout feat/followups-tickets && git -C C:\build\mxaccessgw-ci pull` — which requires the Mac side to have pushed the branch first (the controller pushes; ask if the branch tip you need is not on origin). --- ### Task 1: check-codegen Check 4 — tolerate the Windows `.exe` version banner **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** Task 2, Task 3, Task 4, Task 5, Task 7 **Files:** - Modify: `clients/go/generate-proto.ps1` (pin compare at `:52-67`, pin constants at `:10-11`) - Modify: any doc that states Check 4 is unrunnable on Windows (grep `docs/` and `code-reviews/` for "Check 4"; `docs/GatewayTesting.md` is the likely holder) **Problem.** On Windows the plugins report their invoked name with an `.exe` suffix (`protoc-gen-go.exe v1.36.11`), so the exact-string compare against `'protoc-gen-go v1.36.11'` throws and Check 4 (which shells this script) is unrunnable on any Windows host — including windev, the box where regeneration actually happens. **Step 1: Normalize the banner before comparing.** Add a small helper and use it for both plugin compares (protoc stays warn-only and gets the same normalization for a fair warn): ```powershell function Get-NormalizedToolVersion { # On Windows a plugin reports its argv[0] name, so the banner carries an `.exe` # suffix ("protoc-gen-go.exe v1.36.11"). Strip it so the pin compare is # host-independent; the version part must still match exactly. param([string]$RawBanner) return ($RawBanner -replace '\.exe(?=\s)', '') } ``` Apply to `$protocGenGoVersion`, `$protocGenGoGrpcVersion`, `$protocVersion` at the point each is read (`:54`, `:59`, `:64`), keeping the pinned constants unchanged. Update the comment block at `:4-9` to note the normalization. **Step 2: Verify on macOS.** Acquire the build lock, then run `pwsh scripts/check-codegen.ps1` — all 4 checks must pass (banner has no `.exe` here, so this proves no regression). Also assert the helper logic directly: `pwsh -c "& { ; Get-NormalizedToolVersion 'protoc-gen-go.exe v1.36.11' }"` must print `protoc-gen-go v1.36.11`, and a name-only match check for `protoc-gen-go-grpc.exe 1.6.2` → `protoc-gen-go-grpc 1.6.2`. **Step 3: Fix the stale prose.** Wherever docs state Check 4 cannot run on Windows, rewrite to: Check 4 runs on Windows; the plugin pins must be installed (`go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11`, `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2`). Do NOT edit `docs/plans/2026-08-17-followup-closeout.md` — Task 10 owns plan bookkeeping. **Step 4: Commit** with pathspecs on the touched files: `fix(codegen): normalize .exe off plugin version banners so Check 4 runs on Windows`. Windows-side proof is deferred to Task 9 (windev runs check-codegen 1–4 after installing the 1.6.2 pin). --- ### Task 2: Gradle 9 — revive `checkGeneratedClean` without `Project.exec` **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** Task 1, Task 3, Task 4, Task 5, Task 7 **Files:** - Modify: `clients/java/zb-mom-ww-mxgateway-client/build.gradle` (`checkGeneratedClean`, `:71-92`) - Modify: `docs/GatewayTesting.md` only if it describes the task's mechanism (grep `checkGeneratedClean`) **Problem.** The task's `doLast` calls the script-level `exec {}` (i.e. `Project.exec`), removed in Gradle 9 — the task is dead on any Gradle 9 host. **Step 1: Replace with `ProviderFactory.exec`** (available since Gradle 7.5, so it works on both the current toolchain and Gradle 9, and is configuration-cache safe). Capture the provider reference at configuration time, call `.get()` in `doLast` so the git status runs at execution time: ```groovy tasks.register('checkGeneratedClean') { group = 'verification' description = 'Fails if the committed generated Java tree differs from a fresh regeneration.' dependsOn 'generateProto' def generatedDir = 'clients/java/src/main/generated' def repoRoot = rootProject.projectDir.parentFile.parentFile // Project.exec was removed in Gradle 9; ProviderFactory.exec runs the probe lazily // at doLast time and works on Gradle 7.5+. def gitStatus = providers.exec { workingDir = repoRoot commandLine 'git', 'status', '--porcelain', '--', generatedDir ignoreExitValue = true } doLast { def dirty = gitStatus.standardOutput.asText.get().trim() if (!dirty.isEmpty()) { throw new GradleException( "Generated Java is stale:\n${dirty}\n" + "Regenerate and commit the Java client after a .proto change " + "(gradle :zb-mom-ww-mxgateway-client:generateProto).") } } } ``` Preserve the explanatory comment block above the task (`:61-70`) — amend it, don't delete. **Step 2: Verify.** Acquire the build lock. From `clients/java`: `gradle :zb-mom-ww-mxgateway-client:checkGeneratedClean` must pass (tree is clean), and `gradle --version` must be recorded in your report. Then prove the failure path: touch a scratch edit inside `clients/java/src/main/generated/` (append a comment line to one generated file), rerun the task, confirm it fails with the stale message, then **revert that file with `git checkout -- `** (this narrow file-level checkout is the one permitted use; the file is committed generated output). **Step 3: Commit:** `fix(java-client): checkGeneratedClean via ProviderFactory.exec — Project.exec is gone in Gradle 9`. --- ### Task 3: SettingsPage — RecentFaultLimit / RecentSessionLimit rows **Classification:** small **Estimated implement time:** ~4 min **Parallelizable with:** Task 1, Task 2, Task 4, Task 5, Task 7 **Files:** - Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/SettingsPage.razor` (dashboard rows around `:98-102`) - Test: `src/ZB.MOM.WW.MxGateway.Tests/` — extend the existing settings render coverage (`SettingsPageTagVisibilityRenderTests` or its sibling settings render test class; find with `grep -rl "SettingsPage" src/ZB.MOM.WW.MxGateway.Tests`) - Modify: `docs/GatewayDashboardDesign.md` only if it enumerates settings rows (grep `RecentFaultLimit` / "Snapshot interval" there first) **Problem.** `EffectiveDashboardConfiguration` carries `RecentFaultLimit` and `RecentSessionLimit` (already projected by `GatewayConfigurationProvider.cs:62-63`), but `SettingsPage` renders every member except these two. **Step 1: Write the failing test.** Extend the settings render test: rendered page contains `Recent fault limit` with the configured value and `Recent session limit` with the configured value (use non-default values in the arranged options so the assertion proves plumbing, not defaults). **Step 2: Run it, expect FAIL** (`dotnet test --filter "FullyQualifiedName~SettingsPage"` under the build lock). **Step 3: Add the two rows** next to the other Dashboard rows (after "Snapshot interval", matching the existing `……` idiom): ```razor Recent fault limit@Snapshot.Configuration.Dashboard.RecentFaultLimit Recent session limit@Snapshot.Configuration.Dashboard.RecentSessionLimit ``` **Step 4: Run the filtered test, expect PASS.** **Step 5: Commit:** `feat(dashboard): settings page shows RecentFaultLimit and RecentSessionLimit`. --- ### Task 4: Authentication.md — runnable-as-written examples pass **Classification:** small **Estimated implement time:** ~5 min **Parallelizable with:** Task 1, Task 2, Task 3, Task 5, Task 7 **Files:** - Modify: `docs/Authentication.md` **Problem.** The doc's CLI/scope examples were flagged during the previous branch as not runnable as written — notably samples that grant only invoke/event scopes with no `session:open`, which since Server-004's validation would produce keys that cannot open a session (or, for unknown scope strings, be rejected at create time). The canonical scope catalog is: `session:open`, `session:close`, `invoke:read`, `invoke:write`, `invoke:secure`, `events:read`, `metadata:read`, `admin`; the verb is `apikey create-key` with `--key-id` required. **Step 1: Sweep every example** (` ```-fenced blocks and inline commands) in the doc. For each, either (a) make it runnable as written — canonical verb, required flags, only canonical scopes, scope sets that support what the surrounding prose says the key is for (a key described as opening sessions needs `session:open`) — or (b) if it is deliberately a fragment, mark it explicitly (e.g. "illustrative — not a complete command"). Prefer (a); use (b) only where completeness would obscure the point being made. **Step 2: Cross-check** each corrected scope list against `src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/GatewayScopes.cs` (read-only) — do not invent scope strings. **Step 3: Commit:** `docs(auth): make Authentication.md examples runnable as written`. --- ### Task 5: AlarmsPage — push-driven truncation banner from the snapshot_status frame **Classification:** standard **Estimated implement time:** ~5 min **Parallelizable with:** Task 1, Task 2, Task 3, Task 4, Task 7 **Files:** - Modify: `src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Pages/AlarmsPage.razor` (in-process feed loop around `:199-230`, `_snapshotTruncated` at `:170`/`:401`) - Test: `src/ZB.MOM.WW.MxGateway.Tests/` — the AlarmsPage/dashboard alarm test class (find with `grep -rl "AlarmsPage" src/ZB.MOM.WW.MxGateway.Tests`); if no AlarmsPage render/behavior test exists, add coverage at the level the existing dashboard tests use - Modify: `docs/GatewayDashboardDesign.md` (truncation-banner description, if it says the banner is poll-driven) **Problem.** The truncation banner (`_snapshotTruncated`, rendered at `:39`) updates only from the 3-second poll (`:401`), even though the page already holds an in-process subscription to the alarm feed (the loop at `:211-213` that feeds the provider-status badge). The feed now carries an edge-triggered `snapshot_status` frame (`AlarmFeedMessage.PayloadCase.SnapshotStatus`, shipped `fccf753`) — consume it so the banner flips on the edge instead of up to 3 s late. **Spec:** - In the existing in-process feed loop, add a case for `AlarmFeedMessage.PayloadCase.SnapshotStatus`: set `_snapshotTruncated = message.SnapshotStatus.Truncated` and re-render (same invoke/StateHasChanged pattern the provider-status case uses). - Keep the poll's assignment at `:401` — the poll is the reconcile baseline and both sources derive from the same `GatewayAlarmMonitor` verdict, so they cannot disagree except transiently. Do not remove or restructure the poll. - Remember `StreamAsync` primes every subscriber with an unconditional `snapshot_status` baseline frame after `provider_status` — so on attach the page gets the current verdict push-side too. No extra priming logic needed in the page. - Do NOT touch `GatewayAlarmMonitor` or the proto — gateway emission is done and shipped. **Steps:** failing test first (a `SnapshotStatus` frame delivered through the in-process subscription flips the banner state without a poll tick; a `false` frame clears it), then implement, then filtered dashboard/alarm tests green under the build lock, then commit: `feat(dashboard): alarms page consumes snapshot_status feed frame for the truncation banner`. --- ### Task 6: Server-0xx resolution audit — doc-only "Resolved" entries re-verified **Classification:** standard **Estimated implement time:** ~5 min (audit) + fixes as found **Parallelizable with:** none (runs after Task 4 lands — both may touch `docs/Authentication.md`) **Files:** - Modify: `code-reviews/Server/findings.md` (annotations only — do not rewrite history) - Modify: any file where a claimed correction is found absent (expected candidates: `CLAUDE.md`, `docs/*.md`, XML doc comments named in findings — report each) **Problem.** Server-012 was recorded *Resolved 2026-05-18* claiming two scope-list corrections that were absent from the tree when `feat/followup-closeout` looked — a resolution that regressed or was never applied. Findings that read Resolved are never re-examined, so every *documentation/comment-only* resolution needs the same spot-check. **Procedure:** 1. Enumerate every `Server-0xx` entry in `code-reviews/Server/findings.md` whose Resolution describes documentation-only or comment-only changes (no test named, prose like "Pure documentation change" — at minimum Server-011, Server-012, Server-013/014 remarks rewrites; sweep all entries, don't assume). 2. For each, verify the specific claimed text exists in today's tree (grep the exact phrases/identifiers the resolution names). 3. Where present: append one line to that finding's Resolution: `Re-verified present 2026-08-18 (feat/followups-tickets).` 4. Where absent: re-apply the correction in the target file, and append: `Regressed or never applied; re-fixed 2026-08-18 in (feat/followups-tickets).` (Server-012 itself was already re-fixed on the previous branch — annotate it as such, citing commits `a5f843c`/`f2a422b`, rather than re-fixing.) 5. Report a table: finding id → verified/regressed → action. **Commit:** `docs(reviews): re-verify doc-only Server-0xx resolutions; re-fix regressions`. --- ### Task 7: WorkerPipeSessionTests deterministic failure — investigate and fix (windev) **Classification:** high-risk **Estimated implement time:** investigation timeboxed ~10 min; fix ≤5 min **Parallelizable with:** Task 1–5 (only windev user in wave 1) **Files (starting points — investigation task, report everything touched):** - Test: `src/ZB.MOM.WW.MxGateway.Worker.Tests/Ipc/WorkerPipeSessionTests.cs` (`RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReply`) - Suspect: `src/ZB.MOM.WW.MxGateway.Worker/` pipe-session / frame-protocol sources the test exercises (follow the test's references) **Problem.** The test fails deterministically on windev, and reproduces on `main` — pre-existing, not introduced by any recent branch. Everything else in the worker suite passes (523/523 otherwise). **Procedure (all building/testing on windev over ssh; edits in the Mac tree, pushed by the controller, pulled on the CI clone — coordinate via your report if you need a push):** 1. Repro on the CI clone at this branch: `dotnet test src/ZB.MOM.WW.MxGateway.Worker.Tests/ZB.MOM.WW.MxGateway.Worker.Tests.csproj -p:Platform=x86 --filter "FullyQualifiedName~RunAsync_LongInFlightCommandThatKeepsPumping"`. Capture the full failure output (assertion text, timeout, stack). 2. Read the test and the code under test. Classify: (a) test defect (bad timing assumption, races in the harness), (b) product defect in the pipe session under a long in-flight command, or (c) environment-specific (windev timing/load). **Timebox: if the cause is not isolated after ~10 minutes of investigation, stop and report your best hypothesis with evidence — do not churn.** 3. Fix minimally per the classification. A product fix in the frame/pipe layer is high-risk territory: preserve the frame protocol (`docs/WorkerFrameProtocol.md`), the STA pumping rules, and MXAccess parity. A test fix must keep the scenario's intent — a long in-flight command that keeps pumping must not fault the session and must deliver its reply; don't weaken it into a sleep-and-hope. 4. Verify: the fixed test passes 3 consecutive runs on windev; then the full worker suite (`-p:Platform=x86`) is green. If you touched product code, also build the full `slnx` on windev and run the gateway suite filtered to any shared-surface tests. 5. Docs in the same commit if behavior/rules changed. **Commit:** `fix(worker): — WorkerPipeSessionTests long-in-flight repro` (adjust `fix(worker-tests)` if the defect is in the test). --- ### Task 8: Ack-leg probe — bounded unblock attempt (windev rig) **Classification:** standard **Estimated implement time:** timeboxed ~15 min of probing **Parallelizable with:** Task 6 (runs after Task 7 — serialize windev use) **Files (starting points — investigation task):** - Modify: `docs/AlarmProbeFindings.md` (append a third-attempt section, whatever the outcome) - Reference (read-only): the probe harness locations named in that doc's earlier attempts; the mxaccess analysis project at `C:\Users\dohertj2\Desktop\mxaccess` (windev) **Problem.** The acknowledge leg remains unobserved: every wnwrap ack surface is accepted-but-inert (`rc=0`, state stays `UNACK_ALM`), and `.Acked` is write-rejected (OperationalError 1007). Two recorded unblock paths remain (`docs/AlarmProbeFindings.md`, "Remaining unblock paths for the acknowledge leg"). **Procedure — attempt path 2 first (it is inspectable), path 1 only if non-interactive:** 1. **Path 2 — ack-security configuration:** inspect whether the rig's galaxy/`alarmmgr` is configured with an alarm-acknowledgement security requirement the wnwrap consumer (operator *name* string, no authenticated identity) cannot meet. Look in galaxy configuration (the `ZB` SQL Galaxy Repository — read-only queries only), area/object security settings for the `TestMachine_00x` objects, and any alarm-security docs in the mxaccess analysis project. **Read-only: do not change galaxy security config.** 2. **Path 1 — platform-side ack:** only if a *scriptable, non-interactive* path exists on the rig as-installed (e.g. an existing harness or automation entry point). Do NOT install software, do NOT drive GUI automation, do NOT change rig state beyond the established raise/clear pattern on `TestMachine_001.TestAlarm001`. If only interactive IDE/InTouch paths exist, record that and stop. 3. Whatever the outcome, append the third-attempt section: what was inspected, evidence, and the leg's final status (observed / unavailable-by-configuration / still assumed with the paths requiring a human). If the ack was actually observed, record whether `STATE` reached `ACK_ALM` and whether the GUID survived — that answers the original question and should update the findings table at the top of the doc. 4. **Stop condition:** at ~15 minutes of probing without a decisive result, write up what was learned and conclude "still assumed" — the doc itself notes this is a documentation gap, not a correctness one. **Commit:** `docs(probe): ack-leg third attempt — `. --- ### Task 9: Windev toolchain pin + full verification **Classification:** verification (no review chain) **Estimated implement time:** ~15 min wall (mostly build/test wait) **Parallelizable with:** none (after all code tasks land and are pushed) **Files:** none in-repo except possibly `docs/ToolchainLinks.md` (update the windev `protoc-gen-go-grpc` entry if it records 1.6.1). **Procedure (all over `ssh windev`, CI clone `C:\build\mxaccessgw-ci` at this branch tip):** 1. `go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2` — then `protoc-gen-go-grpc --version` must report 1.6.2. Update `docs/ToolchainLinks.md` if it pins the old version (commit from the Mac tree). 2. Full `slnx` build — 0 warnings / 0 errors (clear Contracts obj/bin on CS2001/CS0016). 3. Worker tests `-p:Platform=x86` — expect the Task 7 outcome (fully green if fixed; otherwise the documented known-failure only). 4. Gateway tests — full suite; rerun filtered on load flakes before believing a failure. 5. `powershell scripts/check-codegen.ps1` — **all 4 checks**, proving Task 1 on the platform that had the bug. 6. `gradle --version` + `gradle :zb-mom-ww-mxgateway-client:checkGeneratedClean` from `clients\java` if Gradle is installed there — record version and result either way. 7. Live MXAccess smoke: `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1`, filter `WorkerLiveMxAccessSmokeTests` — 8/8. 8. Report every result verbatim (counts, not "green"). --- ### Task 10: Bookkeeping — close the follow-ups record **Classification:** trivial **Estimated implement time:** ~3 min **Parallelizable with:** none (last) **Files:** - Modify: `docs/plans/2026-08-17-followup-closeout.md` (follow-ups block `:483-507`): annotate each bullet closed with this branch's closing commit (or precisely narrowed, e.g. ack leg if still assumed) - Modify: `docs/plans/2026-08-18-followups-and-tickets.md` (this file): append as-built notes — per-task commits, review outcomes, verification results, anything learned - Modify: `docs/plans/2026-08-18-followups-and-tickets.md.tasks.json`: final statuses **Commit:** `chore(plan): followups-and-tickets as-built record; prior follow-ups closed`. --- ## Execution notes for the controller - **Model/review chains** (subagent-driven-development skill): Tasks 1–4 small → Sonnet implementer + Haiku code review (diffs <100 LOC). Task 5 standard → Opus implementer, spec (Haiku) ∥ code (Sonnet). Task 6 standard → Opus implementer, spec ∥ code. Task 7 high-risk → Opus implementer, serial spec (Haiku) → code (Sonnet). Task 8 standard → Opus implementer, spec ∥ code. Task 9 verification → Opus, no review. Task 10 trivial → Sonnet, no review. - **Waves:** Wave 1 = Tasks 1, 2, 3, 4, 5, 7 (disjoint files; Task 7 alone on windev). Wave 2 = Task 6 (after 4) and Task 8 (after 7). Wave 3 = push branch, Task 9. Wave 4 = Task 10, final integration review, then hold for the user's merge decision. - **Push cadence:** controller pushes the branch before any windev task needs its code there (Task 7 investigates on-branch; Tasks 8–9 need the wave-1/2 tips).