24 KiB
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, orgit checkout <sha/branch>. Commit with pathspecs on the commit:git commit -m "..." -- <paths>. Ifindex.lockblocks you, wait 5 s and retry. - Build/test lock: before any
dotnet build/dotnet test/cargo/gradle/go teston 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);rmdirit on ALL exit paths, including failure. - Quality gates:
TreatWarningsAsErrors=true,Nullable=enable. Followdocs/style-guides/CSharpStyleGuide.md(file-scoped namespaces,sealedby default,Asyncsuffix). 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_statusframe is a gateway-status frame (likeprovider_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 thefiles_to_editcontract. 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 windevlands in PowerShell. The CI clone isC:\build\mxaccessgw-ci. Quirks: the firstslnxbuild after a pull often fails on stale Contracts obj artifacts (CS2001/CS0016) — clear the Contractsobj/binand 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/andcode-reviews/for "Check 4";docs/GatewayTesting.mdis 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):
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 "& { <paste helper> ; 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.mdonly if it describes the task's mechanism (grepcheckGeneratedClean)
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:
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 -- <that one file> (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 (SettingsPageTagVisibilityRenderTestsor its sibling settings render test class; find withgrep -rl "SettingsPage" src/ZB.MOM.WW.MxGateway.Tests) - Modify:
docs/GatewayDashboardDesign.mdonly if it enumerates settings rows (grepRecentFaultLimit/ "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 <tr><th scope="row">…</th><td>…</td></tr> idiom):
<tr><th scope="row">Recent fault limit</th><td>@Snapshot.Configuration.Dashboard.RecentFaultLimit</td></tr>
<tr><th scope="row">Recent session limit</th><td>@Snapshot.Configuration.Dashboard.RecentSessionLimit</td></tr>
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 needssession: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,_snapshotTruncatedat:170/:401) - Test:
src/ZB.MOM.WW.MxGateway.Tests/— the AlarmsPage/dashboard alarm test class (find withgrep -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.Truncatedand 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 sameGatewayAlarmMonitorverdict, so they cannot disagree except transiently. Do not remove or restructure the poll. - Remember
StreamAsyncprimes every subscriber with an unconditionalsnapshot_statusbaseline frame afterprovider_status— so on attach the page gets the current verdict push-side too. No extra priming logic needed in the page. - Do NOT touch
GatewayAlarmMonitoror 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:
- Enumerate every
Server-0xxentry incode-reviews/Server/findings.mdwhose 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). - For each, verify the specific claimed text exists in today's tree (grep the exact phrases/identifiers the resolution names).
- Where present: append one line to that finding's Resolution:
Re-verified present 2026-08-18 (feat/followups-tickets). - Where absent: re-apply the correction in the target file, and append:
Regressed or never applied; re-fixed 2026-08-18 in <file> (feat/followups-tickets).(Server-012 itself was already re-fixed on the previous branch — annotate it as such, citing commitsa5f843c/f2a422b, rather than re-fixing.) - 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):
- 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). - 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.
- 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. - 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 fullslnxon windev and run the gateway suite filtered to any shared-surface tests. - Docs in the same commit if behavior/rules changed.
Commit: fix(worker): <cause> — 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:
- Path 2 — ack-security configuration: inspect whether the rig's galaxy/
alarmmgris configured with an alarm-acknowledgement security requirement the wnwrap consumer (operator name string, no authenticated identity) cannot meet. Look in galaxy configuration (theZBSQL Galaxy Repository — read-only queries only), area/object security settings for theTestMachine_00xobjects, and any alarm-security docs in the mxaccess analysis project. Read-only: do not change galaxy security config. - 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. - 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
STATEreachedACK_ALMand whether the GUID survived — that answers the original question and should update the findings table at the top of the doc. - 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 — <outcome>.
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):
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2— thenprotoc-gen-go-grpc --versionmust report 1.6.2. Updatedocs/ToolchainLinks.mdif it pins the old version (commit from the Mac tree).- Full
slnxbuild — 0 warnings / 0 errors (clear Contracts obj/bin on CS2001/CS0016). - Worker tests
-p:Platform=x86— expect the Task 7 outcome (fully green if fixed; otherwise the documented known-failure only). - Gateway tests — full suite; rerun filtered on load flakes before believing a failure.
powershell scripts/check-codegen.ps1— all 4 checks, proving Task 1 on the platform that had the bug.gradle --version+gradle :zb-mom-ww-mxgateway-client:checkGeneratedCleanfromclients\javaif Gradle is installed there — record version and result either way.- Live MXAccess smoke:
MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1, filterWorkerLiveMxAccessSmokeTests— 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).