Records the convention in CLAUDE.md -- deferred work is a SEPARATE scope that deserves
its own PR, written by another session, so it is handed off as a writing-plans doc in
docs/superpowers/followup/pr<NNN>-<topic>.md rather than accumulated in the PR that
found it.
Five handoffs from #3803: flasher_recover (convoy-safe recovery for J-Link boards, seven
validated on the rig), the blindness reporting gaps, the usbtest recovery reserve, the
IAR re-run spec, and the pci-rebind stranding question. Each carries what is already
established with its citations and measurements, what remains, and why it was split out.
One doc per follow-up, not one per PR: a per-PR file invites unrelated work into the
same document and rots as a unit.
Two things the rig taught us that the old guidance got wrong.
A usbfs ioctl wedged in D state cannot be freed on a running kernel. It holds
the device lock, so usb_disconnect() blocks behind it; reboot(2) walks
device_shutdown() and takes the same lock, so every userspace reboot stalls too.
Only sysrq b (emergency_restart, which skips device_shutdown) or hypervisor
action clears it -- all cited to the kernel source. The recovery ladder is
generic across rigs now (ci.lan, hifiphile, a bench PC) and ends at hypervisor
escalation only where host access exists. Two claims are corrected outright:
JLinkExe is NOT convoy-safe, and a park-flash cannot free a device-lock owner.
The hil skill's banner list is what an operator agent matches a report against,
so it enumerates the banners that actually exist, including the D-state note --
which is explicitly NOT a wedge, since a healthy in-flight testusb is
uninterruptible for most of every case and a concurrent CI battery would
otherwise turn a clean run red.
A wedged USB device used to take the whole HIL run with it. Every worker that
touched the poisoned node blocked uninterruptibly, the pool could not be joined,
map_async discarded every board's result, and the job ran to the GitHub ceiling
with no report at all -- while the self-hosted runner's single job slot stayed
occupied and every queued job waited behind it.
Bound the calls a worker makes itself. read_sysfs, bounded_open and run_cmd all
answer within a wall clock; read_sysfs distinguishes "absent" from "unknown",
because a blocked read is not evidence of absence, and caps stranded readers at
four (each costs a thread and an fd for the life of the process) after which the
worker declares itself blind. mtype, the gio unmount, the libmtp session and the
arecord/iperf reaps go through those bounds; the MTP session runs in a disposable
subprocess, since libmtp's ctypes calls block unkillably in D state.
Bound the run. A pool guard (HIL_POOL_TIMEOUT, 60 min) fires before any job
ceiling and still writes a report. When the pool will not shut down, the sweep
kills what the workers spawned -- descendants, not just direct children, since
flashers run in their own session -- confirms each kill actually landed, and
exits early so the runner is freed. Whatever survived is named in the report.
Deliberately shallow past that point. We do not re-scan process groups, prove
pid ownership, or escalate through sudo: a root-owned survivor is reported, not
force-killed, because signalling a pid we cannot prove is ours is the worse
failure, and the job ceiling backstops whatever this misses. A D-state holder
was never killable anyway.
Recover instead of reporting a wedge. A HUNG usbtest case reflashes its own DUT
through its roster flasher, but only where the flasher can reach its probe past
a poisoned node -- openocd pinned to a validated vid_pid, or esptool. Where it
cannot, the run says so rather than reserving budget for a path that cannot fire.
Raise the CI ceilings above the pool guard so the guard fires first and still
writes its report, and pin --retry 1 on every HIL leg: the guard is a flat
constant and does not scale with max_retry, so argparse's default of 3 would
triple the serialized usbtest tail against an unchanged guard.
Split the module: execution in hil_test/hil_flash/usbtest, infrastructure in
helper/ (locking, health, selection, shared bounded IO), and the two matrix
generators into .github/scripts/ -- ci_set_matrix.py sat in workflows/, where
GitHub treats every file as a workflow definition. 193 tests cover the bounded
paths, the kill ladder, the guard and the selector against synthetic /proc trees
and PATH-injected fakes; a real wedge cannot be manufactured on demand.
hil, ci: scope HIL builds and tests to the boards a PR affects
Add test/hil/hil_select.py, a stdlib-only selector that maps a PR diff to the
rig boards, tests and BSP families a change can affect, and wire it into CI so
pull requests build and run only those. A port change picks its families' boards,
a class change picks the examples enabling that class, and device/host changes
prune the other role. Anything unclassified — infra, an unmapped port, a selector
error — falls back to the full matrix, and push/schedule runs are untouched.
Move the shared example lists to hil_examples.py; 54 hardware-free tests cover
the rules.
test/hil: add board-pool health check, split hil_test into focused modules (#3794)
Add test/hil/hil_pool_check.py: per-board rig health scan — probe presence,
light-example flash (dfu_runtime; device_info + serial check for host-only
boards), uid re-enumeration, safe recovery (probe authorized-toggle, board
reset), verified board_test re-park, USB topology report, and a markdown
summary table. Missing firmware is built on the spot (tools/build.py, idf.py
for espressif, one get_deps retry); row statuses: ok, flash-failed, failed,
locked. Board locks are always respected, never bypassed.
Refactor hil_test.py into hil_lock.py (flock protocol, controller permits,
hold/release/status CLI; replaces board_lock.py) and hil_flash.py (flashers,
find_firmware, run_cmd). Update WCH probe uids and the board roster in
tinyusb.json; add the hil-pool-check skill.
The plan doc records why the fork exists and how each vendor source was
ported; the interim handoff it superseded is dropped.
CLAUDE.md: a new worktree should symlink the dependency dirs to the
primary checkout rather than re-fetching them, replacing a single
symlink only when the branch needs a different dep revision. Also allow
'linke' in codespell - WCH-LinkE is a product name.
- target-debug: fault frame lives on PSP when EXC_RETURN bit2 set (FreeRTOS
tasks) — decode LR before choosing $msp/$psp (Codex; valid, our verify
happened to fault on MSP)
- esp-target-debug: show the Xtensa gdb for S3 in the attach recipe; clarify
adapter serial = USB-SJ iSerial (colon MAC, hardware-verified) vs the
CP2102N flasher uids in tinyusb.json (Codex; second half of the finding
had the identifiers inverted)
- esp plan: align serial form with the verified command; record the real
console-gate outcome (UART0, USB-SJ console untested) (Copilot)
- target-debug plan: Task 4 now consistently $JB/ARMv7-M matching the
executed JLinkExe path (Copilot)
- drop IDE-local .idea files swept in by the rename commit (Copilot)
Spec (brainstormed): own-skill backend decision, PHY-conflict map, six
verification gates, external-JTAG TODO. Plan executed same-day: all gates
run on the rig; apptrace resolved per its own gate rule as (untested).
- Vector catch + Cortex-M fault autopsy, verified with a deliberate bad-load
on stm32f407disco: CFSR=0x8200 (BFARVALID|PRECISERR), BFAR = exact bad
address, stacked pc addr2lined to the faulting line; gotchas recorded
(stale FPB comparators fire phantom SIGTRAPs — scrub first; arm DEMCR
after reset; loads precise / stores imprecise; ARMv6-M has no CFSR/BFAR)
- SWO exception trace + hw PC sampling gate PASSED on F407: 680 KB of
packets in 3 s (0x17 PC samples in flash range, 0x0E SysTick enter/exit);
JLinkSWOViewerCL decodes stimulus only — raw SWORead is the recipe;
SWOStart needs an explicit speed headless
- verifybin 'Verify successful.'; FreeRTOS -rtos plugin lists all 6
cdc_msc_freertos tasks after a run->stop cycle (plain attach = 0xDEAD
placeholder); semihosting anti-note; monitor-mode pointer (untested)
- Intrusiveness table gains the new rows; agent playbook bullet updated;
retrieval gate 5/5 with a fresh reader; executed plan committed
- getting_started.rst: the reST inline-link markup rendered literally
inside the code-block (not a runnable command) and lacked sudo — use a
plain `sudo cp examples/device/99-tinyusb-examples.rules ...`.
- tools/88-tinyusb.rules: normalize the six MODE="666" entries to the
4-digit octal MODE="0666" used everywhere else in the file.
Fourth view in the USB debugging toolset (usbmon = host URBs, usb-debug =
host reasoning, usb-target-debug = device firmware, usb-sniffer = what
actually crossed D+/D-). Covers the ataradov/usb-sniffer analyzer:
headless pcapng capture (--speed ls/fs/hs, --fold, --limit self-exit),
Wireshark/tshark analysis recipes, and the wire realities that bite:
downstream broadcast, sniffer self-capture noise, xHCI devnum != wire
address, tap-point-dependent reset visibility (hub choreography anchors),
FS-behind-HS-hub splits. Every recipe hardware-validated on the rig,
including the capture-window floor (a 3 s window provably misses the
enumeration ladder; 3M packets minimum).
Two udev files with distinct audiences, not one:
- examples/device/99-tinyusb-examples.rules (renamed from 99-tinyusb.rules):
the user-facing rules the examples need — cafe VID access, hidraw, the
ModemManager blacklist, a couple of board probes. getting_started.rst,
the webusb_serial README and its source comment point here.
- tools/88-tinyusb.rules: the HIL rig's private probe/analyzer allowlist,
now with the sniffer (6666:6620 + blank FX2LP 04b4:8613). Installed on
the rig only; the usb-sniffer skill references it.
Opus-tier agent charter for backgrounding a long hardware debug session:
instrument -> build -> flash under one held board lock -> dual-side
capture -> correlate -> refine, strictly one instance, skills as source
of truth (usb-target-debug, usbmon, usb-debug, usb-sniffer, usb-recover,
hil). The charter encodes what dogfooding established:
- diagnosis standard: evidence must show the mechanism, or a fix must
flip the ORIGINAL failing case on hardware; stop after two
evidence-free cycles and hand back a partial diagnosis
- lock cadence: hold for the whole session, release around hil_test.py
runs (it self-locks per board)
- revert semantics: "fix stays, probe goes, re-verify clean" —
instrumentation reverted, candidate fix left uncommitted and
re-verified on a clean build, pristine firmware reflashed before
lock release
Returns a machine-parseable diagnosis report including ruledOut[] —
disproven hypotheses are deliverables. Spec roster updated (opus/xhigh,
effort requested per agent() call).
Completes the debugging toolset (usbmon = what the host exchanged,
usb-debug = why the host acted, usb-sniffer = what crossed the wire):
TU_LOG/RTT capture, per-probe GDB autopsy without reset, RAM ring-buffer
event trace, J-Link DWT_PCSR PC-sampling, dual-side capture posture, and
board-lock rig discipline. Includes the implementation plan it executes.
Hard-won warnings baked in from real bring-up sessions: volatile ring
buffers vs -Os dead-store elimination, RTT NO_BLOCK_SKIP post-mortem
limits (no overwrite mode exists), DHCSR validity anchors for register
snapshots, release-lock-before-hil_test, and that a marginal just-recabled
link can fake a deterministic firmware bug.
Also ignore .claude/worktrees/.
Confirmed by a 10-finder / 28-verifier adversarial review pass:
board_lock.py — the flock is now the sole authority: drop cmd_hold's
pid-liveness pre-gate (a live hil_test.py pool worker's stale record no
longer blocks a genuinely free board); cmd_release probes the flock and
only signals a verified holder, refuses to kill hil_test.py holders
(CI mid-test), handles PermissionError; the holder daemon truncates its
lock records on SIGTERM and keeps the success pipe clear of fds 0-2
(closed-stdio hold used to leave an orphan holder while reporting
failure); --config default resolves beside the script.
hil_test.py — truncate the lock record on per-board release (pool
workers outlive their flocks); warn instead of silently failing open
when the lock dir is unusable; error out on -b names absent from the
config (was a silent zero-test exit 0, readable as a green HIL run);
drop an emptied board row in accumulate_report (variant boards left a
blank ghost row).
workflows — remove the stray positional arg that made the validate size
stage exit 2 on every run; wrap JSON.parse(args) in all six scripts;
factor pr-babysit's drifted reply recipe into postReplyRecipe and dedup
refutation replies across cycles; validate args.pr and maxCycles;
driver-review rejects an empty dimensions list; hil-validate drops a
dead guard clause and retries diagnostics with -v -r 1.
agents/docs — port-dev scopes git clang-format to its own files
(concurrent workers reformatted each other in shared checkouts);
hil-operator/hil skill wording matches actual fail-fast output; the
implementation plan is now a DO-NOT-EXECUTE historical record (banner +
checked boxes) so plan-executing agents cannot revert shipped files.
Verified: lock storm 1-winner-in-10, stale-record hold, closed-stdio
hold, dead-pid cleanup, CI-holder refusal, ghost-row 4-scenario merge,
unknown-board exit 1, py_compile + check.sh on all six workflows,
pre-commit clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upj4hta5TNoAbidqeC1zZ6
Review-fix batch (owner-confirmed) on the multi-agent harness:
- board_lock: detach holder stdio so a captured `hold` cannot hang on the
daemon's inherited pipe; probe locks by holder-pid liveness instead of a
momentary flock, which could spuriously fail a concurrent acquirer
(storm-tested: 1 winner in 10, 0/15 acquire failures under probe storm)
- hil_test: locked board renders a visible board-locked fail row so the
report matches the exit code; stale marker cleared on a real re-run
- pr-babysit: autoPush now opt-in (default dry run); resolve recipe
paginates reviewThreads; post-push resolve gets issue-comment fallback
- validate: size stage honors non-default base via --base-branch; pvs
stage delegated to the new agent
- new static-analyzer agent (sonnet): PVS-Studio SAST+MISRA for one
board, structured findings gated on files changed vs base
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rn1AN5DsTdFhRwhugfgKZi
Add worker agents (builder, port-dev, driver-reviewer, hil-operator,
pr-monitor), deterministic workflows (validate, fanout-dev, driver-review,
hil-validate, full-check, pr-babysit) and a /pre-pr gate skill, so sessions
can fan build/test/review/PR-triage work out to tiered subagents. pr-babysit
drives a PR to green: triage CI + bot reviews, fix validated findings, verify,
push, and reply-to + resolve each inline review thread (fixed or refuted).
Replace the stop-the-runner HIL discipline with per-board flock locks:
test/hil/board_lock.py plus a fail-open guard in hil_test.py let CI and dev
sessions share the rig per board (locked boards fail fast and re-run;
HIL_NO_BOARD_LOCK=1 is a user-authorized bypass). The actions-runner is
never stopped.
Design spec, implementation plan, and real-rig smoke evidence under
docs/superpowers/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rn1AN5DsTdFhRwhugfgKZi
Relocate docs/info/changelog/ -> docs/changelog/ (docs/info held only the
changelog) and update all references: the toctree, README + MAINTAINERS links,
make_release.py, and the make-release skill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- restore MAINTAINERS.rst (drop the .md); make the changelog link absolute so it
resolves both on GitHub and in the rendered docs
- docs/maintainers.rst includes the root file (same pattern as README) and is
wired into the Documentation toctree
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- changelog: fix 3 silent RST mis-renders carried over from the old monolith
— Markdown link in 0.7.0, mismatched/single backticks in 0.18.0 / 0.20.0
- make-release skill contributors one-liner: parallelize the gh fetch, anchor
the bot filter (was a substring that dropped handles like "abbott"), and join
with ", " (paste -sd cycles the delimiter -> "@a,@b @c"); finalize now stages
only the reviewed set (git add -A -- ':!.idea'); drop the obsolete CRLF gotcha
- make_release.py: emit LF not CRLF in the repository.yml insertion (fixes the
gotcha at the source)
- docs sidebar: drop the hardcoded furo component list; override brand.html to
include furo's own template + the sponsor button (decoupled from furo
internals, no upper-bound pin needed); move sponsor styles into custom.css
- .pre-commit-config.yaml: drop stale end-of-file-fixer excludes for the
deleted contributors/CoC include shims
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- make-release skill: stage new files with git add -A in finalize, use
unprefixed release tags (X.Y.Z), and run validate build steps in subshells
so the cwd doesn't leak between commands
- docs/requirements.txt: require furo>=2025.12.19 so the sidebar component
list in conf.py (incl. variant-selector.html) is always available
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- README: restore the separate CH32V103 / CH583,CH582 rows in the Supported
CPUs table — the merged row wrongly applied "ISO data loss [2]" to CH583/2
- Fix typos in historical changelog entries (santiy, Venndor, dynamnic, bagde,
masstorage, techinical, walkaround, "Update Update") flagged by Copilot
- Replace the AGENTS.md Release section with a pointer to the make-release
skill; add a Finalize step (maintainer commit/tag/push + GitHub release) to
the skill so nothing is lost
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Release:
- Bump version to 0.21.0 and regenerate tusb_option.h, library.json,
repository.yml, sonar-project.properties, boards.rst, dependencies.rst
- Add 0.21.0 release notes and split the changelog into per-release files
under docs/info/changelog/ (date out of title, driver/class groups as
sub-headings, DCD & HCD section after Device/Host stacks)
Contributors:
- Credit each release's PR authors in a Contributors section
- Drop the curated contributors page; add MAINTAINERS.rst
Docs:
- Update Code of Conduct to Contributor Covenant 3.0 (keep it in the repo,
remove it from the built docs)
- Sidebar: add a home entry, rename the group to "Documentation", move
Changelog into it, add a GitHub Sponsor button, merge external links,
rename FAQ; hide the inline toctree on the landing page
- Add the make-release skill and update the AGENTS.md release process
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Order CFG_TUH_DEVICE_MAX before CFG_TUH_HID in the host config snippets
(cdc_msc_hid, hid_controller) so the documented snippet has no forward
macro reference when copied into tusb_config.h.
- Fix stale `examples.rst` references: the generator now writes per-group
`docs/examples/<group>/index.rst` pages, so update the conf.py comment and
the build-doc SKILL.md accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documentation tooling:
- Add the `build-doc` skill and `tools/build_doc.py` wrapper for local
Sphinx builds (clean / -W / open).
- Enable Markdown (MyST) in conf.py and auto-collect
examples/{device,host,dual}/*/README.md into a 3-level Examples nav
(Examples > Device/Host/Dual > example), noting each page's source
location and normalizing headings to a single H1.
- Remove the stale `.claude/commands/build-doc.md`; point the AGENTS.md
Documentation section at the skill.
Example docs:
- Add a README.md for every device/host/dual example: what it does, USB
interface table, notable tusb_config.h settings, generic CMake + Make
build steps, and how to try it.
- Fold each *_freertos variant into its base README, noting the FreeRTOS
source path and any RTOS-specific behavior.
Generated docs/examples/ output is git-ignored. Builds clean with
`sphinx-build -W`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BSP family and MCU option were named "ch58x"/"CH58X", but the supported part is
the CH583/CH582 (and the SDK repo is openwch/ch583); CH585 is a separate MCU family,
so the CH58x umbrella was misleading. Rename to the specific family:
- hw/bsp/ch58x -> hw/bsp/ch583 (dir), and the BSP-local files ch58x_it.* ->
ch583_it.*, system_ch58x.* -> system_ch583.* (include guards/refs updated). The
vendor SDK files (CH58x_common.h, CH58x_*.c in hw/mcu/wch/ch583) keep their names.
- OPT_MCU_CH58X -> OPT_MCU_CH583 in tusb_option.h, tusb_mcu.h, and the shared WCH
USBFS driver (ch32_usbfs_reg.h, dcd_ch32_usbfs.c). OPT_MCU_CH582 is kept as an
alias (same value), so either name selects the same code.
- FAMILY_MCUS CH58X -> CH583, CFG_TUSB_MCU=OPT_MCU_CH583, mcu:CH58X -> mcu:CH583 in
the example skip lists, the CI build matrix (ci_set_matrix.py), the get_deps family
tag, and docs/reference/boards.rst.
Board names (ch582m_evt, yd-ch582m) are unchanged. Verified: make + cmake build for
ch582m_evt, and ci.lan HIL (all device examples pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore original "Other types of transfers are not affected" statement,
removing the conditional USBHS_INT_BUSY_EN / USBFS-unconfirmed wording.
Co-authored-by: Zixun LI <HiFiPhile@users.noreply.github.com>
- Remove duplicate toctree entry for reference/device_issues from docs/index.rst
(already included via docs/reference/index.rst)
- Fix WCH section title: CH32V10X/CH32V20X/CH32V30X → CH32F20x/CH32V20x/CH32V30x
to match the three actual README table entries (CH32F20x, CH32V20x, CH32V305/307)
- Update README anchor links to match the renamed section
- Qualify USBFS non-ISO transfer safety claim: USBHS is protected by
USBHS_INT_BUSY_EN but USBFS behavior is not yet confirmed
- Fix LPC54600 note: "read errata" → "see device issues"
- Remove "totally" from LPC54600 description
Co-authored-by: Ha Thach <hathach@users.noreply.github.com>
Host driver (midi2_host.c):
- midih2_open() now returns actual parsed length instead of max_len,
preventing composite device interface conflicts
- Parsers (alt0/alt1) refactored to return const uint8_t* end pointer
following midi_host.c switch/case pattern
- Alt 1 CS Endpoint now parses MIDI 2.0 layout (bNumGrpTrmBlk at
offset 3 with MIDI_CS_ENDPOINT_GENERAL_2_0 subtype check) instead
of reusing MIDI 1.0 struct (bNumEmbMIDIJack)
- midih2_set_config() now issues SET_INTERFACE control request via
tuh_interface_set() before completing configuration. Falls back to
alt 0 if SET_INTERFACE fails
- Extracted midih2_set_config_complete() and midih2_set_interface_cb()
for async SET_INTERFACE handling
Device driver (midi2_device.c):
- midi2d_open() skip loop now checks bInterfaceNumber, stopping at
interfaces that belong to other functions in composite devices
- SET_INTERFACE handler now rejects alt > 1 (returns false/stall)
- Named constants for GTB descriptor types and MIDI protocol values
Descriptor macros (usbd.h):
- TUD_MIDI2_DESC_ALT1_HEAD: iInterface set to 0 (consistent with
Alt 0), wTotalLength now uses TUD_MIDI2_DESC_ALT1_CS_LEN to cover
all Alt 1 class-specific descriptors
- TUD_MIDI2_DESC_ALT1_EP: now accepts GTB ID list via variadic args,
emitting complete CS endpoint descriptor
Host example:
- CMakeLists.txt restricted to rp2040 family (display.c requires
Pico SDK headers)
- display.c: null terminator after strncpy in log scroll
Documentation:
- class_drivers.rst updated to reflect SET_INTERFACE behavior and
auto-select with fallback
Addresses: Codex P1 (#1, #2, #3), Copilot (#4-#9)