v0.6.0
[0.6.0] - 2026-09-03
The correctness release. Every open server, dashboard, docs and release-process item from the v0.5.0 acceptance pass (#369–#377) and the post-release backlog, plus a cloud review of the eval engine and all nine tool handlers, landed as five PRs (#398, #399, #400, #401 and the follow-up) with a regression test behind each code change. New in one call: evaluate_output runs every bundle with eval_type: "all"; deployed rules can be paused and resumed without deleting them; a browser can sign in to an --api-key dashboard; --version and --purge exist; the release workflow publishes to the Official MCP Registry and verifies npm, GHCR, the GitHub release and the registry from outside before it reports green. Three behaviour changes are called out in bold below: retention now sweeps stored evaluations, a same-name deploy_rule errors unless replace: true, and a client-supplied trace_id on HTTP ingest is rejected instead of silently replaced.
The server-correctness batch. Closes the open server-side items from the 0.5.0 acceptance pass (#370 items 2–3, #371, #372, #373 items 3–5, #374, #376 items 1 and 3, #377 items 3–4) and the "Accuracy" and "Developer experience" rows of the post-release backlog: places where a stranger who installed from npm saw the product contradict its own description, lose their data, or get a wrong verdict. Every item below has a regression test that fails on the previous code; root suite 1017 → 1095.
Security
- Stored evaluations are now swept by retention, and everything can be purged.
eval_resultsholdsoutput_textverbatim — a PII detector necessarily stores the PII it found — and itstrace_idisON DELETE SET NULL, so the startup retention sweep removed old traces while every evaluation they ever produced stayed behind, readable, forever. The sweep now also deletes evaluations older thanretention.days(deleteEvalResultsOlderThan, oncreated_at), then checkpoints and truncates the write-ahead log so the swept rows do not survive as plain text iniris.db-wal. A new--purgeflag deletes every stored trace, span and evaluation, rebuilds the file (VACUUM) and truncates the WAL, then exits; deployed rules, the audit log and preferences are kept (stop a running server first — the file is compacted in place). SQLite'ssecure_deleteis now on for every connection, so a deleted row's bytes are zeroed rather than left in place until the page is reused.IRIS_HOMEis created owner-only (0700) to match the0600the data files already get; a pre-existing home keeps the permissions its owner chose. (#372 items 2–3.) Behaviour change: evaluations older thanretention.days(default 30) are deleted at the next startup. Setretention.days: 0inconfig.jsonbefore upgrading if you keep evaluation history longer than that. - Demo mode refuses trace ingest.
--demoserves the dashboard — and with itPOST /api/v1/traces— againstdemo.db, a disposable database that--demo-cleardeletes wholesale. A reader following the README top to bottom could point a capture client at the demo port, watch real traces land beside the fake ones, and lose all of them to the cleanup command the banner recommends; nothing warned at any step. Ingest into the demo store now returns403with a message that says what demo mode is and where real traces go (iris-mcp --dashboard), the demo banner says so up front, and the demo's own seeded data and rule deploys are unaffected.
Fixed
--self-testnow probes the CONFIGUREDIRIS_HOMEinstead of passing on a temp home. The diagnostic printed✓ PASS — this install workswith exit 0 against anIRIS_HOMEthe server could not write, because every check ran inside its isolated scratch home; the real server then died on startup with a rawEPERMstack trace — the single highest-priority item in the batch, since--self-testis the thing new users run first. A new first step creates the configured home (through the same call the server makes), proves it can write there, creates the database directory whenIRIS_DB_PATHpoints elsewhere, and — only when the real database already exists — opens it and takes a write lock without changing a byte (BEGIN IMMEDIATE … ROLLBACK), so a read-only file or a corrupt one fails here rather than at the first insert. The step fails the run with the path and errno named but does not halt the isolated checks, so the report says both whether the home is usable and whether the install works. The report footer now printshomebesidestorage. Relatedly,loadConfigno longer surfaces a baremkdirSyncstack for an unwritable home: one line namesIRIS_HOME, the path and the permission problem. (#371.)- Same-name
deploy_ruleredeploys are refused, and every deployed rule's result carries its id. Deploying a name that was already deployed silently created a second rule; both fired, and becauserule_resultscarried no rule id the sameruleNameshowed PASS and FAIL in one response with no way to tell them apart. A second deploy of an existing name is now rejected with the existing rule's id named;replace: truedeletes the earlier same-named rule(s) — audit rows written, unregistered from the live engine — and deploys the new one in their place, listing what was replaced.rule_results[]entries produced by a deployed rule now carryruleId(rule-XXXX), so two same-named results are distinguishable and a reader can find the rule that spoke. (#373 item 3.) Behaviour change:deploy_rulewith a name that is already deployed now errors unlessreplace: true— scripts that redeployed to "update" a rule should passreplace: true. - The MCP endpoint's 429 is a JSON-RPC error.
express-rate-limit's stock body —{ "error": "Too many requests" }— is not a JSON-RPC message, so a strict client surfaced a protocol failure instead of "wait, then retry". The MCP limiter now answers with a JSON-RPC 2.0 error envelope (code-32029) that echoes the request id, names the limit and the seconds until the window resets, and points atsecurity.rateLimit.mcpinconfig.json. The dashboard REST limiter is unchanged. (#373 item 4.) - A non-timestamp
since/untilerror names the value it rejected ("yesterday" must be an ISO 8601 timestamp…), on the tool and onGET /api/v1/tracesalike, as the crossed-bound errors already named theirs. Found by the v0.6.0 acceptance pass. --purge --dashboardis refused (exit 2) instead of purging and silently dropping the dashboard flag; the--helpfooter names$IRIS_HOME/preferences.jsonrather than a hardcoded~/.irispath. Found by the v0.6.0 acceptance pass.verify_citationsfails closed when the judge never ran. With a wrong API key every resolved citation carried anautherror and the tool still answeredpassed: true, overall_score: null— a verdict a caller would ship on. When citations resolved and the judge failed on all of them the tool now returns an error naming the cause and stores nothing; "no citations to judge" keeps its honest null verdict. Found by the v0.6.0 acceptance pass.get_tracesrejects ranges that could never match.min_score: 0.9, max_score: 0.1,sincelater thanuntil, a score outside0..1, or asince/untilthat is not a timestamp at all ("yesterday","08/01/2026") were accepted and returned an empty page — which reads as "no such traces" when the truth is "no trace could match this". Each is now a validation error naming both values;since/untilaccept ISO 8601 timestamps (with offsets) or calendar dates;offsetmust be a non-negative integer. The argument descriptions state exactly this, anduntilis documented as inclusive, which it always was. (#373 item 5.) Behaviour change: a caller passing garbage timestamps or crossed bounds now gets an error instead of an empty result.- Unknown keys inside a
custom_rulesentry ordeploy_rule.definitionare rejected. Top-level strictness shipped in 0.5.0 and stopped there:custom_rules: [{ name, type, config, wieght: 5 }]still parsed withwieghtdiscarded, so the rule ran at the default weight and the score moved for a reason nothing in the response could show. Both nested rule shapes now reject an unknown key with the valid key list and the entry's path (at custom_rules[0]); a rule'sconfigkeys stay free-form, as documented, because they depend ontype.weightmust be positive on both paths (the store already required it; the tool used to let0through to a raw store error). (#376 item 1.) - An unknown
trace_idis a clear error, checked before anything runs.evaluate_outputwith atrace_idthat matched no stored trace ran the whole evaluation and then failed at the INSERT with SQLite's own words —FOREIGN KEY constraint failed— naming no field, no value and no fix. The three eval tools (evaluate_output,evaluate_with_llm_judge,verify_citations) now check the trace exists first and refuse with thetrace_idnamed and "nothing was evaluated or written"; for the two paid tools that check runs before the provider call, so no money is spent on an evaluation that cannot be stored. The constraint error is also translated for the race where the trace is deleted between check and write. (#376 item 3.) no_piisays when it ignored documentation placeholders.bob@example.com, a 555 number or a published test card is suppressed by design — and the rule reported a bareNo PII detected, so a builder smoke-testing with exactly those values concluded detection was broken. The pass message now readsNo PII detected (2 documentation placeholders ignored: Email ×2 — example.com/.org/.net addresses, 555 and toll-free phone numbers, published payment test cards and masked keys are never counted as PII; real values in the same shapes still fail). A real value beside a placeholder still fails as before. (#370 item 2.)no_piicatches ISO dates of birth after a label.DOB: 03/15/1987was caught whileDate of birth: 1987-03-15— the shape every structured record uses — walked through, because the label-anchored DOB pattern only knew the numeric-with-separators date form. The pattern gains an ISOYYYY-MM-DDalternative; it stays label-anchored (an unlabeled date is still not a date of birth), the pattern count stays at 19, and the new alternative passes the same backtracking probe as every other built-in. (#374, DOB part.)- The public playground catches the same ISO date of birth. The website vendors its own copy of the rule library (
website/src/lib/eval/rules.ts) and kept the old DOB pattern after the server gained the ISO alternative, soDate of birth: 1987-03-15passedno_piion iris-eval.com while the shipped server flagged it — the playground under-reporting a leak the product exists to catch. The vendored pattern is now byte-identical to the server's, an unlabeled date still does not fire, and a root test pins the two patterns together so they cannot drift apart again. deploy_rule's description told the truth's opposite about when a rule fires. It claimed a deployed rule fires when "eval_type matches OR eval_type='custom'". Rules register under their ownevalTypeand nothing else;eval_type="custom"runs only rules deployed under "custom" (plus the call's inlinecustom_rules). The description now states exactly that — and thateval_type="all"runs every bundle — rather than the behaviour changing: making"custom"run every deployed rule would have turned every already-deployed high/critical rule into a veto on evaluations that never asked for it.list_rulessays the same thing.verify_citationsreturns thesourcekeys its description documents. The description promisedcontent_type/bytes_fetched; the handler passed the verifier's internal camelCase object through, so a client parsing per the docs readundefined. The response now mapssourceto{ url, status, content_type, bytes_fetched, truncated }.- The rule toggle the descriptions promised now exists.
delete_ruleanddeploy_ruletold users to disable a rule without deleting it via "the dashboard's toggle affordance" — which existed on no surface: the store hadsetEnabled()and nothing called it.delete_rulenow takes an optionalenabledargument:falsedisables the rule (kept in the store with its id, provenance and history; unregistered from the live engine immediately; not loaded at the next boot),truere-enables it; the response reportstoggledand the rule's current state, and an unknown id reportstoggled: false. Re-enabling is idempotent — the engine'sregisterRulenow replaces an existing registration under the same id instead of stacking a second copy that fired twice. Chosen over a tenth tool because every install surface (.well-known/mcp.json, the plugin manifest, the README's tool count) enumerates the nine tools by name; the dashboard route and UI for the same toggle land separately against the store's existingsetEnabled. @iris-eval/initregisters the server underiris-eval, like every other install surface, and--uninstallremoves the legacy key too. The installer wrote the entry underiris, so a user who also followed the README's paste-in config (iris-eval) ended up with two live entries spawning two servers — duplicate tool names in the agent's tool list — and--uninstallremoved only one. Installing now migrates an existingirisentry toiris-eval(one entry, not two) across the JSON-map, Zed and Codex strategies, and uninstall removes both keys.@iris-eval/langchaindepends on@iris-eval/mcp-server@^0.5.0. The range was^0.4.0, which for a 0.x package means>=0.4.0 <0.5.0and excluded the current release: any install of the package resolved the pre-veto, pre-sandbox 0.4.x line.
Added
evaluate_outputacceptseval_type: "all". One call runs every bundle — completeness, relevance, safety, cost, the rules deployed under "custom", and any inlinecustom_rules— in a single pass sharing one regex budget, and returns the usual overallscore/passed(the critical veto spans every bundle: a PII leak anywhere fails the whole verdict) plus acategoriesmap with the same threshold-plus-veto arithmetic per bundle. Eachrule_resultsentry carriescategoryso a reader can regroup; rules whose context is missing skip exactly as in a single-bundle run. The evaluation is stored undereval_type: "all", and the dashboard's safety-violation counter reads those rows too. The omitted-eval_typenote now points at"all"as well as"safety". (#370 item 3.)--version, and the version in the--helpbanner. The README's troubleshooting recipe pointed at a flag that did not exist.iris-mcp --versionprints the bare version on stdout and exits 0, touching nothing. (#369 item 5.)deploy_ruleaccepts snake_case argument names as canonical —eval_type,source_moment_id— matching every sibling tool; the camelCase spellingsevalType/sourceMomentIdremain accepted as aliases (pass one spelling of each, not both).definition.nameis now optional: the server always overwrote it with the top-levelname, so requiring a value it then discarded only invited a mismatch. (#377 items 3–4.)- Storage adapter:
deleteEvalResultsOlderThan(tenantId, days),purge(tenantId)andcheckpoint()onIStorageAdapter;EvalEngine.evaluateAll(),hasRule(id), and id-idempotentregisterRule;EvalRuleResult.ruleId/.category;EvalResult.categories. - The mode flags
--demo,--demo-clear,--self-testand--purgeare mutually exclusive and refused together with exit 2, before anything touches the filesystem —--self-test --purgeused to run only the first one it saw.
Changed
iris-eval.com/docsnow resolves. The dashboard's command palette ("Open Iris docs") and older directory listings link there, and the site had no such route — the entry landed on a 404./docsand/docs/<path>now redirect (temporarily, until a hosted docs site exists) to thedocs/directory on GitHub. The$schemaURL.claims.jsonhas carried since the truthbase was introduced,https://iris-eval.com/claims-schema-v1.json, also resolves now instead of 404ing.
The dashboard tells the truth. The dashboard is the product's face for anyone who runs --demo, and a first-run walkthrough found it contradicting the engine in the places that matter most: a PII leak wearing an amber PARTIAL chip, a delete dialog promising a safety rule would keep enforcing until a restart, a "cost cap" that was really a length check, a docs link that 404'd, and a tab title from the previous brand. Every item below has a test that fails on the previous code.
Added
- A browser can sign in to an
--api-keydashboard. Auth was Bearer-only, which is right for MCP clients and capture SDKs and useless for a browser — the README's own "production deployment" command produced a dashboard that 401'd every page load (#373 item 6). Any dashboard URL now accepts the key once as?key=<api key>: the server exchanges it for a random session token in an HttpOnly, SameSite=Lax, path-scoped cookie and redirects to the same page with the key stripped from the address bar. A page opened without a session gets a sign-in form instead of a JSON 401, and the form does the same exchange. The key is never stored in the browser; sessions live only in the server process; the key exchange is capped at 10 attempts per client address per minute, and the whole session layer — cookie check, Bearer check and the exchange alike — sits behind a per-address limiter of its own, mounted directly ahead of it, so no authorization decision runs unthrottled. API clients keep sendingAuthorization: Bearer, unchanged. When no key is configured nothing changes. - Deployed rules can be paused without deleting them.
delete_ruleandlist_ruleshave pointed users at "the dashboard's toggle affordance" since v0.4; no route, no UI call and no MCP tool ever invoked the store'ssetEnabled, so the advertised affordance did not exist.PATCH /api/v1/rules/custom/:idwith{ "enabled": true | false }now flips it, and the Rules page carries a labelled switch per rule with a plain state line ("Enabled · fires on the next evaluation" / "Disabled · kept for audit, does not fire"). The engine follows in lockstep the way delete does: a disabled rule stops firing on the very nextevaluate_output, a re-enabled one fires again, idempotently, no restart either way. The switch is optimistic and rolls back with the error inline if the request fails. GET /api/v1/rules/builtinserves the engine's own rule roster — name, category, description, weight, and whether the rule is critical — derived from the rule registry rather than restated. The dashboard's charts now read rule categories from it (with the vendored table as the fallback while the request is in flight), and a root test pins that table to the registry so it cannot drift again.- The rule preview honours
sampleOutput.deploy_rule's description has always said to usePOST /api/v1/rules/custom/preview"for dry-run validation against sample output"; the endpoint accepted the key and ignored it, returning only the historical replay with no sign the sample was never read (#373 item 2). AsampleOutputstring is now judged against the proposed rule and the verdict comes back undersample—passed,score,message, andskippedwith its reason — alongside the replay.
Fixed
- A safety violation no longer renders as an amber PARTIAL chip. The moment verdict is arithmetic over rule counts, so an output that leaked an SSN while every other rule passed was "partial" by the count — and the chip beside a red SAFETY VIOLATION banner read PARTIAL with a tooltip about "a mix of failures and passes" (#377 item 1; the release's flagship behaviour, inverted on its own dashboard). Safety-violation moments now render a danger-coloured SAFETY FAIL chip on the timeline, the failures landing page and the moment detail; on the detail, an evaluation vetoed by a deployed high/critical rule renders SAFETY FAIL when the vetoed evaluation is in the safety category and a danger FAIL otherwise. The tooltips say what the veto means.
- The rule composer's severity field says what severity does. v0.5.0 turned
high/criticalfrom a sort key into a hard veto, and the composer kept rendering barelow/medium/high/criticalwith no label, hint or tooltip — while pre-selectinghighfor every safety-derived moment (#375 item 3). The field is now labelled "Severity — what a failure does", each option says "score only" or "hard-fail (veto)", a hint under the select spells the consequence out in the same sentencedeploy_ruleuses (the two are drift-locked by an existing test), and the deploy confirmation repeats it when the chosen severity vetoes. - A cost-spike moment pre-fills a cost rule. Opening the composer from a moment where
cost_under_thresholdfailed pre-filled a rule namedmy_cost_cap, described "Per-trace cost cap.", whose actual check type wasmax_lengthwith value0.05— "output must be at most 0.05 characters". The pre-fill is now acost_thresholdrule withmax_cost: 0.05(the canonical config key), the composer offers the cost check type with a USD field, and the suggested severity ismediumso a budget overrun only becomes a hard gate when the author chooses. - The delete confirmation no longer claims a restart is needed. It read "It will stop firing on subsequent iris-mcp restart" while the route hot-removes the rule from the live engine — wrong in the dangerous direction: someone deleting a safety rule was told it was still enforcing when it was not. The copy now says the rule stops on the very next evaluation and points at the Enabled switch for pausing instead.
- The welcome banner names the real preferences file. It hardcoded
~/.iris/preferences.json, which is wrong wheneverIRIS_HOMEis set and always wrong in--demomode (demo-preferences.json) (#377 item 2).GET /api/v1/preferencesnow reports adisplayPath— the actual file name under the actual home, spelled$IRIS_HOME/…or~/.iris/…— and the banner shows that. The absolute path still never goes over the wire (it embeds the OS username; #334). - The first-run tour no longer reappears when switching between demo and real dashboards. Dismissal was recorded only in server preferences, which are per server (
preferences.jsonvsdemo-preferences.jsonvs whateverIRIS_HOMEpoints at), so the same browser was toured again on every switch. The browser now remembers the dismissal in localStorage, as the banner already did; the server preference is still written and either source suppresses the auto-open. - Charts classify safety failures by rule category, not by name substring. Two charts (the Drift view's per-rule grid and the Health view's top failing rules) decided "safety" with
includes('pii') || includes('injection') || …, which did not knowno_hallucination_markershad joined the safety bundle in v0.5.0 — so a fabricated-citation failure drilled through to the wrong filter for a whole release. Both now use the category map the server derives from the engine. /api/v1/healthtrace_countis the all-time count. It read the dashboard summary's one-hour window, so on the demo database health said0while/api/v1/tracessaid 253 (#373 item 1) — a liveness field that contradicts the data it fronts.POST /api/v1/tracesacceptseval_type: "all". The ingest schema stopped one bundle short ofevaluate_output's list and the route only ever called the single-bundle engine, so the every-bundle verdict the MCP tool returns — one pass, the critical veto spanning every bundle, a per-bundlecategoriesmap,categoryon every rule result — was unreachable over HTTP. Ingest now takes the sameevaluateAllpath and stores the result undereval_type: "all", and theevaluationblock also carriescritical_skipped, which the tool already returned and the ingest response dropped.GET /api/v1/tracesrefuses the rangesget_tracesrefuses.sincelater thanuntil, asince/untilthat is not an ISO 8601 timestamp or date,min_scoreabovemax_score, a score outside 0..1 or a negativeoffsetreturned an empty page from the dashboard query while the MCP tool rejected them (#373 item 5) — one product, two answers. The query now runs the tool's own validators (shared, not copied) and answers400naming both values; it also acceptsmin_score/max_score, which the tool had and the query did not.POST /api/v1/rules/customrefuses a name that is already deployed.deploy_rulehas refused a duplicate name unlessreplace: truesince the batch above; the dashboard's deploy route kept accepting one, so two same-named rules could still be created — and fire with indistinguishablerule_results— through the very surface the tool description sends people to. Both paths now call one helper: a taken name is a409naming the existing rule's id in the same sentence the tool uses, andreplace: trueretires the earlier rule(s) (audit rows kept, unregistered from the live engine) and reports what it replaced.definition.namestays optional on the route and is always replaced by the rule name.POST /api/v1/tracesrejects unknown keys instead of silently dropping them. The HTTP twin of the v0.5.0 strict-tool-args fix (#376 item 2):{ evaluate: true, eval_typ: "safety", output: "<PII>" }dropped the misspelled key, ran the default completeness bundle and returned a green result. The body is now strict, and the rejection names the offending key and lists the valid ones. The schema used to rely on stripping to discard a client-suppliedtrace_id; that is now an explicit rejection whose message says the server mints it. Behaviour change: a client that was sendingtrace_id(and having it silently replaced) now gets a 400 — read the id from the 201 response instead. The rule deploy, preview and toggle bodies are strict too, one level intodefinitionas well (configstays free-form by design), anddefinition.nameis optional there since the server always replaces it with the rule name (#377 item 3).- "Open Iris docs" in the command palette opens the docs. It pointed at
iris-eval.com/docs, a route the website never had, so the product's only in-app docs entry point was a 404 in every shipped release. It now opens the repository'sdocs/tree. - The dashboard's tab title carries the current tagline.
index.htmlstill read the pre-rebrand positioning, and the built artifact that ships to npm carried it too. The title is now filled from.claims.jsonbrand.taglineat build time, the same source the website and README read. - Demo traces are never dated in the future. The demo seeder spread each day's traces across the whole day, including the part of today that had not happened yet, so a demo started in the morning showed traces stamped for tonight — rendered as "just now" and counted as "new since you last looked" on every visit. Anything past the seed moment now lands inside the hour before it; the seeding stays deterministic. Found by the v0.6.0 acceptance pass.
- The Decision Moments header says what the page does. It promised "safety violations and cost spikes surface to the top; happy-path passes recede" over a list that is ordered newest-first, so the first cards a reader saw were passes. The copy now says newest-first and points at the significance filter (and the dashboard's Failures view, which does lead with them). Found by the v0.6.0 acceptance pass.
- No release numbers in dashboard copy. The moment detail captioned its call to action "workflow inversion · v0.4" and the sidebar carried a never-rendered Settings entry promising "coming v0.5" — two releases after 0.5 shipped. Both are gone (the caption now says what the button does: the rule is born from this moment and its source is recorded for audit), and the hardcoded-claim scanner gains a
dashboard-version-captionpattern that flags anyv0.xliteral in dashboard UI code that is not the shipping version, comments excepted. Found by the v0.6.0 acceptance pass. - The website's cloud waitlist form no longer forces a sideways scroll on phones. At 375px the email field and the button sat side by side and overran the page by a few pixels, so the whole home page scrolled horizontally. The form stacks below the
smbreakpoint and the field can shrink. Found by the v0.6.0 acceptance pass.
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
.cosign.bundle(Sigstore bundle — signature + cert + Rekor entry) attached to this release. Verify with:cosign verify-blob \ --bundle iris-npm-sbom.spdx.json.cosign.bundle \ --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.6.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
- chore(process): the release verifies its own external effects; labels, milestones and manifests say the same thing by @irparent in #398
- fix(server): close the 0.5.0 acceptance backlog server-side (#370 #371 #372 #373 #374 #376 #377) by @irparent in #400
- docs: make every public surface tell the truth about v0.5.1 by @irparent in #399
- fix(dashboard): make the dashboard tell the truth — verdict chip, severity, toggle, strict ingest, browser sign-in by @irparent in #401
- fix: close the cross-package mirrors the polish arc handed itself by @irparent in #402
- fix(dashboard): three truths the v0.6.0 acceptance pass found the demo telling wrong by @irparent in #403
- fix: main CI red on a self-test timeout, and a sideways scroll on phones by @irparent in #404
- fix: the acceptance pass's last five — name the rejected value, refuse --purge --dashboard, fail closed when the citation judge never ran by @irparent in #405
- chore(release): v0.6.0 — the correctness release by @irparent in #410
Full Changelog: v0.5.1...v0.6.0