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

```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 "& { <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.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 -- <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):

```razor
<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 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 <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 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): <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.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).

---

## 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 (`6b5c737` — `ToolchainLinks.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.
