Consume results programmatically
Scope: everything downstream of a passing gate, meaning structured output, run history, and regression detection. Installing the gate is Add moose-tracevals to 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 moose-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 moose-tracevals run <trace> --deterministic-only \ --format json --output moose-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)"' \ moose-tracevals-report.jsonAnything a human needs to look at:
jq -r '.evalResults[] | select(.outcome == "needs-review") | .evalName' moose-tracevals-report.jsonArtifacts the session referenced but could not be found:
jq -r '.coverage[] | select(.resolved == false) | "\(.kind) \(.ref)"' moose-tracevals-report.jsonSummary counts for a dashboard:
jq '.summary' moose-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 moose-tracevals run traces/*.jsonl --deterministic-only \ --format json --output moose-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))"' \ moose-tracevals-batch.jsonThe traces to open first, for one eval that is failing:
jq -r '.evals[] | select(.evalName == "forbidden-tool") | .failingTraces[]' \ moose-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)"' moose-tracevals-batch.jsonAnything that was never actually checked, with the reason. An exhausted judge 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("; "))"' \ moose-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 moose-tracevals run <trace> --deterministic-only --historyThe file defaults to .moose-tracevals/history.jsonl and is configurable:
tracevals: history: file: .moose-tracevals/history.jsonlEach line is a HistoryEntry. It carries timestamp, trace file, session id, project, the summary
counts, exit code, judge cost, 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 moose-tracevals run traces/session.jsonl --deterministic-only \ --format markdown --output moose-tracevals-report.md npx moose-tracevals run traces/session.jsonl --deterministic-only \ --format json --output moose-tracevals-report.json
- name: Publish if: always() uses: actions/upload-artifact@v5 with: name: moose-tracevals-report path: moose-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, plus seams for keeping your own suite offline.