3"""The tools this server exposes, and the JSON schema that advertises them.
5One function per tool, plus the :data:`TOOLS` table mapping the advertised
6name to its schema and handler. Implementation and schema live in the same
7file on purpose: a tool whose schema drifts from its handler is invisible to
8the server -- the client is told about a parameter nothing reads, or a
9parameter the handler requires is never sent -- and nothing else in the
10protocol layer can detect that.
13from __future__
import annotations
30_JUST_LAUNCHER = (
"/bin/bash",
"-p",
"scripts/dev/run_just.sh")
33def _just_argv(*args: str) -> list[str]:
34 """Return a Just command routed through the repository-owned resolver."""
35 return [*_JUST_LAUNCHER, *args]
41def tool_list_apps(args: dict[str, Any]) -> str:
42 """List discovered firmware apps, optionally filtered by a substring."""
43 needle = str(args.get(
"filter",
"")).lower()
44 apps = discover_apps()
47 app_identifier = app[
"id"]
48 hay = f
"{app_identifier} {app['group']} {app['description']}".lower()
49 if needle
and needle
not in hay:
51 rows.append(f
" {app['group']:<28} {app_identifier:<60} {app['description']}")
52 header = f
"{len(rows)} firmware app(s)" + (f
" matching '{needle}'" if needle
else "")
53 body =
"\n".join(rows)
if rows
else " (none matched)"
56 " build: just apps::build <app> | flash: just apps::hardware::flash <app> | "
57 "emulate: just apps::emulator::run <app>\n\n"
58 f
"{' TIER/GROUP':<30} {'APP IDENTIFIER':<60} DESCRIPTION\n{body}\n"
62def tool_app_info(args: dict[str, Any]) -> str:
63 """Show details for one firmware app: location, description, boot files, README."""
64 name = str(args.get(
"app",
"")).strip()
65 app = require_app(name)
66 app_dir = REPO_ROOT / app[
"dir"]
75 "src/secure_exception.c",
76 "src/trustzone_init.c",
77 "inc/trustzone_init.h",
79 "linker_script_cpu1.ld",
86 if (app_dir / f).is_file()
90 f
"group/tier: {app['group']}",
91 f
"directory: {app['dir']}",
92 f
"description: {app['description'] or '(none)'}",
93 f
"files: {', '.join(boot_files)}",
95 "build: just apps::build " + app[
"id"],
96 "flash: just apps::hardware::flash " + app[
"id"] +
" (local J-Link)",
97 "run on the emulator: just apps::emulator::run " + app[
"id"] +
" (tools/ra8_emulator)",
99 readme = app_dir /
"README.md"
101 out += [
"",
"--- README.md (first 60 lines) ---", read_text(readme, 60)]
102 return "\n".join(out)
105def tool_search_code(args: dict[str, Any]) -> str:
106 """Search first-party source with ripgrep (falls back to grep -r)."""
107 pattern = str(args.get(
"pattern",
"")).strip()
109 msg =
"pattern is required"
110 raise ValueError(msg)
111 glob = str(args.get(
"glob",
"")).strip()
112 max_results = int(args.get(
"max_results", 80))
143 "--exclude-dir=third_party",
144 "--exclude-dir=build",
155 result = run_command(argv, timeout=30)
156 lines = result.splitlines()
157 if len(lines) > max_results + 3:
158 lines = [*lines[: max_results + 3], f
"[... capped at {max_results} hits ...]"]
159 return "\n".join(lines)
162def tool_repo_overview(_args: dict[str, Any]) -> str:
163 """Summarise the target hardware, key commands, and repo layout."""
164 apps = discover_apps()
166 "ra8-firmware -- bare-metal RA8D2 firmware (hand-written HAL, CMake + "
167 "arm-none-eabi-gcc).\n\n"
168 "TARGET: Renesas R7KA8D2KFLCAC -- Cortex-M85 @ 1 GHz (+ Helium) primary, "
169 "Cortex-M33 @ 250 MHz secondary; 1 MB MRAM, 2 MB SRAM (ECC); EK-RA8D2 board.\n\n"
170 f
"APPS: {len(apps)} firmware apps under examples/ and apps/board/. "
171 "Use list_apps / app_info.\n\n"
172 "COMMON WORKFLOWS (also exposed as tools):\n"
173 " just apps::build <app> cross-compile one app\n"
174 " just apps::hardware::flash <app> build + flash via local J-Link\n"
175 " just apps::emulator::run <app> run the real .elf on ra8_emulator\n"
176 " just quality::gate::run unit-tests host unit tests (portable)\n"
177 " just quality::local::mcdc DO-178C Level B MC/DC coverage report\n"
178 " just quality::gate::run <name> run one registered quality gate (portable)\n"
179 " just hil::flash <app> flash the Pi-attached HIL board\n\n"
180 "AUTHORITATIVE DOCS are exposed as MCP resources (CLAUDE.md, STYLE_GUIDE, "
181 "RING_AND_WORLD, CONTRIBUTING, the HUM chapter map, the app catalogue)."
185def tool_hum_lookup(args: dict[str, Any]) -> str:
186 """Look up Hardware User's Manual chapter page ranges from the chapter map."""
187 query = str(args.get(
"query",
"")).strip().lower()
188 chapter_map = REPO_ROOT /
"docs" /
"reference" /
"CHAPTER_MAP.md"
189 if not chapter_map.is_file():
190 return "docs/reference/CHAPTER_MAP.md not found in this tree."
191 lines = read_text(chapter_map).splitlines()
193 return read_text(chapter_map, 60)
194 hits = [ln
for ln
in lines
if query
in ln.lower()]
196 return f
"No chapter-map entry matched '{query}'."
197 return f
"CHAPTER_MAP.md entries matching '{query}':\n" +
"\n".join(hits[:40])
200def tool_build_app(args: dict[str, Any]) -> str:
201 """Cross-compile one firmware app via Just and return the log tail."""
202 app = require_app(str(args.get(
"app",
"")).strip())
203 return run_command(_just_argv(
"apps::build", app[
"id"]), timeout=900)
206def tool_run_tests(_args: dict[str, Any]) -> str:
207 """Compile and run unit tests in the host's supported CI environment."""
208 return run_command(_just_argv(
"quality::gate::run",
"unit-tests"), timeout=900)
211_GATES: dict[str, str] = {
212 "format-check":
"format",
216 "cppcheck":
"cppcheck",
217 "check-annotations":
"annotations",
219 "cite-check":
"cite-check",
220 "ai-attribution":
"no-ai-attribution",
221 "inclusive":
"inclusive-terminology",
225def tool_quality_gate(args: dict[str, Any]) -> str:
226 """Run one named quality gate (formatting, lint, ASCII, citations, ...)."""
227 gate = str(args.get(
"gate",
"")).strip()
228 registered_gate = _GATES.get(gate)
229 if registered_gate
is None:
230 msg = f
"unknown gate '{gate}'. Choose one of: {', '.join(sorted(_GATES))}"
231 raise ValueError(msg)
232 timeout = 900
if gate
in (
"tidy",
"cppcheck",
"mcdc")
else 300
233 return run_command(_just_argv(
"quality::gate::run", registered_gate), timeout=timeout)
236def tool_coverage(args: dict[str, Any]) -> str:
237 """Run the DO-178C Level B MC/DC coverage build and return its tail.
239 MC/DC (modified condition / decision coverage) is the certification bar this
240 tree targets; the tail carries the per-decision summary and any gaps.
243 return run_command(_just_argv(
"quality::gate::run",
"mcdc"), timeout=1200)
246def tool_emu_app(args: dict[str, Any]) -> str:
247 """Boot one app's real ``.elf`` on the ra8_emulator Unicorn emulator -- no hardware.
249 Runs ``scripts/emu/smoke.sh <app>`` headlessly: it builds the app + the
250 emulator, runs the firmware, and asserts it reaches its run budget without
251 faulting -- plus its real peripheral UART banner where known. Returns the
252 per-app verdict + log tail. The single way to exercise an app without a board.
254 app = require_app(str(args.get(
"app",
"")).strip())
255 return run_command([
"bash",
"scripts/emu/smoke.sh", app[
"id"]], timeout=900)
258def _capture(argv: list[str], timeout: int = 20) -> str:
259 """Return the stripped stdout of ``argv`` (or a short error marker)."""
261 proc = subprocess.run(
264 stdout=subprocess.PIPE,
265 stderr=subprocess.STDOUT,
270 except (FileNotFoundError, subprocess.TimeoutExpired)
as exc:
271 return f
"[not run: {exc}]"
272 return (proc.stdout
or "").strip()
or "(no output)"
275def tool_git_status(args: dict[str, Any]) -> str:
276 """Read-only repo state: current branch, working-tree status, recent commits, open PRs."""
278 branch = _capture([
"git",
"rev-parse",
"--abbrev-ref",
"HEAD"])
279 status = _capture([
"git",
"status",
"--short",
"--branch"])
280 commits = _capture([
"git",
"log",
"--oneline",
"-8"])
292 "number,title,headRefName",
294 "{{range .}}#{{.number}} {{.title}} ({{.headRefName}})\n{{end}}",
298 else "(gh not on PATH)"
301 f
"branch: {branch}\n\n"
302 f
"--- working tree ---\n{status}\n\n"
303 f
"--- recent commits ---\n{commits}\n\n"
304 f
"--- open PRs ---\n{prs}\n"
308def tool_hum_citation(args: dict[str, Any]) -> str:
309 """Emit the HUM register-citation skeleton the project requires above every MMIO access."""
310 chapter = str(args.get(
"chapter",
"")).strip()
311 section = str(args.get(
"section",
"")).strip()
312 page = str(args.get(
"page",
"")).strip()
314 msg =
"chapter is required, e.g. '38.2.3' (use hum_lookup to find it)"
315 raise ValueError(msg)
316 sect = f
' "{section}"' if section
else ' "<section name>"'
317 pg = f
" p {page}" if page
else " p <NNNN>"
319 f
"/* HUM Ch {chapter}{sect}{pg} */\n\n"
320 "Place this comment IMMEDIATELY above the register read/write (CLAUDE.md\n"
321 "External HUM Citations policy). Fill the section name + page range from\n"
322 "docs/reference/CHAPTER_MAP.md (the hum_lookup tool / chapter-map resource).\n"
323 "Cite the Hardware User's Manual, never an in-tree file:line."
327def tool_flash_app(args: dict[str, Any]) -> str:
328 """Build and flash an app to a locally attached EK-RA8D2 (hardware write).
330 Gated: with ``confirm`` false (the default) this only previews the exact
331 command. Pass ``confirm`` true to actually program the board.
333 app = require_app(str(args.get(
"app",
"")).strip())
334 argv = _just_argv(
"apps::hardware::flash", app[
"id"])
335 if not bool(args.get(
"confirm",
False)):
337 "[dry run] hardware write withheld. This would program a locally "
338 "attached EK-RA8D2 via J-Link.\n"
339 f
"Command: just apps::hardware::flash {app['id']}\n"
340 "Re-call flash_app with confirm=true to execute."
342 return run_command(argv, timeout=300)
345_HIL_ACTIONS: dict[str, str] = {
346 "flash":
"hil::flash",
347 "recover":
"hil::recover",
348 "flash-retry":
"hil::flash_retry",
349 "reflash":
"hil::reflash",
350 "erase":
"hil::erase",
351 "probe":
"hil::probe",
352 "dlm-reset":
"hil::dlm_reset",
356def tool_hil(args: dict[str, Any]) -> str:
357 """Drive the Pi-attached hardware-in-the-loop rig (hardware action).
359 Gated like flash_app: previews unless ``confirm`` is true. ``flash`` /
360 ``recover`` / ``flash-retry`` / ``reflash`` require an ``app``; ``probe`` /
361 ``erase`` / ``dlm-reset`` do not.
363 action = str(args.get(
"action",
"")).strip()
364 recipe = _HIL_ACTIONS.get(action)
366 msg = f
"unknown action '{action}'. Choose: {', '.join(sorted(_HIL_ACTIONS))}"
367 raise ValueError(msg)
368 recipe_args = [recipe]
369 if action
in (
"flash",
"recover",
"flash-retry",
"reflash"):
370 app = require_app(str(args.get(
"app",
"")).strip())
371 recipe_args.append(app[
"id"])
372 if not bool(args.get(
"confirm",
False)):
374 "[dry run] HIL hardware action withheld.\n"
375 f
"Command: just {' '.join(recipe_args)}\n"
376 "Re-call hil with confirm=true to execute."
378 return run_command(_just_argv(*recipe_args), timeout=600)
385def _schema(props: dict[str, Any], required: list[str] |
None =
None) -> dict[str, Any]:
386 return {
"type":
"object",
"properties": props,
"required": required
or []}
389TOOLS: list[dict[str, Any]] = [
392 "description":
"List discovered RA8D2 firmware apps (name, tier/group, "
393 "description). Optional 'filter' substring narrows the list.",
394 "inputSchema": _schema(
395 {
"filter": {
"type":
"string",
"description":
"case-insensitive substring filter"}}
397 "handler": tool_list_apps,
401 "description":
"Details for one firmware app: directory, description, which "
402 "boot files are present, build/flash/emulator commands, README head.",
403 "inputSchema": _schema({
"app": {
"type":
"string",
"description":
"app name"}}, [
"app"]),
404 "handler": tool_app_info,
407 "name":
"repo_overview",
408 "description":
"One-shot orientation: target hardware, app count, the common "
409 "build/test/flash/HIL workflows, and where the docs live.",
410 "inputSchema": _schema({}),
411 "handler": tool_repo_overview,
414 "name":
"search_code",
415 "description":
"Search first-party source (apps, libs, examples, tests, "
416 "port, scripts, tools, docs) for a regex pattern. Skips third_party and build.",
417 "inputSchema": _schema(
419 "pattern": {
"type":
"string",
"description":
"regex to search for"},
420 "glob": {
"type":
"string",
"description":
"optional file glob, e.g. *.c"},
421 "max_results": {
"type":
"integer",
"description":
"hit cap (default 80)"},
425 "handler": tool_search_code,
428 "name":
"hum_lookup",
429 "description":
"Look up Hardware User's Manual chapter page ranges from "
430 "docs/reference/CHAPTER_MAP.md (useful for register citations).",
431 "inputSchema": _schema(
432 {
"query": {
"type":
"string",
"description":
"chapter number or section keyword"}}
434 "handler": tool_hum_lookup,
438 "description":
"Cross-compile one firmware app (just apps::build <app>) and return the "
439 "build log tail with the real exit status.",
440 "inputSchema": _schema({
"app": {
"type":
"string",
"description":
"app name"}}, [
"app"]),
441 "handler": tool_build_app,
446 "Host-compile and run the unit-test suite (just quality::gate::run unit-tests)."
448 "inputSchema": _schema({}),
449 "handler": tool_run_tests,
452 "name":
"quality_gate",
453 "description":
"Run one named quality gate: format-check, tidy, ascii, "
454 "version, cppcheck, check-annotations, mcdc, cite-check, "
455 "ai-attribution, inclusive.",
456 "inputSchema": _schema({
"gate": {
"type":
"string",
"description":
"gate name"}}, [
"gate"]),
457 "handler": tool_quality_gate,
461 "description":
"Build and flash an app to a locally attached EK-RA8D2 via "
462 "J-Link. HARDWARE WRITE -- previews unless confirm=true.",
463 "inputSchema": _schema(
465 "app": {
"type":
"string",
"description":
"app name"},
466 "confirm": {
"type":
"boolean",
"description":
"true to actually flash"},
470 "handler": tool_flash_app,
474 "description":
"Drive the Pi-attached HIL rig: action in flash, recover, "
475 "flash-retry, reflash, erase, probe, dlm-reset. HARDWARE -- previews "
476 "unless confirm=true. flash/recover/flash-retry/reflash need an app.",
477 "inputSchema": _schema(
479 "action": {
"type":
"string",
"description":
"HIL action"},
480 "app": {
"type":
"string",
"description":
"app name (for flash actions)"},
481 "confirm": {
"type":
"boolean",
"description":
"true to actually run"},
489 "description":
"Boot one app's real .elf on the ra8_emulator Unicorn emulator (no "
490 "hardware): build + run headless, assert it reaches its run budget "
491 "without faulting plus its peripheral UART banner. Returns the verdict.",
492 "inputSchema": _schema({
"app": {
"type":
"string",
"description":
"app name"}}, [
"app"]),
493 "handler": tool_emu_app,
497 "description":
"Run the DO-178C Level B MC/DC coverage gate and return "
498 "the per-decision summary tail.",
499 "inputSchema": _schema({}),
500 "handler": tool_coverage,
503 "name":
"git_status",
504 "description":
"Read-only repo state: current branch, working-tree status, the last "
505 "few commits, and open GitHub PRs.",
506 "inputSchema": _schema({}),
507 "handler": tool_git_status,
510 "name":
"hum_citation",
511 "description":
"Emit the HUM register-citation comment skeleton the project requires "
512 "immediately above every MMIO access. Args: chapter (e.g. 38.2.3), "
513 "optional section + page.",
514 "inputSchema": _schema(
516 "chapter": {
"type":
"string",
"description":
"HUM chapter, e.g. 38.2.3"},
517 "section": {
"type":
"string",
"description":
"section name (optional)"},
518 "page": {
"type":
"string",
"description":
"page or range, e.g. 2181 (optional)"},
522 "handler": tool_hum_citation,
526TOOL_INDEX: dict[str, dict[str, Any]] = {t[
"name"]: t
for t
in TOOLS}