Build contract

SkillAI & models

Lets your agent build a project change and report whether it passes, with the first error if it fails.

Use Build contract in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add Build contract and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use the Build contract skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Build contractStart free
About this skill

The project build contract. Use when a change must be verified by building, when an agent needs the boards a diff affects, or when a workflow asks for "the project's build". Resolves scope to boards, builds in a private dir, reports pass/fail with the first error.

What this skill tells your AI

The instructions your AI receives, as published by hathach/tinyusb in .claude/skills/build/SKILL.md and read by ahel’s review.

scripts/check_build.py is the entry point for verifying a change; every agent and workflow that does so calls it rather than composing cmake lines. Two builds are not verification and stay on tools/build.py directly: the debug build below, and the PVS compile-database build for the pvs-studio agent, which needs one configured tree for the analyser, not a sweep.

B=.claude/skills/build/scripts/check_build.py
python3 $B --scope <changed paths or dirs>   # boards a change affects, one per family
python3 $B --base master                      # same, from the branch diff
python3 $B --board stm32f407disco [-e device/cdc_msc] [-T target]
python3 $B --board stm32f407disco -e host/cdc_msc_hid --cflag=-DCFG_TUH_CDC_FTDI_LATENCY=16
python3 $B ... --shared                       # canonical cmake-build-<board>, the dir HIL flashes from
python3 $B --board <b> --shared --variants <hil config>   # one cmake-build-<variant> per roster variant
python3 $B --board <b> --shared --variants <hil config> --receipt .hil-remote/<name>.json   # plus the build receipt a delegated HIL run checks (hil skill)
python3 $B ... --fetch-deps                   # fetch a family's missing deps instead of failing

The last stdout line is JSON: pass, per-board status (ok, failed, skipped, error), built (elf files this run wrote), firstError, buildDir, how the boards were resolved, and the scope paths nothing builds, split into nothingToBuild and uncovered. Exit 0 pass, 1 a board failed, 2 usage or resolution error with the message in error (a missing dependency names its remedy there) or a refused receipt with its reason in receipt.error, 3 an uncovered path. -v streams the build to stderr.

Judgment

  • Full example set by default. -e narrows to named examples for a quick check; a change in src/ or a class driver is verified by the full set on every resolved board before it is called green.
  • Scope, not guesswork. --scope takes changed paths, expanding a directory into its tracked files first, and runs tools/ci_select.py: one board per affected family; a board whose own hw/bsp/<family>/boards/<board>/ files changed is built itself, else a rig-roster board of the family, else the first in the family. A full-matrix answer (core or unclassified paths) builds stm32f407disco and raspberry_pi_pico, plus any board whose own files changed, plus one board per family a port, bsp or mcu path in the scope names: the pair stands in for the matrix on core code, not on a port it does not contain. A changed driver under src/portable/ adds, per family it maps to, a board whose hw/bsp/family.json row lists the driver and, where the host preprocessor can say, selects the USB IPs its guard names: stm32l412nucleo for fsdev next to a DWC2 pick on stm32l4, feather_rp2040_max3421 for hcd_max3421.c. A special target the changed path needs (for example examples-membrowse-upload for a membrowse build-time script, ci_select rule 2d) is -T; a default sweep is no evidence for it.
  • A path nothing builds is one of two kinds, in ci_select's words, and the script sorts them so that a mixed change gets the stricter answer:
    • nothingToBuild (docs, .claude/, *.md, unit tests, HIL harness): there is nothing a build could verify, which is not a failure. A scope of only such paths is pass true with no boards, exit 0; quote the reason in your notes so nobody mistakes it for a build that ran.
    • uncovered (a class or typec no example config enables, a lib nothing builds, a port mapping to no board family, a family the selector pruned because nothing in it builds, an example or class no built board wrote an elf for, a port driver no built board of its families kept a line of after preprocessing): firmware this run did not compile, judged against the boards and elfs it actually produced. The port test is the driver's own compile command run with -E on every instance in an example this run built, so a board whose configuration guards the driver away (dcd_nrf5x.c on NRF54, hcd_rp2040.c under MAX3421) is not coverage whatever else went green; the reason names each board's verdict. -e or -T hands coverage to you: the script then does not report what you chose to leave out, and a -T target that writes no elf is not a verified build of anything. Exit 3 and pass false, even when every other path in the scope built green — a class driver plus the core file that registers it is the common shape, and the green boards say nothing about the driver. The verdict is false and the uncovered reason, quoted verbatim, is the finding: a human adds coverage (the next bullet) or accepts the gap.
  • Private dirs for parallel agents. Builds land in cmake-build/cmake-build-agent-<pid>-<board>; remove yours when done. --shared writes cmake-build/cmake-build-<board>, the dir hil_test.py flashes from by default, and needs exclusive ownership of that board: never while a HIL run or another agent is on it. --variants <hil config> builds instead each variant that roster gives a named board, into the cmake-build-<variant> hil_test.py runs it from, with your own defines and flags followed by the variant's, so a repeated -D takes the variant's value; a --cflag macro the variant's flags also set is a -Werror redefinition, not an override (a board without a variant list as itself); it refuses a board the roster lacks.
  • hw/bsp/family.json is what a board's default configure compiles: its mcu, the tested defines, family_mcus, the MAX3421_HOST option and its drivers under src/portable/, one row per cmake board, null for the two Make-only pic32mz boards. tools/build.py rewrites the row whenever it runs a default configure (cmake, gcc, no -D, no --cflag, into a dir that did not exist yet, which every private dir is; --configure-only does just that), so a change that moves a board's configuration leaves the catalog modified: the run's JSON says so in familyJsonChanged and per board in familyJson, and the row is committed with the change. The pre-commit hook family-json repairs the catalog in place, like codespell: it configures each board without a row once into a fresh private dir, drops rows of boards that are gone, rewrites the file canonical, and fails the commit when it changed anything, so you stage the file and commit again (python3 tools/family_json.py fix by hand does the same). A board this machine cannot configure (no toolchain, ESP-IDF not exported) stays reported with tools/build.py's reason.
  • Dependencies. The family's entries in tools/get_deps.py must exist. In a worktree, symlink them from the primary checkout (CLAUDE.md, Working Rules); --fetch-deps is for a fresh clone. The script never fetches on its own.
  • Code no example enables. A branch behind a CFG_* option no board sets is not compiled by any sweep, so a change there is unverified until you build it with the option on: --cflag=-D<OPTION>=<value> (repeatable), narrowed with -e to one example that reaches the code. -D passes a build-system define (LOG=2) the same way, except on Espressif boards, where tools/build.py hands defines to cmake only, so the run is refused rather than built without them. Never hand-roll a cmake line for this.
  • Espressif boards go through tools/build.py's ESP-IDF path; . "$IDF_PATH/export.sh" must be in the environment first, and only ESP-IDF examples build for them.
  • Debug builds: -DCMAKE_BUILD_TYPE=Debug -DLOG=2 -DLOGGER=rtt via tools/build.py -D directly; this script builds MinSizeRel. ETM trace builds add -DTRACE_ETM=1 the same way. python3 tools/build_utils.py board-info <board> prints the board's J-Link device name and its reference Ozone project. With several J-Links attached, -DJLINK_OPTION="-USB <serial>" pins the <example>-jlink flash target. rp2350 at a non-default core clock: pass SYS_CLK_KHZ together with PLL_SYS_VCO_FREQ_HZ/PLL_SYS_POSTDIV1/PLL_SYS_POSTDIV2 from the SDK's scripts/vcocalc.py as compile definitions; 240 MHz also needs SYS_CLK_VREG_VOLTAGE_AUTO_ADJUST=1 and SYS_CLK_VREG_VOLTAGE_MIN=VREG_VOLTAGE_1_15.
  • Printing a command is not verification. A verdict is the JSON of a run that happened; an agent that cannot run the script reports the build as not run.

Signals

GitHub stars
7k
Forks
2k
Last commit
Oct 2026
Advanced
Item type
skill
Key
build-hathach
Source
github.com/hathach/tinyusb