v0.115.0 #1265
v0.115.0
#1265
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
What
Added
nika check --jsoncarries one-obvious-way lints.hints[]rows with
kind: "one-obvious-way"andcode: "one-obvious-way/NNN"(advice starts with the rule id), the same door native-first already
had. Warnings, never errors —
cleanstays true.release.ymlbuilds--features local-infer,access-harness. The160 KB
nika-harnesscrate has been on main, API-frozen, since2026-08-08, and was compiled out of every downloadable binary.
agent:tasks can sit on a detected harness via--access <seat>once that tag ships. Infer-grade harness (P4 ·
infer:on a seat)stays parked.
crates/nika-acpstays a quarantined workspace (theofficial SDK's
preserve_ordermust never unify into the engine);Diamond CI now runs its five batteries against
nika-harnessovera process boundary.
metalstays off (candle 0.10 kernel dies atfirst token).
infer:tasks can use a proved Codex subscription seat. Theadapter runs
codex exec --jsonwith schema enforcement, rejects extraturns or implicit tools, and records subscription quota without inventing
a token count, USD price, or responding-model claim.
pack gained a manifest-driven family gate (
pack_examples_family): eachexample is launched through
nika try(or a stagednika runfor thehuman-gated ones) under the offline mock seat, hermetically. Skips are
explicit and counted — the seatbelt-confined git/cargo examples run as
known-fail legs that fail the gate the day they heal.
Changed
capture: structuredno longer turns a jail EPERM into a green run(capture: structured turns a permits-boundary denial into a green run #1068). A confinement denial (
cat: …: Operation not permitted/Permission denied, asandbox-exec:/bwrap:line, status 126) isNIKA-SEC-001in every capture mode. A program's own non-zero staysdata under
structured. Seatbelt and bwrap share the same stderr tableso Linux cannot re-open the hole the macOS path already closed.
for_eachfailure names the item. The parent note and theerror message carry the compact item identity (and which values
recovered when
on_error: skipnulled a slot). The trace no longerrecords only a count.
GET /v1/openapi.jsonnames400, 408, 409, 413, 415, 503, and 507 on
POST /v1/jobsnext to200/202/401/422.
info.descriptionnames thatPOST /v1/runisabsent.
GET /v1/jobs/{id}/statusstays{status}only; diagnosislives on
GET /v1/jobs/{id}and SSE.GET /v1/jobs/{id}grows anoptional
{error:{code,message}}onfailed. SSE settled/refusedframes may carry the same redacted pair. Paths and secret-shaped
fields stay dropped. Succeeded jobs omit
error./v1/jobs422 names the capture diagnosis. A parse-fatal orcheck-fatal world returns
{error:{code,message}}with the NIKA codewhen the engine stamped one, including analysis codes (AUTH/SEC)
that live on
finding.code. Symlink and other capture refuses stayadmission_refused. Paths stay dropped.ServerError::Credentialnow namesunreadable, not a regular file, insecure mode, or invalid material.
nika serve --bindstill prints the openssl mint and never echoesbytes. Missing
--token-fileis unchanged. Paths stay dropped.nika doctoruses the same token-file policy asnika serve. Ashort, non-graphic, or symlink
.nika/serve.tokenis a fail with theopenssl mint, not an owner-only green. Group/world-readable still
names
chmod 600. The row stays silent when the file is absent.nika serve --bindprints the token mint instead of an opaquecredential refusal. Missing
--token-file, a short secret, or agroup/world-readable file all teach
umask 077 && openssl rand -hex 24 > .nika/serve.token && chmod 600 .nika/serve.tokenand never echo the bytes. The HTTP door stillrefuses to mint a secret on its own.
GET /health, authenticatedGET /v1/openapi.json, andPOST /v1/jobswith Bearer plus Idempotency-Key. A non-loopbackbind adds that the blast radius is every workflow in
--workflows.GET /healthJSON stays the ADR-117 identity allowlist.install.shresolves thelatest published binary, which lags the tagged tree until release.yml
finishes. The smoke workflow now lives outside the checkout so project
discovery cannot walk into this engine's
nika.yaml. An additiveworking-directoryinput on the composite action is the seam.localrow counts what is supported, not what is here. Under a« this machine » header it listed
ollama · lmstudio · llamacpp · localai · vllmwith nothing listening on any of them — the names arewhat the BINARY supports, the header promises what the MACHINE has. The
row now says how many keyless engines exist and names the probe
(
nika doctor --ping).nika catalogstill names them, where namingthem is true.
check --fixkeeps the filed kill --fix: son contrat de surete est faux (29/37 corrompus avec « repair applied ») · 1023 lignes pour des flag-days #905 fixture YAML-parseable. Theround judge already rolled back a broken write; the under-indented
description: |body (29 of 37 files in the original wave) is now aunit of that judge, and
nika check --fixon the 7-line repro cannotannounce a repair and leave PARSE-001. A repair that cannot re-parse
still refuses; the file on disk stays YAML.
permits.fs~/and$HOME/grants expand at run. A confinedexeccan name~/.gitconfiginstead of one laptop's/Users/…path, so the grant that makes
git(orpython3, or any tool thatreads its own installed state) work is one a shared workflow can
commit. Expansion uses the operator
HOME;~userand anothertree stay out, and
permits.exec: ["git"]still does not grant thehomedir — the author names
~/.gitconfig.check, before the spend.nika new chainlaid itsSLOTmarkers as YAML comments, which theparser drops by construction — so
checkmentioned none of them, therun exited 0, and
output.mdwas left holding the scaffold's ownprompt echoed back by a mock: a file that outlives the terminal line
explaining it. A comment cannot refuse, so the marker descends into
the VALUE (
<SLOT: what should the model do with the gathered text?>)where the parser sees it. Seven shipped skeletons now carry one, and
checknames each unfilled slot with its line and closes on onepasteable command. The refusal is worded as a step, not a fault —
not a workflow yet — 1 slot to fill, then it audits— becausesomeone who just typed
nika newdid nothing wrong. Fill it and thesame file audits clean and runs; that second half is pinned too, so
the refusal can never widen into a wall.
checknames aninfer:/agent:task nomodel:reaches. TheMODELS rung answered for a model it could not resolve and said nothing
at all for a model that was never declared — an empty requirement list
is the same shape as a workflow with no inference tasks. The rung now
names the task, and the author reads it before the run instead of
meeting the omission as a provider error.
nika doctorstops calling price rows models (doctor counts 633 pricing rules as models; catalog lists 69 #1179). The pricingline formatted the vendored snapshot's rule count as « 633 models »,
one command away from
nika catalog's « 69 models » — two inventoriesunder one word, an order of magnitude apart, both on a first session.
It now reads
633 price rules · 10 providers priced. Same numbers,each naming the facet it counts: several patterns can price one model,
and a pattern can price models this catalog never lists.
nika catalogships 38 vendors and this binary carries adapters for 16;until now the human list, the
catalog_version: 1JSON and the MCPnika_catalogtool rendered a reachable and an unreachable vendoridentically, so the fact only arrived after the choice, at the MODELS
rung. Every provider row now carries
resolves, derived fromnika_providers::CANONICAL_IDSand never typed: the JSON emits thefield (additive within v1 — no envelope bump), the human column reads
catalog only — does not resolve in this build, and the MCP tooldescription tells an agent to pick only from rows where it is true.
The word is the refusal rung's own — « wired » already names a
different set (the run registry,
mockexcluded) one line above, anda synonym there would re-create the confusion
machine_truth.rsexists to cure.
nativeno longer invitesnika model pullinto adoor that refuses at
check. One traversal (catalog_family.rs, therustCI job) holds all 38 rows to the same answer across bothprojections and the MODELS rung, and is the
proven_byfor their 38ledger rows.
nika --jsonanswers the same next step as the terminal. Thescreen's one next step is now computed once and rendered by every
projection: the
Next:block, the new top-levelnextfield,start(a one-element array where it used to carry the pre-cascadetriple
nika try 01-hello·nika init·nika new), and everyinference_choice.rungs[].next. Those rungs hardcodednika new hello, so a harness reading them in a directory that already heldthe file was told to run a command that refuses on
--force. The onederivation carries the audited laws too — a red sole file yields
nika check <file>, a priced one yields the--max-cost-usdcap.nikaandnika welcomeused to print the cascade's singleNext:step and,eleven lines below it, the
start heremenu that step had replaced —two different doors for a stranger to choose between, and
nika initoffered twice. The menu is gone; the body keeps
this machine,this binary, the sample block and thelearn:line. The cascade'shardware row now reads
this hardware · N GBand the local rung nolonger repeats the RAM, so the words
this machinename one thing onthe screen instead of three. 43 lines down to 38.
row said
gate: an edge did not admit— no edge, no upstream, nooutcome — while the trace already carried
blocked_by. It now readsblocked · <task> settled <outcome> · no binding here admits that, sothe common case is legible: a
tasks.X.errorbinding admits a failureor a skip, and a task that SUCCEEDED has no error to read.
L0
nika-dataflowcrate, sonika-runtimeisnot paying its 15k wall in doc comments.
nika-runtimesat at 98.6% ofthe production-LOC cap, and a written pull request could not land because
branch and
mainwere each under it while their merge crossed. That PRhad already compressed three explanatory doc blocks to one-liners and
still did not fit: a comment is the cheapest line to delete, so it is the
first line deleted, and the budget gets satisfied by removing exactly the
thing that makes a crate maintainable.
TaskRecordand the${{ }}/cel-subset/0.1/ jq evaluation over it are one concept and are now onecrate — 14,787 → 14,053 production LOC under the corrected gate on
current
main. Every historicalnika_runtime::path still resolves, and the four evaluation errorclasses keep their historical public constructors and match arms, plus
their wire codes (
NIKA-VAR-001·-002·-004·-005·-006),byte for byte.
The L0 jq seam refuses ambient clock/timezone, host diagnostics, and
process-control natives, including a typed-error fallback that prevents a
jq halt exception from exiting the engine. Its newly public scope and
record DTOs also follow FCI-002: authority fields are private behind safe
constructors and the public dataflow types are non-exhaustive.
nika cataloggroups what runs, instead of tagging what does not.Marking each unreachable vendor was correct and unreadable: 22 of 38
rows repeated the same suffix, so the fact a reader wanted had to be
found by scanning for its ABSENCE. LOCAL and CLOUD now hold only the
16 providers this build resolves, in the same doctrine order; the rest
sit under one new head,
CATALOG ONLY (no adapter in this build · nika check refuses these). The group is the marker, said once.nativeleaves the « zero key · zero network » banner it could never honour.
The
--jsonand MCP projections are unchanged — a machine filters onresolves, it does not read headings.✔ MODELSgreen line no longer reads as « the run will reachthe model ». The rung judges resolution in this binary — never key
presence on this machine — and the line now says so: « key presence on
this machine not judged (advisory: check --json access_plan · the run
gate refuses NIKA-INFER-001) ». The access truth existed but was
advisory-only (
check --json,explain); the sentence now names thatdeferral instead of letting a green claim cover it. What the gate
covers is unchanged.
Fixed
nika notes.nika.yamlused toanswer
unrecognized subcommand— a wall where a run was meant. Acolleague sends a
.nika.yamland the person types its name the way oneopens a file. The routing is narrow on purpose: the name must end in the
workflow suffix AND be a file on disk, so a typo'd verb keeps clap's
did-you-mean and a suffix naming nothing keeps clap's error.
of racing. Three concurrent
lefthook run pre-pushprocesses over one30G
target/turned three unrelated ratchets red, none of whichreproduces on an idle machine. Two were timeouts; the third,
credential-headers, reported a credential leak that does not exist(the header it named is listed, and the gate reads that file directly —
6/6 green idle). A security vector that cries wolf trains the next reader
to wave it through. Measured the same evening: one gate takes 210-620s,
three in parallel took 1958s and all three failed. The gate now takes an
exclusive lease (
mkdir, the one atomic primitive macOS and the Ubunturunners agree on) whose holder records its pid so a dead owner's lease is
reclaimed rather than wedging every later push.
NIKA_GATE_NO_LOCK=1optsout, and says what that re-enables.
that serialises the pre-push gate, with a self-test proving mutual
exclusion, stale reclaim, and that a live owner is never robbed — and
nothing ran it. Not CI, not even pre-push, because a hook helper has no
runner. It was a green that had never been asked a question. The
issue-proofgate, shipped in the same merge, reopened The pre-push gate is not safe to run concurrently — three unrelated vectors flaked under load today #1064 withinminutes for exactly that: a close whose proof does not exist. The first
thing it caught was its own author.
gate-lockjoins the ratchet matrixbeside
credential-headersandchangelog-fragments, which are there forthe same reason one degree less severe, and it fails closed when the
self-test is missing rather than reading an absent file as a pass.
nika lspwatches the host it was told to watch.--clientProcessIdwas accepted by clap and discarded at the call site, and the
initializerequest's own
processIdwas never read either, so a client that diedwithout closing the pipe left the language server running forever — the
ordinary path hid it, because a clean shutdown closes stdin and the server
exits on EOF. Both channels now feed one watchdog (argv wins, the payload
covers hosts that send only that), and the server exits when its declared
parent goes, per LSP
initialize§processId.run in four.
set_current_diris process-global andcargo test --libruns a crate's tests as parallel threads in one process.
nika-clihadthree independent guards — one private to the budget tests, one private to
arm fire— and a third chdir site,run --example, with no guard atall. Three private mutexes over one global resource do not compose: a test
could hold its own lock for its whole body and still have the ground moved
under it. Measured across four CI runs of effectively identical trees (two
byte-identical),
access-harness testsfailed once, always on the samethree
verbs::check::budget::tests— the ones whose assertions derivefrom the current directory. Every site now takes
crate::cwd, and therace is pinned by a test that runs a chdir storm against a lease holder.
An intermittent red teaches everyone to press the button again, and that
habit is how a real red gets waved through.
issue-proofjudges every close, instead of only the ones the filerarmed it for. The gate exists to refuse the lying checkmark GitHub
prints when a merge auto-closes an issue — but its trigger required the
filer to have written
proven_by:in the body, or someone to haveremembered a label. So an issue filed before the gate existed could never
opt in, and the merge that closed it is precisely the event the gate was
built for. Measured one minute apart on one issue:
skipped, thensuccessonce the string was added. The condition is not widened, it isdeleted — a guard that cannot be false cannot drift. The honest
non-capability close now opts out through the closer's own act (GitHub's
not_planned/duplicate, or ano-capability/question/invalidlabel), which is fixture-pinned in
scripts/ci/test-issue-proof.sh.wontfixdeliberately does not waive: it is added at filing time, andhonouring it would hand the opt-out straight back to the filer.
issue-proofcan now read an issue that talks aboutissue-proof. Thebody parser took the first line containing
proven_by:anywhere, so issue-proof is opt-in by the filer, so the closes most likely to lie are the ones it skips #1200— the issue about this very gate — could never be closed: its body quotes
the old guard,
contains(github.event.issue.body, 'proven_by:'), inside acode fence, and the parser extracted
)as the job name and reopened theissue. Measured, not reasoned:
JOB=[)]. An issue that discusses the proofmechanism is exactly the issue most likely to report a defect in it, so the
parser has to be able to read its own subject matter. A trailer is now a
line that is only the trailer, and the last one wins — which steps past a
fenced quote, an inline backticked mention, and a prose line that happens to
wrap onto column 0.
.agents/**is classified as the vendored copy it is. The estate called it the authored source « mirrored downstream into nika-plugins » — the exact inverse of the clients-resync lane, which re-vendors.agents/from the plugins SSOT with--delete. The misclassification is what let two merged pull requests live only here, until the lane proposed reverting them and rolling four manifests back from 0.114.0 to 0.111.0. The rule now names the lane, the SSOT, and the gate that judges the heal.check-doc-private-itemsno longer files a broken toolchain as documentation drift. The « cargo could not answer » branch tested the line prefix, but a tool abandoning printserror: could not document …— a prefix match — so the branch could never fire in exactly the situation it described (measured: a full disk undertarget/doc-checknamed crates nobody had touched, on a clean tree). The verdict now discriminates on the failure mode: incapacity verbs route to « the tool failed to run », and only when no genuine lint diagnostic stands beside them.errornow has a proven reader.EdgeKind::FailureObservationadmits{failure, skipped}, andskippedis in that set solely so a downstreamtasks.X.errorbinding has a consumer — but only the record was pinned, never the admission. The new runtime test drives a real consumer across the seam and asserts all three legs: the consumer is admitted (success, never cancelled), the bound value is the typed record with its code intact in the exact command bytes, and the same consumer against a succeeded producer settles cancelled, so the pass-set cannot silently narrow OR widen again.git -Cscopes the directory, never the index: an inheritedGIT_INDEX_FILE(orGIT_DIR/GIT_WORK_TREE) pointed its fixture commits at the caller's branch while the test printed PASS. The test now sanitizes the git environment at entry and proves on exit, from its trap, that the caller's HEAD and worktree are byte-identical to what they were.nika checknow refuses athinking:block the run would reject orignore. The parser validated each field's type and stopped: a
budget_tokensat or overmax_tokensaudited green, then the providerrefused the call (the reasoning share lives inside the cap), and
thinking: { enabled: true }on a seat the catalog knows cannot reasonwas a dead declaration the wire dropped or 400'd. Both are MODELS-rung
findings now — the seat check fires only when the catalog positively
knows the exact model (a provider row or an exact pricing pattern), so a
newly shipped model the dated snapshot has not met keeps its benefit of
the doubt, and a templated
model:is judged through its declareddefault or left to the run.
nika checkno longer greens a plain YAML number or boolean where thespec says string.
prompt: 123rode through the parser because a plainscalar's source text restringifies silently — the field audited clean on a
value whose type the author never checked. Every string-typed verb field
(infer/agent
prompt·system·model, execshell·cwd·stdin,commandelements,envvalues, agenttools/skillsentries, invoketool/workflow) now refuses a plain scalar that resolves as anint/float/bool and teaches the quoted form (
"123"). The YAML 1.1 aliasesyes/nostay strings (onlytrue/falseare booleans in thisdialect), and
${{ }}templates are untouched. A sweep test walks theclosed verb key sets against a checked-in field→type table, so a new verb
field without a refusing fixture fails the suite.
after: { x: unwind }cleanup that failed — a permit refusal, a non-zero exit, a timeout, even
a closed
when:gate — used to vanish without a frame: the trace readpixel-identical to a cleanup that never fired. The lane still never
propagates (best-effort by spec), but its outcome is journaled now: one
permit_checkedframe on planeon_finallyper skipped, failed, ortimed-out cleanup, naming the error code (
NIKA-SEC-004and friends) —spec 03's « its errors are logged » guarantee, kept.
fix, and the refusal tail. Three surfaces each read a different source of
access truth.
welcomeanddoctor --jsonclassified a machine with asigned-in harness seat as
installed · no inference path(the probe filledfrom provider rows only, the seat rows one call away and never joined in);
the doctor per-seat fix taught
install: @zed-industries/claude-agent-acp@…— the wrapper id
run --accessrefuses as retired (NIKA-1802) — and theNIKA-1800/INFER-001 witnesses never named the seat escape hatch nor the
credential's custody. The census (
nika_providers::census::AccessCensus)joins provider rows and harness seats in ONE read; the adoption ladder gains
the
seat_readyrung betweenkey_presentandreal_ready; the cascadenames seats by their live pin token (
claude-code, not the wrapper id);doctor's seat fixes name
--access <seat>plus the gesture; the admissionwitness says
<VAR> unset in process env; and the L4 refusal render plusnika explainappend « or use a signed-in seat:--access …» — printedonly when this machine actually has one.
concurrent_run_rooms_are_serialized_and_restore_the_callerobservedthe process-global cwd twice — the baseline and the restoration —
without holding
cwd::hold(), so it measured whichever siblinghappened to own the process. It went red on three consecutive
maincommits with
left=cwd::tests's chdir-storm room, while thistest's own restore had already put the process back. The production
path was never at fault:
enter_roomtakes the crate lease and ridesfchdirexactly ascwd.rsdocuments. Both reads now ride the lease,which is what makes the claim well-defined. Reproduced 2/20 by running
the test WITH the storm; 40/40 after (the full suite hides it — 12/12
green either way, which is why the red only ever surfaced in CI).
It said "16 runnable —
nika doctornames them". Measured on avirgin machine: plain
nika doctornames five — the local line — andfolds the ten cloud rows into « 10 providers unconfigured »;
mockgets no row at all. A stuck user was sent to a surface that named five
of the sixteen, and only
--verboseimproved on it. Both refusals nowpoint at
nika catalog, which groups all sixteen under LOCAL andCLOUD, and the unknown-provider one adds where the id they typed sits
(
CATALOG ONLY). The claim is pinned per run:the_named_surface_actually_names_themcounts the rows the listingrenders before that head and refuses any drift from
CANONICAL_IDS.len().directions at once. It guards a maintainability budget, so every line
it mis-charges gets paid for in deleted doc comments — which is how nika-runtime is 6 LOC from the 15k wall, and the wall is being paid in doc comments #1203
surfaced it. Three defects: braces inside string literals ended a
#[cfg(test)]module early (412 lines of test body charged to productionin one file),
#[cfg(test)] mod foo;— an attribute on a declaration —swallowed whichever block came next and hid production lines, and a
phantom trailing line was charged once per file.
nika-displaywasover-charged 846 lines,
nika-cap733;nika-clihad 31 production lineshidden,
nika-runtime65. The awk filter in_lib.shhad already learnedthe string-literal rule in August; three python copies never did. The
counter now lives in one proven file (
scripts/ci/prod-loc.py) thatrefuses to render a verdict unless its own fixtures pass, and the gate
fails closed if they are missing. Corrected, the crate nearest the wall is
nika-checkat 14,697.Security
--resumeof a trace thatalready journaled a
nika:promptsuccess now refuses--answeronthat same task (environment class). A recorded NO cannot be flipped
into a shipment. A paused gate (no
task_completedyet) stillaccepts
--answer. Measured againstnika 0.114.0 (80d62b8f8)onhuman-gated-ship.NIKA_SANDBOXunset (
auto) plus apermits:workflow plus no OS jail is NIKA-1710,not a composed Noop run. The composition gate is tested with a Noop
decision so deleting the refuse arm cannot hide behind a host that
ships Seatbelt. Doctor display of the row stays S3.
agent:is still a named flow (a sanctioned secret makes three lanes assert the negation of the file, and JOURNEY is blind to agent tool grants #1041).JOURNEY takes the agent's
tools:intersected withpermits.net.httpas the destination set. SECRETS stops saying « no declared secret
reaches an effect » after the author applied the tool's own
egress:advice. A sanction authorizes a flow; it does not deny the flow exists.
signed-in Claude Code no longer reads as a runnable harness seat when
claude-agent-acp— a different npm package, the binary a sessionspawns — is not on PATH.
readynow needs three things, not two: theapp is here, it is signed in, and its ACP adapter answers. The row
stays visible and names the one command that closes the gap
(
npm i -g @zed-industries/claude-agent-acp); hiding an app the personhas would trade one lie for another.
doctor --jsonstops reportingready: trueandchosen_access: claude-agent-acpon a machine whereno such binary exists — an agent reads that field and acts on it.
Install
Tarballs below: macOS arm64 / x64 · Linux x64 / arm64, plus
SHA256SUMS.Verify: three independent proofs
Provenance
Built from tag
v0.115.0byrelease.ymlon GitHub-hosted runners. Provenance is published twice: GitHub's native
build attestation (proof 2) and the SLSA generator's
multiple.intoto.jsonlrelease asset (proof 3). The release itself is a claim on the
machine-verified timeline: https://nika.sh/timeline
What's Changed
Full Changelog: v0.114.0...v0.115.0
This discussion was created from the release v0.115.0.
All reactions