0.9.0 - 2026-09-30
Release Notes
Breaking. A run’s verdict now carries its provenance, and a verdict that
nobody produced is no longer reported as an approval. See
Upgrade notes below — if you gate CI on verdict, read that
section first.
The release is the engine half of a larger correctness pass. Every item below was found by looking at what the code actually did, not by asserting what it should do, and several were defects the existing suite was passing over.
Fixed
- A verdict is no longer a pass nobody granted.
Verdictdefaulted toApproved, and two paths produced one: the SingleAgent fast path ran no Reviewer and still reportedApproved, and the MultiAgent arm setverdictwithout ever settingverdict_source, so a genuine reviewer’s approval arrived with no provenance.RunOutcome(reviewed/revision_requested/self_verified/not_evaluated/blocked/cancelled/failed) is now derived at the singlePipelineResultbuild point, andverdictis a projection of it. - The security audit ran after the review. With
[security] enabled, the reviewing agent finished before the auditor produced anything, so the audit could never reach the agent it exists to inform. The auditor is now injected ahead of the Reviewer on both the config and risk-tier paths. - A SecurityAuditor
Rejectedhad no effect on a run that had a Reviewer — the normal configuration. It was computed, recorded, and rendered into the report, and then ignored. A rejection now forces a revision and a later Reviewer approval cannot overturn it. - Network egress was allowed unconditionally.
web_fetchandweb_searchdeclarePermissionRequirement::Allow, and the rule that forbade it lived inpermissions::resolve_tool, which no product code calls. Egress is now denied fail-closed outside an explicitbypass/dontaskopt-out. - A patch that failed to apply warned and continued. The Tester then verified
a tree that did not contain the change, the Reviewer judged a verdict about
code that was never written, and a branch was cut anyway. On the solo fast
path, a repair that also failed fell through to
verdict = Approvedand completed the run with no change in the working tree. All of these now stop the run with an error naming what failed. - State writes could vanish.
save_task_recorddiscarded its write error at all ten call sites, the closing record write warned,context.jsonwent through two discardedlet _ =s, and the session init andafter_plannercheckpoint were discarded — so/undoand/rewindcould silently not work for a whole run. All propagate now. write_atomicderived its temp name withwith_extension, which replaces the extension rather than appending:task.jsonand a siblingtaskboth becametask.tmp-niki-atomic. Two writers would clobber each other’s partial write and rename a truncated file into place.- Cancelling was read once per revision round — four sequential LLM calls — and never at all on the solo path, which has no loop. A user who pressed Esc watched the rest of the round play out.
- A repair retry’s tokens were billed to nobody.
run_agentassembled its usage total before the repair loop, so a stage that needed two rounds to produce a valid artifact under-reported its own cost to the user and to the spend cap — precisely on the most expensive runs. - The container backend was unreachable on Windows.
connect_container_runtimewas#[cfg(unix)]and the non-Unix arm bounddockertoNone, so Docker Desktop being installed and running changed nothing. Ctrl-C cleanup was Unix-gated too, so cancelled Windows runs leaked their containers. - The error modal was fully tested and completely unreachable.
Modal::Errorhad a renderer, a hit-test, key handling and three passing tests that constructed the variant by hand; nothing in the product ever did. A failed stage now opens it. That made live aBox::leakin its render path, which leaked on every redraw. - The embedded-asset manifest was stale on master: the ratchet guarding the prompts and schemas compiled into the binary had been red since an earlier commit.
Added
outcomeandindependently_reviewedin the--output-format jsonenvelope, and anOutcome:line in the markdown report naming who produced the verdict.reconcile_result_record_manifestcross-checks the pipeline result,task.jsonandmanifest.jsonbefore a run is considered finished. A disagreement is a hard error, not a warning: by then the artefacts are on disk, so warning would mean shipping a self-contradictory run.- Regression tests for each of the above, each verified to fail against the
pre-fix code — including two (
tests/cancellation.rs) that had to be rewritten because their first drafts passed against the bug they were written to catch.
Changed
finish_stageandrecord_isolationare now the only implementations of per-stage bookkeeping. Seven copies of the tail existed in two orders and two had droppedenforce_spend_cap, enforcing it one stage late. Six hand-copies ofprovider_cache_keycollapsed into the one function.body_stages_foris actually called. The SingleAgent collapse was satisfied only because that arm looks the Coder up by hand.permissions::resolve_tooland its three tests are removed; the rule that made it worth keeping now lives in the path that runs.ModalAction::Skipis removed — no code path ever produced it.
Fixed
- Three gates reported success without measuring anything. The acceptance
suite printed
pass_test 'Git/worktree integrity'with no assertion near it — the product’s headline promise, unchecked. The same script skipped the whole product E2E and exited 0 whenmock_llm.pywas missing, so the gate could vanish from CI and still read as a pass. TheManifest parityjob called a dead release URLPENDING, so seven manifest URLs pointed at an unpublishedv0.9.0whilebrew install niki— the README’s primary install command — returned 404, with the job green. - The setup wizard wrote a config the machine could not run. It recorded a
provider and a model and no backend, so on a machine with no container
runtime — the machine the README opens by describing — the wizard reported
success and the next command could not start.
niki doctorthen failed that same machine for a missing container runtime, having no notion that a second backend needs none, andniki smokeinherited the bug while its own help text called the worktree path the zero-setup route. - The goal loop forgot everything it had learned. It accumulated
context_summaryandnegative_knowledge, persisted them, and handed the pipeline a task built from the description alone — so iteration 2 could repeat iteration 1’s mistake, the specific thing a multi-iteration runner exists to prevent. - Promoted skills were invisible to the agents that load them. Promotion
wrote to the configured
output_dir; the runtimeskill_list/skill_loadtools read a hardcoded.niki/skills. Under a custom output dir a skill was promoted with a success message and never loaded. - An expired key defeated the fallback chain. Any non-5xx error returned immediately, so a 401 from a stale primary — the most common reason anyone configures a fallback — killed the run before the fallback was reached.
- A test asserted the wrong value for the bash-timeout clamp, which is what
made
mainred. The production arithmetic was always correct; the assertion claimed the ceiling where the floor was right, and the property it was written to protect was not the one it checked.
Added
niki-starter/— a small project with a real failing test and one command, plus the setup guide, a troubleshooting guide, an honesty page, and a guide to readingreport.md. Solving its task is a regression, and a test says so.tests/docs_consistency.rs— a document may not state a version the crate does not have, a stated MSRV must be the one Cargo pins, and the README’s numbers are re-derived rather than trusted.- Journeys J20–J25: the wizard,
doctor,smokeand the first run, verified on a keyless machine with no container runtime — including one that forces the worktree backend so that path is exercised even where containers exist. sandbox::detect_container_runtime— one probe, so the wizard,doctorand the runner can no longer disagree about what a machine can run.
Changed
tests/claims.rscovers the documentation site,niki.example.toml, the root documents and the handover starter, by walking declared roots rather than listing three filenames. It is what stoppeddocs/launch-audit.mddescribing version 0.4.0 for six weeks and five releases.docs/launch-audit.mdrewritten against the current tree, with its version-shaped facts re-derived on every build.- The documentation site’s quickstart leads with the no-key, no-container path the README already described, instead of requiring Rust, Podman and an API key for a product that needs none of the three.
Upgrade notes
If you gate CI on verdict, this is the change that matters. A run that
produces no independent review no longer reports verdict: "Approved". If
your script is:
niki run "..." --output-format json | jq -e '.verdict == "Approved"'
it will now correctly fail for runs that were never reviewed, and pass for runs that were. To keep the old effective behaviour of “did the run produce an approved artifact”, gate on the outcome instead:
niki run "..." --output-format json | jq -e '.independently_reviewed'
task.json gains an outcome object with the same shape
({"outcome": "...", "by": "...", "verdict": "..."}). It is optional when
reading old records. verdict_source remains a plain string and is now
populated on the multi-agent path, where it was previously always None.
Config. No new keys. [security] enabled = true changes behaviour: the
auditor now runs before the Reviewer and a Rejected verdict from it gates
the run. [permissions] mode gates network egress in the tool loop; use
dontask to keep the previous permissive behaviour.
Platform. Windows can now use the container backend. The new path is
unverified by compilation here — cross-checking to x86_64-pc-windows-msvc
needs MSVC tooling this machine does not have. Each bollard API was checked
against the vendored 0.18.1 source, but a Windows CI job is the real gate and
should be added.
Install niki 0.9.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/RavaniRoshan/niki/releases/download/v0.9.0/niki-installer.sh | sh
Install prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/RavaniRoshan/niki/releases/download/v0.9.0/niki-installer.ps1 | iex"
Download niki 0.9.0
| File | Platform | Checksum |
|---|---|---|
| niki-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| niki-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| niki-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| niki-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| niki-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |