% Copyright (c) 2026 Rana Umar Nadeem, Samrah Mumtaz, Muhammad Imran # Fixed Checks Six checks make the loop stronger without another LLM call. Each is a `LoopOptions` field that is on by default, and an ablation method turns it off (see [Evaluation Harness](../06_mace_evaluation/harness.md)). | Check | Field | Module | What it does | |---|---|---|---| | Plan check | `plan_check` | `mace/plan_check.py` | Rejects a plan that cannot work before any build | | Raw evidence | `raw_evidence` | `mace/triage.py` | Gives the re-plan the build's own report beside the diagnosis | | Narrowing | `narrowing` | `mace/narrowing.py` | Rebuilds a failed task with its changes undone to find which one breaks it | | Fast path | `fast_path` | `mace/fast_path.py` | Runs every plan without the per-task LLM calls, and a plan that builds one design as a single task | | Gate pass ends the run | `gate_pass_ends_run` | `mace/orchestrator.py` | Passes the run once one task ran every gate program to a pass, whatever the plan's other tasks did | | Carried edits | `carry_edits` | `mace/orchestrator.py` | Starts the next iteration's tasks from the RTL edits of the last task that built, and tells the planner so | ## Plan Check `_plan_checked()` in `mace/orchestrator.py` makes the iteration's plan: one `plan()` call, then a look at the result before any build. The look has three parts. `strip_unknown_defines()` removes each RTL define that no file mentions. `define_index()` collects the upper-snake-case words of the text files under `piton/design`, `piton/verif`, `piton/tools/src`, and `piton/tools/verilator` in the first checkout, once per process. A define in none of them changes nothing, so a plan that carries one only builds a second model that behaves like the first. A define the objective names stays. `add_required_defines()` then adds each RTL define that a sentence of the objective names as needed to every task that builds: `config`, `workload`, and `rtl` tasks, not `unit_test` tasks. The objective states the define, so fixed code applies it rather than asking the planner again: the planner had left the PicoRV32 define out of its `rtl` tasks, so every repair built a design that timed out. Each task it changes is recorded as a `required_define_added` entry, which does not reject the plan. `check_plan()` then returns one `PlanProblem` per reason the plan cannot work. It builds nothing and calls no LLM. | Rule | A task is rejected when | |---|---| | `source_edit` | In a run without RTL edits, it has kind `rtl`, or its `config` or `workload` instruction asks to edit a Verilog or SystemVerilog file | | `required_define` | It builds without an RTL define that a sentence of the objective names as needed; after `add_required_defines()` this fires only for a task kind that function does not cover | | `way_rule` | Its caches break [Ariane's way rule](../04_chia_openpiton/piton_config.md) | | `missing_module` | It is a `unit_test` of a path that is not in the checkout | | `repeated_failure` | It builds the same flags as a configuration that already failed in the run, a failed probe included | | `known_bad_change` | It makes every change of a set that narrowing showed breaks the design | A task that builds on `rtl` edits skips the last two rules, since its flags do not show its edits. The `missing_module` rule needs a checkout, and the other rules need only the plan and the run's history. `wrapper_skeleton` applies while a [Generic Tile](../05_mace_cores/generic.md) core's wrapper is still exactly the skeleton the descriptor generates, in the checkout and in the edits the iteration starts from (`mace.generic_core.unwritten_wrapper()`). A written wrapper may keep the skeleton's `wrapper body` comments, so the comments alone do not count. It rejects each `config` or `workload` task that does not depend on an `rtl` task, and a plan with no `rtl` task at all. No core runs on such a build, so the task can only time out. The planner spent all five iterations of two Ibex runs on config tasks, and in other runs put a config task first, so the `rtl` task that depended on it never ran. A rejected plan goes back to `plan()` with the reasons in its `rejection` argument, as a call in the `plan_retry` phase. A planner reply that holds no plan at all goes back the same way, with the line format in `rejection`: a `TASK:` line whose third field names no known kind, an empty reply, or a graph with a cycle. A bare known kind in that field no longer counts as no plan (see [Planner](planner.md)). Such a reply used to end the run as `planning_failed`, and one that arrived right after an expensive narrowing lost the probes' work. An iteration sends its plan back at most `MAX_PLAN_REJECTIONS` times, which is 2, and the two kinds of retry share that count. A plan still rejected after that runs as it stands, and so does the rejected plan when the retry gives no usable plan. A reply with no plan after the last retry ends the run as `planning_failed`. The seeded-fault `plan_hook` runs after the check, so a fault always reaches a build. `record_plan_check()` writes one row of the `plan_checks` table for each look that found a problem, removed a define, or added one, and for each retry's look. `action` is `rejected`, `accepted`, `kept`, or `unusable` (a reply with no plan), and the row holds the rules, tasks, and reasons as JSON, and the stripped defines. `mace.eval.plan_replay.replay_plans()` runs the same check over the plans recorded in a database, with no LLM call and no build. ## Raw Evidence The note a failed task adds to the feedback holds four parts: ```text Task () failed (build succeeded: , run verdict: ). Changes from the defaults: What the build and simulation reported: Triage, an LLM's reading of this evidence: diagnosis=