summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/superpowers/followup/pr3853-rtt-harness-adoption.md72
-rw-r--r--docs/superpowers/plans/2026-08-24-rtt-skill.md423
-rw-r--r--docs/superpowers/specs/2026-08-24-rtt-skill-design.md164
3 files changed, 659 insertions, 0 deletions
diff --git a/docs/superpowers/followup/pr3853-rtt-harness-adoption.md b/docs/superpowers/followup/pr3853-rtt-harness-adoption.md
new file mode 100644
index 000000000..b8a6c4f7c
--- /dev/null
+++ b/docs/superpowers/followup/pr3853-rtt-harness-adoption.md
@@ -0,0 +1,72 @@
+# 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.
+
+## 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.
+- `test_host_device_info` honors `"logger": "rtt"` (hil_test.py, `test_host_device_info`; the eof fail-fast assert sits in its read loop):
+ in RTT mode it resets via the flasher BEFORE opening the console (which
+ then owns the probe; Commander delivers the buffered boot burst) and its
+ read loop fails fast on `JlinkRtt.eof` instead of blaming the board.
+
+## Remaining gaps
+
+1. **`test_host_cdc_msc_hid` and `test_host_msc_file_explorer` (hil_test.py) still call `hil_util.get_serial_dev(flasher["uid"], ...)`
+ directly** — on a `logger: rtt` board with `is_cdc`/`is_msc` fixtures they
+ would fail with the same "No serial device found" the console work fixed
+ for device_info (an interim load-time gate in `hil_test.py` now rejects
+ that combination up front; delete the gate when this lands). Fix: route
+ both through `open_board_console(board)` — but design the conversion
+ reset-aware rather than hand-copying device_info's dual branch: hoist a
+ `reset=` parameter into `open_board_console` that does the per-console
+ ordering itself (RTT: reset via flasher BEFORE opening — the console owns
+ the probe; VCOM: reset after open to catch the banner), and REMOVE the
+ existing post-open `# reset device to catch mount messages` blocks in both
+ tests (grep the marker — line numbers churn) — kept as-is on an RTT board they reset
+ while the console holds the probe. `JlinkRtt` carries input for their
+ menus and implements the `reset_input_buffer()` those tests call.
+2. **`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_board_console` in hil_test.py and this branch), each
+ with its own reset-ordering. Fix: hoist `open_board_console()` into
+ `hil_util.py` with the `reset=` parameter from item 1 and collapse
+ pool_check's branch onto it; keep the `do_reset` flush semantics for the
+ VCOM path intact.
+3. **`board_putchar` is not LOGGER-aware** (`hw/bsp/board.c:173`): it writes
+ via `board_uart_write` while `board_getchar` reads via `sys_read` — so
+ with `LOGGER=rtt` console input arrives over RTT but `board_putchar`
+ output goes to the UART (a `-1` stub on lpc40: measured on ea4088, the
+ board_test echo vanishes while a printf echo comes back byte-for-byte).
+ Candidate upstream fix: route `board_putchar` through `sys_write` for
+ symmetry. Src-level, affects every board/logger combination — needs its
+ own build sweep, not a drive-by.
+4. **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.
+5. **Deferred `sysview` pointer**: when branch `claude/add-systemview-debug`
+ merges, propose (to the user, before editing — curated-skills rule)
+ replacing sysview SKILL.md's duplicated "stand up an RTT server" recipe
+ with a pointer to the `rtt` skill. No edits to `sysview_ci.py`.
+
+## Validation for this follow-up
+
+Run the ea4088 local host suite (a board with a `is_cdc`+`is_msc` capable
+device attached to J3, or the rig's frdm_k64f/mimxrt1064 with a temporary
+`logger: rtt` entry) so cdc_msc_hid and msc_file_explorer actually execute
+over RTT; then a `hil_pool_check.py` pass on a no-VCOM board. Delete this doc
+when the follow-up PR lands.
diff --git a/docs/superpowers/plans/2026-08-24-rtt-skill.md b/docs/superpowers/plans/2026-08-24-rtt-skill.md
new file mode 100644
index 000000000..e2a40c448
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-24-rtt-skill.md
@@ -0,0 +1,423 @@
+# `rtt` Skill 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:** 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.
+
+**Architecture:** Knowledge lives in `.claude/skills/rtt/SKILL.md` + `boards.md`; the single code implementation is `test/hil/helper/hil_util.py::RttConsole` (cherry-picked from branch `hil-add-ea4088qs`) exposed via a thin CLI `test/hil/helper/rtt.py`. Existing docs (target-debug, CLAUDE.md, hil) shrink their RTT recipes to pointers.
+
+**Tech Stack:** Python 3 (stdlib only, matching hil_util), JLinkExe, OpenOCD, TinyUSB `LOGGER=rtt` builds, TDD-for-skills (superpowers:writing-skills).
+
+> **Historical record — EXECUTED 2026-08-24/25.** The shipped shape evolved past
+> this plan during review rounds: the implementation is `tools/rtt.py` (classes
+> `JlinkRtt`/`OpenocdRtt`, `--backend` required), not `test/hil/helper/`. The
+> spec's "Tooling home" section is the current truth; do not re-execute this plan.
+
+**Spec:** `docs/superpowers/specs/2026-08-24-rtt-skill-design.md` — read it first; every content decision below argues from it.
+
+## Global Constraints
+
+- Branch: `rttconsole-skill`, worktree `/home/hathach/.herdr/worktrees/tinyusb/rttconsole-skill`. Never touch the primary checkout's branch.
+- Commit messages: imperative mood, **no `Co-Authored-By:`/`Claude-Session:` trailers, no footers of any kind** (user's standing authorship rule — overrides harness defaults).
+- **Never push.** Commit locally; final report says "ready to push".
+- Curated-skills rule: smallest possible diffs to existing skills/agents/CLAUDE.md; anything beyond the pointer edits listed here must be proposed to the user first.
+- Iron Law (superpowers:writing-skills): no SKILL.md content and no edit to an existing skill without a failing/baseline test first.
+- Hardware rules: **never point OpenOCD at a J-Link-firmware probe** (LPC-Link2 611000000, the J-Trace (nickname `jtrace`; its serial is private — read it with ShowEmuList on the bench) — it drops them off USB; each attempt costs the user a physical replug). J-Trace is wired to raspberry_pi_pico2 (never set a custom JLinkScript for RP2350). Prefix any step needing the user's hands with **[ACTION]**.
+- ci.lan rig work: hold per-board locks per `.claude/skills/hil/SKILL.md` §Board locks; the actions-runner keeps running. Use the hil-operator agent for rig sweeps (strictly one instance).
+- Scratch files go in the session scratchpad, never `/tmp`, never committed.
+- `pre-commit run --all-files` must pass before declaring done.
+
+---
+
+### Task 1: Bring the tooling onto this branch
+
+**Files:**
+- Modify: `test/hil/helper/hil_util.py` (via cherry-pick + docstring fix)
+- Modify: `test/hil/hil_test.py` (via cherry-pick)
+
+**Interfaces:**
+- Produces: `hil_util.RttConsole(board: dict, timeout: float = 0.1)` where `board = {'flasher': {'uid': '<probe-serial>', 'args': '-device <JLINK_DEVICE>'}}`; methods `read(size)->bytes`, `write(bytes)->int`, `in_waiting->int`, `close()`, attr `timeout`. Also `hil_test.open_board_console(board)`.
+
+- [ ] **Step 1: Symlink missing deps** (worktree has `lib/SEGGER_RTT` but not the MCU SDKs):
+
+```bash
+cd /home/hathach/.herdr/worktrees/tinyusb/rttconsole-skill
+python3 - <<'EOF'
+import os, sys
+sys.path.insert(0, 'tools'); import get_deps
+main = os.path.expanduser('~/code/tinyusb')
+for dep in get_deps.deps_all:
+ src, dst = os.path.join(main, dep), dep
+ if not os.path.exists(dst) and os.path.isdir(src):
+ os.makedirs(os.path.dirname(dst), exist_ok=True); os.symlink(src, dst); print('link', dep)
+EOF
+```
+
+- [ ] **Step 2: Cherry-pick the console commit** (object store is shared across worktrees):
+
+```bash
+git cherry-pick d98e77bac
+```
+
+Expected: clean pick of `hil: read the host console over RTT when the probe has no VCOM` (touches only hil_util.py + hil_test.py). If it conflicts, resolve keeping d98e77bac's hunks verbatim — master has not touched these regions.
+
+- [ ] **Step 3: Fix the stale docstring.** `RttConsole`'s docstring opens with "JLinkGDBServer owns the probe and serves RTT channel 0 over TCP" but the code launches `JLinkExe` (J-Link Commander). Edit the docstring's first paragraph to:
+
+```
+ J-Link Commander (JLinkExe) owns the probe and serves RTT channel 0 on -RTTTelnetPort --
+ what JLinkRTTClient talks to, minus its banner. Exposes the slice of pyserial the tests
+ use (read, in_waiting, write, close, timeout) so a caller does not care which console it got.
+```
+
+- [ ] **Step 4: Import smoke test:**
+
+```bash
+python3 -c "import sys; sys.path.insert(0,'test/hil/helper'); import hil_util; print(hil_util.RttConsole.__doc__.splitlines()[1].strip()[:20])"
+```
+
+Expected: `J-Link Commander (JL`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add test/hil/helper/hil_util.py
+git commit -m "hil: RttConsole docstring names the tool it actually runs (JLinkExe)"
+```
+
+---
+
+### Task 2: RED — baseline scenarios without the skill
+
+Per superpowers:writing-skills, run the failing test before writing any skill text. These are **plan-only** subagents (they must output the exact commands they would run and MUST NOT execute anything against hardware — a wrong baseline attempt costs a probe replug). The lpc4088 session's real lost hour is the primary RED datapoint; these probes map the gap precisely.
+
+**Files:**
+- Create: `<scratchpad>/rtt-baselines.md` (verbatim findings; not committed)
+
+- [ ] **Step 1: Scenario S1 (console/harness routing + technique).** Dispatch a general-purpose subagent, no mention of RTT:
+
+> In the TinyUSB repo at /home/hathach/.herdr/worktrees/tinyusb/rttconsole-skill: board ea4088_quickstart is flashed via an LPC-Link2 running J-Link firmware (serial 611000000). The probe exposes no VCOM and hw/bsp/lpc40/family.c's board_uart_read/write return -1. PLAN ONLY — do not run any hardware command. First list which repo skill(s) (.claude/skills/) you would load for this task and why. Then produce the exact commands to (a) get the firmware's printf/TU_LOG output on this PC headlessly and (b) send keystrokes to the firmware. State every failure mode you anticipate.
+
+- [ ] **Step 2: Scenario S2 (capture technique, OpenOCD/ST-Link).** Same rules:
+
+> PLAN ONLY. TinyUSB repo, board stm32h743nucleo flashed over an ST-Link. The firmware was built with LOG=2 LOGGER=rtt. Produce the exact commands to capture 20 seconds of its RTT log headlessly on Linux, and explain how you locate the RTT control block and what can go wrong right after a reset.
+
+- [ ] **Step 3: Record baseline verbatim** in `<scratchpad>/rtt-baselines.md`: which skills each agent said it would load (expected gap: nothing routes, or target-debug loaded for a non-debugging task), which tool each picked (expected: JLinkRTTLogger or bare JLinkGDBServer for S1; full-RAM `rtt setup` scan for S2), which known gotchas each missed (control-block-after-first-printf, probe-by-serial, exact CB address via nm, attach-only after flash-reset, drain-limited/lossy, probe ownership). Every missed item becomes required SKILL.md content; every wrong routing becomes description-keyword input.
+
+- [ ] **Step 4: Gate.** If a baseline agent nails everything (no gaps), STOP and tell the user — the skill may not be needed in that area and the plan's GREEN content shrinks. (Do not expect this; the lpc4088 session is an existence proof of the failure.)
+
+---
+
+### Task 3: `rtt.py` CLI (TDD)
+
+**Files:**
+- Create: `test/hil/helper/rtt.py`
+- Test: fake-probe harness in `<scratchpad>/fakejlink/` (not committed)
+
+**Interfaces:**
+- Consumes: `hil_util.RttConsole` from Task 1.
+- Produces: CLI `python3 test/hil/helper/rtt.py --probe <serial> --device <JLINK_DEVICE> [--seconds N] [-i]` — streams channel-0 bytes to stdout; `--seconds 0` (default) runs until Ctrl-C/EOF; `-i` forwards stdin to the target. Exit 0 on clean close, 1 on connect failure.
+
+- [ ] **Step 1: Write the fake probe** `<scratchpad>/fakejlink/JLinkExe` (`chmod +x`):
+
+```python
+#!/usr/bin/env python3
+# Stands in for J-Link Commander: serves -RTTTelnetPort, greets, echoes input back
+# uppercased, exits when stdin says exit (mirrors RttConsole's close() contract).
+import socket, sys, threading
+port = int(sys.argv[sys.argv.index('-RTTTelnetPort') + 1])
+srv = socket.socket(); srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+srv.bind(('127.0.0.1', port)); srv.listen(1)
+def serve():
+ conn, _ = srv.accept()
+ conn.sendall(b'hello from target\r\n')
+ while True:
+ d = conn.recv(4096)
+ if not d: return
+ conn.sendall(d.upper())
+threading.Thread(target=serve, daemon=True).start()
+for line in sys.stdin:
+ if line.strip() == 'exit': break
+```
+
+- [ ] **Step 2: Run the failing test:**
+
+```bash
+cd /home/hathach/.herdr/worktrees/tinyusb/rttconsole-skill
+PATH=<scratchpad>/fakejlink:$PATH timeout 15 python3 test/hil/helper/rtt.py --probe 000 --device FAKE --seconds 2
+```
+
+Expected: FAIL — `No such file or directory` (rtt.py does not exist).
+
+- [ ] **Step 3: Implement** `test/hil/helper/rtt.py`:
+
+```python
+#!/usr/bin/env python3
+"""Stream a board's RTT channel-0 console to stdout over a J-Link probe.
+
+Thin CLI over hil_util.RttConsole -- the same implementation the HIL harness uses.
+The probe is owned for the whole run: flash and reset BEFORE starting this, never
+reset the target while it is attached. Select the probe by serial; rigs run several.
+"""
+import argparse
+import sys
+import threading
+import time
+
+import hil_util # same directory when run by path
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument('--probe', required=True, help='J-Link probe serial (JLinkExe -USB value)')
+ ap.add_argument('--device', required=True, help='JLINK_DEVICE string from the board.cmake/family.cmake')
+ ap.add_argument('--seconds', type=float, default=0, help='capture duration; 0 = until Ctrl-C/EOF')
+ ap.add_argument('-i', '--interactive', action='store_true', help='forward stdin to the target')
+ args = ap.parse_args()
+
+ board = {'flasher': {'uid': args.probe, 'args': f'-device {args.device}'}}
+ try:
+ con = hil_util.RttConsole(board, timeout=0.1)
+ except RuntimeError as e:
+ print(e, file=sys.stderr)
+ return 1
+
+ if args.interactive:
+ def pump_stdin():
+ for line in sys.stdin:
+ con.write(line.encode())
+ threading.Thread(target=pump_stdin, daemon=True).start()
+
+ deadline = time.monotonic() + args.seconds if args.seconds else None
+ try:
+ while deadline is None or time.monotonic() < deadline:
+ chunk = con.read(con.in_waiting or 1)
+ if chunk:
+ sys.stdout.buffer.write(chunk)
+ sys.stdout.buffer.flush()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ con.close()
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
+```
+
+- [ ] **Step 4: Run the tests, verify they pass:**
+
+```bash
+P=<scratchpad>/fakejlink
+PATH=$P:$PATH timeout 15 python3 test/hil/helper/rtt.py --probe 000 --device FAKE --seconds 2 # expect: hello from target
+echo hi | PATH=$P:$PATH timeout 15 python3 test/hil/helper/rtt.py --probe 000 --device FAKE --seconds 2 -i # expect: hello from target + HI
+pgrep -f '[J]LinkExe -USB 000' && echo LEAK || echo CLEAN # expect: CLEAN (bracket: else pgrep matches its own shell)
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add test/hil/helper/rtt.py
+git commit -m "hil: add rtt.py, a CLI over RttConsole"
+```
+
+---
+
+### Task 4: GREEN — write `.claude/skills/rtt/SKILL.md` + `boards.md` skeleton
+
+Write the skill addressing Task 2's recorded failures — nothing more (minimal GREEN). All facts below are established in the spec; the drafting job is assembling them into the sibling-skill shape (structure model: `sysview` SKILL.md; ~150–200 lines).
+
+**Files:**
+- Create: `.claude/skills/rtt/SKILL.md`
+- Create: `.claude/skills/rtt/boards.md`
+
+- [ ] **Step 1: Frontmatter.** Name `rtt`. Description (trigger-only, third person, no workflow — superpowers:writing-skills SDO; extend with keywords from Task 2's routing misses):
+
+```yaml
+---
+name: rtt
+description: Use when you need console or printf I/O, TU_LOG capture, or a raw byte channel over a debug probe on real hardware — the board has no UART wired or its probe no VCOM, a LOGGER=rtt build needs reading or writing, "RTT Control Block not found", an RTT server won't come up or drops output, JLinkRTTLogger/JLinkRTTClient/JLinkGDBServer/openocd rtt misbehave, or another workflow (HIL console, SystemView capture) needs RTT stood up on a J-Link, ST-Link, CMSIS-DAP or WCH-Link probe.
+---
+```
+
+- [ ] **Step 2: Body sections**, each carrying exactly this content (wording final at execution, facts verbatim from the spec):
+ 1. **Overview** — RTT is nothing but RAM (control block `_SEGGER_RTT`, magic "SEGGER RTT", up/down rings `{sName,pBuffer,SizeOfBuffer,WrOff,RdOff,Flags}`); host must write RdOff back to drain; channel 0 = console, SystemView's "SysView" buffer coexists.
+ 2. **When to use / when not** — console & capture here; timing/profiling → etm-trace/sysview; debugging decision flows → target-debug; Espressif console → esp-target-debug.
+ 3. **Transport matrix (quick reference table)** — spec §v1 backend matrix verbatim, per-TRANSPORT rows: ARM memory-AP (live, zero intrusion) / RISC-V SBA (live where implemented) / WCH SDI (**dump only, never live** — DM reads kill USB ~1.9 s in) / OpenOCD-on-J-Link-fw-probe (forbidden, USB drop + physical replug).
+ 4. **Console (bidirectional)** — `LOGGER=rtt` builds route TU_LOG + `sys_read` to channel 0 (`hw/bsp/board.c`); tooling `test/hil/helper/rtt.py` (CLI) / `hil_util.RttConsole` (harness, `"logger": "rtt"` board switch); flash+reset BEFORE opening, console owns the probe.
+ 5. **Capture: J-Link route** — `JLinkExe -USB <sn> -device <dev> -if swd -speed 4000 -NoGui 1 -AutoConnect 1 -RTTTelnetPort <port>` + socket/`nc`; proven standalone. `JLinkGDBServer -RTTTelnetPort` locates the block on some parts only with a GDB client attached (LPC4088 measured) — per-part variance, use JLinkExe when headless. `JLinkRTTLogger`: never (single search at attach, 0/6 measured).
+ 6. **Capture: OpenOCD route (native probes)** — exact CB address first (`arm-none-eabi-nm <elf> | grep _SEGGER_RTT`), then `-c 'rtt setup <addr> 0x1000 "SEGGER RTT"' -c 'rtt polling_interval 1' -c 'rtt start' -c 'rtt server start <port> 0'`; attach without reset when the flash step already reset (SAMD5x DSU `reset run` leaves the core held); read path validated on 13 boards (sysview campaign), write path per boards.md.
+ 7. **Post-mortem** — undrained NO_BLOCK_SKIP ring holds the FIRST KB after boot, not the wedge tail; overwrite mode (`SEGGER_RTT_WriteWithOverwriteNoLock`) keeps the last N bytes with no live host; manual ring read: `nm` the ELF for `_SEGGER_RTT`, `mem32` the aUp[0] descriptor, `savebin` the buffer — debug-AP reads don't halt the target (moved here from target-debug).
+ 8. **Buffer modes & locking** — SKIP/TRIM/BLOCK (BLOCK spins the target — dangerous in ISRs); non-ARM ports must supply `SEGGER_RTT_LOCK/UNLOCK` (worked example: `hw/bsp/ch583/sysview_rtt_lock_wch.h` on branch `claude/add-systemview-debug` — generic RISC-V lock traps mcause=2 on QingKe).
+ 9. **Common mistakes** — attach before first printf (block doesn't exist yet); reset while attached; probe not pinned by serial; two probes on one SWD header; treating RTT as lossless (24.6 KiB/s drain measured, drops at the target); full-RAM scan matching stale RAM after soft reset.
+ 10. **Per-board notes** → pointer to `boards.md`.
+
+- [ ] **Step 3: `boards.md` skeleton** — header modeled on sysview's boards.md (row = board, probe/transport, backend+direction validated, JLINK_DEVICE/openocd cfg, caveats), plus the two measured rows seeded from the spec: `ea4088_quickstart` (J-Link/LPC-Link2 611000000, read+write-accepted, `LPC4088`, "probe has no VCOM; BSP has no UART; never OpenOCD on this probe") and a placeholder-free note that all further rows land during Tasks 7–8 validation (no unvalidated rows allowed).
+
+- [ ] **Step 4: Length check:** `wc -l .claude/skills/rtt/SKILL.md` — expect ≤ ~200 (siblings: hil 168, etm-trace 203).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add .claude/skills/rtt/
+git commit -m "skills: add rtt - RTT transport and console reference"
+```
+
+---
+
+### Task 5: GREEN verification + REFACTOR
+
+- [ ] **Step 1: Re-run S1 and S2** (Task 2 prompts verbatim, still plan-only) with fresh subagents. Success criteria: S1 routes to the `rtt` skill, picks `rtt.py`/JLinkExe route, names probe-by-serial + flash-before-attach; S2 uses exact CB address via `nm`, attach-only, and the openocd command block.
+- [ ] **Step 2: REFACTOR.** Any missed item or new wrong turn → tighten the specific SKILL.md section (form per writing-skills "Match the Form to the Failure": these are technique/reference failures → recipes and required table slots, not prohibitions) → re-run that scenario until it passes.
+- [ ] **Step 3: Commit** (`git add .claude/skills/rtt/SKILL.md && git commit -m "skills: rtt - close gaps found in scenario verification"`) — only if Step 2 changed anything.
+
+---
+
+### Task 6: Pointer edits in existing docs
+
+Iron Law for skill edits: the failing test is S3 below, run BEFORE editing.
+
+**Files:**
+- Modify: `.claude/skills/target-debug/SKILL.md:224-253`
+- Modify: `CLAUDE.md:77`
+- Modify: `.claude/skills/hil/SKILL.md` (one added line)
+
+- [ ] **Step 1: S3 baseline (failing test).** Plan-only subagent:
+
+> PLAN ONLY. In this TinyUSB repo, a HIL host test on a board whose flasher probe has no VCOM fails with "No serial device found for /dev/serial/by-id/usb-*_<uid>-if*". Which repo skill(s) would you load, and what is the fix path?
+
+Expected FAIL today: the agent loads `hil` (correct routing) but `hil` says nothing about RTT consoles, so the fix path is rediscovery. Record verbatim.
+
+- [ ] **Step 2: Edit `hil/SKILL.md`** — add one line under its Prerequisites section (placement judgment at execution; content fixed):
+
+```
+- A board whose probe has no VCOM (or whose BSP has no UART) uses RTT as its console: `"logger": "rtt"` + `"build": {"args": ["LOGGER=rtt"]}` in its config entry — see the rtt skill.
+```
+
+- [ ] **Step 3: Edit `target-debug/SKILL.md`.** (a) Replace the two RTT lines of the capture block at 224-226 with:
+
+```bash
+# RTT (probe console; details, servers, gotchas: rtt skill):
+timeout 20s python3 test/hil/helper/rtt.py --probe <sn> --device <JLINK_DEVICE> > /tmp/rtt.log
+```
+
+(b) Replace the OpenOCD RTT block (232-237) with the single line: `` OpenOCD RTT (native probes): rtt skill §OpenOCD — exact CB address from `nm`, attach-only. `` Keep the drain-preference sentence that follows. (c) Keep the drain-model paragraph (242-247) unchanged; replace 248-253 (GDBServer/RTTLogger/manual-ring-read) with:
+
+```
+Stand up the drain per the **rtt** skill: JLinkExe's `-RTTTelnetPort` is the
+headless-proven route; GDBServer's needs a GDB client on some parts, and
+JLinkRTTLogger never works. The manual ring read for a wedged target
+(`nm`/`mem32`/`savebin`) lives there too.
+```
+
+(d) Line 334's correlation one-liner: swap `JLinkRTTClient` for the `rtt.py` invocation from (a). Keep the capture-channel table rows 64-65 unchanged.
+
+- [ ] **Step 4: Edit `CLAUDE.md:77`** to:
+
+```
+**RTT:** build `LOG=2 LOGGER=rtt`; capture/console via the `rtt` skill (`.claude/skills/rtt/SKILL.md`).
+```
+
+- [ ] **Step 5: GREEN for the edits.** Re-run S3 (expect: hil → rtt route, `logger: rtt` fix path) AND re-run S1 once more (expect: unchanged pass — the removed target-debug text must be reachable through the pointers). Also grep for dangling references: `grep -rn "JLinkRTTClient\|RTTTelnetPort" CLAUDE.md .claude/ | grep -v skills/rtt` — every remaining hit must be a deliberate pointer or the sysview branch's own copy.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add .claude/skills/target-debug/SKILL.md .claude/skills/hil/SKILL.md CLAUDE.md
+git commit -m "docs: route RTT recipes through the rtt skill"
+```
+
+---
+
+### Task 7: Dogfood on the local htpc bench
+
+Follow ONLY the SKILL.md text (dogfood discipline: gaps found here are REFACTOR input, fixed in SKILL.md before moving on). **[ACTION]-gate with the user before first hardware touch**: confirm LPC-Link2 (611000000) is back on USB and J-Trace (`jtrace`) is on pico2 with pico2 powered.
+
+**Files:**
+- Modify: `.claude/skills/rtt/boards.md` (validated rows)
+- Modify: `.claude/skills/rtt/SKILL.md` (only if dogfood exposes gaps)
+- Create: `test/hil/local.json` (untracked — copy from the lpc4088 worktree)
+
+- [ ] **Step 1: Probe roster check:** `JLinkExe -CommandFile <(echo -e 'ShowEmuList\nexit')` (or `lsusb`) — expect 611000000 and the jtrace probe. Missing probe → **[ACTION]** ask the user, do not improvise.
+
+- [ ] **Step 2: ea4088 bidirectional echo (board_test).** Build + flash + echo, exactly as SKILL.md describes it:
+
+```bash
+cd examples/device/board_test && mkdir -p build-ea4088 && cd build-ea4088
+cmake -DBOARD=ea4088_quickstart -DLOG=2 -DLOGGER=rtt -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. && cmake --build .
+ninja board_test-jlink # flashes via the LPC-Link2; resets the target
+cd ../../../..
+(sleep 1; echo ping) | timeout 15 python3 test/hil/helper/rtt.py --probe 611000000 --device LPC4088 --seconds 8 -i | tee <scratchpad>/ea4088-echo.log
+```
+
+Expected: board_test's periodic print lines AND the echoed `ping` (board_test echoes `board_getchar()`). This is the first true validation of target-side console INPUT consumption (the 8550-byte measurement only proved the socket accepted the bytes).
+
+- [ ] **Step 3: ea4088 HIL host suite over RTT.** Copy the untracked config: `cp /home/hathach/.herdr/worktrees/tinyusb/hil-add-ea4088qs/test/hil/local.json test/hil/local.json`. Build the full example set (`cd examples && cmake -B cmake-build-ea4088_quickstart -DBOARD=ea4088_quickstart -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . && cmake --build cmake-build-ea4088_quickstart` — LOGGER=rtt comes from local.json's `build.args`; verify the harness applies it, else add `-DLOGGER=rtt -DLOG=2`). Run per `.claude/skills/hil/SKILL.md` §Local execution against `local.json`. Expected: ≥ 16 passed / 0 failed (parity with d98e77bac's measured result).
+
+- [ ] **Step 4: pico2 second-probe/second-architecture capture.** Two J-Links are attached — the flash target MUST pin the probe:
+
+```bash
+cd examples/device/cdc_msc && mkdir -p build-pico2 && cd build-pico2
+cmake -DBOARD=raspberry_pi_pico2 -DLOG=2 -DLOGGER=rtt -DJLINK_OPTION="-USB <jtrace-serial>" -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. && cmake --build .
+ninja cdc_msc-jlink
+cd ../../../..
+timeout 15 python3 test/hil/helper/rtt.py --probe <jtrace-serial> --device rp2350_m33_0 --seconds 8 | tee <scratchpad>/pico2-rtt.log
+```
+
+(Verify `-DJLINK_OPTION` is the pin mechanism in `hw/bsp/rp2040/family.cmake` before flashing; if the variable differs, use the family's actual one — do NOT flash with an unpinned `-jlink` target.) Expected: TinyUSB init/TU_LOG lines. Silence → check SKILL.md's own troubleshooting first (block-after-first-printf, wrong device string); if it doesn't resolve the silence, that's a dogfood gap → REFACTOR.
+
+- [ ] **Step 5: Record boards.md rows** for ea4088_quickstart (upgrade: write path VALIDATED via echo) and raspberry_pi_pico2 (J-Trace, `rp2350_m33_0`, "pin probe by serial — bench runs two J-Links; never a custom JLinkScript"). Apply any SKILL.md refactors the dogfood forced.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add .claude/skills/rtt/
+git commit -m "skills: rtt - htpc dogfood rows (ea4088 bidirectional, pico2 capture)"
+```
+
+---
+
+### Task 8: ci.lan rig sweep — all applicable boards
+
+Goal: a boards.md row per rig board, per its transport. Drive hardware through the hil-operator agent (one instance), locks per hil skill. Builds: `LOGGER=rtt LOG=2` `board_test` per board (echo validates both directions where the backend supports writes). Firmware left on boards is fine — CI reflashes every run.
+
+**Files:**
+- Modify: `.claude/skills/rtt/boards.md`
+- Create: `<scratchpad>/rtt_sweep/` (per-board logs; not committed)
+
+- [ ] **Step 1: Build matrix.** From `test/hil/tinyusb.json` take all boards; groups: jlink×12, openocd×9, stlink×3; excluded with reasons recorded in boards.md: esptool×2 (no SEGGER-RTT path in our builds — USB-Serial-JTAG console), ek_tm4c123gxl (lm4flash only, no probe path configured on the rig). For each included board build `examples/device/board_test` with `-DLOG=2 -DLOGGER=rtt` locally where the toolchain exists (arm-none-eabi covers all but WCH); WCH boards (nanoch32v203, ch32v103, ch32v307, ch582m): build only if the riscv toolchain is present locally or on ci.lan — otherwise record `skipped: no riscv toolchain` rather than silently dropping (no silent caps).
+
+- [ ] **Step 2: Stage on ci.lan:** `scp` each ELF/bin + `test/hil/helper/{hil_util.py,rtt.py}` to `[email protected]:~/rtt-sweep/`.
+
+- [ ] **Step 3: Per-board procedure** (hil-operator executes on ci.lan; lock → flash → capture → echo → release):
+ - **jlink boards:** flash with the board's rig flasher recipe (uid + `-device` from tinyusb.json `flasher.args`), then `(sleep 1; echo ping) | timeout 15 python3 ~/rtt-sweep/rtt.py --probe <uid> --device <dev> --seconds 8 -i`. PASS = periodic board_test output + `ping` echoed.
+ - **stlink + openocd boards (native probes):** CB address from the local ELF (`arm-none-eabi-nm board_test.elf | grep _SEGGER_RTT`, computed before scp, carried in the sweep table). Then on ci.lan, one session per board using the board's existing openocd args from tinyusb.json plus: `-c 'adapter serial <uid>' -c 'rtt setup <addr> 0x1000 "SEGGER RTT"' -c 'rtt polling_interval 1' -c 'rtt start' -c 'rtt server start <port> 0'`; attach WITHOUT reset (flash already reset it). Read: `timeout 8 nc localhost <port>`. Write test: `(sleep 1; echo ping; sleep 3) | nc localhost <port>` — PASS/FAIL per direction recorded separately; a write failure here is a finding, not a blocker (spec: OpenOCD write path is the open question this phase answers).
+ - **WCH boards (WCH-Link, SDI):** NO live streaming, NO rtt server during USB traffic. Validation = post-mortem-style read only: flash, let it run 5 s, then `halt; read the ring via nm address + mdw/dump_image; resume` in one short openocd/wlink session. PASS = ring contains board_test's boot output. Any anomaly → stop, quiesce the DM (rig standing rule), record.
+- [ ] **Step 4: Per-board rows into boards.md** — board, transport, read/write verdicts, device string / cfg, caveat. Every board in tinyusb.json appears: validated, failed (with symptom), or skipped (with reason). If OpenOCD write path validated, update SKILL.md's transport matrix row; if not, matrix row says "read-only validated; write untested/failed on <boards>".
+- [ ] **Step 5: Restore rig state:** release all locks; run a normal single-board HIL smoke (`stm32f407disco`) per hil skill to confirm the rig is healthy for CI.
+- [ ] **Step 6: Commit**
+
+```bash
+git add .claude/skills/rtt/
+git commit -m "skills: rtt - ci.lan rig validation matrix"
+```
+
+---
+
+### Task 9: Follow-up doc, final validation, report
+
+**Files:**
+- Create: `docs/superpowers/followup/pr-rtt-pool-check.md` (rename to `pr<NNN>-…` once the PR number exists)
+
+- [ ] **Step 1: Follow-up handoff doc** (superpowers:writing-plans style, per CLAUDE.md "Deferred work"): adopting `RttConsole` in `hil_pool_check.check_host_serial` (`test/hil/helper/hil_pool_check.py:354` — bidirectional, VCOM-assuming; needs `open_board_console` hoisted from `hil_test.py` into `hil_util.py`), citing the ea4088 validation as established ground. Also note the deferred sysview SKILL.md pointer (that branch owns its file; propose to user when it merges).
+- [ ] **Step 2: `pre-commit run --all-files`** — expect pass (~55 s; HIL hooks exercise real timeouts).
+- [ ] **Step 3: Commit follow-up doc:** `git add docs/superpowers/followup/ && git commit -m "docs: follow-up - pool-check adoption of RttConsole"`
+- [ ] **Step 4: Report** to the user: commit list, validation matrix summary (htpc + rig, per-direction verdicts), open findings (e.g. OpenOCD write path), and **ready to push — not pushed**.
+
+---
+
+## Self-Review (completed at planning time)
+
+- Spec coverage: scoring→spec only; scope/sections→Task 4; tooling→Tasks 1,3; measured-evidence carriage→Task 4 step 2; doc edits→Task 6; validation strategy→Tasks 7,8; non-goals→Task 4 §2 + exclusions in Task 8. Deferred sysview pointer→Task 9. No gaps.
+- Placeholder scan: `<scratchpad>` is the session scratchpad path (known at execution); `<port>/<addr>/<uid>` are computed per-board by given commands; Task 4 prose is assembled from enumerated facts (TDD forbids pre-writing final skill text before RED completes). No TBDs.
+- Type consistency: `RttConsole(board, timeout)` board-dict shape identical in Tasks 1, 3; CLI flags identical in Tasks 3, 6, 7, 8; skill name `rtt` throughout.
diff --git a/docs/superpowers/specs/2026-08-24-rtt-skill-design.md b/docs/superpowers/specs/2026-08-24-rtt-skill-design.md
new file mode 100644
index 000000000..7a621726f
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-24-rtt-skill-design.md
@@ -0,0 +1,164 @@
+# `rtt` skill — design & decision record
+
+Date: 2026-08-24. Branch: `rttconsole-skill`. Author sessions: lpc4088 handoff
+(measurements), sysview handoff (mechanics + probe matrix), this session
+(verification + decision). User approved promotion and the name `rtt` on
+2026-08-24.
+
+## Decision
+
+Promote SEGGER RTT from an inline technique in `.claude/skills/target-debug/`
+to a standalone skill `.claude/skills/rtt/`, scoped as **transport core +
+console layer**: getting bytes on/off RTT channels over any debug probe, plus
+the bidirectional console tooling the HIL harness ships. Consumer-specific
+layers (SystemView encode/decode/licensing, TU_LOG conventions, debugging
+methodology) stay in their skills and cross-reference.
+
+## Scoring against the promotion criteria
+
+Criteria: `docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md`
+§"Skill vs technique — promotion criteria" (exists only on branch
+`claude/add-systemview-debug`; read via `git show`). Two or more of four
+required. Score: **3/4**.
+
+1. **Ships tooling — yes.** `hil_util.JlinkRtt` (commit d98e77bac: probe
+ selection by serial, dynamic port allocation, non-blocking bidirectional
+ socket, process-group teardown) plus a thin CLI added by this plan.
+ Precedent: `hil` and `code-size` are skills wrapping repo-versioned tools;
+ "recipes over already-installed tools" is what RTT was *before* this code
+ existed (why SWO stayed a technique at 1.5/4 — see `SWO_SKILL_HANDOFF.md`).
+2. **Answers its own routed question — yes.** "Give this board a console /
+ printf I/O with no UART and no VCOM" is asked from harness and bring-up
+ contexts that never load target-debug (whose trigger is *misbehaving
+ firmware*). Measured cost of the missing route: the lpc4088 session burned
+ an hour rediscovering a gotcha already written at target-debug
+ SKILL.md:249-253.
+3. **Carries validation state — yes.** Measured tool matrix (below), 13-board
+ OpenOCD read-path campaign from the sysview cycle, WCH SDI A/B proof,
+ SAMD5x DSU gotcha, lock-porting example, per-probe constraints.
+4. **Long but conditionally relevant — yes.** The transport knowledge is a
+ page+ that most target-debug sessions don't need and harness sessions
+ can't find there.
+
+## Measured evidence the skill must carry
+
+From the lpc4088 session (LPC4088 + LPC-Link2 J-Link fw 611000000, SWD 4 MHz;
+single board — re-verify on more hardware during validation):
+
+- `JLinkExe -RTTTelnetPort <port> -AutoConnect 1`: 6/6 reliable; delivers the
+ buffered boot burst; accepted an 8550-byte write in one call. **The proven
+ standalone path.**
+- Drain rate 24.6 KiB/s (253,127 B / 10.0 s) against a saturating printf
+ firmware that produced 689,896 lines — 0.6 % delivered. RTT console is
+ **drain-limited and lossy under saturation; drops happen at the target**
+ (NO_BLOCK_SKIP, 1 KB default buffer).
+- `JLinkRTTLogger`: 0/6 — "RTT Control Block not found" even given
+ `-RTTAddress`, block plainly readable over SWD. Searches once at attach,
+ never retries. **Never build on it.**
+- `JLinkGDBServer -RTTTelnetPort` with **no GDB client attached**: served the
+ port, never located the control block (this board). target-debug's
+ GDBServer+JLinkRTTClient recipe was proven in flows where GDB attaches, and
+ CLAUDE.md's recipe worked on other parts — treat as per-part variance,
+ document both; do not "correct" either into a flat contradiction.
+- OpenOCD (jaylink) driving this J-Link-firmware probe: transport failure
+ (`LIBUSB_ERROR_TIMEOUT`, `jaylink_swd_io() failed`), probe drops off USB,
+ **physical replug needed** — twice, reproducible. Standing rule: never
+ point OpenOCD at that class of probe (J-Link OB firmware on a debug-probe
+ board like the LPC-Link2). Genuine SEGGER J-Links work under jaylink —
+ routine in the sysview campaigns (metro_m4_express).
+
+From the sysview cycle (branch `claude/add-systemview-debug`, 13-board
+campaign 2026-08-12):
+
+- OpenOCD `rtt setup <exact CB addr> … ; rtt start; rtt server start <port>
+ <ch>` **read path validated** on ST-Link, CMSIS-DAP and J-Link probes
+ (`test/hil/sysview_ci.py`). Exact CB address from
+ `arm-none-eabi-nm <elf> | grep _SEGGER_RTT` beats a full-RAM scan (slower,
+ can mis-hit stale RAM after soft reset).
+- The real transport requirement is **autonomous memory access while the core
+ runs**: ARM memory-AP (zero intrusion), RISC-V SBA where implemented.
+ **WCH QingKe SDI has neither** — Debug Module abstract commands perturb the
+ running core; A/B-proven kill ~1.9 s into USB traffic. Per-transport rule:
+ SDI = halt→read→resume / post-mortem dump only, never live streaming.
+- SAMD5x + OpenOCD: in-session `reset run` via the DSU CPU Reset Extension
+ leaves the core held — attach without reset when the flash step already
+ reset the board (general preference: attach-only capture).
+- Lock porting example: `hw/bsp/ch583/sysview_rtt_lock_wch.h` (QingKe CSR
+ 0x800 brace-scoped save/restore; generic RISC-V lock traps mcause=2).
+- Drain hierarchy: J-Link native > OpenOCD polling; matters only at
+ SystemView bandwidths (workable buffers 2048–8192); console logs never
+ overflow the drain in practice.
+- RTT mechanics for the concepts section: control block `_SEGGER_RTT` (magic
+ "SEGGER RTT") + ring buffers {sName, pBuffer, SizeOfBuffer, WrOff, RdOff,
+ Flags}; the HOST must write RdOff back to drain; modes NO_BLOCK_SKIP (log
+ default) / NO_BLOCK_TRIM / BLOCK_IF_FIFO_FULL (target spins — dangerous in
+ ISRs); post-mortem mode = `SEGGER_RTT_WriteWithOverwriteNoLock` (target
+ drags RdOff, ring holds last N bytes, no live host needed); channel 0 =
+ "Terminal" console, SystemView claims its own "SysView" up-buffer —
+ coexist on one control block.
+
+## Gotchas the skill centralises
+
+Control block exists only after the target's first printf (early reader sees
+nothing; Logger gives up). The console owns the probe: flash and reset before
+opening it; never reset while attached. An undrained NO_BLOCK_SKIP ring holds
+the FIRST KB after boot, not the wedge tail. Always select probes by serial
+(`-USB <sn>` / `adapter serial`) — rigs run several. Two probes wired to one
+SWD header wedge the target.
+
+## v1 backend matrix
+
+| Backend | Read (capture) | Write (console input) |
+| ----------------------------------------------------- | ---------------------------- | ------------------------------------------ |
+| J-Link native (`JLinkExe -RTTTelnetPort`) | validated | validated (8.5 KB writes) |
+| OpenOCD on native probes (ST-Link/CMSIS-DAP/WCH-Link) | validated (sysview campaign) | unvalidated — validate in the ci-rig phase |
+| OpenOCD on the LPC-Link2 (J-Link OB fw, measured) | forbidden (USB drop) | forbidden |
+| WCH SDI (any tool) | halt→dump only | n/a |
+
+`JlinkRtt`/CLI are J-Link-only in v1; OpenOCD console-write support is
+added only if the ci-rig phase validates it.
+
+## Tooling home
+
+Single implementation in `tools/rtt.py`: a stdlib-only importable module
+(shared socket-console base + `JlinkRtt` + `OpenocdRtt`) that doubles as
+the CLI. `hil_util` imports and re-exports the classes (the harness keeps
+addressing `hil_util.JlinkRtt`), so the dependency points harness → tools,
+never tools → harness. Because `hil_util` loads it at import time, the file
+is harness-critical: it is classified with `test/hil/` in `ci_select`'s full
+rule and covered by the pre-commit `hil-test` hook (test_hil_rtt.py).
+Precedent: `code-size` wrapping `tools/metrics_compare_base.py` — the skill
+is md-only and points at the tool. `open_board_console()` stays in
+`hil_test.py` for now; pool-check adoption is a follow-up doc, not this PR.
+
+## Doc edits (curated-skills rule: smallest possible diffs)
+
+- `target-debug/SKILL.md`: capture-channel rows and the drain-model warning
+ stay; the two capture recipe blocks and the RTTLogger/GDBServer paragraph
+ shrink to one-liners pointing at `rtt`; the manual ring-read recipe
+ (`nm`/`mem32`/`savebin`) moves into `rtt` §post-mortem.
+- `CLAUDE.md` GDB section RTT line becomes build flag + pointer.
+- `hil/SKILL.md` gains one routing line (the fix that would have prevented
+ the lost hour).
+- `sysview/SKILL.md` pointer is **deferred** until that branch merges, and
+ proposed to the user first. No edits to `sysview_ci.py` or the sysview
+ skill now.
+
+## Validation strategy (user-directed)
+
+1. **Dogfood on the local htpc bench first**: ea4088_quickstart via LPC-Link2
+ (replugged; OpenOCD attempts on it are skipped outright) and
+ raspberry_pi_pico2 via the J-Trace (nickname `jtrace`, serial private; now wired to pico2; RP2350 =
+ `rp2350_m33_0`, never a custom JLinkScript). Follow only the SKILL.md
+ text (dogfood = REFACTOR input).
+2. **Then all boards on the ci.lan rig**, per-transport smoke capture, rows
+ recorded in `.claude/skills/rtt/boards.md`. Exclusions recorded honestly
+ (esptool boards: no SEGGER-RTT path in our builds — USB-Serial-JTAG
+ console instead; tm4c: no probe path configured on the rig).
+
+## Non-goals
+
+Timing/profiling (etm-trace, sysview, parked swo-trace), SystemView
+encode/decode/licensing, TU_LOG conventions, debugging decision flows
+(target-debug), Espressif USB-Serial-JTAG console (esp-target-debug), WCH SDI
+live streaming (impossible — see matrix).