Skip to content

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 20:06
· 188 commits to main since this release
de814cf

[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_results holds output_text verbatim — a PII detector necessarily stores the PII it found — and its trace_id is ON 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 than retention.days (deleteEvalResultsOlderThan, on created_at), then checkpoints and truncates the write-ahead log so the swept rows do not survive as plain text in iris.db-wal. A new --purge flag 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's secure_delete is now on for every connection, so a deleted row's bytes are zeroed rather than left in place until the page is reused. IRIS_HOME is created owner-only (0700) to match the 0600 the data files already get; a pre-existing home keeps the permissions its owner chose. (#372 items 2–3.) Behaviour change: evaluations older than retention.days (default 30) are deleted at the next startup. Set retention.days: 0 in config.json before upgrading if you keep evaluation history longer than that.
  • Demo mode refuses trace ingest. --demo serves the dashboard — and with it POST /api/v1/traces — against demo.db, a disposable database that --demo-clear deletes 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 returns 403 with 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-test now probes the CONFIGURED IRIS_HOME instead of passing on a temp home. The diagnostic printed ✓ PASS — this install works with exit 0 against an IRIS_HOME the server could not write, because every check ran inside its isolated scratch home; the real server then died on startup with a raw EPERM stack trace — the single highest-priority item in the batch, since --self-test is 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 when IRIS_DB_PATH points 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 prints home beside storage. Relatedly, loadConfig no longer surfaces a bare mkdirSync stack for an unwritable home: one line names IRIS_HOME, the path and the permission problem. (#371.)
  • Same-name deploy_rule redeploys 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 because rule_results carried no rule id the same ruleName showed 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: true deletes 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 carry ruleId (rule-XXXX), so two same-named results are distinguishable and a reader can find the rule that spoke. (#373 item 3.) Behaviour change: deploy_rule with a name that is already deployed now errors unless replace: true — scripts that redeployed to "update" a rule should pass replace: 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 at security.rateLimit.mcp in config.json. The dashboard REST limiter is unchanged. (#373 item 4.)
  • A non-timestamp since/until error names the value it rejected ("yesterday" must be an ISO 8601 timestamp…), on the tool and on GET /api/v1/traces alike, as the crossed-bound errors already named theirs. Found by the v0.6.0 acceptance pass.
  • --purge --dashboard is refused (exit 2) instead of purging and silently dropping the dashboard flag; the --help footer names $IRIS_HOME/preferences.json rather than a hardcoded ~/.iris path. Found by the v0.6.0 acceptance pass.
  • verify_citations fails closed when the judge never ran. With a wrong API key every resolved citation carried an auth error and the tool still answered passed: 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_traces rejects ranges that could never match. min_score: 0.9, max_score: 0.1, since later than until, a score outside 0..1, or a since/until that 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/until accept ISO 8601 timestamps (with offsets) or calendar dates; offset must be a non-negative integer. The argument descriptions state exactly this, and until is 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_rules entry or deploy_rule.definition are rejected. Top-level strictness shipped in 0.5.0 and stopped there: custom_rules: [{ name, type, config, wieght: 5 }] still parsed with wieght discarded, 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's config keys stay free-form, as documented, because they depend on type. weight must be positive on both paths (the store already required it; the tool used to let 0 through to a raw store error). (#376 item 1.)
  • An unknown trace_id is a clear error, checked before anything runs. evaluate_output with a trace_id that 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 the trace_id named 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_pii says 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 bare No PII detected, so a builder smoke-testing with exactly those values concluded detection was broken. The pass message now reads No 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_pii catches ISO dates of birth after a label. DOB: 03/15/1987 was caught while Date 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 ISO YYYY-MM-DD alternative; 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, so Date of birth: 1987-03-15 passed no_pii on 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 own evalType and nothing else; eval_type="custom" runs only rules deployed under "custom" (plus the call's inline custom_rules). The description now states exactly that — and that eval_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_rules says the same thing.
  • verify_citations returns the source keys its description documents. The description promised content_type / bytes_fetched; the handler passed the verifier's internal camelCase object through, so a client parsing per the docs read undefined. The response now maps source to { url, status, content_type, bytes_fetched, truncated }.
  • The rule toggle the descriptions promised now exists. delete_rule and deploy_rule told users to disable a rule without deleting it via "the dashboard's toggle affordance" — which existed on no surface: the store had setEnabled() and nothing called it. delete_rule now takes an optional enabled argument: false disables the rule (kept in the store with its id, provenance and history; unregistered from the live engine immediately; not loaded at the next boot), true re-enables it; the response reports toggled and the rule's current state, and an unknown id reports toggled: false. Re-enabling is idempotent — the engine's registerRule now 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 existing setEnabled.
  • @iris-eval/init registers the server under iris-eval, like every other install surface, and --uninstall removes the legacy key too. The installer wrote the entry under iris, 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 --uninstall removed only one. Installing now migrates an existing iris entry to iris-eval (one entry, not two) across the JSON-map, Zed and Codex strategies, and uninstall removes both keys.
  • @iris-eval/langchain depends 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.0 and excluded the current release: any install of the package resolved the pre-veto, pre-sandbox 0.4.x line.

Added

  • evaluate_output accepts eval_type: "all". One call runs every bundle — completeness, relevance, safety, cost, the rules deployed under "custom", and any inline custom_rules — in a single pass sharing one regex budget, and returns the usual overall score / passed (the critical veto spans every bundle: a PII leak anywhere fails the whole verdict) plus a categories map with the same threshold-plus-veto arithmetic per bundle. Each rule_results entry carries category so a reader can regroup; rules whose context is missing skip exactly as in a single-bundle run. The evaluation is stored under eval_type: "all", and the dashboard's safety-violation counter reads those rows too. The omitted-eval_type note now points at "all" as well as "safety". (#370 item 3.)
  • --version, and the version in the --help banner. The README's troubleshooting recipe pointed at a flag that did not exist. iris-mcp --version prints the bare version on stdout and exits 0, touching nothing. (#369 item 5.)
  • deploy_rule accepts snake_case argument names as canonicaleval_type, source_moment_id — matching every sibling tool; the camelCase spellings evalType / sourceMomentId remain accepted as aliases (pass one spelling of each, not both). definition.name is now optional: the server always overwrote it with the top-level name, so requiring a value it then discarded only invited a mismatch. (#377 items 3–4.)
  • Storage adapter: deleteEvalResultsOlderThan(tenantId, days), purge(tenantId) and checkpoint() on IStorageAdapter; EvalEngine.evaluateAll(), hasRule(id), and id-idempotent registerRule; EvalRuleResult.ruleId / .category; EvalResult.categories.
  • The mode flags --demo, --demo-clear, --self-test and --purge are mutually exclusive and refused together with exit 2, before anything touches the filesystem — --self-test --purge used to run only the first one it saw.

Changed

  • iris-eval.com/docs now 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. /docs and /docs/<path> now redirect (temporarily, until a hosted docs site exists) to the docs/ directory on GitHub. The $schema URL .claims.json has 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-key dashboard. 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 sending Authorization: Bearer, unchanged. When no key is configured nothing changes.
  • Deployed rules can be paused without deleting them. delete_rule and list_rules have pointed users at "the dashboard's toggle affordance" since v0.4; no route, no UI call and no MCP tool ever invoked the store's setEnabled, so the advertised affordance did not exist. PATCH /api/v1/rules/custom/:id with { "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 next evaluate_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/builtin serves 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 use POST /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). A sampleOutput string is now judged against the proposed rule and the verdict comes back under samplepassed, score, message, and skipped with 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/critical from a sort key into a hard veto, and the composer kept rendering bare low/medium/high/critical with no label, hint or tooltip — while pre-selecting high for 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 sentence deploy_rule uses (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_threshold failed pre-filled a rule named my_cost_cap, described "Per-trace cost cap.", whose actual check type was max_length with value 0.05 — "output must be at most 0.05 characters". The pre-fill is now a cost_threshold rule with max_cost: 0.05 (the canonical config key), the composer offers the cost check type with a USD field, and the suggested severity is medium so 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 whenever IRIS_HOME is set and always wrong in --demo mode (demo-preferences.json) (#377 item 2). GET /api/v1/preferences now reports a displayPath — 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.json vs demo-preferences.json vs whatever IRIS_HOME points 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 know no_hallucination_markers had 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/health trace_count is the all-time count. It read the dashboard summary's one-hour window, so on the demo database health said 0 while /api/v1/traces said 253 (#373 item 1) — a liveness field that contradicts the data it fronts.
  • POST /api/v1/traces accepts eval_type: "all". The ingest schema stopped one bundle short of evaluate_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-bundle categories map, category on every rule result — was unreachable over HTTP. Ingest now takes the same evaluateAll path and stores the result under eval_type: "all", and the evaluation block also carries critical_skipped, which the tool already returned and the ingest response dropped.
  • GET /api/v1/traces refuses the ranges get_traces refuses. since later than until, a since/until that is not an ISO 8601 timestamp or date, min_score above max_score, a score outside 0..1 or a negative offset returned 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 answers 400 naming both values; it also accepts min_score / max_score, which the tool had and the query did not.
  • POST /api/v1/rules/custom refuses a name that is already deployed. deploy_rule has refused a duplicate name unless replace: true since the batch above; the dashboard's deploy route kept accepting one, so two same-named rules could still be created — and fire with indistinguishable rule_results — through the very surface the tool description sends people to. Both paths now call one helper: a taken name is a 409 naming the existing rule's id in the same sentence the tool uses, and replace: true retires the earlier rule(s) (audit rows kept, unregistered from the live engine) and reports what it replaced. definition.name stays optional on the route and is always replaced by the rule name.
  • POST /api/v1/traces rejects 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-supplied trace_id; that is now an explicit rejection whose message says the server mints it. Behaviour change: a client that was sending trace_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 into definition as well (config stays free-form by design), and definition.name is 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's docs/ tree.
  • The dashboard's tab title carries the current tagline. index.html still read the pre-rebrand positioning, and the built artifact that ships to npm carried it too. The title is now filled from .claims.json brand.tagline at 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-caption pattern that flags any v0.x literal 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 sm breakpoint 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 via npm audit signatures or 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 verify or cosign 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