Repository navigation
v0.16.0
[0.16.0] - 2026-09-22
Earn the comparison. 0.16.0 is the release where Iris can be opened beside any incumbent and every noun on the table is either shipped and measured or named as a gap in Iris's own words. A trace carries a session; a run can be a baseline and a comparison lists the cases the two runs disagree on; a moment fires a webhook; a CI job gets a one-line Action; Python gets a client and a pytest plugin; every OpenTelemetry framework gets a recipe bound to a fixture that proves it; every listing renders from the truthbase; the fourteen compare pages hold against the vendors' pages as they read today, with a cost-to-run row and a five-question FAQ; the response schema is published and locked; the verdict's decision path is exported; and how long an evaluation takes is measured rather than asserted.
Check before upgrading.
suggestionsis gone from every evaluation response, deprecated in 0.13.0 and removed here on the two-minor schedule. Readinterpretations[](who the sentence is for, and the setting that changes it) andrule_results[](the failing rules and each skip's reason) instead. The database column stays, written as[], so a database 0.16.0 writes still opens on 0.15.0.docs/roadmap.mdis gone. The capability map is the roadmap:docs/capabilities.mdand https://iris-eval.com/capabilities.iris-eval.com/roadmapredirects there.- A trace's
session_idis a column, not metadata. Migration 014 adds it with its index on first start, beside 001–013. A trace that carried a session id inmetadatakeeps it there; new writes fill the column fromsession_id,gen_ai.conversation.id, or SEP-414 baggage. - Migration 013 adds
runs.baselineand its partial unique index — one baseline per agent, enforced by the database rather than by a read. notify.webhookis strict like the rest of the configuration: an unknown key refuses startup and names the nearest known one.- The compare pages carry a thirteenth row (cost to run), so a tool that parsed the twelve-row table by position needs to read by
id.
Added
-
The run comparison lists its discordant cases, and a run can be the baseline.
compare_runsandPOST /api/v1/compareanswer withdiscordant[]— the paired cases whose verdict flipped, regressions first, each with both evaluations, the direction and the rules that flipped (the b + c McNemar counts, named) — anddiscordant_total; the Runs page lists them under the comparison and each row opens the moment.PATCH /api/v1/runs/:id { "baseline": true }pins a run as the baseline (one per tenant, kept by a partial unique index — migration 013);GET /api/v1/runscarriesbaseline; an omittedbeforeoncompare_runsandPOST /api/v1/comparemeans the pinned baseline, and the Runs page's compare form starts from it. The interval stays: nothing about the statistics changed. -
Sessions. A trace carries
session_id— explicit onlog_traceandPOST /api/v1/traces, orgen_ai.conversation.idon the OTLP door, or the SEP-414 baggagesession_idwhen a request named nothing else — in a column with an index (migration 014).get_tracesandGET /api/v1/tracesfilter bysession; the trace drawer shows the session strip (which turn this is, the previous and the next, every turn linked);compare_tracestakesgroup_by: session, so a conversation answered both ways reads as flaky. A sessions page is later work, once sessions exist in stored data. -
A webhook fires on a moment (#5).
notify.webhook: { url, secret, events, cooldownMinutes, format, timeoutMs }in the strictconfig.json, orIRIS_WEBHOOK_URLandIRIS_WEBHOOK_SECRET. Five events —verdict_fail,detector_veto,cost_anomaly,regression_alarm(the CUSUM line crossed at this evaluation),flaky_case(the first attempt that disagreed with every earlier one) — read from the store after the evaluation row is durable, whichever door wrote it, so nothing waits on the receiver. The body is the Standard Webhooks shape (id,type,timestamp,data) with the moment's ids, verdict, rules and numbers and never the agent's text; signed two ways any receiver already verifies (webhook-id/webhook-timestamp/webhook-signature: v1,…overid.timestamp.body, andx-iris-signature: sha256=…over the body); one attempt and three retries at 500 ms, 2 s, 8 s (±25%) on a network error, a timeout, 408, 429 or 5xx, then a logged drop; a cooldown per (event, agent, subject), ten minutes by default; a queue of a hundred.format: slackandformat: discordpost the bodies those services render. A typo'd event name, a non-http URL, or theirisformat without a secret refuses startup naming it.iris://capabilitiesreportsnotify.webhook— events, host, format, signed — never the URL's token or the key. Found on the way: the strict schema's did-you-mean skipped keys under a.nullable()object; it unwraps those now. Guide: docs/webhooks.md. Tests:tests/unit/notify/{webhook,events,config,doors}.test.ts. -
A GitHub Action for the CI gate.
uses: iris-eval/mcp-server/.github/actions/gate@v0.16.0withtraces: traces.ndjsonrunsiris-eval ingest --evaluate --fail-on …(and--dataset), fails the job on the verdicts named, writes the receipt — trace ids, bases, rules, span labels, never the text — to the job summary, and on a pull request posts it as one comment updated in place (a marker names the traces file; a fork's read-only token, a missing permission or the comment turned off each skip it and say so; an empty traces file fails rather than passing green). A composite action with three small scripts and no third-party action to pin; dogfooded on this repository's CI (gate-action: the walk-through's leak caught inside the dataset, a clean file passing, the comment updated not duplicated). Guide: docs/ci-gate.md. Tests:tests/ci-gate-action.test.ts; the action file is held to the docs contract (flags, variables) and the unpublished-package scan. -
The Python client,
iris-evalon PyPI (#6).packages/python:IrisClientandAsyncIrisClientoverhttpx—log_trace,evaluate_output(through the ingest door withevaluate: true, so the output is stored and shows on the dashboard),get_traces,get_trace,health,capabilities; Bearer auth; typed answers (iris_eval.typesmirrors the API reference key for key); a refusal raisesIrisErrorwith the server's own sentence, the status and the validation details; no server raisesIrisConnectionErrorwith the recipe. The server is found fromIRIS_URLor the running dashboard'sruntime.json, verified with the health route. A pytest plugin registers itself: theirisfixture (skips with the recipe when no server is there, fails withIRIS_REQUIRE=1) andassert_iris(output, expect="pass"), which asserts on the verdict's state and reads like the dashboard when it fails. CI runs the client's contract test against a keyed server this checkout builds on Python 3.10–3.13 and installs the wheel into a fresh environment;publish-python.ymlpublishes by PyPI trusted publishing on apy-v*tag once the project and the publisher are registered. A decorator SDK is deliberately not shipped: Iris reads the OpenTelemetry traces a Python framework already emits. Tests:packages/python/tests. -
The OTel recipes page.
docs/otel-recipes.md: one recipe per framework — Pydantic AI, Google ADK, LangGraph via LangSmith's export, CrewAI via OpenInference, AutoGen, Microsoft Agent Framework, Semantic Kernel, Vercel AI SDK, Mastra — the vendor's own setup lines (read from the page each recipe cites) with Iris as the OTLP endpoint, what Iris reads out of that vocabulary, and the fixture in this repository that proves the reading; where the fixture proves the vocabulary and not the framework's own capture (AutoGen, Mastra), the recipe says so. Held bytests/docs-otel-recipes.test.ts: every recipe names a fixture that exists, every convention fixture is named by a recipe, every recipe cites its source and points at the door; the page is inllms.txtand linked from the OTel guide and the README. -
Every listing inherits the truth.
server.jsoncarriestitle,websiteUrlandicons(the SVG and the PNG the site serves), validated against the vendored 2025-12-11 registry schema intests/listings-hygiene.test.ts; the README's works-with table is rendered fromclients.jsonthrough the truthbase (a blocknpm run llms:renderrewrites andllms:checkholds); an Install-in-VS-Code button and the Cursor button in its current install form, both registering the server asiris-eval; the mcp.so badge on the listing's real address; a Docker MCP Catalog entry (docs/launch/listings/docker/server.yaml, with its copy) ready for the registry pull request; a Cursor plugin manifest (.cursor-plugin/plugin.json, on the version gates) so the repository installs as a Cursor plugin;smithery.yamlretired with its reason (Smithery lists a local server as an MCPB bundle now, and the file's docs page returned 404);localhost:3838→localhost:6920in the site's two mockups, held by the test; the historical launch drafts held to their "do not repost" banners; and a demo GIF at the top of the README — the demo database landing on Failures, a failure opened, two runs compared — recorded byscripts/demo-media.mts(Playwright video → ffmpeg), under 3 MB and asserted so, with the MP4 source atwebsite/public/demo.mp4. -
The compare pages hold against today's vendor pages. Every vendor cell's sentence is checked against a plain download of its page by
npm run compare:verify(scripts/verify-compare-quotes.mjs), which now reads a page the way a reader would — inline Markdown markup gone, since most vendors' docs answer a plain fetch with Markdown, and the spacing a stripped tag leaves before punctuation closed — and--writerecordsquoteVerifiedandquotesCheckedOnin the files. Twenty-two cells corrected from a second read of the vendors' pages (Langfuse's server-side code evaluators, Weave's Signals, Judgment'sevaluate_tracesandrun_test, Promptfoo's trajectory and tool-call assertions, Opik's 40+ metrics, Phoenix MCP named, Patronus's judge evaluator, CoreWeave's completed acquisition of Weights & Biases on its own page). A thirteenth row, Cost to run, on every comparison, read from the vendor's pricing page and dated, or saying the vendor's pages do not answer. A five-question FAQ on every page in the buyer's words — two written, three derived from the table so the FAQ cannot say what the table does not — emitted as FAQPage JSON-LD too; a playground link in the call to action. The four-way article, "Iris vs Langfuse vs Phoenix vs Promptfoo: where each wins, where each loses", drafted with every vendor statement linked to a page its compare file lists as a source, held unpublished bypublished: falseand a future date; the Dev.to crossposter now honourspublished: false.tests/compare-contract.test.ts,tests/compare-quote-verifier.test.tsandtests/compare-article-draft.test.tshold all of it. -
The verdict says which layer decided, and now you can read the whole path.
verdictPath(result, config)returns the composer's decision node by node — a configured gate, a detector's veto, a critical check that could not answer, evidence the deployment requires, the risk estimate — each naming what it found and whether it decided.compose()reads the node that decided and stamps the verdict from it, so the two cannot disagree; a path stops at the decider, because the layers after it were never asked. Exported from@iris-eval/mcp-server/enginewith theVerdictNodetype, and asserted in the packed-and-installed probe CI runs. -
The response schema is published and locked.
https://iris-eval.com/response-schema-v1.json— JSON Schema 2020-12, rendered from the same zod schemaevaluate_outputvalidates its answer against (npm run schema:render), so a client generating types or validating a stored response reads what the server actually returns. A test renders it and fails when the committed file has drifted, and validates a response the engine produced against it. -
How long an evaluation takes is measured and published.
npm run proofnow timesEvalEngine.evaluateAll— the callevaluate_outputmakes — over every case in the proof corpus, discards a warm-up and reports p50 and p95 with the machine that produced them, on the proof page and inproof/RESULTS.md. It is excluded fromproof --check(latency is a property of the machine, and CI's runner is not your laptop) and the hardcoded-claim scanner's neweval-latencypattern refuses a number anywhere that disagrees with the truthbase. -
OTLP traces arrive as protobuf too.
POST /v1/tracesacceptsapplication/x-protobufbesideapplication/json— the Python OTLP exporter's only encoding, so until now no Python framework (Pydantic AI, Google ADK, LangGraph via LangSmith's export, CrewAI, AutoGen, Microsoft's Agent Framework) reached the door without a Collector between. The decoder issrc/otel/protobuf.ts: a wire-format reader for the one message Iris accepts, about two hundred lines and no dependency (againstprotobufjsat 3.8 MB and@bufbuild/protobufat 1.9 MB), producing exactly the OTLP/JSON object the JSON path already parses, so the mapping and every test below it are unchanged. Held to the real thing:tests/fixtures/otlp/*.pbare the request bodiesopentelemetry-exporter-otlp-proto-http1.44.0 posted, and the decoder must reproduce their JSON twins byte for byte. Any other content type is a 415 naming both encodings; a malformed protobuf body is a 400 with the byte offset. gRPC is still not served —docs/otel-integration.mdcarries the one-file Collector bridge. -
The OTLP door speaks every convention a buyer will test. Beside the GenAI conventions it already read,
POST /v1/tracesnow reads OpenInference (input.value,output.value,llm.token_count.*,llm.model_name,session.id,openinference.span.kind), OpenLLMetry (traceloop.entity.input/output,traceloop.span.kind,llm.request.type, the indexedgen_ai.prompt.N.content), LangSmith's export (langsmith.span.kind), Semantic Kernel'sgen_ai.response.prompt_tokens/completion_tokens, and the Vercel AI SDK's legacyai.prompt/ai.response.text/ai.usage.*/ai.model.id/ai.toolCall.*;gen_ai.agent.namenames the agent before the"otel"default; the model lands inmetadata.model(what the judge's same-family check reads),gen_ai.conversation.idinmetadata.session_id, andgen_ai.tool.definitionsintools[]forvalid_tool_arguments. Token usage is summed over the leaf carriers only — a framework that puts the run's totals oninvoke_agentbesidechatchildren that carry their own (Microsoft Agent Framework, the Vercel SDK'sgenerateTextover itsdoGenerate) is counted once, where every span used to be added; Pydantic AI's whole-rungen_ai.aggregated_usage.*wins when present. One fixture per framework, authored to the vendor's documentation, holds each of these (tests/fixtures/otlp/conventions/). -
Trace context is carried (SEP-414).
log_traceandevaluate_outputread W3Ctraceparent/tracestate/baggagefrom the request's_meta(and the MCP session id from the transport), andPOST /api/v1/tracesandPOST /v1/tracesfrom the headers of the same names; what arrived is stored undermetadata.trace_context(the header, the trace id, the caller's parent span id, the sampled flag, the session id) —evaluate_outputwrites it onto its linked trace when that trace has none. The API returns it and the trace drawer shows it. A root that joined the caller's trace also carriesmcp.method.nameandmcp.session.idper the OTel MCP conventions. A trace stored with a context is exported toIRIS_OTEL_ENDPOINTunder the caller's trace id with its root parented to the caller's span, so Iris's evaluation appears inside the agent's own trace in Langfuse, Phoenix, Logfire or Jaeger. Without a context nothing changes. Claude Code sends_metaas null ontools/call(anthropics/claude-code#76391), so from that client the capture plugin is the session link. -
Expected-trajectory rules, measured — and an off-task answer no longer reads clean. Four built-in rules (25 now), each with a corpus family and its precision and recall on the proof page.
tool_sequencecompares the calls the caller expected (expected_trajectory.tool_callsonevaluate_output) with the calls made, in a mode —strict,unordered,subset,supersetorordered_subset(the default; a two-pointer subsequence check) — matching by tool name and, when the expectation names arguments, byexactorsubsetinput;step_budgetholds the call count to the budget the caller stated for THIS task (expected_trajectory.step_budget, or the number of expected calls) times a tolerance (default 1.5); both are policies that gate because the caller stated the expectation, and both skip without one.tool_choicefinds the wrong tool for the ask from the cataloguevalid_tool_argumentsalready takes: it scores the ask's actionable terms (the ones some tool's name, description or input properties mention) against each tool and fires when an uncalled tool fits at least 0.5 and beats the tools called by at least 0.34 — a heuristic detection that lowers the score and never vetoes.answers_the_askanswers the composer question: a truncated or off-topic answer readpassed: true, basis: cleanbecause the two relevance rules are measurements, which inform the score, and only policies and detections gate. Measured on the 141-case composite corpus before the rule existed, gating on both measurements failing together flipped no classless clean case, caught four of the eight off-task cases and six stub cases the composer missed — so the pair now decides:answers_the_askis a policy with no number of its own — when the input is present and bothkeyword_overlapandtopic_consistencyfail at their thresholds, the output answers something else and the verdict fails onpolicy_gate, at the shipped defaults too. It skips whenever either measurement skips and on an ask with fewer than two content terms; a right answer that reuses none of the ask's words reads as off task (three of the corpus' twenty-five negatives), and the two measurements' thresholds are the knob. Two failure classes join the vocabulary (wrong_trajectory,wrong_tool) and one need (expected_trajectory);off_taskjoins the composite corpus' tier A, so the composite is regenerated. The capability map's Q3×S3 and Q4×S3 move from gap to has with evidence that resolves.
Fixed
- Two processes opening one cold database at the same instant no longer lose one of them on the WAL switch.
PRAGMA journal_mode = WALupgrades the connection's SHARED lock to EXCLUSIVE, and SQLite does not run the busy handler on that upgrade (two connections each waiting for the other's SHARED lock would never return), so the 0.14.0 fix — the timeout set before the switch — covered a plain wait and never this one: the losing process still failed withdatabase is lockedwhateverbusy_timeoutsaid. The adapter now retries the switch itself with a short backoff inside the same five-second budget, on either driver's busy error; an error that is not BUSY is thrown as it came, and a failed boot closes the handle. Found by a CI run on the Node 22 built-in driver. passedmeans one thing across the four eval tools.evaluate_with_llm_judgeandverify_citationsstored one judgment row each and reported their own pass/fail beside it, so a stored judgement read back with no verdict at all (the composer derives a verdict on read only from a row that carries provenance) and the two tools'passedwas the threshold's word where every other surface's was the composer's (#375 item 2). Both rows now go through the engine's composer before they are stored — provenance and all — and both responses carry the composer'sverdict(state,basis,by), the same objectevaluate_outputprints: a failed judgment gates (policy_gateby the judge), a passed one isclean, and a citation check that judged nothing isunknown(no_rules), never a pass.tests/unit/tools/judge-verdict.test.tsdrives both tools through the MCP surface with the model mocked and reads the rows back.- The demo is the engine's own output.
--demoused to write hand-shaped evaluations — a failing card readFAIL · Fail:with nothing after the colon, a FAIL card said2/2 pass, Runs was empty, the Audit Log was empty and Drift had no prior period. Now every seeded trace is stored and judged through the same functionlog_trace,POST /api/v1/tracesandingestcall, dated when the trace happened, so a demo row carries exactly what a real one does: the verdict with its basis, the evidence spans, the interpretations, the provenance. The traffic spans two weeks (the Drift view's default window has the week before it), two runs on the same twelve questions are seeded with arelease-gatedataset over them (the bad deployment shipped unfilled templates; the comparison shows the recovery with an interval), the demo's two custom rules go through the real store (one enabled and firing, one paused, three audit rows), and the injection case is one the rule actually judges — a hidden directive in a forum post the summarizer complied with, not a quoted phrase the rule rightly reads as discussion. The three LLM-judge rows keep canned scores (the judge needs a key) but are built by the judge tool's own row builder, now one function (src/eval/llm-judge/persisted.ts). - The dashboard's top bar holds at phone width. Below 768 px the command-palette trigger ("Search or jump to… Ctrl K") ran under the status chips (
live,judge off,DEMO) on every page. The trigger is now its icon at that width (the accessible name and the shortcut are unchanged), the title row wraps its chips onto a second line, and the header grows to fit — nothing overlaps and every chip stays visible, because the chips carry live state a phone reader still needs.tests/e2e/header-phone.spec.tsmeasures it: on four pages of a demo server at 400 × 820, no two top-bar children share a pixel and nothing runs past the right edge. - The read routes refuse a misspelled filter. Every dashboard GET query schema was a bare
z.object, which strips an unknown parameter, soGET /api/v1/moments?agent_nme=docs-qareturned every agent's rows exactly as if the filter had applied — the silent-strip defect the mutating bodies were cured of in 0.5.x (#376), one verb over. The thirteen query schemas (/traces,/evals,/runs,/cases,/summary,/eval-statsand its/trend,/driftand/failures,/failures,/moments,/audit,/labels,/issues) are now strict: an unknown parameter is a 400 naming it and the valid ones. Observable for an API caller: a request that sent an extra or misspelled query parameter and got a page back now gets a 400 — check the parameter names againstdocs/architecture.md. - Every README link survives the npm package. The package ships
dist,LICENSE,README.mdandserver.json, so a relative link todocs/or a sibling package was dead for everyone reading the README fromnode_modules(#369 item 1). The last four relative links are absolute, andtests/docs-contract.test.tsrefuses any README link that is relative to a file the package does not carry. POST /api/v1/rules/customdeploys anaction_policy. The route's own copy of the rule-type enum stopped at eight types whiledeploy_ruleaccepted nine, so the policy that gates a deploy could be deployed over MCP and not over HTTP (found by the 0.15.0 stranger's gate phase). The list is one exported constant (RULE_TYPE_VALUES) the store, the tool and the route all build from;tests/unit/dashboard/routes/rules-action-policy.test.tsdeploys one rule of every type over HTTP and reads them back.docs/ci-gate.mdstep 1 shows the config-file form beside the env form — a session that cannot prefix a command with a variable starts the same refusal through--config gate.json— and the walk-through test runs that form.- The dashboard's poll no longer discards a slow response. A poll tick that fired while the previous request was still in flight took the request id, so the pending response was dropped as stale — and the next one, and the next. On a server with enough traffic that a 200-moment window took longer than the cadence, the Drift and Health prior windows never rendered and the banner said "No prior period to compare against yet" over two weeks of data. A tick now waits for the request in flight (a manual refetch still supersedes); the Drift, Health and agent-list reads poll at the normal cadence rather than the live tail's; the change banner says it is comparing while a window loads instead of claiming there is nothing to compare; and the moments and failures routes hydrate a page's evaluations in one batched read (
getEvalsByTraceIds) instead of one query per trace.
Changed
no_tool_loopcatches a repeating cycle of any length. The cycle clause stopped at period 3 ("beyond three the pattern is a plan, not a loop"), so a four-call block repeated three times over — twelve calls that between them asked four questions three times, every answer unchanged — walked straight through (#427). The period searched is now bounded by what can still repeat more than the allowance inside the trajectory (maxCycleLength(n) = floor(n / 3)): a plan repeated twice is a retry and passes; the same plan a third time is the machine stuck. The polling exemption and the shorter-period rule are unchanged. Two corpus cases pin both sides (loop-041fails,loop-042passes); the family measures n = 42, precision 1.00, recall 1.00 on corpusa2825049eb4f(it was n = 40, 1.00 / 1.00 — nothing that fired stops firing, nothing that passed starts). Observable: an agent whose trajectory repeats a block of four or more distinct calls three times over is now judged a loop where it was not.
Removed
suggestionsis gone, on the schedule 0.13.0 announced. Every line it carried was a restatement of a field the same response already had — the failing rules (rule_results), the veto (verdict.basisdetector_vetowithcritical_failures), each skip and its reason (rule_results[].skippedand.skipReason), the critical checks that could not judge (critical_skipped) — and a second copy of a verdict is a second verdict.interpretations[], which has carried the sentences since 0.13.0, says who each one is for and which setting changes it; two sentences that onlysuggestionsused to carry are now written there: when nothing was judged, and when a critical check skipped without making the verdict unknown. Check before upgrading: if you readsuggestionsfromevaluate_output, fromPOST /api/v1/traceswithevaluate: true, or from a stored row, readinterpretations[]andrule_results[]instead. Thesuggestionscolumn stays in the database, written as[], so a database this version writes still opens on 0.15.0.docs/roadmap.mdis gone; the capability map is the roadmap. It said in prose whatdocs/capabilities.mdsays from the truthbase, and it went stale between releases — it still listed OpenTelemetry ingest and datasets as open a release after both shipped.iris-eval.com/roadmapredirects to/capabilities, the links that pointed at the file point at the map, and a test holds that no live surface links it and that the tracks on the site do not call shipped work planned.
Supply-chain transparency
- SBOMs:
iris-npm-sbom.spdx.json+iris-docker-sbom.spdx.json(attached below). Both are SPDX 2.3 JSON, cover direct + transitive dependencies. - SBOM signatures: each SBOM has a companion
.sigstore.json(Sigstore bundle — signature + cert + Rekor entry) attached to this release. The release workflow runs this exact verification itself before it reports success. Verify with:cosign verify-blob \ --bundle iris-npm-sbom.spdx.json.sigstore.json \ --certificate-identity-regexp='https://github.com/iris-eval/mcp-server' \ --certificate-oidc-issuer='https://token.actions.githubusercontent.com' \ iris-npm-sbom.spdx.json - npm provenance: published with
--provenance(verifiable vianpm audit signaturesor on the package page). - Docker signature: image signed with cosign keyless (Sigstore). Verify with:
cosign verify ghcr.io/iris-eval/mcp-server:v0.16.0 \ --certificate-identity-regexp='https://github.com/iris-eval/mcp-server' \ --certificate-oidc-issuer='https://token.actions.githubusercontent.com' - Build attestation: both the npm SBOM and Docker image manifest carry GitHub-signed build-provenance attestations. Inspect with
gh attestation verifyorcosign verify-attestation.
What's Changed
- fix(stranger): the gate rows graded on what Iris printed, never on fetched docs; a dashboard port of its own per phase by @irparent in #564
- feat(demo): the demo is the engine's own output; the dashboard's poll no longer discards a slow response by @irparent in #569
- fix(dashboard): the top bar holds at phone width by @irparent in #570
- fix(tools): passed means one thing across the four eval tools — the judge and citations rows through the composer by @irparent in #571
- fix(dashboard): the read routes refuse a misspelled filter — every query schema strict by @irparent in #572
- docs(readme): every README link survives the npm package by @irparent in #573
- fix(rules): one rule-type list for every surface; the walk-through's config-file form by @irparent in #574
- feat(rules): no_tool_loop catches a repeating cycle of any length by @irparent in #575
- feat(otel): OTLP traces arrive as protobuf too by @irparent in #576
- feat(otel): the OTLP door speaks every convention a buyer will test by @irparent in #577
- feat(otel): trace context is carried — SEP-414 by @irparent in #578
- feat(rules): expected-trajectory rules, measured — and an off-task answer no longer reads clean by @irparent in #579
- feat(runs): the run comparison lists its discordant cases, and a run can be the baseline by @irparent in #580
- feat(sessions): a trace carries its session — on every door, with a filter, grouping in the comparison, and the strip in the trace drawer by @irparent in #581
- feat(notify): a webhook fires on a moment — signed two ways, retried, cooled down, never in the way of an evaluation by @irparent in #582
- feat(ci): a GitHub Action for the gate — the receipt on the job summary, one pull-request comment updated in place, dogfooded by @irparent in #583
- docs(otel): the recipes page — one recipe per framework, each proved by a fixture by @irparent in #585
- feat(python): iris-eval on PyPI — a thin client over the HTTP API and a pytest plugin that gates a test on the verdict by @irparent in #584
- feat(listings): every listing inherits the truth by @irparent in #586
- feat(compare): the compare pages hold against today's vendor pages by @irparent in #587
- feat(engine): suggestions removed, the response schema published and locked, the verdict's path exported, the roadmap is the map, and evaluate_output's latency measured by @irparent in #588
- deps: Bump fast-check from 4.9.0 to 4.10.1 by @dependabot[bot] in #565
- deps(website): Bump @upstash/redis from 1.37.0 to 1.38.4 in /website by @dependabot[bot] in #488
- deps(init): Bump @types/node from 25.6.0 to 26.6.2 in /packages/init in the types group across 1 directory by @dependabot[bot] in #352
- deps(dashboard): Bump the vite group across 1 directory with 2 updates by @dependabot[bot] in #306
- deps(init): Bump vitest from 4.1.11 to 5.0.1 in /packages/init in the testing group across 1 directory by @dependabot[bot] in #351
- deps(dashboard): Bump @typescript-eslint/parser from 8.61.0 to 8.70.0 in /dashboard by @dependabot[bot] in #311
- deps(website): Bump the react group across 1 directory with 4 updates by @dependabot[bot] in #526
- deps(init): Bump tsx from 4.22.4 to 4.23.13 in /packages/init by @dependabot[bot] in #491
- deps(website): Bump the nextjs group across 1 directory with 2 updates by @dependabot[bot] in #566
- deps(dashboard): Bump jest-axe from 10.0.0 to 11.0.0 in /dashboard by @dependabot[bot] in #313
- deps(website): Bump @types/node from 26.1.2 to 26.6.2 in /website in the types group across 1 directory by @dependabot[bot] in #567
- deps(dashboard): Bump lucide-react from 1.23.0 to 1.47.0 in /dashboard by @dependabot[bot] in #316
- deps(init): Bump typescript from 5.9.3 to 7.0.2 in /packages/init by @dependabot[bot] in #353
- deps(website): Bump framer-motion from 12.38.0 to 13.4.0 in /website by @dependabot[bot] in #568
- deps(dashboard): Bump eslint from 10.2.1 to 10.11.0 in /dashboard by @dependabot[bot] in #355
- release: 0.16.0 — Earn the comparison by @irparent in #589
Full Changelog: v0.15.0...v0.16.0