cea
README.md
<p align="center"><img src="docs/demo/triage.gif" alt="cea triage: a suspected miscompile turns out to be signed-overflow UB, in two steps, with evidence IDs" width="860"></p>
# cea: Compiler Engineering Agent
**Give your coding agent a compiler engineer's toolbox — and make it prove every claim.**
cea is an MCP server, a CLI and an agent skill. It wraps the compiler tools you already have (clang, opt, llvm-reduce, llvm-mca, GCC, rustc, Alive2, cvise, Csmith), stores every result as a citable **Evidence record**, and teaches the agent a workflow in which "compiler bug", "this pass is correct" and "this is faster" are things you say only with proof.
Three rules run through all of it:
1. **Semantics before speed.** A faster program with a different answer is a bug.
2. **Evidence before claims.** Every conclusion cites evidence IDs and separates observed, inferred and unknown.
3. **Small before large.** Reduce the test case, then reason about it.
One wrong compiler rewrite breaks everyone's programs. One wrong "it's a compiler bug" wastes a maintainer's day. Agents are fluent at both. cea makes them slow down in the right places.
## 30-second quickstart
Requires Node 20 or newer, and whichever compilers you want to use. cea never installs or downloads a compiler.
```sh
npm install -g cea-agents # the `cea` command
cea doctor # what was found, what is missing, how to fix it
cea triage examples/01-ub-not-a-compiler-bug/overflow.c
```
Give it to your agent (Claude Code shown; any MCP client with stdio works):
```sh
claude mcp add cea -- cea mcp # the MCP server
cea install-skill # the skill, into ~/.claude/skills/compiler-engineering
```
The npm package is [`cea-agents`](https://www.npmjs.com/package/cea-agents); without a global install, `npx -y cea-agents <command>` works too. Setup for other clients, environment variables and troubleshooting: [connection and recovery](skills/compiler-engineering/references/connection-and-recovery.md).
## What it looks like
A program prints `0` at `-O0` and `1` at `-O2`. The agent's first instinct is "miscompile". The demo above is that case, recorded from a real run ([cast](docs/demo/triage.cast)):
```
$ cea triage examples/01-ub-not-a-compiler-bug/overflow.c
[1] environment ok 9/19 providers available; compilers: clang (x86_64-w64-windows-gnu), ...
evidence ev_environment_report_84b6d483
[2] check_ub ok UB found: signed integer overflow: 2147483647 + 1 cannot be represented
in type 'int' at overflow.c:3
evidence ev_check_ub_6a29004c
verdict: undefined_behavior
UB found at overflow.c:3:36 (signed integer overflow ...), not a compiler bug
compiler bug? no
```
Seven steps were available; it stopped after two, because the question was answered.
## Tools
Every tool is an MCP tool and a CLI command: `cea <tool> --json '<input>'` or `cea <tool> --input file.json`. Each call returns one Evidence record.
| Tool | What it does | Docs |
|---|---|---|
| `environment_report` | Detected compilers, versions, targets, providers, and why any are unavailable | [environment](docs/tools-environment.md) |
| `compile` | Compile with exact flags; asm, IR, object or executable, diagnostics, timing | [compile](docs/tools-compile.md) |
| `emit_ir` | Frontend-only IR, before any pass ran (clang, rustc MIR and LLVM IR, GCC GIMPLE) | [compile](docs/tools-compile.md) |
| `pass_timeline` | What each pass did to one function, as paged diffs, no-op passes skipped | [compile](docs/tools-compile.md) |
| `opt_remarks` | Optimisation remarks and missed-optimisation reasons, grouped by function and loop | [compile](docs/tools-compile.md) |
| `check_ub` | UBSan, ASan, MSan or Miri at -O0 and -O1; findings with source locations | [correctness](docs/tools-correctness.md) |
| `differential_run` | Same program across compilers and optimisation levels; compares output and exit codes | [correctness](docs/tools-correctness.md) |
| `bisect_passes` | Binary search over `-opt-bisect-limit`; the first pass that flips the behaviour | [correctness](docs/tools-correctness.md) |
| `reduce_testcase` | Strict interestingness test plus cvise, creduce, llvm-reduce or a built-in reducer | [correctness](docs/tools-correctness.md) |
| `validate_transform` | Alive2 on before/after IR; pass or fail with the counterexample | [correctness](docs/tools-correctness.md) |
| `fuzz_compiler` | Time-boxed Csmith or YARPGen differential campaign; every divergence saved with its seed | [correctness](docs/tools-correctness.md) |
| `bisect_compiler_history` | `git bisect run` on a compiler checkout with a reduced test | [correctness](docs/tools-correctness.md) |
| `analyze_throughput` | llvm-mca on a loop or function for a named CPU, with its limits stated | [performance](docs/tools-performance.md) |
| `benchmark` | Warm-up, repeated runs, median and spread, machine info, output-correctness check | [performance](docs/tools-performance.md) |
| `triage_miscompile` | The whole chain in one call, with early stops and a draft bug report | [triage](docs/triage.md) |
| `inspect_evidence_view` | Read a record, or page through its raw output without flooding the context | [ledger](docs/tools-ledger.md) |
| `list_evidence` | Evidence IDs in the session | [ledger](docs/tools-ledger.md) |
| `record_unknown`, `update_unknown` | Track open questions with supporting and contradicting evidence IDs | [ledger](docs/tools-ledger.md) |
| `export_evidence_bundle` | The whole session as one portable file, for a bug report | [ledger](docs/tools-ledger.md) |
Command-line reference: [docs/cli.md](docs/cli.md).
## The Evidence record
```jsonc
{
"evidence_id": "ev_check_ub_6a29004c", // cite this
"operation": "check_ub",
"status": "ok", // ok | inconclusive | unavailable | error
"summary": "UB found: signed integer overflow ... at input.c:3",
"input_files": [{ "path": "...", "sha256": "...", "bytes": 164 }],
"tools": [{ "name": "clang", "path": "...", "version": "clang version 23.1.5 ..." }],
"commands": [{ "argv": ["clang", "-O0", "-g", "-fsanitize=undefined,address", "..."], "exit_code": 0, "duration_ms": 212 }],
"target_triple": "x86_64-w64-windows-gnu",
"result": { "clean": false, "findings": [{ "sanitizer": "ubsan", "line": 3, "column": 36, "message": "..." }] },
"observed": ["ubsan: signed integer overflow ... at input.c:3:36 (-O0)"],
"inferred": ["The program executes undefined behaviour, so differing output ... is not evidence of a compiler bug."],
"unknowns": [],
"limitations": ["Sanitizers only see undefined behaviour on the paths this input executed ..."],
"coverage": { "opt_levels": ["O0", "O1"], "sanitizers_run": ["undefined", "address"] },
"views": [{ "name": "04-run_O0-undefined_address.stderr.txt", "lines": 2, "sha256": "..." }]
}
```
Records live in a local session directory (JSONL plus raw output files). Later calls refer to earlier ones by ID. Large outputs stay on disk and are paged with `inspect_evidence_view`. Failures are records too: "alive-tv is not installed" is a fact the agent has to report, not work around.
## Example prompts
With the server connected and the skill installed:
- "This prints 0 at -O0 and 1 at -O2. Is it a compiler bug? `examples/01-ub-not-a-compiler-bug/overflow.c`"
- "Why isn't the loop in `scale()` vectorised, and is fixing it actually faster? Show the spread."
- "I want to add a peephole that turns `(x + 1) > x` into `true`. Check it with Alive2 before I write the code."
- "clang crashes on `crash.c`. Reduce it and tell me the crash signature."
- "Sanitizers are clean but -O2 still gives a different answer. Go through the whole triage and tell me what is observed, inferred and unknown."
- "Fuzz clang against gcc for two minutes and save anything that diverges."
## Examples
Five small cases with the real output of a recorded run, each reproducible with one command:
| | Case | Outcome |
|---|---|---|
| [01](examples/01-ub-not-a-compiler-bug/README.md) | "Compiler bug" that is signed-overflow UB | cea stops at step 2: UB at `overflow.c:3`, not a compiler bug |
| [02](examples/02-missed-vectorization-aliasing/README.md) | Missed vectorisation caused by aliasing | remark: "uncountable loop"; with `restrict` 3.55x faster, ranges do not overlap, same output |
| [03](examples/03-alive2-counterexample/README.md) | A wrong InstCombine-style rewrite | Alive2 counterexample at `x = 2147483647`; correct once `nsw` is required |
| [04](examples/04-crash-reduction/README.md) | A clang crash in an 88-line file | reduced to 2 lines under a crash-signature test |
| [05](examples/05-ub-sanitizers-cannot-see/README.md) | UB that sanitizers cannot see (strict aliasing) | full chain runs; verdict "suspected undetected UB", compiler bug "unlikely" |
The numbers come from one machine and one compiler version, stated in each example. Yours will differ.
## Architecture
```mermaid
flowchart LR
A[Agent] -- MCP stdio --> S[server.ts]
H[Human] -- "cea tool --json" --> C[cli.ts]
S --> R[registry.ts<br/>20 tools, zod schemas]
C --> R
R --> I[invoke<br/>validate, run, persist]
I --> T[Toolbox<br/>find providers, versions]
I --> L[(Evidence ledger<br/>JSONL + views)]
I --> X[exec<br/>argv arrays, timeout, output cap]
X --> P[clang, opt, llvm-reduce, llvm-mca,<br/>gcc, rustc, alive-tv, cvise, csmith, git]
I -. opt-in only .-> N[Alive2 online instance]
```
- One tool definition serves both the MCP server and the CLI.
- Providers are thin adapters. They are found on `PATH`, in `CEA_TOOLCHAIN_DIRS`, or through `CEA_TOOL_<NAME>`; nothing is bundled or installed.
- Every process is started with an argument array and no shell, with a timeout and an output cap.
- No network by default. The single remote feature sends the IR being checked to the Alive2 project's online instance, and only if `CEA_REMOTE_PROVIDERS=compiler-explorer` is set.
Design notes: [docs/design.md](docs/design.md).
## Limitations
Read these before trusting a result.
- **Running a test program executes its code.** Test programs get a scrubbed environment, a timeout and an output cap, in a per-call work directory. That limits accidents. It is not a sandbox. See [SECURITY.md](SECURITY.md).
- **Sanitizers are not proof.** A clean `check_ub` means no finding on the paths that ran. Strict-aliasing violations, unsequenced modifications and most uninitialised reads are invisible to it. Example 5 exists to make this concrete.
- **Alive2 is bounded** (loop unrolling, one function at a time, approximated memory and calls) and does not model type-based aliasing. `validate_transform` is unavailable without `alive-tv` unless you opt into the remote provider.
- **Pass bisection names where behaviour flips, not where the fault is.**
- **llvm-mca is a static model**: no caches, no branch misprediction, no frontend effects.
- **`benchmark` measures wall-clock time of whole processes** on one machine with one input. It is not pinned to a core and does not read hardware counters; perf is detected but not used yet.
- **The built-in reducer removes whole lines only.** Install cvise for real reductions.
- **Coverage of this release.** Developed and run on Windows 11 with clang 23.1.5 (llvm-mingw) and GCC 16.2; CI runs unit tests on Linux, macOS and Windows and integration tests on Ubuntu. Csmith fuzzing was run for real with Csmith 2.3.0. The Rust paths (rustc `emit_ir`, Miri in `check_ub`), YARPGen, MSan and a local `alive-tv` binary are implemented from the tools' documentation and unit-tested with fakes, but have not been run against the real tools. [PROGRESS.md](PROGRESS.md) lists exactly what was verified and how.
- GCC support is narrower than LLVM support: no IR validation, tree dumps only in `pass_timeline`, and a flag scan in place of pass bisection.
## Development
```sh
npm install
npm run build
npm run typecheck && npm run lint
npm run test:unit # providers mocked; runs anywhere
npm run test:integration # real compilers; a test whose provider is missing is skipped and reported
```
See [CONTRIBUTING.md](CONTRIBUTING.md). Changes: [CHANGELOG.md](CHANGELOG.md).
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues