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?
Emit machine-readable output
Section titled “Emit machine-readable output”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:
npx @hawkeyexl/manni tracevals run <trace> --deterministic-only \ --format json --output tracevals-report.jsonQuery it
Section titled “Query it”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:
jq -r '.evalResults[] | select(.outcome == "fail") | "\(.artifactName) › \(.evalName): \((.findings // [])[].message)"' \ tracevals-report.jsonOnly the findings that were meant to block. A finding’s severity is notice, warning, or
error, set by the eval’s author:
jq -r '.evalResults[] | (.findings // [])[] | select(.severity == "error") | .message' tracevals-report.jsonAnything a human needs to look at:
jq -r '.evalResults[] | select(.outcome == "needs-review") | .evalName' tracevals-report.jsonArtifacts the session referenced but could not be found:
jq -r '.coverage[] | select(.resolved == false) | "\(.kind) \(.ref)"' tracevals-report.jsonSummary counts for a dashboard:
jq '.summary' tracevals-report.json{ "total": 7, "pass": 1, "fail": 1, "error": 0, "needsReview": 0, "skipped": 5 }Query a batch
Section titled “Query a batch”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.
npx @hawkeyexl/manni tracevals run traces/*.jsonl --deterministic-only \ --format json --output tracevals-batch.jsonThe evals that hold least often, worst first. This is the one query that makes a corpus worth keeping:
jq -r '.evals | map(select(.passRate != null)) | sort_by(.passRate)[] | "\(.passRate * 100 | floor)% \(.artifactName) › \(.evalName) (\(.traces) trace(s))"' \ tracevals-batch.jsonThe traces to open first, for one eval that is failing:
jq -r '.evals[] | select(.evalName == "forbidden-tool") | .failingTraces[]' \ tracevals-batch.jsonTraces that could not be read at all. These do not appear in any rate, so they are easy to miss:
jq -r '.traces[] | select(.error) | "\(.file): \(.error)"' tracevals-batch.jsonAnything 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”:
jq -r '.evals[] | select(.passRate == null) | "\(.artifactName) › \(.evalName): \(.skipReasons | join("; "))"' \ tracevals-batch.jsonTrack runs over time
Section titled “Track runs over time”--history appends one line per run to a local JSONL log and compares this run against the previous
one for the same session:
npx @hawkeyexl/manni tracevals run <trace> --deterministic-only --historyThe file defaults to .manni/tracevals/history.jsonl and is configurable:
tracevals: history: file: .manni/tracevals/history.jsonlEach 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.
Detect a regression
Section titled “Detect a regression”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| Line | Meaning |
|---|---|
regression | previously pass, now fail or error |
improvement | previously fail or error, now pass |
added | an eval that did not exist in the previous run |
removed | an eval that existed then and does not now |
no changes | the 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.
Comparing two runs yourself
Section titled “Comparing two runs yourself”If you keep the JSON rather than the history file, the same rule is a few lines:
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)")'Publish a summary
Section titled “Publish a summary”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.
Or skip the CLI
Section titled “Or skip the CLI”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.