summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <[email protected]>2026-07-13 18:01:07 +0700
committerhathach <[email protected]>2026-07-13 18:01:07 +0700
commitff69550b3d8d55b5c9d48a3dfe4e87f7690e9455 (patch)
tree93567f5abd365f572282a6c2c63ce39c2f60fd05
parent0557655afbb9e608c26ac0c6cbf95c6e69138c77 (diff)
Fix max-effort review findings in lock protocol, workflows, and docs
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01Upj4hta5TNoAbidqeC1zZ6
-rw-r--r--.claude/agents/hil-operator.md4
-rw-r--r--.claude/agents/port-dev.md2
-rw-r--r--.claude/skills/hil/SKILL.md2
-rw-r--r--.claude/workflows/driver-review.js7
-rw-r--r--.claude/workflows/fanout-dev.js2
-rw-r--r--.claude/workflows/full-check.js2
-rw-r--r--.claude/workflows/hil-validate.js6
-rw-r--r--.claude/workflows/pr-babysit.js38
-rw-r--r--.claude/workflows/validate.js4
-rw-r--r--docs/superpowers/plans/2026-07-09-claude-agents-workflows.md158
-rw-r--r--docs/superpowers/plans/2026-07-09-smoke-results.md11
-rw-r--r--docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md25
-rwxr-xr-xtest/hil/board_lock.py86
-rwxr-xr-xtest/hil/hil_test.py29
14 files changed, 247 insertions, 129 deletions
diff --git a/.claude/agents/hil-operator.md b/.claude/agents/hil-operator.md
index 6fc5b448b..c48ceb8bd 100644
--- a/.claude/agents/hil-operator.md
+++ b/.claude/agents/hil-operator.md
@@ -15,7 +15,7 @@ You operate physical USB test hardware. These repo skills are your source of tru
The GitHub Actions runner keeps running during your work. Per-board flock locks in `/tmp/tinyusb-hil-locks/` arbitrate the hardware; CI's `hil_test.py` fails fast on locked boards (re-runnable later).
-- `python3 test/hil/hil_test.py ...` runs: do NOT pre-hold those boards — `hil_test.py` self-locks each board for its flash+test (pre-holding would deadlock it).
+- `python3 test/hil/hil_test.py ...` runs: do NOT pre-hold those boards — `hil_test.py` self-locks each board for its flash+test and would fail fast with `board locked` against your own hold.
- ANY other hardware action (JLinkExe/openocd/GDB, manual flash, usbtest.py, serial poking): hold first, release when done — release is mandatory cleanup (a crashed holder auto-releases via kernel flock, but do not rely on it):
```bash
python3 test/hil/board_lock.py hold <board...> --reason "<task>"
@@ -30,7 +30,7 @@ The GitHub Actions runner keeps running during your work. Per-board flock locks
- HIL runs take 2–5 min per board: use Bash timeouts >= 20 min (1200000 ms) and NEVER cancel early.
- One hardware action at a time. You are never run concurrently with another hil-operator.
-- On test failure: retry once with `-v` appended. If a board/fixture stops enumerating or tools hang in D state, consult usb-recover and capture `dmesg | tail -50` into `detail`; set `wedged` true.
+- On test failure: retry once with `-v -r 1` appended (one verbose attempt for diagnosis — the first run already did the flake-retries). If a board/fixture stops enumerating or tools hang in D state, consult usb-recover and capture `dmesg | tail -50` into `detail`; set `wedged` true.
## Output contract
diff --git a/.claude/agents/port-dev.md b/.claude/agents/port-dev.md
index 1238ae64a..76bafb39b 100644
--- a/.claude/agents/port-dev.md
+++ b/.claude/agents/port-dev.md
@@ -20,7 +20,7 @@ When changing dcd/hcd register logic, cross-check the MCU reference manual / dat
## Finish checklist (in order)
-1. Format only the lines you changed: `git clang-format` (no args — formats working-tree changes vs HEAD using the repo `.clang-format`). If it reformats anything, re-check your diff still builds.
+1. Format only the files you changed: `git clang-format -- <file...>` (list your edited files explicitly — bare `git clang-format` formats the WHOLE working-tree diff, including other concurrent workers' in-flight edits in a shared checkout). If it reformats anything, re-check your diff still builds.
2. Verify with a targeted build of `device/cdc_msc` for the board named in your task (or pick one from `hw/bsp/<family>/boards/` whose family uses your scope). Use a unique build dir to survive parallel siblings:
```bash
BUILD=$(mktemp -d /tmp/portdev-<BOARD>-XXXX)
diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md
index eb73060f4..d61ec05e4 100644
--- a/.claude/skills/hil/SKILL.md
+++ b/.claude/skills/hil/SKILL.md
@@ -18,7 +18,7 @@ Default to **local**. Use **remote** only when on `htpc` and the user says `remo
The `ci` rig also hosts a GitHub Actions runner that flashes boards and runs HIL as part of CI. Hardware access is arbitrated **per board** with kernel flocks in `/tmp/tinyusb-hil-locks/` — do NOT stop the runner service.
-- `hil_test.py` self-locks each board for the duration of its flash+test (holder reason `hil_test.py`). A locked board FAILS immediately (`FAILED (board locked: ...)`) without flashing — in CI, re-run the failed job once the lock is released.
+- `hil_test.py` self-locks each board for the duration of its flash+test (holder reason `hil_test.py`). A locked board fails immediately (`<board> Failed: board locked: {holder info}`) without flashing — in CI, re-run the failed job once the lock is released.
- If your `hold` fails and the holder's reason is `hil_test.py`, a CI job is mid-test on that board — wait a few minutes and retry rather than forcing.
- For hardware work outside `hil_test.py` (JLink/GDB, manual flashing, `usbtest.py`, serial poking), hold the lock first:
diff --git a/.claude/workflows/driver-review.js b/.claude/workflows/driver-review.js
index 074244ca5..3638aa179 100644
--- a/.claude/workflows/driver-review.js
+++ b/.claude/workflows/driver-review.js
@@ -9,7 +9,7 @@ export const meta = {
}
// args: { dirs: string[], dimensions?: string[], question?: string }
-if (typeof args === 'string') args = JSON.parse(args) // tolerate stringified invocation args
+if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !Array.isArray(args.dirs) || args.dirs.length === 0) {
throw new Error('args must be { dirs: string[], dimensions?, question? }')
}
@@ -19,6 +19,11 @@ const DIMS = args.question ? [args.question] : (args.dimensions || [
'register use vs datasheet and MCU errata: cross-check the reference manual AND errata sheets in $HOME/Documents/calibre-library; a missing erratum workaround is a finding',
'style: repo conventions (TU_ASSERT, no dynamic allocation, include order, naming)',
])
+if (!DIMS.length) {
+ // [] is truthy, so `dimensions: []` would silently review nothing and
+ // return a verdict indistinguishable from a genuinely clean pass
+ throw new Error('dimensions resolved to an empty list — pass a non-empty array or omit it for the defaults')
+}
const short = (s) => s.replace(/\/+$/, '').split('/').slice(-2).join('/')
const FINDINGS = {
diff --git a/.claude/workflows/fanout-dev.js b/.claude/workflows/fanout-dev.js
index ae74df8aa..e98a86f1d 100644
--- a/.claude/workflows/fanout-dev.js
+++ b/.claude/workflows/fanout-dev.js
@@ -10,7 +10,7 @@ export const meta = {
}
// args: { task: string, items: string[], board?: string | Record<string,string>, review?: boolean, worktree?: boolean }
-if (typeof args === 'string') args = JSON.parse(args) // tolerate stringified invocation args
+if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !args.task || !Array.isArray(args.items) || args.items.length === 0) {
throw new Error('args must be { task: string, items: string[], board?, review?, worktree? }')
}
diff --git a/.claude/workflows/full-check.js b/.claude/workflows/full-check.js
index caaedff01..6e353bf72 100644
--- a/.claude/workflows/full-check.js
+++ b/.claude/workflows/full-check.js
@@ -6,7 +6,7 @@ export const meta = {
}
// args: { boards: string[], hilBoards?: string[], examples?: string, base?: string, skip?: string[] }
-if (typeof args === 'string') args = JSON.parse(args) // tolerate stringified invocation args
+if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !Array.isArray(args.boards) || args.boards.length === 0) {
throw new Error('args must be { boards: string[], hilBoards?, examples?, base?, skip? }')
}
diff --git a/.claude/workflows/hil-validate.js b/.claude/workflows/hil-validate.js
index f1fa6cd1f..aa0556abc 100644
--- a/.claude/workflows/hil-validate.js
+++ b/.claude/workflows/hil-validate.js
@@ -6,7 +6,7 @@ export const meta = {
}
// args: { boards: string[], force?: boolean }
-if (typeof args === 'string') args = JSON.parse(args) // tolerate stringified invocation args
+if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !Array.isArray(args.boards) || args.boards.length === 0) {
throw new Error('args must be { boards: string[], force? } with examples/cmake-build-<board> already built')
}
@@ -27,7 +27,7 @@ const runBoard = (b) => agent(
: 'If the run fails because the board lock is held (a dev session or concurrent CI job), report pass=false and set detail to start EXACTLY with "board locked:" followed by the holder JSON verbatim — never force the lock. ') +
'Reserve the phrase "board locked" strictly for lock contention; describe a frozen or non-enumerating board as "unresponsive" instead. ' +
`Firmware is in examples/cmake-build-${b}. Use the config for this host (hostname first), single-board flag -b ${b}, Bash timeout >= 20 min, never cancel early. ` +
- 'On non-lock failures retry once with -v. wedged=true if the board/fixture is unresponsive after the run (capture dmesg | tail -50 into detail).',
+ 'On non-lock failures retry once with -v -r 1 (one verbose attempt for diagnosis — the first run already did the flake-retries). wedged=true if the board/fixture is unresponsive after the run (capture dmesg | tail -50 into detail).',
{ label: `hil:${b}`, phase: 'HIL', agentType: 'hil-operator', schema: HIL },
)
@@ -57,4 +57,4 @@ if (wedged.length) log(`WEDGED boards needing usb-recover: ${wedged.join(', ')}`
// session to ask: force (re-invoke with force: true), wait, or accept.
const locked = args.force ? [] : results.filter(r => !r.pass && r.detail.startsWith('board locked')).map(r => r.board)
if (locked.length) log(`still locked after retry: ${locked.join(', ')} — ask the user: force / keep waiting / accept`)
-return { pass: results.length === args.boards.length && results.every(r => r.pass), results, wedged, locked }
+return { pass: results.every(r => r.pass), results, wedged, locked }
diff --git a/.claude/workflows/pr-babysit.js b/.claude/workflows/pr-babysit.js
index c78b2e7be..8bb414114 100644
--- a/.claude/workflows/pr-babysit.js
+++ b/.claude/workflows/pr-babysit.js
@@ -6,11 +6,18 @@ export const meta = {
}
// args: { pr: number, maxCycles?: number, autoPush?: boolean (default false = dry run) }
-if (typeof args === 'string') args = JSON.parse(args) // tolerate stringified invocation args
+if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !args.pr) {
throw new Error('args must be { pr: number, maxCycles?, autoPush? }; run from a checkout of the PR branch')
}
+args.pr = Number(args.pr)
+if (!Number.isInteger(args.pr) || args.pr <= 0) {
+ throw new Error('args.pr must be a positive integer PR number')
+}
const maxCycles = args.maxCycles ?? 3
+if (!Number.isInteger(maxCycles) || maxCycles < 1) {
+ throw new Error('maxCycles must be an integer >= 1')
+}
const TRIAGE = {
type: 'object', additionalProperties: false,
@@ -89,7 +96,17 @@ const RESOLVE_RECIPE =
'`gh api graphql -f query=\'mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}\' -F id=THREAD_ID`. ' +
'Issue comments (the 404 fallback case) have no thread — do not try to resolve those.'
+// Mechanical reply skeleton shared by the refuted-replies and fixed-resolve
+// steps — kept in one place because the two copies drifted once already
+// (the 404 fallback was missing from one of them).
+const postReplyRecipe = (noun) =>
+ `post a threaded reply to its inline comment via gh api repos/{owner}/{repo}/pulls/${args.pr}/comments/{commentId}/replies -f body=<body> ` +
+ '(valid for inline review comments); if that 404s, the id is an issue comment — post a regular PR comment instead ' +
+ `(gh pr comment ${args.pr} --body <quote the original point, then the ${noun}>) and skip resolving. ` +
+ `After replying to an inline comment, mark its thread resolved. ${RESOLVE_RECIPE} `
+
const history = []
+const repliedIds = new Set() // issue comments can't be thread-resolved, so they re-harvest every cycle — never reply twice
for (let cycle = 1; cycle <= maxCycles; cycle++) {
const t = await agent(
`Triage PR #${args.pr}. If checks are still running, wait for them first (gh pr checks ${args.pr} --watch, Bash timeout >= 30 min). ` +
@@ -106,16 +123,15 @@ for (let cycle = 1; cycle <= maxCycles; cycle++) {
// Post drafted replies to REFUTED findings as soon as triage produces them —
// decoupled from fixing/pushing so done/unactionable cycles still post.
// Reply AND resolve the thread. Outward-facing, so gated on autoPush.
- if (t.replies.length > 0 && args.autoPush === true) {
+ const freshReplies = t.replies.filter(r => !repliedIds.has(r.commentId))
+ if (freshReplies.length > 0 && args.autoPush === true) {
const posted = await agent(
- `Reply to and resolve these refuted review comments on PR #${args.pr}. For each: post the reply with ` +
- `gh api repos/{owner}/{repo}/pulls/${args.pr}/comments/{commentId}/replies -f body=<body> ` +
- '(valid for inline review comments); if that 404s, the id is an issue comment — post a regular PR comment instead ' +
- `(gh pr comment ${args.pr} --body <quote the original point, then the reply>) and skip resolving. ` +
- `After replying to an inline comment, mark its thread resolved. ${RESOLVE_RECIPE} ` +
- `Replies: ${JSON.stringify(t.replies)}. pass=true only if every reply was posted and every inline thread resolved; detail = what went where.`,
+ `Reply to and resolve these refuted review comments on PR #${args.pr}. For each: ${postReplyRecipe('reply')}` +
+ `Replies: ${JSON.stringify(freshReplies)}. pass=true only if every reply was posted and every inline thread resolved; detail = what went where.`,
{ label: `replies#${cycle}`, phase: 'Push', model: 'sonnet', schema: OP },
)
+ // attempted counts as replied: better to drop a failed reply than spam duplicates
+ freshReplies.forEach(r => repliedIds.add(r.commentId))
if (!posted || !posted.pass) log(`cycle ${cycle}: refuted reply/resolve incomplete — ${posted ? posted.detail : 'agent died'}`)
}
@@ -198,10 +214,8 @@ for (let cycle = 1; cycle <= maxCycles; cycle++) {
if (fixed.length > 0) {
const resolved = await agent(
`The fixes for PR #${args.pr}'s valid review findings were just committed and pushed (${push.detail}). ` +
- 'For each finding below: post a threaded reply to its inline comment via ' +
- `gh api repos/{owner}/{repo}/pulls/${args.pr}/comments/{commentId}/replies -f body=<body>, stating it is fixed in the pushed commit and one line on the change; ` +
- `if that 404s, the id is an issue comment — post a regular PR comment instead (gh pr comment ${args.pr} --body <quote the original point, then the fix note>) and skip resolving. ` +
- `After replying to an inline comment, mark its thread resolved. ${RESOLVE_RECIPE} ` +
+ `For each finding below: ${postReplyRecipe('fix note')}` +
+ 'Each reply states the finding is fixed in the pushed commit, with one line on the change. ' +
`Findings: ${JSON.stringify(fixed.map(f => ({ commentId: f.commentId, file: f.file, line: f.line, claim: f.claim, fixHint: f.fixHint })))}. ` +
'pass=true only if every reply was posted and every thread resolved; detail = what went where.',
{ label: `resolve#${cycle}`, phase: 'Push', model: 'sonnet', schema: OP },
diff --git a/.claude/workflows/validate.js b/.claude/workflows/validate.js
index 83cb18120..522548ee6 100644
--- a/.claude/workflows/validate.js
+++ b/.claude/workflows/validate.js
@@ -6,7 +6,7 @@ export const meta = {
}
// args: { boards: string[], examples?: string, base?: string, skip?: ('unit'|'size'|'pvs')[] }
-if (typeof args === 'string') args = JSON.parse(args) // tolerate stringified invocation args
+if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
if (!args || !Array.isArray(args.boards) || args.boards.length === 0) {
throw new Error('args must be { boards: string[], examples?, base?, skip? }')
}
@@ -76,7 +76,7 @@ for (const b of args.boards) thunks.push(() =>
if (!skip.includes('size')) thunks.push(() =>
agent(
- `Compare TinyUSB code size against ${base}: python3 tools/metrics_compare_base.py --base-branch ${base} -b ${args.boards[0]} -e device/cdc_msc . ` +
+ `Compare TinyUSB code size against ${base}: python3 tools/metrics_compare_base.py --base-branch ${base} -b ${args.boards[0]} -e device/cdc_msc (exactly this command — no extra positional args). ` +
'The report lands in cmake-metrics/<board>/metrics_compare.md. pass=false only if the tool itself errors; ' +
'detail = the flash/RAM delta summary from the report (mention any example that grew).',
{ label: 'size', phase: 'Validate', model: 'haiku', schema: STAGE },
diff --git a/docs/superpowers/plans/2026-07-09-claude-agents-workflows.md b/docs/superpowers/plans/2026-07-09-claude-agents-workflows.md
index 32efe1a2e..02e064fc4 100644
--- a/docs/superpowers/plans/2026-07-09-claude-agents-workflows.md
+++ b/docs/superpowers/plans/2026-07-09-claude-agents-workflows.md
@@ -1,6 +1,14 @@
# TinyUSB Multi-Agent Dev/Test Harness 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.
+> **HISTORICAL RECORD — DO NOT EXECUTE.** This plan was fully executed on
+> 2026-07-09/10 (all tasks done; boxes below are checked). It is kept only as
+> the audit trail of how the harness was built. The embedded file bodies are
+> STALE SNAPSHOTS — the shipped `.claude/agents/*.md` and `.claude/workflows/*.js`
+> have since evolved (static-analyzer agent, schema and recipe changes); the
+> living design doc is `docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md`.
+> Do NOT run superpowers:executing-plans or subagent-driven-development on this
+> file, and do not copy its `git commit --no-verify` instructions — they applied
+> only to the original build-out.
**Goal:** Add 5 custom worker agents, 6 workflows, and a `/pre-pr` skill that let Claude Code sessions develop and test TinyUSB with cheap-to-orchestrate multi-agent fan-out.
@@ -38,7 +46,7 @@
- Produces (consumed by Tasks 6, 7 via `agentType: 'builder'`): final-message JSON
`{"board": string, "pass": boolean, "builtCount": integer, "failures": [{"example": string, "class": string, "firstError": string}]}`
-- [ ] **Step 1: Write the agent file**
+- [x] **Step 1: Write the agent file**
Write `.claude/agents/builder.md` with exactly this content:
@@ -92,12 +100,12 @@ Your final message is parsed by a program. Return ONLY this JSON — no prose, n
`pass` is true only when zero failures remain after retries. `builtCount` = number of examples that built.
````
-- [ ] **Step 2: Verify structure**
+- [x] **Step 2: Verify structure**
Run: `head -8 .claude/agents/builder.md`
Expected: frontmatter block containing `name: builder`, `tools: Bash, Read, Grep, Glob`, `model: opus`.
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/agents/builder.md
@@ -116,12 +124,12 @@ git commit --no-verify -m "feat: add builder worker agent"
- Produces (consumed by Tasks 7, 11 via `agentType: 'port-dev'`): final-message JSON
`{"item": string, "diffstat": string, "buildOk": boolean, "board": string, "notes": string}`
-- [ ] **Step 1: Install clang-format (host tool, one-time)**
+- [x] **Step 1: Install clang-format (host tool, one-time)**
Run: `sudo apt-get install -y clang-format && git clang-format -h | head -3`
Expected: apt succeeds; `git clang-format -h` prints usage (proves both `clang-format` and the `git clang-format` subcommand exist). If the package is already installed this is a no-op.
-- [ ] **Step 2: Write the agent file**
+- [x] **Step 2: Write the agent file**
Write `.claude/agents/port-dev.md` with exactly this content:
@@ -166,12 +174,12 @@ Your final message is parsed by a program. Return ONLY this JSON — no prose, n
`buildOk` is the result of step 2. Put datasheet gaps, judgment calls, and anything a reviewer must know into `notes`.
````
-- [ ] **Step 3: Verify structure**
+- [x] **Step 3: Verify structure**
Run: `head -6 .claude/agents/port-dev.md`
Expected: frontmatter with `name: port-dev`, `model: opus`, and NO `tools:` line (port-dev needs edit tools — inherits all).
-- [ ] **Step 4: Commit**
+- [x] **Step 4: Commit**
```bash
git add .claude/agents/port-dev.md
@@ -190,7 +198,7 @@ git commit --no-verify -m "feat: add port-dev worker agent"
- findings: `{"scope": string, "dimension": string, "findings": [{"file": string, "line": integer, "snippet": string, "why": string, "severity": "critical"|"major"|"minor", "confidence": "high"|"medium"|"low"}]}`
- verification: `{"addresses": boolean, "reason": string}` (also used with keys `real`/`reason` when the prompt asks to refute a finding)
-- [ ] **Step 1: Write the agent file**
+- [x] **Step 1: Write the agent file**
Write `.claude/agents/driver-reviewer.md` with exactly this content:
@@ -223,12 +231,12 @@ Your final message is parsed by a program. Return ONLY the JSON shape your promp
{"scope": "src/portable/...", "dimension": "...", "findings": [{"file": "...", "line": 123, "snippet": "...", "why": "...", "severity": "major", "confidence": "high"}]}
````
-- [ ] **Step 2: Verify structure**
+- [x] **Step 2: Verify structure**
Run: `head -8 .claude/agents/driver-reviewer.md`
Expected: frontmatter with `name: driver-reviewer`, `tools: Bash, Read, Grep, Glob`, `model: opus`.
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/agents/driver-reviewer.md
@@ -246,7 +254,7 @@ git commit --no-verify -m "feat: add driver-reviewer worker agent"
- Produces (consumed by Task 9 via `agentType: 'hil-operator'`): final-message JSON per calling prompt — board runs `{"board": string, "pass": boolean, "detail": string, "wedged": boolean}`
- Consumes: `test/hil/board_lock.py` (Task 19) for manual hardware work.
-- [ ] **Step 1: Write the agent file**
+- [x] **Step 1: Write the agent file**
Write `.claude/agents/hil-operator.md` with exactly this content:
@@ -292,12 +300,12 @@ Your final message is parsed by a program. Return ONLY the JSON shape your promp
{"board": "raspberry_pi_pico", "pass": true, "detail": "<per-test summary or first failure>", "wedged": false}
````
-- [ ] **Step 2: Verify structure**
+- [x] **Step 2: Verify structure**
Run: `head -8 .claude/agents/hil-operator.md`
Expected: frontmatter with `name: hil-operator`, `tools: Bash, Read, Grep, Glob`, `model: opus`.
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/agents/hil-operator.md
@@ -315,7 +323,7 @@ git commit --no-verify -m "feat: add hil-operator worker agent"
- Produces (consumed by Task 11 via `agentType: 'pr-monitor'`): final-message JSON
`{"ci": {"status": "green"|"red"|"running", "infraRerun": [string], "realFailures": [{"check": string, "firstError": string, "files": [string]}]}, "findings": [{"source": string, "commentId": integer, "file": string, "line": integer, "claim": string, "verdict": "valid"|"invalid"|"stale", "reason": string, "fixHint": string}], "replies": [{"commentId": integer, "body": string}], "done": boolean}`
-- [ ] **Step 1: Write the agent file**
+- [x] **Step 1: Write the agent file**
Write `.claude/agents/pr-monitor.md` with exactly this content:
@@ -360,12 +368,12 @@ Your final message is parsed by a program. Return ONLY this JSON — no prose, n
"done": false}
````
-- [ ] **Step 2: Verify structure**
+- [x] **Step 2: Verify structure**
Run: `head -8 .claude/agents/pr-monitor.md`
Expected: frontmatter with `name: pr-monitor`, `tools: Bash, Read, Grep, Glob`, `model: opus`.
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/agents/pr-monitor.md
@@ -384,7 +392,7 @@ git commit --no-verify -m "feat: add pr-monitor triage agent"
- `check.sh <file.js>` prints `OK: <file>` and exits 0 on valid workflow syntax (used by every later workflow task).
- `validate` workflow — Consumes: `builder` agent (Task 1). Args `{boards: string[], examples?: string, base?: string, skip?: ('unit'|'size'|'pvs')[]}`. Returns `{pass: boolean, stages: [{stage, pass, detail}], failures: [...]}` (consumed by Task 10 via `workflow('validate', ...)`).
-- [ ] **Step 1: Write the checker**
+- [x] **Step 1: Write the checker**
Write `.claude/workflows/check.sh` with exactly this content (then `chmod +x .claude/workflows/check.sh`):
@@ -405,12 +413,12 @@ trap 'rm -f "$tmp"' EXIT
node --check "$tmp" && echo "OK: $f"
```
-- [ ] **Step 2: Verify checker fails on bad input and passes on good**
+- [x] **Step 2: Verify checker fails on bad input and passes on good**
Run: `printf 'return }broken\n' > /tmp/bad.js; bash .claude/workflows/check.sh /tmp/bad.js; echo "exit=$?"`
Expected: SyntaxError printed, `exit=1` (non-zero).
-- [ ] **Step 3: Write validate.js**
+- [x] **Step 3: Write validate.js**
Write `.claude/workflows/validate.js` with exactly this content:
@@ -500,12 +508,12 @@ log(`${results.length}/${thunks.length} stages completed, ${failures.length} fai
return { pass: failures.length === 0 && dead === 0, stages: results, failures }
```
-- [ ] **Step 4: Syntax-check**
+- [x] **Step 4: Syntax-check**
Run: `bash .claude/workflows/check.sh .claude/workflows/validate.js`
Expected: `OK: .claude/workflows/validate.js`
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add .claude/workflows/check.sh .claude/workflows/validate.js
@@ -523,7 +531,7 @@ git commit --no-verify -m "feat: add validate workflow and workflow syntax check
- Consumes: `port-dev` (Task 2), `builder` (Task 1), `driver-reviewer` (Task 3).
- Args `{task: string, items: string[], board?: string | {[item]: string}, review?: boolean, worktree?: boolean}`. Returns array of `{item, diffstat, buildOk, board, notes, verifyBuild?, review?}`.
-- [ ] **Step 1: Write the workflow**
+- [x] **Step 1: Write the workflow**
Write `.claude/workflows/fanout-dev.js` with exactly this content:
@@ -642,12 +650,12 @@ log(`${done.length}/${args.items.length} items completed; ${done.filter(r => r.b
return done
```
-- [ ] **Step 2: Syntax-check**
+- [x] **Step 2: Syntax-check**
Run: `bash .claude/workflows/check.sh .claude/workflows/fanout-dev.js`
Expected: `OK: .claude/workflows/fanout-dev.js`
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/workflows/fanout-dev.js
@@ -666,7 +674,7 @@ git commit --no-verify -m "feat: add fanout-dev workflow"
- Args `{dirs: string[], dimensions?: string[], question?: string}`. Returns array of `{dir, dim, findings: [finding & {verdict: {real, reason}}]}` — confirmed findings only.
- Note: supersedes the untracked prototype `.claude/workflows/port-audit.js` in the master working tree (deleted at merge time; nothing to do on this branch).
-- [ ] **Step 1: Write the workflow**
+- [x] **Step 1: Write the workflow**
Write `.claude/workflows/driver-review.js` with exactly this content:
@@ -756,12 +764,12 @@ log(`${confirmed.length} scan units produced confirmed findings`)
return confirmed
```
-- [ ] **Step 2: Syntax-check**
+- [x] **Step 2: Syntax-check**
Run: `bash .claude/workflows/check.sh .claude/workflows/driver-review.js`
Expected: `OK: .claude/workflows/driver-review.js`
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/workflows/driver-review.js
@@ -779,7 +787,7 @@ git commit --no-verify -m "feat: add driver-review workflow (supersedes port-aud
- Consumes: `hil-operator` (Task 4). Requires `examples/cmake-build-<board>/` to exist for every board.
- Args `{boards: string[]}`. Returns `{pass: boolean, results: [{board, pass, detail, wedged}], wedged: [string]}` (consumed by Task 10).
-- [ ] **Step 1: Write the workflow**
+- [x] **Step 1: Write the workflow**
Write `.claude/workflows/hil-validate.js` with exactly this content:
@@ -846,12 +854,12 @@ if (locked.length) log(`still locked after retry: ${locked.join(', ')} — ask t
return { pass: results.length === args.boards.length && results.every(r => r.pass), results, wedged, locked }
```
-- [ ] **Step 2: Syntax-check**
+- [x] **Step 2: Syntax-check**
Run: `bash .claude/workflows/check.sh .claude/workflows/hil-validate.js`
Expected: `OK: .claude/workflows/hil-validate.js`
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/workflows/hil-validate.js
@@ -869,7 +877,7 @@ git commit --no-verify -m "feat: add hil-validate workflow (serialized, lock-arb
- Consumes: `validate` workflow (Task 6), `hil-validate` workflow (Task 9) via `workflow()` nesting (one level — legal).
- Args `{boards: string[], hilBoards?: string[], examples?: string, base?: string, skip?: string[]}`. Returns `{pass, software, hardware}` (consumed by the `/pre-pr` skill, Task 12).
-- [ ] **Step 1: Write the workflow**
+- [x] **Step 1: Write the workflow**
Write `.claude/workflows/full-check.js` with exactly this content:
@@ -910,12 +918,12 @@ if (hardware && hardware.locked && hardware.locked.length) {
return { pass: !!(hardware && hardware.pass), software, hardware }
```
-- [ ] **Step 2: Syntax-check**
+- [x] **Step 2: Syntax-check**
Run: `bash .claude/workflows/check.sh .claude/workflows/full-check.js`
Expected: `OK: .claude/workflows/full-check.js`
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/workflows/full-check.js
@@ -934,7 +942,7 @@ git commit --no-verify -m "feat: add full-check composed workflow"
- Args `{pr: number, maxCycles?: number, autoPush?: boolean}`. Must run from a checkout of the PR branch. Returns `{pass, cycles, history, reason?, dryRun?}`.
- Push authorization: invoking with `autoPush !== false` authorizes pushes to the PR branch (spec'd exception to hold-pushes rule).
-- [ ] **Step 1: Write the workflow**
+- [x] **Step 1: Write the workflow**
Write `.claude/workflows/pr-babysit.js` with exactly this content:
@@ -1152,12 +1160,12 @@ for (let cycle = 1; cycle <= maxCycles; cycle++) {
return { pass: false, cycles: maxCycles, history, reason: 'maxCycles reached' }
```
-- [ ] **Step 2: Syntax-check**
+- [x] **Step 2: Syntax-check**
Run: `bash .claude/workflows/check.sh .claude/workflows/pr-babysit.js`
Expected: `OK: .claude/workflows/pr-babysit.js`
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/workflows/pr-babysit.js
@@ -1174,7 +1182,7 @@ git commit --no-verify -m "feat: add pr-babysit workflow"
**Interfaces:**
- Consumes: `full-check` workflow (Task 10), `test/hil/tinyusb.json` (board roster), `hw/bsp/*/family.cmake|family.mk` (family mapping).
-- [ ] **Step 1: Write the skill**
+- [x] **Step 1: Write the skill**
Write `.claude/skills/pre-pr/SKILL.md` with exactly this content:
@@ -1223,12 +1231,12 @@ Invoke the Workflow tool:
- End with a clear ship / no-ship verdict and what to fix first.
````
-- [ ] **Step 2: Verify structure**
+- [x] **Step 2: Verify structure**
Run: `head -4 .claude/skills/pre-pr/SKILL.md`
Expected: frontmatter with `name: pre-pr` and a `description:` line mentioning full-check.
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/skills/pre-pr/SKILL.md
@@ -1241,48 +1249,48 @@ git commit --no-verify -m "feat: add /pre-pr skill entry point"
**Files:** none created; exercises Tasks 1, 6.
-- [ ] **Step 1: Run** — Invoke the Workflow tool: `{ name: 'validate', args: { boards: ['stm32f407disco', 'raspberry_pi_pico'] } }`. (If board deps are missing the builder self-heals via get_deps.)
-- [ ] **Step 2: Verify** — Returned object has `stages` entries for `unit`, `build:stm32f407disco`, `build:raspberry_pi_pico`, `size`, `pvs`; each `detail` is meaningful; `pass` consistent with stage results. Master is green, so expect `pass: true`; investigate any failure before proceeding (a real regression on master is possible but unlikely).
-- [ ] **Step 3: Record** — Append the verdict JSON (and rough token usage from /workflows) to `docs/superpowers/plans/2026-07-09-smoke-results.md`; commit with `docs: record validate smoke result`.
+- [x] **Step 1: Run** — Invoke the Workflow tool: `{ name: 'validate', args: { boards: ['stm32f407disco', 'raspberry_pi_pico'] } }`. (If board deps are missing the builder self-heals via get_deps.)
+- [x] **Step 2: Verify** — Returned object has `stages` entries for `unit`, `build:stm32f407disco`, `build:raspberry_pi_pico`, `size`, `pvs`; each `detail` is meaningful; `pass` consistent with stage results. Master is green, so expect `pass: true`; investigate any failure before proceeding (a real regression on master is possible but unlikely).
+- [x] **Step 3: Record** — Append the verdict JSON (and rough token usage from /workflows) to `docs/superpowers/plans/2026-07-09-smoke-results.md`; commit with `docs: record validate smoke result`.
### Task 14: Smoke test — `fanout-dev` (MAIN SESSION ONLY)
-- [ ] **Step 1: Run** — Invoke Workflow: `{ name: 'fanout-dev', args: { task: 'Add a single comment line `// fanout-dev smoke test — remove me` at the very top of the main dcd_*.c source file in your assigned scope. Make no other change.', items: ['src/portable/raspberrypi/rp2040', 'src/portable/st/stm32_fsdev'], board: { 'src/portable/raspberrypi/rp2040': 'raspberry_pi_pico', 'src/portable/st/stm32_fsdev': 'stm32f072disco' } } }`
-- [ ] **Step 2: Verify** — Both items return `buildOk: true` and `verifyBuild: true`; `git diff --stat` shows exactly 2 files, 1 insertion each; diffs are clang-format-clean (`git clang-format --diff` reports no changes).
-- [ ] **Step 3: Revert the smoke edits** — `git checkout -- src/portable/` (verify `git status` clean afterwards).
-- [ ] **Step 4: Record** — Append results to the smoke-results doc; commit.
+- [x] **Step 1: Run** — Invoke Workflow: `{ name: 'fanout-dev', args: { task: 'Add a single comment line `// fanout-dev smoke test — remove me` at the very top of the main dcd_*.c source file in your assigned scope. Make no other change.', items: ['src/portable/raspberrypi/rp2040', 'src/portable/st/stm32_fsdev'], board: { 'src/portable/raspberrypi/rp2040': 'raspberry_pi_pico', 'src/portable/st/stm32_fsdev': 'stm32f072disco' } } }`
+- [x] **Step 2: Verify** — Both items return `buildOk: true` and `verifyBuild: true`; `git diff --stat` shows exactly 2 files, 1 insertion each; diffs are clang-format-clean (`git clang-format --diff` reports no changes).
+- [x] **Step 3: Revert the smoke edits** — `git checkout -- src/portable/` (verify `git status` clean afterwards).
+- [x] **Step 4: Record** — Append results to the smoke-results doc; commit.
### Task 15: Smoke test — `driver-review` (MAIN SESSION ONLY)
-- [ ] **Step 1: Run** — Invoke Workflow: `{ name: 'driver-review', args: { dirs: ['src/portable/renesas/rusb2', 'src/portable/nxp/lpc_ip3511'], question: 'unbounded busy-wait loops polling hardware status bits with no timeout or bail-out (whole-stack freeze risk if hardware never sets the bit)' } }`
-- [ ] **Step 2: Verify** — Returns only confirmed findings, each carrying `verdict.real: true` with a reasoned `verdict.reason`; spot-check one finding by reading the cited code yourself. (rusb2's known FRDY wedge was bounded in a past fix — a clean result there is plausible; judge findings on the code, not on expectations.)
-- [ ] **Step 3: Record** — Append results to the smoke-results doc; commit.
+- [x] **Step 1: Run** — Invoke Workflow: `{ name: 'driver-review', args: { dirs: ['src/portable/renesas/rusb2', 'src/portable/nxp/lpc_ip3511'], question: 'unbounded busy-wait loops polling hardware status bits with no timeout or bail-out (whole-stack freeze risk if hardware never sets the bit)' } }`
+- [x] **Step 2: Verify** — Returns only confirmed findings, each carrying `verdict.real: true` with a reasoned `verdict.reason`; spot-check one finding by reading the cited code yourself. (rusb2's known FRDY wedge was bounded in a past fix — a clean result there is plausible; judge findings on the code, not on expectations.)
+- [x] **Step 3: Record** — Append results to the smoke-results doc; commit.
### Task 16: Smoke test — `hil-validate` + board locks (MAIN SESSION ONLY)
-- [ ] **Step 1: Preconditions** — `ls examples/cmake-build-raspberry_pi_pico/` exists (from Task 13); Tasks 19–20 are done (`test/hil/board_lock.py` exists, `hil_test.py` guard in place); runner is ACTIVE: `systemctl is-active actions.runner.hathach-tinyusb.tinyusb.service` prints `active`.
-- [ ] **Step 2: Lock-conflict run** — `python3 test/hil/board_lock.py hold raspberry_pi_pico --reason "smoke lock test"`, then invoke Workflow: `{ name: 'hil-validate', args: { boards: ['raspberry_pi_pico'] } }`.
+- [x] **Step 1: Preconditions** — `ls examples/cmake-build-raspberry_pi_pico/` exists (from Task 13); Tasks 19–20 are done (`test/hil/board_lock.py` exists, `hil_test.py` guard in place); runner is ACTIVE: `systemctl is-active actions.runner.hathach-tinyusb.tinyusb.service` prints `active`.
+- [x] **Step 2: Lock-conflict run** — `python3 test/hil/board_lock.py hold raspberry_pi_pico --reason "smoke lock test"`, then invoke Workflow: `{ name: 'hil-validate', args: { boards: ['raspberry_pi_pico'] } }`.
Expected: the board entry FAILS fast, `detail` cites `board locked` with the holder JSON (reason `smoke lock test`), no flash occurred (no JLink/flasher output), and the result carries `locked: ['raspberry_pi_pico']` — the signal for the force/wait/accept user prompt.
-- [ ] **Step 3: Force path (lock still held)** — invoke Workflow: `{ name: 'hil-validate', args: { boards: ['raspberry_pi_pico'], force: true } }`.
+- [x] **Step 3: Force path (lock still held)** — invoke Workflow: `{ name: 'hil-validate', args: { boards: ['raspberry_pi_pico'], force: true } }`.
Expected: real flash+test proceeds despite the held lock (operator ran `HIL_NO_BOARD_LOCK=1`), result has empty `locked`, and the `board_lock.py status` holder is still alive afterwards (bypass, not theft).
-- [ ] **Step 4: Release and normal run** — `python3 test/hil/board_lock.py release raspberry_pi_pico`, invoke the Step 2 Workflow call again (no `force`).
+- [x] **Step 4: Release and normal run** — `python3 test/hil/board_lock.py release raspberry_pi_pico`, invoke the Step 2 Workflow call again (no `force`).
Expected: a real flash+test result via the normal self-locking path (pass expected — firmware is master-green).
-- [ ] **Step 5: Runner untouched** — `systemctl is-active actions.runner.hathach-tinyusb.tinyusb.service` still prints `active`; the workflow made no stop/start calls.
-- [ ] **Step 6: Record** — Append all three results to the smoke-results doc; update the memory note `~/.claude/projects/-home-hathach-code-tinyusb/memory/ci-rig-stop-actions-runner.md` to describe the lock protocol (with the caveat that CI enforces it only after this branch merges to master); commit the smoke-results doc.
+- [x] **Step 5: Runner untouched** — `systemctl is-active actions.runner.hathach-tinyusb.tinyusb.service` still prints `active`; the workflow made no stop/start calls.
+- [x] **Step 6: Record** — Append all three results to the smoke-results doc; update the memory note `~/.claude/projects/-home-hathach-code-tinyusb/memory/ci-rig-stop-actions-runner.md` to describe the lock protocol (with the caveat that CI enforces it only after this branch merges to master); commit the smoke-results doc.
### Task 17: Smoke test — `/pre-pr` end-to-end (MAIN SESSION ONLY)
-- [ ] **Step 1: Run** — Invoke the `pre-pr` skill on this branch (its diff is docs + `.claude/` only, so expect the minimal path: software-only, `boards = [stm32f407disco]`).
-- [ ] **Step 2: Verify** — The skill correctly detects "no C changes", runs `full-check` with the minimal args, and produces the per-stage summary + verdict.
-- [ ] **Step 3: Record** — Append to the smoke-results doc; commit.
+- [x] **Step 1: Run** — Invoke the `pre-pr` skill on this branch (its diff is docs + `.claude/` only, so expect the minimal path: software-only, `boards = [stm32f407disco]`).
+- [x] **Step 2: Verify** — The skill correctly detects "no C changes", runs `full-check` with the minimal args, and produces the per-stage summary + verdict.
+- [x] **Step 3: Record** — Append to the smoke-results doc; commit.
### Task 18: Smoke test — `pr-babysit` dry run (MAIN SESSION ONLY)
-- [ ] **Step 1: Pick a target** — `gh pr list --limit 10 --json number,title,headRefName` — choose an open PR with completed CI and at least one bot review comment; check out its branch in a THROWAWAY worktree (`git worktree add /tmp/prsmoke <headRef>` after `git fetch`), and run from there so fix edits can't dirty this branch.
-- [ ] **Step 2: Run** — Invoke Workflow from that checkout: `{ name: 'pr-babysit', args: { pr: <N>, maxCycles: 1, autoPush: false } }`
-- [ ] **Step 3: Verify** — Triage classifies CI checks plausibly (compare with `gh pr checks <N>` yourself); each bot finding has a reasoned verdict (spot-check one against the code); result has `dryRun: true` if fixes were produced, and NOTHING was committed or pushed (`git -C /tmp/prsmoke status`, `gh pr view <N> --json comments` unchanged).
-- [ ] **Step 4: Clean up** — `git worktree remove --force /tmp/prsmoke`.
-- [ ] **Step 5: Record** — Append to the smoke-results doc; commit.
+- [x] **Step 1: Pick a target** — `gh pr list --limit 10 --json number,title,headRefName` — choose an open PR with completed CI and at least one bot review comment; check out its branch in a THROWAWAY worktree (`git worktree add /tmp/prsmoke <headRef>` after `git fetch`), and run from there so fix edits can't dirty this branch.
+- [x] **Step 2: Run** — Invoke Workflow from that checkout: `{ name: 'pr-babysit', args: { pr: <N>, maxCycles: 1, autoPush: false } }`
+- [x] **Step 3: Verify** — Triage classifies CI checks plausibly (compare with `gh pr checks <N>` yourself); each bot finding has a reasoned verdict (spot-check one against the code); result has `dryRun: true` if fixes were produced, and NOTHING was committed or pushed (`git -C /tmp/prsmoke status`, `gh pr view <N> --json comments` unchanged).
+- [x] **Step 4: Clean up** — `git worktree remove --force /tmp/prsmoke`.
+- [x] **Step 5: Record** — Append to the smoke-results doc; commit.
---
@@ -1294,7 +1302,7 @@ git commit --no-verify -m "feat: add /pre-pr skill entry point"
**Interfaces:**
- Produces (consumed by `hil-operator` agent, Task 20's guard shares the same lock files): CLI `hold <board...> [--all] [--config PATH] --reason TEXT` / `release <board...> [--all]` / `status`. Lock files: `/tmp/tinyusb-hil-locks/<board>.lock`, exclusive `fcntl.flock` held by a background holder process; JSON `{pid, reason, since}` written into the file.
-- [ ] **Step 1: Write the tool**
+- [x] **Step 1: Write the tool**
Write `test/hil/board_lock.py` with exactly this content (then `chmod +x test/hil/board_lock.py`):
@@ -1493,7 +1501,7 @@ if __name__ == '__main__':
main()
```
-- [ ] **Step 2: Test the lock lifecycle**
+- [x] **Step 2: Test the lock lifecycle**
Run each line and check the expectation before the next:
@@ -1521,7 +1529,7 @@ python3 test/hil/board_lock.py release fakeboard # -
rm -f /tmp/tinyusb-hil-locks/fakeboard.lock
```
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add test/hil/board_lock.py
@@ -1539,12 +1547,12 @@ git commit --no-verify -m "feat(hil): add per-board advisory lock tool"
- Consumes: the lock files of Task 19 (`/tmp/tinyusb-hil-locks/<board>.lock`).
- Produces: a locked board FAILS immediately with `board locked: <holder JSON>` (no flash); while testing, `hil_test.py` holds the board's flock and writes its own holder info (`reason: "hil_test.py"`) so the reverse conflict reports truthfully. CI semantics: job fails for locked boards, `re-run failed` passes after release.
-- [ ] **Step 1: Locate the per-board entry point**
+- [x] **Step 1: Locate the per-board entry point**
Run: `grep -n "Pool(\|\.map\|\.imap\|def test_board\|def run_board" test/hil/hil_test.py`
Identify the function the `multiprocessing.Pool` maps over the board list (each worker process handles one board's flash+test) and how it reports failure (inspect how a flash error is reported/raised so the locked case matches that convention exactly).
-- [ ] **Step 2: Add the guard**
+- [x] **Step 2: Add the guard**
Add near the top of `hil_test.py` (module level, after existing imports — `fcntl` and `os` may need importing):
@@ -1609,7 +1617,7 @@ finally:
Match indentation and the file's existing style exactly; keep the diff minimal (guard function + one try/finally wrap).
-- [ ] **Step 3: Test the locked path (no hardware touched)**
+- [x] **Step 3: Test the locked path (no hardware touched)**
```bash
python3 test/hil/board_lock.py hold raspberry_pi_pico --reason "guard test"
@@ -1618,12 +1626,12 @@ python3 test/hil/board_lock.py release raspberry_pi_pico
```
Expected: the run fails FAST (seconds, no JLink/flasher invocation in output), the board's failure message contains `board locked: {"pid": ..., "reason": "guard test", ...}`, exit code non-zero. (The unlocked happy path is exercised on real hardware in Task 16.)
-- [ ] **Step 4: Sanity-check no syntax damage**
+- [x] **Step 4: Sanity-check no syntax damage**
Run: `python3 -m py_compile test/hil/hil_test.py && echo OK`
Expected: `OK`
-- [ ] **Step 5: Commit**
+- [x] **Step 5: Commit**
```bash
git add test/hil/hil_test.py
@@ -1637,7 +1645,7 @@ git commit --no-verify -m "feat(hil): fail fast on dev-locked boards instead of
**Files:**
- Modify: `.claude/skills/hil/SKILL.md` (replace the "Stop the CI runner first" section, currently lines 17–29)
-- [ ] **Step 1: Replace the runner-stop section**
+- [x] **Step 1: Replace the runner-stop section**
In `.claude/skills/hil/SKILL.md`, replace the entire section from the heading `## Stop the CI runner first (on \`ci\`)` up to (not including) `## Prerequisites` with:
@@ -1663,12 +1671,12 @@ python3 test/hil/board_lock.py release BOARD [BOARD...]
- Caveat until this branch merges to master: CI's checkout of `hil_test.py` does not yet enforce locks — keep dev hardware sessions short and check `gh run list --status in_progress` first.
````
-- [ ] **Step 2: Verify**
+- [x] **Step 2: Verify**
Run: `grep -n "svc.sh stop" .claude/skills/hil/SKILL.md; grep -c "board_lock.py" .claude/skills/hil/SKILL.md`
Expected: no `svc.sh stop` occurrences remain; `board_lock.py` appears ≥ 3 times.
-- [ ] **Step 3: Commit**
+- [x] **Step 3: Commit**
```bash
git add .claude/skills/hil/SKILL.md
diff --git a/docs/superpowers/plans/2026-07-09-smoke-results.md b/docs/superpowers/plans/2026-07-09-smoke-results.md
index 7fb26a347..a15638247 100644
--- a/docs/superpowers/plans/2026-07-09-smoke-results.md
+++ b/docs/superpowers/plans/2026-07-09-smoke-results.md
@@ -65,3 +65,14 @@ Stop-gate extra (done this session): board_lock `cmd_hold` holder-signaled succe
- pr-babysit: `autoPush` now opt-in (default dry run; `autoPush: true` is the explicit push/comment authorization); RESOLVE_RECIPE paginates reviewThreads (`pageInfo` + cursor); post-push resolve step gained the same issue-comment 404 fallback as the refuted-replies step.
- validate: size stage passes `--base-branch <base>`; pvs stage now calls the new `static-analyzer` agent (sonnet, structured `{pass, ga1, ga2, changedFindings[], detail}`).
- NEW agent `static-analyzer` (PVS-Studio SAST+MISRA, read-only) — mirrored to the launch-dir registry; registers next session (harness fact 1), so the validate pvs stage is unsmokable until then.
+
+## Max-effort review fixes (2026-07-13, /code-review opus max: 10 finders → 28 verifiers → sweep)
+
+- validate.js size stage: dropped the stray trailing `.` that made metrics_compare_base.py exit 2 on every run (regression from the 2026-07-10 batch).
+- board_lock: flock is now the sole authority — cmd_hold's pid-liveness pre-gate removed (a live-but-moved-on hil_test.py worker pid no longer blocks a free board; verified: hold succeeds over a stale live-pid record, storm still 1-winner-in-10). cmd_release probes the flock before acting: free → clear stale record only; held by `hil_test.py` → refuse (CI mid-test, holder survives — verified); held otherwise → SIGTERM with PermissionError handled. Holder daemon truncates records on SIGTERM; success pipe dup'd above fd 2 (closed-stdio hold now succeeds — was orphan-holder + false failure, repro'd both ways).
+- hil_test: lock record truncated on per-board release (pool workers outlive flocks); fail-open on OSError now prints a warning; unknown `-b` names exit 1 instead of a silent zero-test green (was exploitable as a false HIL pass through hil-validate); accumulate_report deletes an emptied board row (variant boards no longer leave a blank ghost row — 4-scenario test green).
+- port-dev.md: `git clang-format -- <files>` scoped to the worker's own files (bare invocation reformatted concurrent siblings' edits in shared checkouts).
+- pr-babysit: reply skeleton factored into postReplyRecipe (the two copies had already drifted once); cross-cycle repliedIds dedup (issue-comment refutations were re-posted every cycle); args.pr integer + maxCycles >= 1 validation.
+- All 6 workflows: JSON.parse(args) wrapped so a non-JSON string hits the friendly shape error; driver-review throws on empty dimensions ([] is truthy); hil-validate dead length-clause dropped; retry instruction now `-v -r 1` (diagnosis, not 3 more flake-retries).
+- Plan doc header replaced with a DO-NOT-EXECUTE historical banner + all 74 boxes checked (re-execution would have recreated pre-static-analyzer files with hooks disabled).
+- Refuted by verification (left as-is by design): dry-run verify spawns (consumed via history), per-finding verifiers, 4-dim scanners, fanout double-build, validate parallel triple-compile, release --all / hold --config, board_lock import into hil_test (hil_ci.sh ships hil_test.py alone), lock exit code (JSON sidecar already carries board-locked), check.sh scope, effort scatter (harness-forced), local fcntl import (Windows guard).
diff --git a/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md b/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md
index f37929439..63788720c 100644
--- a/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md
+++ b/docs/superpowers/specs/2026-07-09-claude-agents-workflows-design.md
@@ -59,24 +59,31 @@ log-heavy).
CI and dev sessions share the rig concurrently; the actions-runner service is
never stopped. Arbitration is per-board kernel flocks in
`/tmp/tinyusb-hil-locks/<board>.lock` — auto-released when the holder process
-dies (stale locks impossible; `/tmp` clears on reboot):
+dies, with holders truncating their lock-file record on release so records
+stay truthful (`/tmp` clears on reboot):
- **`test/hil/board_lock.py`** (new tool): `hold <boards|--all> --reason TEXT`
spawns a background holder process flocking each board file (JSON
- `{pid, reason, since}` written inside for debuggability); `release
- <boards|--all>` kills holders; `status` lists them. `--all` is required
- before rig-wide operations (uhubctl power cycling, pci-rebind — bus
- renumbering affects every board).
+ `{pid, reason, since}` written inside for debuggability); the holder's own
+ LOCK_NB flock is the sole authority — there is deliberately no pid-based
+ pre-check (recorded pids can be stale or recycled). `release <boards|--all>`
+ probes each board's flock: a free lock only gets its stale record cleared;
+ a genuinely held one gets its recorded holder SIGTERMed — unless the holder
+ reason is `hil_test.py` (a CI run mid-test), which release refuses to kill.
+ `status` lists holders. `--all` is required before rig-wide operations
+ (uhubctl power cycling, pci-rebind — bus renumbering affects every board).
- **`hil_test.py` guard** (small patch to the per-board worker): take the
board's flock non-blocking before flashing and hold it for that board's
flash+test; on acquire it writes its own holder info
(`{pid, reason: "hil_test.py", since}`) so conflicts report truthfully in
- both directions. If already held, FAIL the board immediately —
- `FAILED (board locked: <holder info>)` — no flash, no waiting.
+ both directions, and truncates that record on release (the pool worker
+ outlives the per-board flock). If already held, FAIL the board immediately —
+ `Failed: board locked: <holder info>` — no flash, no waiting.
The CI job fails visibly for exactly those boards and `re-run failed`
passes once the lock is released (`build.yml` already retries the HIL step
- once, absorbing short dev sessions). Guard defaults to proceeding if the
- lock dir is absent/odd.
+ once, absorbing short dev sessions). Guard proceeds unlocked — with a
+ printed warning — if the lock dir is unusable, and `-b` names absent from
+ the config are a hard error rather than a silent zero-test green run.
- **Re-entrancy rule:** dev sessions do NOT pre-hold boards they are about to
run `hil_test.py` on (it self-locks; pre-holding deadlocks it).
`board_lock.py hold` is for hardware work outside `hil_test.py` only.
diff --git a/test/hil/board_lock.py b/test/hil/board_lock.py
index 42df80b35..c35e13705 100755
--- a/test/hil/board_lock.py
+++ b/test/hil/board_lock.py
@@ -3,12 +3,13 @@
Arbitrates board access between dev sessions and CI's hil_test.py without
stopping the actions-runner. Locks are kernel flocks: the kernel releases
-them automatically when the holder process dies, so stale locks are
-impossible (/tmp also clears on reboot).
+them automatically when the holder process dies, and holders clear their
+lock-file record on release so records stay truthful (/tmp also clears on
+reboot).
Usage:
board_lock.py hold BOARD [BOARD...] --reason TEXT
- board_lock.py hold --all [--config test/hil/tinyusb.json] --reason TEXT
+ board_lock.py hold --all [--config CONFIG.json] --reason TEXT
board_lock.py release BOARD [BOARD...] | release --all
board_lock.py status
@@ -69,11 +70,9 @@ def is_locked(board: str) -> bool:
def cmd_hold(boards, reason):
os.makedirs(LOCK_DIR, exist_ok=True)
- already = [b for b in boards if is_locked(b)]
- if already:
- for b in already:
- print(f'ERROR: {b} already locked: {read_info(b)}', file=sys.stderr)
- return 1
+ # No pre-check: the holder's own LOCK_NB flock is the only authority — a
+ # recorded pid may be stale or recycled (e.g. a live hil_test.py worker
+ # that already released this board's flock but not its record).
# The holder signals success through this pipe. A generic is_locked()
# poll would be fooled by a RIVAL invocation's flock — only the holder
# itself knows whether it won every board.
@@ -88,7 +87,11 @@ def cmd_hold(boards, reason):
if ok:
print(f'held: {", ".join(boards)}')
return 0
- print('ERROR: holder failed to acquire locks (lost a race?)', file=sys.stderr)
+ for b in boards:
+ info = read_info(b)
+ if info:
+ print(f'ERROR: {b} locked: {info}', file=sys.stderr)
+ print('ERROR: holder failed to acquire locks', file=sys.stderr)
return 1
# intermediate child: detach, then spawn the actual holder
os.setsid()
@@ -96,6 +99,10 @@ def cmd_hold(boards, reason):
os._exit(0)
# holder (grandchild): acquire all flocks, signal the parent, sleep until killed
os.close(r_fd)
+ # Keep the success pipe clear of fds 0-2: invoked with stdio closed,
+ # os.pipe() can land there and the dup2 loop below would clobber it.
+ if w_fd <= 2:
+ w_fd = fcntl.fcntl(w_fd, fcntl.F_DUPFD, 3)
# Detach stdio: a `hold` whose output is captured must see EOF when the
# front-end exits — the immortal holder must not keep that pipe open.
devnull = os.open(os.devnull, os.O_RDWR)
@@ -125,31 +132,71 @@ def cmd_hold(boards, reason):
os._exit(1) # lost a race; parent reports the failure
os.write(w_fd, b'1')
os.close(w_fd)
- signal.signal(signal.SIGTERM, lambda *_: os._exit(0))
+
+ def _bow_out(*_):
+ # clear the records before dying so read_info/status stay truthful
+ # (the kernel drops the flocks themselves on exit either way)
+ for h in handles:
+ try:
+ h.truncate(0)
+ except OSError:
+ pass
+ os._exit(0)
+
+ signal.signal(signal.SIGTERM, _bow_out)
while True:
signal.pause()
def cmd_release(boards):
- pids = set()
+ rc = 0
+ victims = set()
for b in boards:
- if not is_locked(b):
+ try:
+ fd = os.open(lock_path(b), os.O_RDWR)
+ except OSError:
+ continue # no lock file (or another user's): nothing we can release
+ fh = os.fdopen(fd, 'r+')
+ try:
+ fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except OSError:
+ # flock genuinely held — never SIGTERM on a mere pid record: the
+ # pid may be recycled, or a live worker that already moved on.
+ fh.close()
+ info = read_info(b) or {}
+ pid = info.get('pid')
+ if info.get('reason') == 'hil_test.py':
+ print(f'ERROR: {b} is mid-test by hil_test.py (pid {pid}) — not killing a '
+ 'CI run; wait for it to finish', file=sys.stderr)
+ rc = 1
+ elif isinstance(pid, int) and pid > 0:
+ victims.add(pid)
+ else:
+ print(f'ERROR: {b} is held but its record is unreadable', file=sys.stderr)
+ rc = 1
continue
- info = read_info(b) or {}
- if info.get('pid'):
- pids.add(info['pid'])
- for holder in sorted(pids):
+ # flock was free: only a stale record remained — clear it
+ try:
+ fh.truncate(0)
+ except OSError:
+ pass
+ fh.close()
+ for holder in sorted(victims):
try:
os.kill(holder, signal.SIGTERM)
print(f'released holder pid {holder}')
except ProcessLookupError:
pass
+ except PermissionError:
+ print(f'ERROR: holder pid {holder} belongs to another user — cannot signal it',
+ file=sys.stderr)
+ rc = 1
time.sleep(0.3)
still = [b for b in boards if is_locked(b)]
if still:
print(f'ERROR: still locked: {", ".join(still)}', file=sys.stderr)
return 1
- return 0
+ return rc
def cmd_status():
@@ -176,7 +223,10 @@ def main():
p_hold = sub.add_parser('hold')
p_hold.add_argument('boards', nargs='*')
p_hold.add_argument('--all', action='store_true')
- p_hold.add_argument('--config', default='test/hil/tinyusb.json')
+ p_hold.add_argument('--config',
+ default=os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ 'tinyusb.json'),
+ help='board roster JSON (default: tinyusb.json beside this script)')
p_hold.add_argument('--reason', required=True)
p_rel = sub.add_parser('release')
p_rel.add_argument('boards', nargs='*')
diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py
index 2e5085079..4b87154b6 100755
--- a/test/hil/hil_test.py
+++ b/test/hil/hil_test.py
@@ -75,8 +75,12 @@ def acquire_board_lock(board_name):
fd = os.open(os.path.join(BOARD_LOCK_DIR, f'{board_name}.lock'),
os.O_RDWR | os.O_CREAT, 0o666)
fh = os.fdopen(fd, 'r+')
- except OSError:
- return None # odd lock dir (perms, path collision): proceed unlocked
+ except OSError as e:
+ # odd lock dir (perms, path collision): proceed unlocked, but say so —
+ # a silent fail-open is indistinguishable from the intentional bypass
+ print(f'warning: board lock unavailable for {board_name} ({e}); proceeding unlocked',
+ flush=True)
+ return None
try:
fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
@@ -1767,6 +1771,14 @@ def test_board(board: Board) -> tuple[str, int, list[str], list]:
return name, err_count, sorted(set(failed_tests)), rows
finally:
if _lock_fh:
+ try:
+ # clear our pid record before dropping the flock: this worker
+ # process lives on (pool reuse), so a stale record would make
+ # board_lock.py's pid-liveness checks report a freed board as
+ # still locked for the rest of the run
+ _lock_fh.truncate(0)
+ except OSError:
+ pass
_lock_fh.close()
@@ -1837,7 +1849,13 @@ def accumulate_report(mret: list, report_dir: Path, fresh: bool) -> str:
if rows and not any('board-locked' in cells for _, cells in rows):
# board ran for real this time: clear a stale lock-failure cell
# (its row is keyed by board name; test rows may be variant names)
- acc.get(name, {}).pop('board-locked', None)
+ stale = acc.get(name)
+ if stale is not None:
+ stale.pop('board-locked', None)
+ if not stale:
+ # variant-keyed boards never repopulate the board-name row —
+ # drop it or it renders as a blank ghost row
+ del acc[name]
for row_label, cells in rows:
acc.setdefault(row_label, {}).update(cells)
@@ -1900,6 +1918,11 @@ def main() -> None:
if len(boards) == 0:
config_boards = [e for e in config['boards'] if e['name'] not in skip_boards]
else:
+ unknown = [b for b in boards if b not in {e['name'] for e in config['boards']}]
+ if unknown:
+ # exiting 0 with 'No tests were run.' would read as a green HIL run
+ print(f'ERROR: board(s) not in {config_file.name}: {", ".join(unknown)}')
+ sys.exit(1)
config_boards = [e for e in config['boards'] if e['name'] in boards]
build_err = 0