Files
mxaccessgw/docs/plans/2026-08-18-followups-and-tickets.md
T
Joseph Doherty f4b974eec1
ci / windows-x86 (push) Successful in 1m30s
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 3m38s
ci / portable (push) Successful in 11m12s
chore(plan): followups-and-tickets as-built record; prior follow-ups closed
2026-08-18 07:29:15 -04:00

29 KiB
Raw Blame History

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 <sha/branch>. Commit with pathspecs on the commit: git commit -m "..." -- <paths>. 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):

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.2protoc-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 14 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:

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 (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 <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, _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 <file> (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 15 (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): <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:

  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 — <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):

  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.ps1all 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 14 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 89 need the wave-1/2 tips).

As-built record (2026-08-18)

All 10 tasks completed on feat/followups-tickets; every classification-driven review chain resolved Approved (Tasks 5, 7, and 8 after fix rounds; Task 7's high-risk chain took two rounds, ending with the mid-window auto-release race made structurally unreachable — 30 s BlockedDispatchSafetyNet > every test's 20 s cancellation). The final integration review returned Ready with reservations; every reservation was fixed on-branch: I-2 (6b5c737ToolchainLinks.md recorded @latest for plugins the regeneration script hard-pins), I-3 (db7b1db — three doc sites claimed full-DN GroupToRole/GroupToTag keys work; the Server-040 restoration proves they never match the pre-stripped groups; rows were pre-existing on main), M-2/M-5 (1605f54), M-3 (fb68bdb), M-4 (753d070), and M-1 was discharged by evidence (a --configuration-cache --rerun-tasks discriminator proved providers.exec evaluates at execution time after generateProto; no change needed).

Verification (Task 9, windev CI clone at 5417222, post-fix commits re-verified individually by their owners at their pushed tips): full slnx 0W/0E; worker x86 524 passed / 11 skipped / 0 failed — the suite's first fully green windev run; gateway 1155/1155 with no reruns; check-codegen.ps1 4/4 on Windows under pwsh 7, and after 8ae0c2f also end-to-end under Windows PowerShell 5.1; Gradle 9.4.1 checkGeneratedClean green on windev (and 9.5.1 on macOS); live MXAccess smoke 8/8; toolchain pins now real on windev (protoc-gen-go v1.36.11, protoc-gen-go-grpc 1.6.2).

Notable findings made along the way:

  • Check 4 had a second Windows-only blocker behind the .exe banner: Windows PowerShell 5.1 strips embedded double quotes when marshaling native-exe args, breaking the Python grpcio-tools version probe (clients/python/generate-proto.ps1:39). Fixed (8ae0c2f) by swapping quote nesting; no other native-exe invocation in the codegen scripts carries the pattern.
  • The ack leg is blocked by galaxy security configuration, not a missing verb: the probe fixtures carry MxSecurityOperate, and no AlarmAckByName overload can convey an authenticated identity (inferred from mechanism, honestly caveated). gateway.md and docs/Grpc.md now carry the "acceptance is not application" caveat on the acknowledge RPC.
  • The fca978d tracking-marker sweep deleted substantive prose along with markers in at least one place (Server-040's precedence comment plus an operator-facing RDN paragraph that was never part of the finding). A targeted re-read of that commit's larger comment deletions is ticket-worthy (203 files swept; only the audited file was examined).
  • generateProto's up-to-date check does not notice an out-of-band deletion of a single generated file (protobuf-gradle-plugin behavior, orthogonal to this branch) — a manually deleted generated file stays missing until a .proto change or --rerun-tasks invalidates the task.
  • Windev process notes: the CI clone sat on a detached HEAD, so bare git pull silently no-ops (use git pull origin <branch>); concurrent MSBuilds on the clone can kill each other's child nodes (MSB4166) — -m:1 avoids it; ssh can transiently refuse with "Too many authentication failures" under agent concurrency (back off and retry).

Follow-ups recorded, not started (deliberately small):

  • The fca978d sweep re-read (above) — the one genuinely ticket-worthy item.
  • DashboardGroupTagMapping's class-level <summary> still describes the lookup as "full DN first, leading-RDN fallback" — accurate mechanics, but it could point at the short-name-keys consequence the inline comment now records.
  • wwtools mxa read human-readable formatter throws RuntimeBinderException on a failed read (ReadCommand.cs:137); --llm-json works. Different repo, noted here so it isn't lost.

Explicitly decided, not an omission: ../scadaproj/CLAUDE.md needs no update — no .proto, contract, command, or architecture fact the umbrella index records changed on this branch.