AN-001Booley application note · 18 min read

Why AI agents struggle with RTL debugging, and what helps

Abstract

I asked an AI coding agent to fix a bug in the RISC-VAn open instruction set architecture. Many open-source processor cores implement it. processor inside CaliptraA large open-source system on chip from CHIPS Alliance. It is a hardware root of trust: a security block with its own RISC-V processor and firmware.. It usually found the faulty line of RTLRegister-transfer level: the code that describes a chip’s hardware, usually written in Verilog or SystemVerilog. A simulator runs it; synthesis tools turn it into gates. within five minutes. Proving the fix took much longer, because the agent first had to make the bug show up in simulation. Over ten trials, two hardware-specific problems took most of that time: the internal signals that explain a failure are invisible unless the simulation records them, and simulations run long enough that the agent has to guess when they finish. A third problem, a build that depends on settings the agent's shell doesn't keep, isn't specific to hardware but is common in RTL projects. With tools aimed at all three, the same agent's mean time to a verified fix fell from 56 minutes to 21.

Coding agents are built mostly for software work. Their working loop assumes that a test runs in seconds, that a failure prints a useful message, and that a command is cheap to rerun. Hardware design breaks all three. Chips are described in RTLRegister-transfer level: the code that describes a chip’s hardware, usually written in Verilog or SystemVerilog. A simulator runs it; synthesis tools turn it into gates. code and tested in a simulator. A simulation of a large design takes minutes to compile and run, so every rerun is expensive, and the state that explains a failure lives in internal signals that nobody printed.

Current models reason well about RTL. This note looks at what happens when that reasoning meets a real hardware debug loop. It asks where the agent's time goes, which of that time comes from hardware work rather than from agent setups in general, and what the agent's harnessThe environment around an AI agent: the tools it can call, how its commands run, and the instructions it gets. can do about it.

The scope is debugging: finding a bug, reproducing it, and proving the fix. Other RTL tasks, such as writing new design code or building verification environments, run into different problems. Later notes will cover those.

To answer it, I ran the same agent on the same bug in two setups: a plain shell, and Booley, an open-source agent harness for FPGA and ASIC work. I build Booley and ran this experiment myself, so weigh the comparison with that in mind. The problems described below, though, come from the plain-shell runs and apply to any agent setup for RTL.

The case: a forwarding bug in a RISC-V core

I wanted a bug where the environment, not the model's reasoning, would limit the agent. I started with bug-fixing tasks from a subset of CVDP, NVIDIA's benchmark of hardware design and verification problems. Those tasks were too small for this question: agents solved them quickly with any tooling, so the environment never had a chance to matter. I needed a design where reproducing a bug takes real simulation work.

So I looked for the largest high-quality open-source RTL codebase I could find. I chose Caliptra, a hardware root of trust: a security subsystem with its own processor and firmware.

I settled on a historical bug in VeeR EL2, the RISC-V processor inside Caliptra, and reintroduced it by reverting an upstream fix from the VeeR EL2 1.4 release.

In the scenario the bug report describes, firmware checks the contents of the processor's ROMRead-only memory. Here it holds the processor’s boot program. during startup. Under a particular sequence of reads and writes, that check fails even though the stored program is correct: the word at ROM address 0 reads back as zero.

The agent's task was to find and fix the cause in the RTL, then add a test that fails before the fix and passes afterward.

The bug report

The agent received an issue-style report describing this behavior:

Early in boot, the firmware recomputes a digest over its own ROM image and compares it with a constant fixed at build time. Normally the check passes. In one case the digest comes out wrong, even though the ROM is unchanged and the constant is correct. No fault, no exception, no ECC event, fully deterministic. The trigger is a coding pattern: the failing routine reads ROM word by word and, in the same tight loop, writes partial results to a scratch buffer in DCCMData closely coupled memory: the processor’s fast on-chip data memory, reached without going over the system bus.. Keep the accumulator in registers, or move the buffer out of DCCM, and the digest is correct. Only the first word of the ROM input looks corrupted.

Find and fix the RTL root cause. Add a test that reproduces the problem and passes after the fix.

The upstream release notes describe the fix as "Store data collision with DCCM DMA error when address is 0x0".

Why reproducing it was harder than finding it

The processor uses store-to-load forwarding: a read can take its value directly from a recent write to the same address instead of waiting for memory. To save power, it also blanks the address and data of a write that goes to its on-chip data memory (DCCM), setting both to zero. The forwarding check was missing its "this write goes out on the bus" condition, so it compared each read against that blanked address of zero. A read from ROM address 0, issued right after an on-chip write, "matched", skipped the real ROM read, and got the blanked value: zero.

In both setups, the agent usually identified the faulty line within five minutes. It then had to write a test that failed on the original design and passed after the fix. That would demonstrate the fault and help catch it if it returned later.

Two details made that test difficult to write:

  • The test template marked ROM as a region where accesses may have side effects. That setting disables forwarding and hides the bug.
  • The processor works on several instructions at once, each at a different stage of its pipeline. The bug needs the write and the read back to back, one stage apart. Fetching the test's instructions from ROM over the bus left gaps between them, so the test had to run its loop from the processor's fast on-chip instruction memory (ICCM) instead.

Most of the work went into building a test, simulating it, inspecting what happened, and adjusting it until it failed for the right reason.

Finding the bug took reading, and current models read code well. Reproducing it took repeated experiments: build, simulate, look at the result, change the test. Hardware and software differ most in that loop, so the rest of this note measures it.

Method

I ran ten trials in each of two setups, with the same agent, model, prompt, and container:

  • Claude Code alone: a shell plus Caliptra's existing Makefiles and test scripts. This is the default when you point an agent at an RTL repository.
  • Claude Code plus Booley: the same agent with a harness designed for RTL work, configured for Caliptra before the trials. The timings cover debugging only, not the one-time work of adapting Caliptra to Booley.

The Booley setup's AGENTS.md (also linked as CLAUDE.md) explains how to use Booley's tools and has two lines describing the repository; it contains no build commands, environment variables, or debugging hints.

Experiment settings and versions

Table 1. Test conditions, identical for both setups unless noted

AgentClaude Code 2.1.289, Sonnet 5.5, medium effort
BooleyPublished release 0.3.0 (with-Booley setup only)
DesignCaliptra at commit e379800c, with the historical VeeR fix reverted
EnvironmentSame container image, 8 CPUs, 10 GB RAM, no internet access
TaskSame prompt, 90-minute elapsed-time limit per trial
TrialsTen per setup, b1 through b10; matching numbers form pairs

The harness changed three things, each aimed at one of the problems described below:

  • Waveform queries. Both setups could record a waveformA recording of how internal signals change, cycle by cycle, during a simulation.; only the Booley setup had a tool, B-Wave, to search it and read signal values at particular cycles.
  • One blocking simulation command. Booley's sim Flow builds and runs a simulation, then returns its result when the job ends. It works the same way in every project, so the agent doesn't have to learn each project's shell commands.
  • Build configuration in files. FuseSoC, the open-source build tool Booley uses, described the source files, simulator settings, dependency paths, and firmware build steps in core filesFuseSoC description files, one per component, listing its source files, dependencies, and build settings. and a test list.

Where the time went

Both setups produced an accepted fix and a reproducing test in all ten trials, so the comparison is about time. Mean completion time was 55.8 minutes with Claude Code alone and 21.3 minutes with Booley (Figure 1); the medians were 53.3 and 21.4 minutes. Average cost per trial at Sonnet 5.5 API prices was $2.33 and $1.98.

Mean time per bug fix, split by activity With Booley: build and simulation 11.5 minutes, observing signals 4.6, sleeping on finished jobs 0.4, everything else 4.8, total 21.3. Claude Code alone: build and simulation 19.7, observing signals 14.0, simulation hung 2.7, sleeping on finished jobs 11.6, everything else 7.7, total 55.8. 0102030405060 min 11.5 4.6 4.8 0.4 21.3 min With Booley 19.7 14.0 2.7 11.6 7.7 55.8 min Claude Code alone Build and simulation Observing internal signals Sim hung: env vars lost Sleeping on finished jobs Everything else
Figure 1. Mean time per bug fix, split by activity. Ten paired Caliptra bug-fix trials, all accepted. Segment values in minutes. Each minute counts in one category only; a wait after a job had finished counts as waiting even when it followed a hung simulation.

In the plain-shell runs, only about a third of the time went into building and simulating. The rest went to observing internal signals, sleeping on jobs that had already finished, and recovering from broken builds. Each recurred across the trials. The first two follow from how hardware is simulated; the third is a general agent problem that showed up here too. I identified them from the run histories; I didn't run separate experiments to isolate the contribution of each fix.

Problem 1: internal state is invisible by default

When a software test fails, it usually prints what went wrong, and a debugger can stop the program anywhere. In RTL simulation, the state that explains a failure is spread across thousands of internal signals, and none of them are visible unless the testbench records a waveform or prints them. Adding a print means editing the design or testbench and recompiling. Caliptra simulates with Verilator, which compiles the design into a C++ program, so each recompile and rerun takes several minutes.

Claude Code alone, trial b3 85 min

Observing internal signals: 37.8 min

With Booley, trial b3 27 min

Observing internal signals: 9.8 min

Minutes since the agent started

  • Observing internal signals
  • Build and simulation
  • Everything else (reading, reasoning, editing)
  • Rebuild to add debug prints (7×)
  • Waveform query (16×)
Figure 2. Seeing inside the design, trial pair b3. Claude Code alone added $display prints, rebuilt, reran and searched the output seven times; with Booley the agent recorded a waveform once and queried it 16 times.

This matters most when a test passes despite a suspected bug, which happened repeatedly here, and the agent needs to see why the bug didn't fire.

What the agent did with only a shell. It debugged the simulator the way it would debug a software program: with prints. In pair b3 (Figure 2), it added $displayVerilog’s print statement, similar to printf in C. statements, rebuilt the simulator, reran the test, and searched the output. Every new question meant another recompile. It repeated that loop seven times and spent 37.8 minutes on signal observation.

Recording a waveform was an option in this setup too, but a recording only helps if the agent can read it. Caliptra's waveforms are in FST, a compressed binary format that takes a dedicated library to parse, and a quick Python script is too slow on a dump this size.

What worked: record once, then query. With Booley, the agent built once with waveform recording enabled and asked B-Wave 16 questions about the recorded signals. That work took 9.8 minutes.

In trial b1, the agent searched the waveform to find whether the faulty address comparison ever fired. Listing 1 shows equivalent command lines, with output trimmed.

$ bwave find trace.fst '*bus_intf.ld_addr_rhit_lo_lo' 1
cycle 4687 caliptra_top_tb.caliptra_top_dut.rvtop.veer.lsu.bus_intf.ld_addr_rhit_lo_lo 1
cycle 4696 caliptra_top_tb.caliptra_top_dut.rvtop.veer.lsu.bus_intf.ld_addr_rhit_lo_lo 1
...
# 32 matches found
Listing 1. B-Wave search for the faulty address comparison, trial b1. Output trimmed.

The comparison fired 32 times, yet the test still passed. The agent then asked for the surrounding signal values at the first match (Listing 2). Signal names end in their pipeline stage: _m for the load, _r for the store one stage ahead of it.

$ bwave value trace.fst --at 4687 -s '*bus_intf.ld_*' -s '*bus_intf.lsu_busreq*' -s '*bus_intf.is_sideeffects_m' ...
# Snapshot at cycle 4687
lsu_busreq_m                             = 1
is_sideeffects_m                         = 1
lsu_busreq_r                             = 0
ld_addr_rhit_lo_lo                       = 1
ld_full_hit_m                            = 0
Listing 2. Signal values at the first match, cycle 4687. Output trimmed; the two lines that hid the bug are shaded.

The read matched a write that should not have been eligible for forwarding: a DCCM store (lsu_busreq_r = 0) whose address had been blanked to 0. But the test's memory setting (is_sideeffects_m = 1) blocked forwarding (ld_full_hit_m = 0) and hid the bug.

The agent changed that setting in the test. Half a minute later, the test failed on the buggy design.

The agent could ask follow-up questions of the same recording without rebuilding. Across all ten pairs, it spent 9.4 minutes less on average observing signals with Booley.

The b3 chart's final segment (Figure 2), roughly 47 to 85 minutes, is a sequence of final tests. That time isn't counted as signal observation.

In hardware debugging, a new question about the design should be answerable from an existing recording. A tool that searches a recorded waveform by signal name and reads values at a given cycle gives an agent roughly what a debugger gives it in software.

Problem 2: simulations are long, and the agent guesses how long

Agents' habits come from commands that finish in seconds. In hardware, minutes are normal: compiling a design like Caliptra for simulation and running a firmware test takes several, and synthesis or a full regression can take hours. So the agent runs the job in the background and has to decide when to check on it. If it guesses, it either checks too early or sleeps long after the job has finished.

Claude Code alone, trial b10 60 min

Sleeping after the job already finished: 30.7 min

With Booley, trial b10 13 min

Sleeping after the job already finished: 0.0 min

Minutes since the agent started

  • Sleeping after the job already finished
  • Build and simulation
  • Everything else (reading, reasoning, editing)
Figure 3. Waiting on jobs that are already done, trial pair b10. Claude Code alone polled with fixed sleep calls; Booley returned each result when the job ended.

What the agent did with only a shell. In several runs, it started work in the background and used a fixed sleep before checking the result. In b10 (Figure 3), a build started at 4.3 minutes and failed one second later. The failure was hidden: the command ended with ; tail, so it reported exit code 0. Seconds later the agent requested sleep 540, a nine-minute wait, and didn't check the result until 13.4 minutes.

It made similar waits four times in that run, spending 30.7 of its 60 minutes asleep after jobs had finished.

What worked: return when the job ends. Booley's simulation command waits for the job and returns the result. For long jobs it returns a run ID instead, and the agent calls a poll tool (booley_poll) that returns as soon as the job ends, so there is no fixed sleep to guess.

The Booley run in pair b10 took 13 minutes in total. Across all ten pairs, it spent 11.2 minutes less on average waiting on finished jobs. These waits affected six pairs. I counted only waits where I could establish that the job had finished, so the estimate is a lower bound. The sleeping is an agent habit. Hardware makes it expensive because the jobs are long, so the guessed waits are long too. There is a cheap partial fix: one line in CLAUDE.md telling the agent to wait for the process instead of sleeping would likely recover much of this time. Hiding exit codes behind pipes, as in ; tail, makes it worse, because a failed build looks like a running one.

Not hardware-specific: the build is fragile, and the shell forgets

This problem isn't specific to hardware. Any project whose build depends on undocumented environment variables trips agents up the same way, and RTL projects often have such builds. Caliptra's simulation needs the simulator, toolchain paths, and a firmware image compiled by a separate cross-compiler and loaded into the simulated memory, and its Makefiles locate these through environment variables. An engineer sets them once in a terminal and forgets about them. An agent works through a series of separate shell calls, and state set in one call doesn't necessarily survive to the next. When a step fails, the next one often runs anyway: a simulation without its firmware doesn't crash; it runs until it times out.

Claude Code alone, trial b1 74 min

Build setup and recovery: 30.1 min

With Booley, trial b1 22 min

Build setup and recovery: 0.0 min

Minutes since the agent started

  • Hunting undocumented build env vars
  • Sim hung: env vars lost between shell calls
  • Sleeping after the job already finished
  • Build and simulation
  • Everything else (reading, reasoning, editing)
Figure 4. Getting the build to work at all, trial pair b1. Claude Code alone rediscovered the build environment variables, then lost them between shell calls three times; the Booley run spent no time on setup.

What the agent did with only a shell. All ten plain-shell runs hit missing build configuration; most recovered within a few minutes. In b1 (Figure 4), the agent worked out which variables to set and exported them in a shell call. Those values didn't carry over to later calls in this run. The firmware build then failed to produce program.hex, and the simulation started without a program. It waited until its timeout. This happened three times before the agent put the configuration in a single run.sh script.

The run spent 30 minutes dealing with setup. Sleeps after jobs had already finished, including two minutes after the first hung simulation was killed, are counted separately (Figure 4), as in Figure 1.

What worked: configuration in files, prerequisites checked. The Booley project's FuseSoC core files already held the dependency paths, so the agent didn't have to rediscover the Makefile variables. Each simulation invocation supplied its build configuration and firmware build step, without relying on exports from a previous shell call. If the firmware build failed, Booley stopped the run and reported the build error before starting a simulation without its program.

The Booley run in pair b1 spent no time repairing setup. Across all ten pairs, the average setup saving was 3.6 minutes; b1 was the unusually costly case. Configuring the project once avoided repeating that work during debugging.

Build configuration should live in files that every run reads, and each simulation run should carry its whole configuration and stop at the first failed prerequisite. A checked-in script that the agent's instructions point to gets part of the way there. The plain-shell agent in b1 arrived at exactly that, a run.sh, after 30 minutes.

What the tools didn't change

Both setups still had to work out how to make the bug appear: noticing that the test template's side-effect setting disabled forwarding, and moving the test loop into ICCM to get the store and the load back to back in the pipeline. That is reasoning about microarchitecture, and the harness didn't do it for the agent. It changed the cost of each experiment the agent ran on the way there, and that was enough to cut the total time by more than half.

Lessons for agent setups on RTL

This is a case study, not a benchmark: one bug, one design, one model, and ten trials per setup. I chose a bug that needed substantial simulation work and used an already configured Booley project. These results don't establish the same speedup for other models or other debugging tasks, or tell us which fix would help most on its own. They also say little about design or verification work, which have their own problems and will get their own notes.

Still, the plain-shell runs show where a general-purpose agent loses time on an RTL project, and none of it was spent finding the bug. The lessons below don't depend on a particular tool. The first two are about hardware. The third applies to any agent setup, but RTL projects need it often:

  1. Make internal state queryable. Record waveforms on demand and give the agent a way to search them, so a new question about the design doesn't need a rebuild. This was the largest hardware-specific gap.
  2. Return results when jobs finish. Use a blocking call or a poll that returns on completion, and don't let a pipeline hide a failed exit code.
  3. Keep build configuration in files. Each simulation should be self-contained and should fail fast when a prerequisite, such as the firmware image, is missing.
  4. Plan for reproduction. On this bug, the model found the faulty line quickly; the time went into building a test that made the bug visible. Tooling and time budgets should target that loop.

Booley implements all three tooling fixes, but a project can apply them with any waveform library, build system, and job runner.

Reproducing this study

The Caliptra project used here is public; its README explains the setup. The reproduction details below list the recorded versions, the original prompt, and the patches. To try the same workflow on a smaller design, the PicoRV32 demo comes preconfigured.

How to reproduce (spoilers)

These are the inputs used in the experiment. Prepare the bug before starting the agent, and give it only the prompt below. The patch and the rest of this article reveal the answer.

Table 2. Pinned versions

Caliptrae379800c5467379386f96a9a51a539bd662b608a in chipsalliance/caliptra-rtl.
Adams BridgeAt submodules/adams-bridge: 2f15ffb1a2e9bd99652fa04d434a48ffcdc23f42 in chipsalliance/adams-bridge.
BooleyPublished package booley-rtl==0.3.0, source commit f9ec6d5602f3fcf47f48b678a3ba821fb82c859c in boldaxolotl/booley.
Booley Caliptra projectTag an-001-trial (commit e4b4a20beead7d858f8585d29c8c4619bdfc253b) in boldaxolotl/booley-prj-caliptra, mounted at .booley_project. It holds the exact project files used in the trials. The trials recorded them as commit e9dd79e7, which was lost when the project history was squashed for publication.
AgentClaude Code 2.1.289, model ID claude-sonnet-5-5, effort medium.
Per trial8 CPUs, 10 GB RAM, a 4,096-process limit, and a 5,400-second time limit. Network access was restricted to the model API; web search, web fetching, and subagents were disabled.

Prepare the source

Check out the Caliptra revision above and initialize its submodules, then pin Adams Bridge to the recorded revision (Listing 3):

$ git clone https://github.com/chipsalliance/caliptra-rtl.git caliptra-source
$ cd caliptra-source
$ git checkout --detach e379800c5467379386f96a9a51a539bd662b608a
$ git submodule update --init --recursive
$ git -C submodules/adams-bridge checkout --detach 2f15ffb1a2e9bd99652fa04d434a48ffcdc23f42
Listing 3. Check out the pinned sources.

Both setups also received the same tracing patch. It makes waveform recording opt-in with +trace and changes the trace depth from 24 to 4. This is a testbench adjustment, separate from the RTL bug below. Download it and the bug mutation outside the source directory, then apply them from the Caliptra root (Listing 4):

$ patch --dry-run --fuzz=0 -p1 < ../trace-harness.patch
$ patch --fuzz=0 -p1 < ../trace-harness.patch
$ patch --dry-run --fuzz=0 -p1 < ../seed.patch
$ patch --fuzz=0 -p1 < ../seed.patch
Listing 4. Apply the tracing patch and the bug mutation.

The agent received a source snapshot without Git history or the saved patches. The Booley setup also contained the pinned .booley_project, with root-level AGENTS.md and CLAUDE.md symlinked to .booley_project/AGENTS.md. The other setup contained Caliptra's native build scripts without that project directory or guidance. Start each attempt from a fresh copy of the prepared snapshot.

These steps recreate the source mutation and task. The reported timing comparison also used frozen container images and the archived Booley project, so a run with a different toolchain or project revision is not an exact repeat of that environment.

Exact code mutation

The mutation removes & lsu_busreq_r from all four address-match expressions in src/riscv_core/veer_el2/rtl/lsu/el2_lsu_bus_intf.sv. Restoring the removed guards fixes the bug. Listing 5 shows the four changed lines; the downloadable patch also includes the surrounding context.

--- a/src/riscv_core/veer_el2/rtl/lsu/el2_lsu_bus_intf.sv
+++ b/src/riscv_core/veer_el2/rtl/lsu/el2_lsu_bus_intf.sv
@@ -241,4 +241,4 @@
-   assign ld_addr_rhit_lo_lo = (lsu_addr_m[31:2] == lsu_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m & lsu_busreq_r;
-   assign ld_addr_rhit_lo_hi = (end_addr_m[31:2] == lsu_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m & lsu_busreq_r;
-   assign ld_addr_rhit_hi_lo = (lsu_addr_m[31:2] == end_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m & lsu_busreq_r;
-   assign ld_addr_rhit_hi_hi = (end_addr_m[31:2] == end_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m & lsu_busreq_r;
+   assign ld_addr_rhit_lo_lo = (lsu_addr_m[31:2] == lsu_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m;
+   assign ld_addr_rhit_lo_hi = (end_addr_m[31:2] == lsu_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m;
+   assign ld_addr_rhit_hi_lo = (lsu_addr_m[31:2] == end_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m;
+   assign ld_addr_rhit_hi_hi = (end_addr_m[31:2] == end_addr_r[31:2]) & lsu_pkt_r.valid & lsu_pkt_r.store & lsu_busreq_m;
Listing 5. The bug mutation in el2_lsu_bus_intf.sv. Scroll sideways for the full lines.

The downloadable seed.patch has SHA-256:

247a8a96d300af8c5b243a9fffaef2ce2762df317d477f547c94fc98d0aea0b0

Exact agent prompt

Both setups received this text unchanged (Listing 6). Download the prompt to preserve the original line breaks.

Bug report against this Caliptra RTL snapshot.

Runtime ROM self-integrity digest is wrong, but only when the digest loop uses a DCCM scratch buffer

Early in the boot flow the runtime firmware recomputes a digest over its own
ROM image (a self-integrity check) and compares it against a constant that was
fixed at build time. The check normally passes.

We hit a case where the computed digest is wrong even though the ROM image on
the part is byte-for-byte unchanged and the compare constant is correct. No
error, fault, or status bit is set anywhere; the core reports no exception and
the memories report no ECC or parity events. The wrong digest is fully
deterministic across resets.

While bisecting the firmware we found the trigger is a coding pattern, not the
data: the digest routine that fails reads the ROM image word by word and, in
the same tight loop, writes each partially-accumulated result to a small
scratch buffer that the linker placed in DCCM. A functionally identical
version of the routine that keeps the accumulator only in registers (no scratch
writes in the loop), or that moves the scratch buffer out of DCCM, produces the
correct digest every time. The failing and passing routines compute the same
math and touch the same ROM bytes; only the placement/interleaving of the
scratch writes differs.

The corruption is confined to the very start of the digested region: the
recomputed digest diverges from the expected one as if one word of the ROM
input had been replaced, and everything after that point is consistent with the
true ROM contents.

No existing test reproduces the problem.

Find and correct the RTL root cause. Add a test that reproduces the problem
and passes after your fix, then run the relevant verification checks.
Listing 6. The agent prompt, identical for both setups.

The downloadable agent-prompt.txt has SHA-256:

55fc823860c02e841b3c7b1ee46d81a014220b8244304dd5d7d04148ed731ea7