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 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?

Terminal window
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:

Terminal window
npx moose-tracevals run <trace> --deterministic-only \
--format json --output moose-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)"' \
moose-tracevals-report.json

Anything a human needs to look at:

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

Artifacts the session referenced but could not be found:

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

Summary counts for a dashboard:

Terminal window
jq '.summary' moose-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 moose-tracevals run traces/*.jsonl --deterministic-only \
--format json --output moose-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))"' \
moose-tracevals-batch.json

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

Terminal window
jq -r '.evals[] | select(.evalName == "forbidden-tool") | .failingTraces[]' \
moose-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)"' moose-tracevals-batch.json

Anything 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”:

Terminal window
jq -r '.evals[] | select(.passRate == null)
| "\(.artifactName) › \(.evalName): \(.skipReasons | join("; "))"' \
moose-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 moose-tracevals run <trace> --deterministic-only --history

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

moose.config.yaml
tracevals:
history:
file: .moose-tracevals/history.jsonl

Each 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.

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.

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 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.

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.