Both lists predate the scope rename and would mislead anyone creating a key:
CLAUDE.md's Authentication section still named the pre-rename scopes, and
docs/Authentication.md's ops.alice example passed 'read,write', which
GatewayScopes.ValidateScopes rejects outright. Same defect family as the
Build/Test/Run sample fixed in a5f843c.
Recorded as a follow-up: code review finding Server-012 claims it fixed the two
CLAUDE.md lists on 2026-05-18, but neither correction was present — a Resolved
finding is not re-examined, so the sibling Server-0xx doc resolutions want a
spot-check for the same pattern.
29 KiB
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, orgit checkout <sha/branch>. Commit with explicit pathspecs on the commit itself:git commit -m "..." -- <paths>— nevergit add -A,git commit -a, or a baregit commitafter add (a concurrent task's staged files would be swept in). - Build/test mutual exclusion: before any
dotnet build/dotnet test, acquire the lock viamkdir /private/tmp/claude-501/-Users-dohertj2-Desktop-MxAccessGateway/f36938ae-bbca-4245-b5c9-fac512d69e22/scratchpad/buildlock(retry with backoff while it fails);rmdirit 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,sealedby default,Asyncsuffix, 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 fullslnxonly build on windev. git commithittingindex.lockcontention → 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/(existingGatewayAlarmMonitortest 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:
- New message next to
AlarmProviderStatus(:1025-1030):
// 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;
}
- New oneof case in
AlarmFeedMessage(:1006-1023, next free field 5):AlarmSnapshotStatus snapshot_status = 5;
Gateway (GatewayAlarmMonitor.cs):
ApplyReconcile(:632-694) currently records_snapshotTruncated = snapshotTruncatedat:691with a comment saying truncation "needs no special handling here". Change: detect the edge (old value ≠ new value) and, when it changes, broadcastnew AlarmFeedMessage { SnapshotStatus = new AlarmSnapshotStatus { Truncated = snapshotTruncated } }viaBroadcastToAll(:720-732— the provider-status precedent at:531). Match the existing locking discipline exactly: compute/record under_syncthe wayApplyProviderModeChangeAsync(:513-566) does, broadcast the way it does. Update the now-stale comment at:627-631.- Late-joiner priming in
StreamAsync(:748-800): after theprovider_statusframe (:782) and before the cachedactive_alarmframes (:786), yield onesnapshot_statusframe 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_snapshotTruncatedto 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.StreamAlarmsis a pass-through — no change.IGatewayAlarmService.SnapshotTruncateddoc 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):
- Reconcile flipping truncated false→true emits exactly one
snapshot_statusframe (truncated=true) to an attached subscriber; a second identical reconcile emits none. - true→false emits the clearing frame.
- A subscriber attaching while truncated=true receives, in order:
provider_status,snapshot_status(truncated=true), cachedactive_alarms,snapshot_complete. - 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-99StreamAlarms handler contract.docs/DesignDecisions.md:206-230("truncation is reported per record…", dated 2026-08-17): extend the entry — per-record stays forQueryActiveAlarms(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(viascripts/publish-client-proto-inputs.ps1), Go + Python generated bindings (per-clientgenerate-proto.ps1) - Modify:
clients/dotnet/ZB.MOM.WW.MxGateway.Client.Cli/MxGatewayClientCli.cs:1552-1565(feed renderer — add BOTH the missingProviderStatuscase and the newSnapshotStatuscase) - Modify:
clients/go/cmd/mxgw-go/main.go:1101-1114(formatAlarmFeedMessage— same two cases) - Modify:
clients/rust/crates/mxgw-cli/src/main.rs:2233-2264and:2266+(alarm_feed_message_summary/alarm_feed_message_to_json— addSnapshotStatusarm) - 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
DashboardLiveDataServicetest 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 theCapturingHubContextshape fromDashboardEventBroadcasterTests.cs:23and theDashboardSnapshotPublisherTestsBackgroundService-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.csis 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):
- Pull the branch; build what the probe needs (worker x86 + integration tests).
- Drive
AuthenticateUser("Administrator","")→WriteSecured(true)againstTestMachine_001.TestAlarm001(env overridesMXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_USER/_PASSWORDexist if a real Galaxy account is available — never echo credential values). A throwaway variant ofWnWrapConsumerProbeTests(Worker.Tests/Probes/,Skip=nulllocally only,GROUPfixed to the findings run'sTestArea,MaxAlarmsPerFetchdroppable to 1–2) is the vehicle. - If the secured write lands: answer Q1 — GUID stability across
UNACK→ACK(AlarmAckByGUID) and across clear-then-re-raise; and Q2 —ALARM_RECORDS/@COUNTtotal-active vs records-in-reply undermaxAlmCnt1–2 with all three TestMachine alarms active. Record both inAlarmProbeFindings.md; update the threeWnWrapAlarmConsumercomment blocks to "observed" with the answer. Do not changeIsTruncatedFetchbehavior in this task — if Q2 says@COUNTis total-active, record that an exact-detection follow-up is now unblocked; the heuristic change is its own reviewed task later. - 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;IsTruncatedFetchexactness 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-closeoutoffmainbefore 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
mainwithout 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
switchthat 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:
WorkerPipeSessionTests.RunAsync_LongInFlightCommandThatKeepsPumping_DoesNotFaultAndDeliversReplyfails deterministically on windev and onmain— pre-existing, needs its own investigation.check-codegen.ps1Check 4 is unrunnable on Windows: theprotoc-gen-goversion banner carries a.exesuffix that the exact-string compare inclients/go/generate-proto.ps1:10,55does not tolerate.- Windev has
protoc-gen-go-grpc1.6.1 against the repo's pinned 1.6.2. clients/java'scheckGeneratedCleanis dead under Gradle 9 (Project.execwas removed); it needsExecOperationsinjection to work again.SettingsPagerenders everyEffectiveDashboardConfigurationmember exceptRecentFaultLimit/RecentSessionLimit(pre-existing, predates this branch).- The ack-leg probe stays blocked; unblock paths are in
docs/AlarmProbeFindings.md. - The dashboard
AlarmsPagetruncation banner is still poll-driven — it could consume the newsnapshot_statusfeed frame instead. - 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 — theCLAUDE.mdBuild/Test/Runapikeysample and theCLAUDE.mdAuthentication-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'sops.aliceexample). The bookkeeping is the follow-up: otherServer-0xxentries 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.
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.