Skip to content

Consume results programmatically

Scope: everything downstream of a passing gate, meaning structured output, run history, and regression detection. Installing the gate is Run it in CI; the field-by-field structure is in the reference.

The gate answers did it pass? This page is about answering is it getting worse?

Terminal window
npx @hawkeyexl/manni tracevals run <trace> --deterministic-only --format json

--output additionally writes the rendered report to a file, in whichever format --format selected. Both together give you one artifact to keep and one stream to pipe:

Terminal window
npx @hawkeyexl/manni tracevals run <trace> --deterministic-only \
--format json --output tracevals-report.json

The report is one object, so jq gets you most things in a line. Every field is documented in Reports & exit codes.

Which evals failed, and why:

Terminal window
jq -r '.evalResults[]
| select(.outcome == "fail")
| "\(.artifactName) › \(.evalName): \((.findings // [])[].message)"' \
tracevals-report.json

Only the findings that were meant to block. A finding’s severity is notice, warning, or error, set by the eval’s author:

Terminal window
jq -r '.evalResults[] | (.findings // [])[]
| select(.severity == "error") | .message' tracevals-report.json

Anything a human needs to look at:

Terminal window
jq -r '.evalResults[] | select(.outcome == "needs-review") | .evalName' tracevals-report.json

Artifacts the session referenced but could not be found:

Terminal window
jq -r '.coverage[] | select(.resolved == false) | "\(.kind) \(.ref)"' tracevals-report.json

Summary counts for a dashboard:

Terminal window
jq '.summary' tracevals-report.json
{ "total": 7, "pass": 1, "fail": 1, "error": 0, "needsReview": 0, "skipped": 5 }

Evaluating more than one trace emits a different object. It is a BatchReport whose evals and artifacts arrays carry rates instead of outcomes. The summary counts are the same fields, summed, plus four per-trace tallies.

Terminal window
npx @hawkeyexl/manni tracevals run traces/*.jsonl --deterministic-only \
--format json --output tracevals-batch.json

The evals that hold least often, worst first. This is the one query that makes a corpus worth keeping:

Terminal window
jq -r '.evals
| map(select(.passRate != null))
| sort_by(.passRate)[]
| "\(.passRate * 100 | floor)% \(.artifactName) › \(.evalName) (\(.traces) trace(s))"' \
tracevals-batch.json

The traces to open first, for one eval that is failing:

Terminal window
jq -r '.evals[] | select(.evalName == "forbidden-tool") | .failingTraces[]' \
tracevals-batch.json

Traces that could not be read at all. These do not appear in any rate, so they are easy to miss:

Terminal window
jq -r '.traces[] | select(.error) | "\(.file): \(.error)"' tracevals-batch.json

Anything that was never actually checked, with the reason. An exhausted turn budget shows up here, and it is the difference between “this held every time” and “this was never run”:

Terminal window
jq -r '.evals[] | select(.passRate == null)
| "\(.artifactName) › \(.evalName): \(.skipReasons | join("; "))"' \
tracevals-batch.json

--history appends one line per run to a local JSONL log and compares this run against the previous one for the same session:

Terminal window
npx @hawkeyexl/manni tracevals run <trace> --deterministic-only --history

The file defaults to .manni/tracevals/history.jsonl and is configurable:

manni.config.yaml
tracevals:
history:
file: .manni/tracevals/history.jsonl

Each line is a HistoryEntry. It carries timestamp, trace file, session id, project, the summary counts, exit code, the turns the run spent, and one { artifact, evalName, outcome } per eval. Outcomes only; findings and consensus are not stored, which keeps the log small enough to commit if you want to.

A regression is a specific, defined event: an eval that was pass in the previous run and is now fail or error. That precision is what makes an alert worth wiring up.

With --history, the comparison is appended to non-JSON output:

History vs 2026-07-01T09:14:22.104Z:
regression: fix-bug › used-read
added: fix-bug › reproduces-before-fixing
LineMeaning
regressionpreviously pass, now fail or error
improvementpreviously fail or error, now pass
addedan eval that did not exist in the previous run
removedan eval that existed then and does not now
no changesthe outcome set is identical

added and removed matter more than they look. They are how you notice that an eval was quietly deleted, or that an artifact stopped being referenced at all.

If you keep the JSON rather than the history file, the same rule is a few lines:

Terminal window
jq -n --slurpfile before before.json --slurpfile after after.json '
($before[0].evalResults | map({key: "\(.artifact)›\(.evalName)", value: .outcome}) | from_entries) as $b
| $after[0].evalResults
| map(select($b["\(.artifact)›\(.evalName)"] == "pass"
and (.outcome == "fail" or .outcome == "error")))
| map("\(.artifactName) › \(.evalName)")'

For a human-readable artifact next to the machine-readable one, run the markdown reporter:

- name: Evaluate and keep both formats
run: |
npx @hawkeyexl/manni tracevals run traces/session.jsonl --deterministic-only \
--format markdown --output tracevals-report.md
npx @hawkeyexl/manni tracevals run traces/session.jsonl --deterministic-only \
--format json --output tracevals-report.json
- name: Publish
if: always()
uses: actions/upload-artifact@v7
with:
name: tracevals-report
path: tracevals-report.*

The second run is free. Judged verdicts are cached, and under --deterministic-only there is no model call at all.

Are you building something that runs evaluations rather than reading their output? The library gives you the typed report directly, with no parsing step in between. The API is the tracevals namespace export of the package:

import { tracevals } from "@hawkeyexl/manni";
const { loadConfig, runEvals, render } = tracevals;
const report = await runEvals({
tracePath: "./traces/session.jsonl",
projectDir: "./my-repo",
config: await loadConfig(),
deterministicOnly: true,
});
console.log(report.summary, report.exitCode);
console.log(render(report, "markdown"));

report is the same structure --format json prints, so everything on this page applies to it field for field. Build on manni tracevals carries the entry points and the injection seams that keep your own suite offline. It also covers registering a grader the built-ins do not.