<feed xmlns='http://www.w3.org/2005/Atom'>
<title>tinyusb.git/.claude/agents, branch master</title>
<subtitle>Unnamed repository; edit this file 'description' to name the repository.</subtitle>
<id>http://cgit.235523.xyz/tinyusb.git/atom/.claude/agents?h=master</id>
<link rel='self' href='http://cgit.235523.xyz/tinyusb.git/atom/.claude/agents?h=master'/>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/'/>
<updated>2026-08-28T04:28:15Z</updated>
<entry>
<title>pr-review-validator: done waits for every auto-reviewer to settle on the head SHA</title>
<updated>2026-08-28T04:28:15Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-28T04:28:15Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=e413f05ee6eb6062a8b82c48ed685f0414234497'/>
<id>urn:sha1:e413f05ee6eb6062a8b82c48ed685f0414234497</id>
<content type='text'>
A cycle running before the bots posted saw zero findings and reported done;
with a fast-green CI the babysit loop could exit unreviewed. done now needs
every reviewer settled for the current head: Copilot's verdict review
(commit_id), Codex's verdict comment (Reviewed-commit line), its thumbs-up
reaction on the PR body, or the named claude-review check run — with quota/
error notices and the reaction freshness-gated on push time (check-suite
creation, not committer date) and every lookup paginated. pr-babysit re-arms
with backoff on a pending reviewer instead of exiting unactionable, skipping
the pointless final-cycle wait.
</content>
</entry>
<entry>
<title>agents: split pr-monitor into pr-ci-watcher + pr-review-validator; rename port-dev/driver-reviewer to code-writer/code-verifier; pin model+effort on every agent</title>
<updated>2026-08-27T05:21:04Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-27T05:21:04Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=2b9ddd8b00eb57225fd9caeecd0da1a7954aee06'/>
<id>urn:sha1:2b9ddd8b00eb57225fd9caeecd0da1a7954aee06</id>
<content type='text'>
</content>
</entry>
<entry>
<title>hil: make hil_report.md a rendering of hil_report.json (#3840)</title>
<updated>2026-08-25T08:04:42Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-25T08:04:42Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=375e20090a0a60fd2d47ba6553d189e05482dfea'/>
<id>urn:sha1:375e20090a0a60fd2d47ba6553d189e05482dfea</id>
<content type='text'>
hil_report.json and hil_report.md were written independently. Four writers
produced the markdown and three wrote no JSON at all -- and those three are the
paths where a run died, so a JSON consumer saw nothing exactly when it mattered:
the per-board verdicts an agent hands back reported the whole fleet as "no
report row" while a human read the real story from the markdown.

Every writer now goes through render_report(), so a table can never contain
something the JSON does not. The document gains `scope` (a three-board PR run
and a full run that lost 24 boards were indistinguishable) and `caveat` (how the
run ended). `banner` carries rig health across an --accumulate retry; `caveat`
records how a run ended and must not -- conflating them made a clean retry
publish an abandonment that never happened.

helper/hil_report.py owns the document end to end, dissolving the import cycle
that forced write_timeout_report to compose its own markdown and removing a
duplicate cell classifier kept in sync by hand. hil_summary.py is deleted; its
CLI moves there. hil_ci.sh uploads the sidecar so a remote --accumulate has a
merge bas</content>
</entry>
<entry>
<title>hil, docs: reference toolchains by their official env vars, not one rig's paths</title>
<updated>2026-08-20T10:43:44Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-20T10:43:44Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=21d7332d5884e0abbf9e3f3762deefa589564783'/>
<id>urn:sha1:21d7332d5884e0abbf9e3f3762deefa589564783</id>
<content type='text'>
~/code/pico/pico-sdk and $HOME/code/esp-idf/export.sh are the ci rig's private layout;
written into instructions they silently stop being true on tusb, a dev PC, or any
future rig. The docs now use the variables the vendors define -- PICO_SDK_PATH for the
Pico SDK and IDF_PATH for ESP-IDF, activated explicitly as `. "$IDF_PATH/export.sh"` --
and leave where the checkouts live to each host's profile.

The variables are only useful if the shells that agents actually get can see them, and
`ssh &lt;rig&gt; 'cmd'` is non-interactive AND non-login: it reads no profile, and Debian's
sshd-sourced ~/.bashrc returns at the interactive guard before most of the file. The ci
rig already keeps its exports in the section ABOVE that early-return; IDF_PATH now sits
there beside PICO_SDK_PATH, and the whole chain is verified from a plain non-interactive
ssh: both variables visible, `. "$IDF_PATH/export.sh"` activates ESP-IDF v5.5.3 with
idf.py on PATH -- no login shell, no alias, no hard-coded path. hil-pool-check documents
that placement so the next rig is set up the same way.
</content>
</entry>
<entry>
<title>hil: run every board in one hil_test.py and hand results across as JSON</title>
<updated>2026-08-20T09:43:49Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-20T09:43:49Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=6905639b07c69fec68e9ebc77f7d27ac2775ee41'/>
<id>urn:sha1:6905639b07c69fec68e9ebc77f7d27ac2775ee41</id>
<content type='text'>
hil-validate ran one hil-operator per board. That parallelizes at the wrong layer:
hil_test.py already schedules boards across host controllers and budgets concurrent
flashes and usbtest batteries per controller (FLASH_PARALLEL/USBTEST_PARALLEL), and
those permits live in one process - N parallel runs multiply the budget onto the same
uPD720201 cards for no wall-clock gain over one run that already parallelizes. The
workflow now spawns ONE operator with every board as repeated -b.

The operator no longer retypes the report table. Four consecutive max-effort review
rounds found ~15 defects in this file and every one was in reconstructing board
identity from transcribed prose: report rows are named per VARIANT (nanoch32v203 only
ever produces -fsdev/-usbfs rows), a variant need not start with its board's name,
lock contention is a `board-locked` cell rather than a phrase, and each fix introduced
the next round's bug - including a fake-green test that asserted an invariant with the
one input shape that could not break it. The new helper test/hil/helper/hil_summary.py
does the join where the roster lives and emits one machine verdict per board
({board, ran, pass, locked, detail}); the operator returns that JSON verbatim plus
`wedged`, the only field it authors, and the workflow reads fields, never parses a
string. Its cell classifier mirrors hil_test.py's own tally exactly: failures are
always marked ('fail' or a ❌ prefix, TestFail's contract), everything unmarked is a
pass - a passing test may return a plain metric cell like '13443 KB/s', and the
mirrored rule is what keeps a green table from becoming a red verdict.

hil_ci.sh kept only the LAST -b, so multi-board remote runs staged one board's
binaries and every other board died on the rig after its lock and flash slot were
spent. It now parses every -b spelling argparse accepts (with the -bt arms ordered
first, longest-match, so the &lt;config&gt;.failed retry form is never read as a board named
"t..."), pre-flights roster membership and build dirs for ALL boards before anything
is wiped or staged, warns per declared variant with no build dir (which hil_test.py
would silently green-skip), forwards HIL_* knobs as export lines in one %q word the
remote evals ('; '-joined so it round-trips under dash - an authorized
HIL_NO_BOARD_LOCK force must not silently no-op), keeps HIL_REPORT_DIR local because
the copy-backs look in REMOTE_DIR, and copies hil_report.json and the .failed re-run
spec back beside the markdown, deleting stale local copies first so a green run cannot
leave last run's spec looking current.

Retries preserve the fleet: the documented path is the &lt;config&gt;.failed spec, which
already begins with --accumulate; a fresh scoped re-run would unlink the report and
collapse the whole-fleet table to the retried boards alone.

The risky logic is executable, not argued about: .claude/workflows/test-hil-validate.mjs
pins the lookup/verdict helpers and runs in pre-commit (hil-validate-logic); nine
staging tests drive hil_ci.sh through an ssh stub that models the real thing (argv
joined into one string the remote re-splits, heredoc on stdin - the naive echo-stub
passed while the feature was broken); and deliberate mutations of the verdict logic
are all caught.

Validated on the rig: a 2-board run (usbtest 30/30 on both; the pre-fix classifier,
replayed against that run's real report, fails the fully-green stm32f723disco on its
two passing '13443 KB/s' cells), the .failed --accumulate retry (merged report kept
every earlier row), and a 10-run soak over random subsets of a 22-board pool - 43
board-slots, every failure signature matched pre-existing CI state or known flake,
zero tooling failures, no locks left behind.
</content>
</entry>
<entry>
<title>skill(read-doc): search the Calibre database instead of the filesystem</title>
<updated>2026-08-18T09:58:58Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-18T08:28:58Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=f59c8948729debc6d57c4dfade5486176468edbf'/>
<id>urn:sha1:f59c8948729debc6d57c4dfade5486176468edbf</id>
<content type='text'>
Finding documents by walking the library tree misses anything the filename
does not carry - Calibre stores only a truncated title and the author there, so
the tags, series, publisher and description that hold most part numbers and
errata IDs are invisible to it. A zero-result tree search then reads as "the
document does not exist" rather than as a bad search; that happened here, and
led to a confident claim that a fully populated 14,000-file library was empty.

search.py queries metadata.db, ANDs its keywords across every metadata field
(including the stored filename), and prints the best matches first with the
exact path to read. Matching is NFKC + casefold, so a typed ASCII apostrophe or
mu reaches the titles that store the typographic ones.

Every printed path is checked on disk. Calibre renames &lt;author&gt;/&lt;title&gt; (&lt;id&gt;)
when metadata is edited and leaves the old directory behind, so a miss retries
by the stable book id before reporting MISSING - which distinguishes "the file
is not here right now" from "no such document". The gate tests for metadata.db
rather than the directory, since an unmounted or half-synced mountpoint is
still a directory.

Consumers that prescribed their own tree search - driver-reviewer, port-dev,
the driver-review workflow, and the calibre-library references in CLAUDE.md,
usbtest, etm-trace and target-debug - now point at the skill, which owns the
library's location.
</content>
</entry>
<entry>
<title>skills, docs: rewrite USB recovery from the live incidents</title>
<updated>2026-08-18T05:19:09Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-13T18:08:50Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=f822f69a9871b2115c5213889d70411da66ca1b1'/>
<id>urn:sha1:f822f69a9871b2115c5213889d70411da66ca1b1</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>hil: split hil_test.py into hil_lock/hil_flash, add pool_check, update rig probes (#3794)</title>
<updated>2026-07-29T10:29:59Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-07-29T10:29:59Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=e88fc441ddcaaa5abd4f4673ef2bf29499522dc0'/>
<id>urn:sha1:e88fc441ddcaaa5abd4f4673ef2bf29499522dc0</id>
<content type='text'>
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.</content>
</entry>
<entry>
<title>agents: target-debugger may escalate to etm-trace, prompt-gated</title>
<updated>2026-07-24T08:56:14Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-07-24T08:45:16Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=d7f1bbb6e554820911c4a568e56b6f42f5b4931d'/>
<id>urn:sha1:d7f1bbb6e554820911c4a568e56b6f42f5b4931d</id>
<content type='text'>
Instruction-level trace outranks PC-sampling when samples cannot resolve a
mechanism, but the J-Trace is exclusive per-board hardware: the agent uses
it only when its prompt says the board is trace-wired or the user asked,
and otherwise proposes it in notes - mirroring the lock-force consent rule.
</content>
</entry>
<entry>
<title>docs(skills): tighten esp-target-debug/target-debug; agent routes backends by board family</title>
<updated>2026-07-24T07:55:59Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-07-24T07:48:44Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=22a155f091283596ee34550c42c075967841159b'/>
<id>urn:sha1:22a155f091283596ee34550c42c075967841159b</id>
<content type='text'>
- esp-target-debug: de-narrated (~1000 -&gt; 847 words) — session-diary phrasing
  ('this unit', dates, 'we measured') replaced with durable reference facts;
  restructured per-board (P4 vs S3 notes); new Scripted-session gotchas
  section consolidates telnet-halt pattern, ESP_ONLYCPU, and ROM-frame
  guidance; UART-reset recipe stated once
- target-debug: fix run-on seam from the -singlerun insertion
- target-debugger agent: charter now resolves the board family FIRST and
  routes Espressif boards to esp-target-debug as primary playbook; skills
  table re-aligned
- retrieval regression: 4/4 (agent routing, S3 keep-alive quirk, OpenOCD RTT
  on ST-Link, ROM-frame guidance)
</content>
</entry>
</feed>
