Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions .claude/skills/build/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ description: The Adafruit TinyUSB Arduino build contract. Use when a change must
# Build contract

`scripts/check_build.py` is the entry point for verifying a change; agents and workflows call it rather than composing
`arduino-cli compile` lines.
`arduino-cli compile` lines. It is this library's profile for agentrc's `arduino` skill (`arduino_build.py`), loaded
from the agentrc commit `test/hil/agentrc.json` pins: run `python3 .claude/skills/hil-contract/scripts/agentrc_pin.py
fetch` once per pin. That skill's page has the mechanics this library shares with Adafruit SPIFlash.

```bash
B=.claude/skills/build/scripts/check_build.py
Expand Down Expand Up @@ -65,8 +67,11 @@ quotes `firstError` (and `error`). Each `warnings` entry is quoted in `notes`.
dir for `flash_args`. Example eligibility mirrors ci-arduino `build_platform.py`: `.<platform>.test.skip`,
a `.skip.txt` line equal to the platform, and `.<platform>.test.only` when any `.*.test.only` exists; a rig board
takes its CI platform's files (`filters`). A board whose core is not installed is exit 2 naming
`arduino-cli core install`; a missing library dependency fails that sketch, the README's `arduino-cli lib install`
line installs them. Local cores need not be CI's versions; the record names what built.
`arduino-cli core install`. Local cores need not be CI's versions; the record names what built.
- **Dependencies** (`arduino` skill): `library.properties` `depends=` plus `EXTRA_DEPENDENCIES` (CI's `ARDUINO_LIBS`
beyond it), and theirs, transitively, must be installed before a build: a missing one is exit 2 naming the
`arduino-cli lib install` line. A compile that takes any other library, outside the core, fails its sketch; the
JSON's `dependencies` and each row's `libraries` say what was used.
- **Every compile** is `arduino-cli compile --clean --format json --warnings all --fqbn <F> --library <this checkout>
--build-path <dir>`, so the core and every library rebuild from scratch and this library comes from this checkout
whatever else is installed (the primary checkout under `~/Arduino/libraries`, a core's bundled copy). The build
Expand All @@ -90,10 +95,11 @@ quotes `firstError` (and `error`). Each `warnings` entry is quoted in `notes`.
not checked, since the SAMD ELF's first starts at 0x0. For an esptool board it is the core's `flash_args`: a line of
options, then each image at its offset, every file in the build dir, ascending, not overlapping, inside the span.
A device sketch needs its VID:PID: a `-D` define, on ESP32 the variant's `pins_arduino.h` (the S3's 303a:1001),
else the library's fallback. It also refuses a foreign header that differs. It records HEAD, the FQBN, the platform
(id, version, install dir, and commit and dirty state when the core is a git checkout), the arduino-cli version, the
roster's and harness's sha256, the board's role, ESP `chip` and `fixture` (path and sha256), and per sketch its
images with sha256, load spans, VID:PID, foreign headers and, for esptool, `segments` (the build's `flash_args`
else the library's fallback. It also refuses a foreign header that differs. It records (schema 2, the `arduino`
skill's The HIL build record) HEAD, the FQBN, the platform (id, version, install dir, and commit and dirty state
when the core is a git checkout), the arduino-cli version, the resolved dependencies with their contents' sha256,
the roster's sha256, the contract files' and the agentrc engines' sha256, the board's role, ESP `chip` and `fixture` (path and sha256), and per sketch its
images with sha256, load spans, VID:PID, foreign headers, used libraries and, for esptool, `segments` (the build's `flash_args`
hashed beside them); paths are checkout-relative so the record stages to a rig unchanged.
- **Experimental records** (`--experimental`, with `--record`) are for debug and instrumentation: a dirty tree and
`--build-property` are allowed, untracked files under `src/`, `examples/` or `test/hil/` are refused (put
Expand Down
695 changes: 34 additions & 661 deletions .claude/skills/build/scripts/check_build.py

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions .claude/skills/hil-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ description: The Adafruit TinyUSB Arduino HIL contract, under agentrc's `hil` ru

# HIL contract

Read agentrc's `hil` skill first; this page supplies its contract sections. Manual runs only: no CI job runs this
battery. Scripts, all in `scripts/`:
Read agentrc's `hil` skill first; this page supplies its contract sections. The mechanics are agentrc's `arduino`
skill (`arduino_hil.py`, `arduino_remote.py`), shared with Adafruit SPIFlash; the battery, the host step and the roster
checks are this library's (`hil_test.py`). Manual runs only: no CI job runs this battery. Scripts, all in `scripts/`:

- `hil_test.py`: the harness on the host with the boards.
- `hil_remote.py`: drives the harness on ci.lan from the dev PC that built the firmware.
Expand Down Expand Up @@ -34,10 +35,11 @@ A roster entry:

- `name`: the board's `check_build.py` rig name, which is also its lock name. The names are tinyusb's farm names,
so this harness and tinyusb's exclude each other on a shared board.
- `uid`: the DUT's USB serial under Arduino firmware (Limits, commissioning); for a host board or a `usb_port`
- `uid`: the DUT's USB serial under Arduino firmware (Limits, commissioning); for a host board or a `no_serial`
board, the board's tinyusb farm id, unused here.
- `usb_port`: the sysfs bus port the DUT enumerates at, for firmware with an empty serial string (ESP32 under
Arduino, Limits); the harness then tells the DUT by port, not serial. Required for esptool boards.
- `usb_port`: the sysfs bus port the DUT enumerates at; the DUT must be there. Required for esptool boards.
- `no_serial`: `true` for firmware with an empty serial string (ESP32 under Arduino, Limits): the harness then tells
the DUT by `usb_port` alone, as the serial-less device there.
- `flasher`: `name` `openocd`, `jlink` or `esptool`, `uid` the probe serial (esptool: its USB-UART bridge's), `args`,
for openocd `vid_pid` and `verify`.
- `debug.openocd`: the openocd config the GDB session uses; with `debug.tool` `openocd-esp32`, exactly
Expand Down Expand Up @@ -274,11 +276,13 @@ human.
reached the device (usbmon, 2026-11-02). The open is not bounded by the test deadline; allow for it in any host-side
timing claim.
- **Commissioning.** `hil_test.py commission -b <board> ROSTER` finds the DUT's serial under Arduino firmware
(exactly one device re-enumerating across a flash and a probe reset), or for a `usb_port` board its port. It runs
(exactly one device re-enumerating across a flash and a probe reset, at the roster's `usb_port` when it has one), or
for a `no_serial` board its port, then checks the restoration sketch with `cdc_echo`. It runs
from a checkout on the board's host and is not staged by `hil_remote.py`; host boards have nothing to commission.
The ci.lan boards' uids come from tinyusb's roster; the battery confirmed the Pico's and the Fruit Jam's.
- **ESP32 identity:** under Arduino the ESP32-S3 and ESP32-P4 enumerate with an empty serial string (the library's ESP32
`TinyUSB_Port_GetSerialNumber` returns none), so the roster pins each by `usb_port` (S3 11-1.3, P4 11-1.1 on ci.lan):
`TinyUSB_Port_GetSerialNumber` returns none), so the roster marks each `no_serial` and pins it by `usb_port` (S3
11-1.3, P4 11-1.1 on ci.lan):
the DUT candidate is the device there with no cached sysfs `serial`, since while esptool runs the S3's ROM
USB-Serial/JTAG appears at the same port as `303a:1001` with its MAC as serial; a failed serial read would also leave
none, so the test run on the candidate is the evidence. The S3 enumerates as `303a:1001`, its core variant's
Expand Down
31 changes: 29 additions & 2 deletions .claude/skills/hil-contract/scripts/agentrc_pin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""The agentrc commit the HIL harness loads its shared scripts from (agentrc `hil` skill, Shared mechanisms).
"""The agentrc commit the build and HIL contracts load their shared scripts from (agentrc `arduino` and `hil` skills).

agentrc_pin.py fetch fetch the pinned commit into the user cache (never touches another agentrc checkout)
agentrc_pin.py check exit 0 when the cached checkout is the pin and clean, else 1 naming why
Expand All @@ -21,6 +21,9 @@
ROOT = Path(__file__).resolve().parents[4]
PIN_FILE = ROOT / 'test' / 'hil' / 'agentrc.json'
FETCH_HINT = 'python3 .claude/skills/hil-contract/scripts/agentrc_pin.py fetch'
# the agentrc engines the contracts run; their FILES closures are what a staged rig tree carries and a record binds
ENGINES = ('skills/arduino/scripts/arduino_build.py', 'skills/arduino/scripts/arduino_hil.py',
'skills/arduino/scripts/arduino_remote.py')


class PinError(Exception):
Expand Down Expand Up @@ -108,7 +111,8 @@ def closure(root, scripts):
out = set()
for rel in scripts:
out.add(rel)
out.update(files_of(root, rel))
if (Path(root) / rel).is_file():
out.update(files_of(root, rel))
missing = sorted(r for r in out if not (Path(root) / r).is_file())
if missing:
raise PinError(f'agentrc at {root} lacks {missing}')
Expand All @@ -126,6 +130,29 @@ def loader(root):
return sys.modules[spec.name].load(spec.name, spec.origin)


def staged():
"""A rig tree hil_remote.py staged: no .git, its agentrc files under agentrc/."""
return not (ROOT / '.git').exists() and (ROOT / 'agentrc').is_dir()


def root():
"""The agentrc this process loads from: the staged copy on a rig, else the pinned checkout."""
return ROOT / 'agentrc' if staged() else checkout()


def load(rel):
"""agentrc's `rel`, loaded through its hil_load once the engines' closure is all present."""
r = root()
closure(r, ENGINES)
return loader(r).load_file(rel)


def identity():
"""{commit, root, files}: the pin, where it is loaded from, and the engines' closure (agentrc-relative)."""
r = root()
return {'commit': pin()[1], 'root': r, 'files': closure(r, ENGINES)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved


def main(argv):
cmd = argv[0] if argv else ''
try:
Expand Down
Loading
Loading