v0.112.0
What
The instrument-honesty release. Three features and sixteen fixes, several
of which began as a measurement disagreeing with what a surface said. A cargo
test binary can no longer open the OS keychain — an ACL is bound to the
requesting binary, and a test binary's hash changes on every compile, so the
prompt could never be answered once and for all. nika check's JOURNEY rung
counts model endpoints instead of tasks, ending a card that contradicted
itself four lines apart: COST read no infer/agent tasks while JOURNEY read
3 model endpoints. A fan-out that recovered now says so in its record and
not only in its prose, which is what spec 13 requires of the pair. And the
exec: fit lane gained an fs arm, so a leg jailed away from its own script
is refused at check instead of exiting 126 under a green card.
Added
nika listnames every workflow below the current directory. Output is
stable and root-relative, nested workflows are included, project metadata
and hidden/build directories stay out, and an incomplete walk refuses
instead of presenting a partial inventory as exhaustive.- Bare
nikaon a terminal opens one continuous thread. Model turns
stream through the existingagent:runtime;/workflowposts a workflow
into the thread,/runexecutes it there, and Ctrl-C interrupts the active
turn without closing the outer conversation.
Removed
- BREAKING —
VirtualClockloses its dead time-mover (nika-clock).
VirtualClock::advanceandVirtualClock::elapsed_totalare removed:
advancedocumented itself as « the ONLY mover of virtual time » while
having zero production callers — virtual time never moved, so under
run: { clock: virtual }(orentropy: none | seeded(N), which imply
it) a tasktimeout:budget raced an instantly-ready timer and every
deadline was already settled at dispatch. The clock is now honestly
FROZEN by construction (Copybases, no shared offset): an author who
needs a real deadline honored against real work must not declare
clock: virtual. Wiring the tasktimeout:budget to the exec
runner's own deadline (linger: false) is a follow-up wave. - The exec runner's process-group kill is removed
(nika-exec-runner).terminate_group(SIGTERM→SIGKILL the whole
process group) plus theprocess_group(0)spawn setup were correct
and tested but unreachable in production — they fired only on
TimedOut/Cancelled, and the engine never assigns
ShellCommand.timeoutnor invokescancel. Cancellation stays
kill_on_drop(INV-011 · future-drop, the ADR-016 primary); detached
grandchildren are no longer group-killed on the embedder-facing
timeout/cancel arms, and thenixdependency goes with them.
Changed
- BREAKING — the event error codes leave the Shield reservation
(nika-event).NIKA_420/421/422(serialize failed · buffer full ·
lock poisoned) were minted inside the locked Shield band (380-429), so
a full event buffer surfaced as « Shield security policy blocked the
operation » — a refusal its reader would read as security. They are
renumbered into their own Observability band (800-819) as
NIKA_801/802/803, constants renamed with them.
Fixed
-
A cargo test binary no longer opens the OS keychain. A macOS keychain
ACL is bound to the requesting BINARY, and a test binary is
target/<profile>/deps/<name>-<hash>whose hash changes on every
recompile — so "Always Allow" grants a binary that will never exist
again and the prompt returns forever, on every worktree, with no
operator-side gesture that stops it.NIKA_KEYCHAIN=offnow skips the
custody, and a test binary skips it by default whether or not anyone
remembers the flag. An installednikaandcargo runland outside
deps/and keep their custody unchanged. All eight keyring call sites
sit behind the flag, held there by a ratchet that walks the crate's own
source. -
The JOURNEY rung counts model endpoints, not tasks. The envelope
model:is a fallback for a task that HAS a model · it was applied to
every task, so a body of builtin invokes read3 model endpointswhile
the COST rung four lines above readno infer/agent tasks— one card
contradicting itself.model_endpoint_ofalready typed the task and
threw the answer away on the next line. Across the shipped corpus 45 of
99 cards carried an inflated count; each is now exactly the number of
infer:/agent:tasks the file declares, and no verdict changed. -
nika checkjudges every secret sharing one effect independently. A
sink that referenced two secrets previously retained only the first IFC
trace, so clearing that first edge could hide an uncleared second edge.
Direct references and task-localwith:/for_eachitem aliases now
produce one consent verdict per distinct secret while the existing
singular output-propagation trace stays unchanged and bounded. Literal
for_each.itemssecret references also appear in the data journey. -
nika checknow judges the script anexec:interpreter must open.
The runtime jails everyexec:child to the declaredpermits.fsset, so
exec: ["bash", "leg.sh"]with nofs.readgrant could never open its
own script — measured on seatbelt, the leg exits 126 with empty
stdout. The audit was ✔ on all fourteen lanes and the run rendered it
✔ legwith rc 0: a leg that did nothing, reported as a success. The
exec:fit lane gained an fs arm beside its net arm (which shipped
2026-07-29 for the same sentence one boundary over), so the escape is a
NIKA-SEC-004finding at check with the one-line repair. The claim is
narrow on purpose — only a literal argv whose program is an interpreter,
on its script positional, through a literalcwd:; everything else stays
the runtime's verdict, and the verdict models the jail (which binds a
grant's literal prefix and never globs) rather than the stricter lexical
walk.--infer-permitslearned the same fact in the same change, so the
boundary it writes cannot self-refuse the workflow it came from. Swept
over the shipped corpus: 63 of 63 unchanged. -
The TRIFECTA tick is derived, so it cannot outlive the gate that
bought it. Measured on 0.111.0, one card printed✔ TRIFECTA … without a human gatefour lines above twoNIKA-SEC-014rows proving that same
gate lets the effect fire on 'no'; a control run with the prompt deleted
raisedNIKA-SEC-009, so the trifecta was complete and the tick was
bought entirely by a rubber stamp. The lane credited a blocking prompt
(one task, one key lookup) while the consent lane ran the full
refusal-substitution walk — and the clearance discarded WHICH gate it
credited, so a trifecta cleared by a gate and one cleared by a missing
leg were the same empty vec. The clearance now publishes its credit and
the rung withholds the tick where another lane refutes it, pointing at
the code that owns the repair. No second finding: the consent row
already names the defect and teaches the fix. -
The
nika-errorcrate-spec band table matches the one-voice
registry. It still read330-379 Binding/template · 380-429 Provider
while the registry moved Provider to 330-379 (2026-05-11) and reserves
380-429 for Shield. The stale rows invited exactly the collision the
reservation exists to prevent.
Install
brew install supernovae-st/tap/nika # macOS · Linux
curl -LsSf https://nika.sh/install.sh | sh # script install
docker run --rm ghcr.io/supernovae-st/nika:0.112.0 --versionTarballs below: macOS arm64 / x64 · Linux x64 / arm64, plus SHA256SUMS.
Verify: three independent proofs
# 1 · checksum: the bytes you hold are the bytes CI hashed
sha256sum -c SHA256SUMS --ignore-missing # macOS: shasum -a 256 -c
# 2 · attestation: GitHub-signed build provenance for this exact artifact
gh attestation verify nika-<platform>-0.112.0.tar.gz --repo supernovae-st/nika
# 3 · SLSA provenance: the intoto asset, verifiable offline
slsa-verifier verify-artifact nika-<platform>-0.112.0.tar.gz \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/supernovae-st/nika --source-tag v0.112.0Provenance
Built from tag v0.112.0 by release.yml
on GitHub-hosted runners. Provenance is published twice: GitHub's native
build attestation (proof 2) and the SLSA generator's multiple.intoto.jsonl
release asset (proof 3). The release itself is a claim on the
machine-verified timeline: https://nika.sh/timeline
What's Changed
- feat(pack): tts-self lesson pays the last showcase debt by @ThibautMelen in #1021
- docs(skill): nika inspect is live — strike inspect-unwired by @ThibautMelen in #1023
- fix(nika-clock): nuke the two motor lies — frozen virtual time + dead group kill by @ThibautMelen in #1017
- fix(check): jq-as-map admits a map piped from any bound name by @ThibautMelen in #1024
- feat(cli): open the nika workflow door by @ThibautMelen in #1027
- test(cli): cover list binary end to end by @ThibautMelen in #1031
- feat(cli): show arm in default help by @ThibautMelen in #1030
- test(cli): prove bare tty thread end to end by @ThibautMelen in #1033
- chore(deny): refuse the typosquat, keep the version whose deps are empty by @ThibautMelen in #1035
- fix(authoring): make discovery precede creation by @ThibautMelen in #1036
- fix(authoring): keep local guidance honest by @ThibautMelen in #1044
- fix(authoring): keep intent and thread guidance exact by @ThibautMelen in #1052
- fix(cap): a glob declares its walk root, so check stops greening a file the run kills by @ThibautMelen in #1028
- fix(types): a retired spelling is told what replaced it by @ThibautMelen in #1059
- fix(schema): a task key from another dialect, or the wrong depth, teaches by @ThibautMelen in #1049
- fix(nika-check): name mcp secret flows by @ThibautMelen in #1061
- fix(check): the exec fit judges the script its interpreter must open by @ThibautMelen in #1048
- fix(authoring): the fan-out block teaches its own nesting by @ThibautMelen in #1043
- fix(check): a prompt default is a consent decision, not a formatting detail by @ThibautMelen in #1056
- fix(check): the trifecta tick derives, so it cannot outlive its gate by @ThibautMelen in #1054
- fix(dap): a test binary cannot open the OS keychain by @ThibautMelen in #1072
- fix(runtime): a recovered fan-out says so in its record, not only in prose by @ThibautMelen in #1058
- fix(check): the JOURNEY rung counts model endpoints, not tasks by @ThibautMelen in #1076
- fix(dap): a stripped chain header is a BREAK, not a pre-0.96 era by @ThibautMelen in #1078
- docs+test: drop internal branch topology, and stop naming a real private path in a fixture by @ThibautMelen in #1082
- fix(nika-check): judge every secret at a shared sink by @ThibautMelen in #1084
- fix(estate): the manifest is proven at the tag, not at every commit by @ThibautMelen in #1085
- chore(release): 0.112.0 by @ThibautMelen in #1081
Full Changelog: v0.111.0...v0.112.0