Track deferred work in GitHub issues (#3900)
diff --git a/CLAUDE.md b/CLAUDE.md
index 9536e2d..75da3fa 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -54,4 +54,4 @@
 - Before opening or updating a PR, follow Build and Validate; use `pre-pr` when workflows are available.
 - Use imperative commit/PR subjects; keep scope focused, link relevant issues, and include test/build evidence.
 - After opening a PR, use `pr-babysit` (`.claude/workflows/pr-babysit.js`) to drive reviews and CI to green. If workflows are unavailable, use `gh pr checks <num>` and `gh pr view <num> --comments`; fix failures, push, and resolve review threads.
-- **Deferred work** — Separate scope gets a separate PR/session. Use `superpowers:writing-plans` for one handoff per topic at `docs/superpowers/followup/pr<NNN>-<topic>.md` (`NNN` = originating PR). Include evidence, remaining work, and why deferred; delete when its PR lands.
+- **Deferred work** — Separate scope gets a separate PR/session. Create one GitHub issue per topic with `gh issue create --label followup`; link the originating PR and preserve the full handoff in the issue body, including evidence, remaining work, and why deferred. Add revalidation, new findings, and changes to remaining work as issue comments. Close the issue when its implementing PR lands.
diff --git a/docs/superpowers/followup/pr3803-hil-iar-rerun-spec.md b/docs/superpowers/followup/pr3803-hil-iar-rerun-spec.md
deleted file mode 100644
index fe377f7..0000000
--- a/docs/superpowers/followup/pr3803-hil-iar-rerun-spec.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# IAR HIL Leg Re-run Spec Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Let the `hil-hfp-iar` CI leg re-run only its failed boards, as the other two HIL
-legs already do.
-
-**Architecture:** `hil_test.py` writes a `<config>.failed` spec into `HIL_REPORT_DIR`; a
-workflow step reads it on the next attempt and passes the boards back as arguments. The IAR
-leg passes `--retry 1` like the others but sets no `HIL_REPORT_DIR` and has no read-back
-step, so its spec is written into the workspace and never read.
-
-**Tech Stack:** GitHub Actions YAML, self-hosted runner.
-
-## Global Constraints
-
-- `.github/workflows/build.yml`. The two working legs are `hil-tinyusb` (matrix) — see its
-  `Set HIL report dir (per run+job; persists across run attempts)` and `Get re-run spec from
-  previous attempt` steps — and they are the pattern to copy.
-- The report dir must be keyed by run id AND job so a matrix leg does not collide with
-  another, and must survive across run attempts (that is the whole point).
-- The IAR leg is the only HIL job that BUILDS inline; its `Build` step is bounded at
-  `timeout-minutes: 30` under a 120-minute job ceiling. Do not disturb that.
-
-## What is already established
-
-- Verified by reading the workflow: `hil-hfp-iar` has neither `HIL_REPORT_DIR` nor a
-  `Get re-run spec` step, while passing `--retry 1`.
-- Consequence: a GitHub re-run of that job re-tests its whole matrix. **This is not a
-  regression** — that leg never had the mechanism — and the unread spec costs only a file.
-- The report artifact upload for that leg is named `hil-report-hfp-iar`.
-
-**Why this is a separate PR:** it is CI plumbing with no code change, it needs a real
-re-run on the self-hosted runner to prove, and it duplicates ~15 lines of workflow that
-would be better factored — a decision worth making on its own.
-
-## File Structure
-
-- `.github/workflows/build.yml` — the `hil-hfp-iar` job only.
-
----
-
-### Task 1: Give the IAR leg a persistent report dir and a re-run spec
-
-**Files:**
-- Modify: `.github/workflows/build.yml` (job `hil-hfp-iar`)
-
-**Interfaces:**
-- Consumes: `hil_test.py`'s existing `--report-dir` / `.failed` behaviour — no code change.
-- Produces: `env.HIL_REPORT_DIR` for the job, and `$RERUN_ARGS` for the test step.
-
-- [ ] **Step 1: Copy the two steps from `hil-tinyusb`, before the Build step**
-
-```yaml
-      - name: Set HIL report dir (per run+job; persists across run attempts)
-        run: |
-          BASE=$HOME/hil-reports
-          echo "HIL_REPORT_DIR=$BASE/${GITHUB_RUN_ID}-hfp-iar" >> "$GITHUB_ENV"
-
-      - name: Get re-run spec from previous attempt
-        run: |
-          SPEC="$HIL_REPORT_DIR/hfp.json.failed"
-          if [ -f "$SPEC" ]; then
-            echo "RERUN_ARGS=$(cat "$SPEC")" >> "$GITHUB_ENV"
-            echo "re-running only: $(cat "$SPEC")"
-          fi
-```
-
-Match the exact spec filename `hil_test.py` writes for this leg's config — read
-`_write_failed_spec` and the `failed_fname` construction rather than assuming.
-
-- [ ] **Step 2: Pass the spec to the test step**
-
-```yaml
-          python3 test/hil/hil_test.py --retry 1 $SEL_ARGS hfp.json $RERUN_ARGS
-```
-
-`--retry 1` stays FIRST so argparse's last-wins keeps any explicit override working.
-
-- [ ] **Step 3: Point the artifact upload at the report dir**
-
-```yaml
-          path: ${{ env.HIL_REPORT_DIR }}/hil_report.md
-```
-
-- [ ] **Step 4: Validate the YAML**
-
-Run: `python3 -c "import yaml,sys; d=yaml.safe_load(open('.github/workflows/build.yml')); j=d['jobs']['hil-hfp-iar']; print(j['timeout-minutes'], [s.get('name') for s in j['steps']])"`
-Expected: the ceiling is still 120, the Build step still carries `timeout-minutes: 30`, and
-the two new steps appear before Build.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add .github/workflows/build.yml
-git commit -m "ci: let the IAR HIL leg re-run only its failed boards"
-```
-
----
-
-### Task 2: Prove it on a real re-run
-
-**Files:** none — evidence only.
-
-- [ ] **Step 1:** Push and let `hil-hfp-iar` run to a failure (or force one).
-- [ ] **Step 2:** Confirm `$HIL_REPORT_DIR/hfp.json.failed` exists on the runner after the
-      job.
-- [ ] **Step 3:** Use GitHub's "Re-run failed jobs" and confirm the log line
-      `re-running only: ...` and that only those boards are tested.
-- [ ] **Step 4:** Record the run URL in the PR body.
-
----
-
-## Consider first
-
-Three jobs would then carry the same ~15 lines. Factoring them into a composite action, or
-computing the report dir inside `hil_test.py` from `GITHUB_RUN_ID`, may be the better
-change — decide that before copying the block a third time.
diff --git a/docs/superpowers/followup/pr3840-mret-board-result.md b/docs/superpowers/followup/pr3840-mret-board-result.md
deleted file mode 100644
index 77b76b6..0000000
--- a/docs/superpowers/followup/pr3840-mret-board-result.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Give the HIL worker result a name
-
-**Origin:** split out of PR #3840 (making `hil_report.md` a rendering of `hil_report.json`).
-Delete this file when its own PR lands.
-
-> **SUPERSEDED IN PART (2026-08-26).** Written against a 7-field tuple whose index 5 was
-> `blind`. The sysfs blindness subsystem is gone: `test_board` now returns **6** fields with
-> `stray` at index 5, and its board-locked early return is 5 wide. The problem described
-> below is unchanged and still worth fixing — three producers, three widths, and
-> `len(r) > 5 and r[5]` reads a WRONG SLOT rather than raising. But drop the `blind` field
-> from the proposed NamedTuple and re-derive every index from `hil_test.test_board` before
-> executing, or `_stray_note` starts reading a duration as a stray count.
-> `StrayNoteSurvivesTheTupleWidth` pins the current shape.
-
-## What is established
-
-`test_board()` returns a bare tuple that three producers build and fourteen call sites read
-positionally. It has grown 5 → 6 → 7 fields, and the code already works around its own
-shape:
-
-```python
-hil_test.py:1992   dirty = [(r[0], r[6]) for r in mret if len(r) > 6 and r[6]]
-hil_test.py:2014   blind = [r[0] for r in mret if len(r) > 5 and r[5]]
-hil_test.py:2386   for name, _, _, _, dur, *_ in mret:
-hil_report.py:306  for name, _, _, rows, *_ in mret:
-```
-
-Two facts make this worth closing rather than tolerating:
-
-- **The declared type is already wrong.** `hil_test.py:1711` says
-  `tuple[str, int, list[str], list, float]` — five fields — while the main return at `:1872`
-  yields seven (`+ sysfs_blind(), stray`).
-- **A wrong slot is a wrong verdict, not a crash.** Field 5 is `blind`, which decides whether
-  a board's red cells are reported as broken hardware or as "could not tell". Inserting a
-  field mid-tuple makes `r[5]` read the wrong slot and keep running.
-
-It has bitten once already: `test_hil_bounded.py`'s
-`test_both_row_widths_survive_the_report_writers` exists because the blindness flag widened
-the tuple to 6 while the pool-timeout path still synthesised 5-field rows, and *"a
-fixed-width unpack in either one raises INSIDE the containment path, which is where a raise
-costs every board's results."* That is why the unpacks end in `*_`.
-
-## What remains
-
-A `NamedTuple` with defaults. Verified to pickle across the pool boundary and to stay
-fully tuple-compatible — existing `r[0]`, `e[1]`, `for name, _, _, rows, *_` and `len(r)`
-all keep working, so it lands without touching the fourteen consumers:
-
-```python
-class BoardResult(NamedTuple):
-    """What one worker returns. Field ORDER is load-bearing: it is unpacked positionally
-    in a dozen places, and the pool-timeout path synthesises one by hand."""
-    name: str
-    err_count: int
-    failed_tests: list[str]
-    rows: list | None          # None from the pool-timeout synthesis, never []
-    duration: float
-    blind: bool = False        # defaults, so a synthesised result is full-width
-    stray: int = 0
-```
-
-Then a second, smaller step removes the coupling itself: `accumulate_report` takes
-`[(name, rows)]` pairs instead of `mret`, and `hil_test` does the extraction because it owns
-the shape. One line at each end; the subtle merge logic — stale lock clearing,
-`BOUNDARY_CELL`, `duration=None` preservation — is untouched.
-
-## Sizing
-
-| | Sites |
-|---|---|
-| Producers to convert | 4 (`hil_test.py:1724`, `:1872`, `:2283`, `:2327`) |
-| Arity guards deleted | 2 (`:1992`, `:2014`) |
-| Wrong annotation fixed | 1 (`:1711`) |
-| `hil_report`'s coupled line | 1 (`:306`) |
-| Positional consumers (optional migration) | 14 |
-| **Test fixtures building tuples by hand** | **34** |
-
-Production code is roughly ten changed lines. **The work is dominated by the test
-fixtures**, which is also the risk.
-
-## Do this first, or the refactor is unverifiable
-
-`test_hil_report.py` (27 sites) and `test_hil_bounded.py` (7) construct plain tuples by
-hand — `('boardA', 0, [], [], 1.0, True)`. A producer that forgot to switch to
-`BoardResult`, or a pickling regression, **passes the entire 310-test suite** and surfaces
-only on the rig. Convert the fixtures to build `BoardResult` as task 1, before touching any
-producer. This ordering is not optional.
-
-Second trap: `rows` is `None` on the pool-timeout path (`hil_test.py:2283`), never `[]`, and
-`accumulate_report` guards with `if rows and ...`. A well-meaning `rows: list = []` default
-silently changes that path. Pin it with a test before the conversion.
-
-## Why it was split out
-
-PR #3840 touches the report document. This touches `test_board`'s return and the containment
-paths, where a raise costs every board's results rather than one board's — a different blast
-radius, needing its own review and its own rig run. #3840 is twice-reviewed and dogfooded
-ten times on hardware; folding this in would reset that surface for a latent-trap cleanup
-that is not causing bugs today.
diff --git a/docs/superpowers/followup/pr3840-write-report-atomicity.md b/docs/superpowers/followup/pr3840-write-report-atomicity.md
deleted file mode 100644
index 2094207..0000000
--- a/docs/superpowers/followup/pr3840-write-report-atomicity.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# `write_report` commits the two artifacts non-atomically
-
-**Origin:** split out of PR #3840, surfaced by its second review round. Delete this file
-when its own PR lands.
-
-```python
-md = render_report(doc) + '\n'
-report_dir.mkdir(parents=True, exist_ok=True)
-(report_dir / REPORT_JSON).write_text(json.dumps(doc, indent=2) + '\n')
-(report_dir / REPORT_MD).write_text(md, encoding='utf-8')
-```
-
-Rendering before writing closed the *render-failure* case: a raise can no longer commit a
-sidecar the markdown contradicts. It does not close the *interrupted-between-writes* case. A
-kill between those two lines leaves the pair disagreeing — and this runs on the containment
-path, on the way to `os._exit`, on a rig whose jobs get cancelled by the GitHub job ceiling.
-
-**What remains:** write both to temp files, then `os.replace` both. The window shrinks from
-two full writes to two renames, and neither file is ever observed half-written. `os.replace`
-is atomic per file on POSIX; the pair is still not transactional, which is acceptable and
-should be said in the docstring rather than implied away.
-
-Worth pairing with a test that kills between the writes — or, more practically, one that
-asserts no partial file is ever visible by checking the temp-then-rename shape directly.
-
-## Why it was split out
-
-A durability edge, not a wrong verdict. PR #3840 closed the render-failure half of this
-(nothing is written until the markdown renders); the interrupted-between-writes half needs
-a temp-then-rename and is better reviewed on its own.
diff --git a/docs/superpowers/followup/pr3851-msc-host-tur-retry.md b/docs/superpowers/followup/pr3851-msc-host-tur-retry.md
deleted file mode 100644
index 462b90f..0000000
--- a/docs/superpowers/followup/pr3851-msc-host-tur-retry.md
+++ /dev/null
@@ -1,149 +0,0 @@
-# MSC host: bound the Test Unit Ready retry loop and act on sense data
-
-> Split out of PR #3851 (`etmtrace-rp2350`, rp2350 ETM trace + stock clocks):
-> a host-stack MSC bug with no relation to that branch's scope.
-
-**Goal:** stop `msch_open`'s enumeration retry from spinning forever when a
-device answers Test Unit Ready with CHECK CONDITION, and use the sense data the
-driver already fetches to decide whether to keep waiting, give up, or report.
-
----
-
-## What is already established
-
-### The loop is unbounded, and the source says so
-
-`src/class/msc/msc_host.c:445-472` is a two-function cycle with no counter:
-
-```c
-static bool config_test_unit_ready_complete(...) {
-  if (csw->status == 0) {
-    ... tuh_msc_read_capacity(...);            // ready -> proceed to mount
-  } else {
-    // Note: During enumeration, some device fails Test Unit Ready and require a few retries
-    // with Request Sense to start working !!
-    // TODO limit number of retries                        <-- :459, pre-existing
-    TU_LOG_DRV("SCSI Request Sense\r\n");
-    TU_ASSERT(tuh_msc_request_sense(dev_addr, cbw->lun, enum_buf,
-                                    config_request_sense_complete, 0));
-  }
-  return true;
-}
-
-static bool config_request_sense_complete(...) {
-  TU_ASSERT(csw->status == 0);
-  TU_ASSERT(tuh_msc_test_unit_ready(dev_addr, cbw->lun,
-                                    config_test_unit_ready_complete, 0));   // :472
-  return true;
-}
-```
-
-Two defects, independent of each other:
-
-1. **No bound.** TUR fail -> Request Sense -> TUR -> ... forever. `tuh_msc_mount_cb()`
-   is never called and the application is never told anything; the device sits
-   enumerated-but-unmounted indefinitely.
-2. **Sense data is fetched and discarded.** `config_request_sense_complete`
-   checks only the CSW status. `enum_buf` holds a `scsi_sense_fixed_resp_t`
-   whose `sense_key` / ASC / ASCQ distinguish "Not Ready — becoming ready"
-   (retry is correct) from "Not Ready — medium not present" (a card reader with
-   no card; retrying can never succeed) from a hard error. The driver cannot
-   currently tell these apart because it never looks.
-
-### Measured on hardware (2026-08-25)
-
-Rig: `raspberry_pi_pico` (RP2040) + Pico-PIO-USB host on GP20/21, probe
-`E6614103E719612F`, console over the probe's CDC. Build:
-`-DCFG_TUH_RPI_PIO_USB=1 -DLOG=2`.
-
-- `examples/host/msc_file_explorer` never mounts. Debug log over ~25 s:
-  **1× `SCSI Test Unit Ready`, 350× `SCSI Request Sense`**, zero
-  `SCSI Read Capacity`, zero mount callbacks. `dd` reports
-  `no MSC device mounted`.
-- **The transfers themselves all succeed** — every CBW/CSW pair logs `OK`
-  (`Queue EP 02 with 31 bytes ... OK`, `Queue EP 81 with 13 bytes ... OK`), so
-  this is a SCSI-state-machine problem, not a bulk-transfer or PIO-USB timing
-  problem.
-- Reproduced with **two different drives** (`24a9:1802` "STORAGE DEVICE" and the
-  drive swapped in after it), so it is not one device's quirk.
-- Control transfers on the same target are fine: `examples/host/device_info`
-  reads full descriptors from the same drive on the same board
-  (`bcdUSB 0210`, `bMaxPacketSize0 64`, i.e. full-speed).
-- **The very same drive mounts and sustains I/O on RP2350**
-  (`pico2_etm_trace` carrier): `msc_file_explorer` + `dd` returns
-  `dd: 524288 bytes in 8448 ms = 62 KB/s`. Confirmed by the maintainer at the
-  bench, so the device is healthy and the "not ready" answer is provoked by
-  something specific to the RP2040 setup.
-- **Bumping Pico-PIO-USB does not fix it.** Retested with upstream HEAD
-  `5a37a66` (10 commits ahead of the pinned `675543b`, including
-  `512d3a2` "Place calc_usb_crc16 in RAM like calc_usb_crc5 and the CRC
-  tables", which looked like a promising RP2040 timing fix, and `cbf055d`
-  transaction-length clamp) via `-DPICO_PIO_USB_PATH=<clone>`: identical
-  failure, no mount.
-- Clock is **not** a factor: identical failure at 120 MHz, 133 MHz and
-  156 MHz on RP2040 (and on RP2350 all of 120/125/126/138/150/156/162/174/186/240 MHz
-  behave identically).
-
-### What is NOT established
-
-- The actual sense key/ASC/ASCQ the failing drives return — the driver never
-  logs it. **Task 1 below exists to capture it**, and its answer decides whether
-  a bounded retry is sufficient or a "medium not present" path is also needed.
-- **Why the RP2040 setup provokes the not-ready state.** Leading suspect is
-  VBUS quality rather than firmware: the RP2350 carrier feeds J5 through a
-  proper load switch, while the RP2040 rig is a bare Pico whose GP22 "VBUS
-  enable" drives nothing (no load switch on a bare Pico), so the drive is fed
-  directly off the VBUS pin through hookup wire. A bus-powered drive that
-  cannot spin up answers exactly this "not ready" forever. Measure VBUS at the
-  device under load, or retest with a powered hub / self-powered device,
-  BEFORE attributing the stall to the host stack.
-- The actual sense key (Task 1) — still the gate for any policy change.
-
----
-
-## What remains
-
-### Task 1: Log the sense response (diagnostic, ship-able on its own)
-
-**Files:** `src/class/msc/msc_host.c` (`config_request_sense_complete`, ~:467)
-
-Add a `TU_LOG_DRV` of `sense_key`, `add_sense_code`, `add_sense_qualifier` from
-the fixed-format response in `usbh_get_enum_buf()`. `scsi_sense_fixed_resp_t` is
-already declared in `src/class/msc/msc.h`.
-
-Verify on the rig above: rebuild `msc_file_explorer` with `-DLOG=2`, flash, read
-the probe CDC, and record the triple. Expected candidates:
-`0x02/0x04/0x01` (becoming ready) or `0x02/0x3A/0x00` (medium not present).
-
-### Task 2: Bound the retry
-
-**Files:** `src/class/msc/msc_host.c`, `msch_interface_t` (add a retry counter),
-`src/class/msc/msc_host.h` (a `CFG_TUH_MSC_TUR_RETRY_COUNT`-style knob with a
-sane default; follow the existing `CFG_TUH_MSC_*` naming in
-`src/tusb_option.h`).
-
-On exhaustion, stop the cycle and surface the failure rather than silently
-looping — the application currently has no way to learn the device is stuck.
-
-### Task 3: Decide behaviour per sense key
-
-Gated on Task 1's measurement. At minimum: keep retrying on "becoming ready",
-stop immediately on "medium not present". Do not invent policy for sense keys
-that were not observed.
-
-### Task 4: Regression coverage
-
-`test/unit-test/` has no MSC host suite today; adding one means mocking
-`tuh_msc_*` completions. Confirm with the maintainer whether a unit test or a
-HIL case on a known not-ready device (an empty card reader is the cheap
-reproducer) is the wanted evidence before building either.
-
----
-
-## Why it was split out
-
-Found while sweeping PIO-USB clocks on the `etmtrace-rp2350` branch, which
-touches only rp2040/rp2350 clock pinning and ETM trace config. This bug is in
-the class-driver layer, affects every MCU running the MSC host, and predates
-that branch (the `// TODO limit number of retries` is already in master). It
-deserves its own PR and its own hardware evidence.
diff --git a/docs/superpowers/followup/pr3853-board-putchar-logger.md b/docs/superpowers/followup/pr3853-board-putchar-logger.md
deleted file mode 100644
index 46a4417..0000000
--- a/docs/superpowers/followup/pr3853-board-putchar-logger.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# `board_putchar` is not LOGGER-aware
-
-**Origin:** surfaced while validating the RTT console in PR #3853 (the `rtt` skill
-promotion), which is harness-only scope. This is a src-level fix to `hw/bsp/board.c`
-that touches every board/logger combination, so it needs its own build sweep rather
-than a drive-by. Delete this file when its own PR lands.
-
-## Established (with evidence)
-
-`hw/bsp/board.c` retargets stdio through `sys_write`/`sys_read`, which are compiled
-per logger: `SEGGER_RTT_Write`/`SEGGER_RTT_Read` under `LOGGER_RTT`, ITM under
-`LOGGER_SWO`, `board_uart_write`/`board_uart_read` by default. The two board-level
-character helpers do not agree:
-
-```c
-168: int board_getchar(void) {
-169:   char c;
-170:   return (sys_read(0, &c, 1) > 0) ? (int) c : (-1);
-171: }
-172:
-173: int board_putchar(int c) {
-174:   if (board_uart_write((const char *)&c, 1) > 0) {
-```
-
-`board_getchar` follows the logger; `board_putchar` always goes to the UART. So with
-`LOGGER=rtt` console input arrives over RTT while the echo goes out the UART.
-
-Measured on ea4088_quickstart (`LOGGER=rtt`, `board_uart_write` is a `-1` stub on
-lpc40): the `board_test` echo vanishes entirely while a `printf` echo — same console,
-same keystroke — comes back byte-for-byte. `LOGGER=swo` has the same asymmetry by
-construction (ITM out of `sys_write`, UART out of `board_putchar`), unverified on
-hardware.
-
-## What remains
-
-Candidate fix: route `board_putchar` through `sys_write(0, ...)` for symmetry with
-`board_getchar`. Two things to settle while doing it:
-
-- `board_putchar` currently passes `&c` of an `int` to a `const char*` — it writes
-  the low byte only on little-endian. Narrow to a `char` local as part of the change.
-- The default (UART) path must keep its current return contract: `board_uart_write`
-  returns negative when the UART is a stub, and the default `sys_write` breaks out of
-  its retry loop on that, returning a short count — so `board_putchar` still has to
-  map "wrote nothing" to `-1`.
-
-## Validation
-
-Build sweep across loggers and families — at minimum one UART board, one
-`LOGGER=rtt` board and one `LOGGER=swo` board — plus a hardware check that the
-`board_test` echo comes back on an RTT board (ea4088_quickstart reproduces the bug
-today) and that a plain UART board's echo is unchanged.
-
-## Why it was split out
-
-PR #3853 promotes a debug-tooling skill and touches `test/hil/*.py` and
-`tools/rtt.py`. A `hw/bsp/board.c` change lands in every example on every board and
-belongs in a review that carries the build evidence for it.
diff --git a/docs/superpowers/followup/pr3853-rtt-harness-adoption.md b/docs/superpowers/followup/pr3853-rtt-harness-adoption.md
deleted file mode 100644
index 80562e7..0000000
--- a/docs/superpowers/followup/pr3853-rtt-harness-adoption.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Follow-up: finish RTT-console adoption in the HIL harness
-
-Split out of the `rtt` skill-promotion PR #3853. That PR deliberately ships the skill + CLI and leaves the harness's remaining
-VCOM assumptions in place — converting them is separate test-infra scope that
-deserves its own review and HIL runs. Scope here is `test/hil/*.py` only; the
-src-level `board_putchar` asymmetry this work surfaced has its own handoff
-(`pr3853-board-putchar-logger.md`).
-
-## Established (with evidence)
-
-- `hil_util.JlinkRtt` + `open_board_console()` work end-to-end:
-  ea4088_quickstart runs its host suite over RTT (16 passed / 0 failed / 3
-  skipped, the 'hil: read the host console over RTT when the probe has no VCOM' commit), and the `rtt` skill's boards.md carries the
-  validated matrix.
-- All three host tests honor `"logger": "rtt"`: `test_host_device_info`,
-  `test_host_cdc_msc_hid` and `test_host_msc_file_explorer` open through
-  `open_console_reset()` (hil_test.py), which does the per-console reset
-  ordering — RTT resets via the flasher BEFORE opening (the console owns the
-  probe; Commander delivers the buffered boot burst), VCOM resets after — and
-  each read loop fails fast on `JlinkRtt.eof` instead of blaming the board.
-  Landed with the ea4088_quickstart roster entry; the interim load-time gate
-  that rejected `logger: rtt` + `is_cdc`/`is_msc` is gone.
-
-## Remaining gaps
-
-1. **`hil_pool_check.check_host_serial` carries its own inline RTT branch**
-   (reset → `JlinkRtt` → poll through `hil_util.strip_banner`) — RTT boards
-   ARE health-checkable today, but the console-opening logic now lives in
-   two places (`open_console_reset` in hil_test.py and this branch), each
-   with its own reset-ordering. Fix: hoist `open_console_reset()` into
-   `hil_util.py` next to `open_board_console()` and collapse pool_check's
-   branch onto it; keep the `do_reset` flush semantics for the VCOM path
-   intact.
-2. **OpenOCD console backend in the harness**: the skill's CLI
-   (`tools/rtt.py --backend openocd`, class
-   `OpenocdRtt` in the same module) is built, deduplicated behind a shared
-   base class next to `JlinkRtt` in `tools/rtt.py`, re-exported by
-   `hil_util`, and hardware-validated (all 20 rig boards through the CLI on
-   both backends, incl. the 8 native-probe ones). What remains is only the
-   `open_board_console` plumbing: choosing `OpenocdRtt` for a
-   `"logger": "rtt"` board with an openocd/stlink flasher needs the per-test
-   flashed-ELF path (for the control-block address) and, for stlink
-   flashers, an openocd target-cfg mapping the roster doesn't carry — until
-   then the config-load gate keeps rejecting non-jlink rtt boards.
-
-## Validation for this follow-up
-
-A `hil_pool_check.py` pass on a no-VCOM board (ea4088_quickstart), plus the
-ea4088 host suite to show the collapse did not change the reset ordering the
-three tests depend on. Delete this doc when the follow-up PR lands.
diff --git a/docs/superpowers/plans/2026-08-18-claude-doc-audit.md b/docs/superpowers/plans/2026-08-18-claude-doc-audit.md
index 0d58614..0971f65 100644
--- a/docs/superpowers/plans/2026-08-18-claude-doc-audit.md
+++ b/docs/superpowers/plans/2026-08-18-claude-doc-audit.md
@@ -1,5 +1,7 @@
 # `.claude/` Instruction-Surface Audit Implementation Plan
 
+> Deferred-work instructions below describe the original file-based workflow. Follow CLAUDE.md instead: create a GitHub issue labeled `followup`, preserve the full handoff in its body, and add revalidation and new findings as comments.
+
 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
 
 **Goal:** Give every falsifiable claim in the 4,689-line `.claude/` + `CLAUDE.md` instruction surface a verdict backed by a citation, correct the ones current source refutes, and remove duplication without deleting hard-earned rig knowledge.
diff --git a/docs/superpowers/plans/2026-08-24-rtt-skill.md b/docs/superpowers/plans/2026-08-24-rtt-skill.md
index e2a40c4..f083c15 100644
--- a/docs/superpowers/plans/2026-08-24-rtt-skill.md
+++ b/docs/superpowers/plans/2026-08-24-rtt-skill.md
@@ -1,5 +1,7 @@
 # `rtt` Skill Implementation Plan
 
+> The follow-up file and commit steps below are superseded by [issue #3899](https://github.com/hathach/tinyusb/issues/3899). Add revalidation and new findings as issue comments, per CLAUDE.md.
+
 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
 
 **Goal:** Promote SEGGER RTT to a standalone skill `.claude/skills/rtt/` (transport core + console layer) with a versioned CLI, validated first on the local htpc bench, then across the ci.lan rig.
diff --git a/docs/superpowers/specs/2026-08-18-claude-doc-audit-design.md b/docs/superpowers/specs/2026-08-18-claude-doc-audit-design.md
index 5b150dc..09e89f5 100644
--- a/docs/superpowers/specs/2026-08-18-claude-doc-audit-design.md
+++ b/docs/superpowers/specs/2026-08-18-claude-doc-audit-design.md
@@ -1,5 +1,7 @@
 # Audit of the `.claude/` instruction surface — design
 
+> Deferred-work instructions below describe the original file-based workflow. Follow CLAUDE.md instead: create a GitHub issue labeled `followup`, preserve the full handoff in its body, and add revalidation and new findings as comments.
+
 **Date:** 2026-08-18
 **Branch:** `claude/hil-doc-audit`
 
diff --git a/test/hil/test/test_hil_bounded.py b/test/hil/test/test_hil_bounded.py
index c30c58c..3697cf4 100644
--- a/test/hil/test/test_hil_bounded.py
+++ b/test/hil/test/test_hil_bounded.py
@@ -1620,7 +1620,7 @@
 class StrayNoteSurvivesTheTupleWidth(unittest.TestCase):
     """_stray_note reads r[5] -- and three producers build this tuple at three widths, so
     `len(r) > 5 and r[5]` reads a WRONG SLOT rather than raising if a field is ever
-    inserted. The live handoff pr3840-mret-board-result.md proposes exactly that, and the
+    inserted. The live handoff in issue #3896 proposes exactly that, and the
     report would then say "no strays" while probes and usbfs nodes stay held into the next
     job. The index changed once already in this branch (r[6] -> r[5])."""