CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to **Nika** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Nika follows [semantic versioning](https://semver.org); 0.124 is the current release line, focused on maintenance, security and the media capabilities workflows need, with native video planned for 0.125 ([ROADMAP.md](ROADMAP.md)).
Nika Diamond is the ground-up rewrite developed on `main`, beginning at
v0.80.0. The legacy v0.79.3 tree remains on the read-only `brouillon` branch.
---
## [Unreleased]
One file per change under [`changelog.d/`](changelog.d/), assembled into the
section below at tag time (`bash scripts/release/changelog-assemble.sh --fold
<version>`). Do not write bullets here: this file is where four concurrent
pull requests collided on 2026-08-24 with no source overlap between them, and
`--check` refuses a hand-written bullet in this section.
## [0.124.3](https://github.com/supernovae-st/nika/compare/v0.124.2..v0.124.3) - 2026-11-10
Nika 0.124.3 lets an application ask what a workflow's declared ACP route offers before anything
runs. On `nika serve`, `GET /v1/access-offers/{name}` opens the sessions a run of that file would
open, selects the file's model in each, reads the models, the exact reasoning efforts and the
service tiers the route lists for that model, and ends without a prompt. The answer says what was
read and when, and it authorizes nothing: a run still selects and reads back on its own session.
Measured on one codex seat with codex-acp 1.13.1 on 2026-11-10, with a debug build of the engine:
reading the `infer:` session of a file took 4.1 s and 5.3 s, reading an `agent:` session took 1.3 s,
and a second request within the minute returned the kept answer in 0.02 s. For
`openai/gpt-6-luna` that seat listed the efforts `low`, `medium`, `high`, `xhigh` and `max` and the
tiers `standard` and `fast`; for `openai/gpt-6.1-sol` it confirmed the model and listed no effort
and no tier. These are observations of one seat on one day, not a description of the route. The
language and the SDK protocol are unchanged; an engine that answers this request lists
`accessSessionOffer` in `supportedCapabilities`.
### Added
- **`nika serve` answers what a workflow's declared ACP route offers,
without a prompt.** `GET /v1/access-offers/{name}` (capability
`accessSessionOffer` on `/health`) admits a served workflow by name, reads
its `run.access`, and, when the file selects ACP sessions (`via` under
`protocol: acp`), opens one session per shape its tasks run in and per
model they resolve to: the route's one-shot completion profile for
`infer:` tasks, its agent session for `agent:` tasks. Each session is
opened as a run opens it, selects the file's model for that session only,
lists the models, the exact reasoning efforts and the service tiers the
route has for that model, and ends: no effort, tier or mode is set and the
path holds no `session/prompt`, so an offer uses no turn. The answer is a
`nika/access-offer@1` document: the file's declared route, then one row
per session with `observed_at` and `fresh_until`. A model the session does
not list is a row state, and its efforts and tiers are then left
`unobserved` instead of being read from another model. A session that
gives no offer is a row with the code a run would meet (`NIKA-1803`,
`NIKA-1804`, `NIKA-1805`). Rows are kept 60 seconds (15 when unavailable),
one reading runs at a time, a ninth concurrent request answers `503
access_offer_busy`, and every listed value reaches the client as a bounded
token. The request names a served workflow and nothing else, and the
answer authorizes no run: `POST /v1/jobs` still selects, reads back and
refuses on its own session. In the library, `nika_harness::session_offer`
reads one session and `nika_service_execution::offer` names the sessions
of a workflow.
## [0.124.2](https://github.com/supernovae-st/nika/compare/v0.124.1..v0.124.2) - 2026-11-10
Nika 0.124.2 lets a workflow ask its route for a service tier. `run.access.service_tier` takes
`standard` or `fast` on the codex seat over ACP (`via: codex`, `protocol: acp`): the session selects
the tier with the model and the effort and reads the selection back before its first prompt. When
the session cannot confirm the tier, the call is refused before the prompt (`NIKA-1805`), and a
route that cannot carry a tier is refused before task 1 (`NIKA-1800`). The receipt keeps what was
requested, offered, sent and read back, and says `unknown` for the tier a turn carried and the tier
that served it, because no ACP answer names them. Omitting the key changes nothing: no option is
sent, and the file keeps its resume identity and its receipt. An ACP task also starts its adapter
once instead of twice. Measured on one codex seat with codex-acp 1.13.1 on 2026-11-10: the single
start saves about 1.6 s per task; an answer of about 400 words took 12.5 s and 13.1 s on the route's
default tier and 9.3 s and 9.1 s with `fast`; one image from the agent's image tool took 15.8 s to
20.7 s on the default tier (3 runs) and 14.8 s to 15.7 s with `fast` (3 of 4 runs; the fourth ended
without an image). These are observations of one seat, not a promise of the tier. A refused seat and
a refused route declaration no longer suggest a pin the same file refuses, `nika check
--infer-permits` names the host an `exec:` URL reaches, and the embedded language pack follows
nika-spec `b4c692f`. The SDK protocol is unchanged; an engine that reads the key lists
`accessServiceTier` in `supportedCapabilities`.
### Added
- **A workflow can ask its route for a service tier.** `run.access` gains
`service_tier`, with the two words `standard` and `fast` (spec 01
§run). On the codex seat over ACP (`via: codex`, `protocol: acp`) the
session selects the model, the effort, the tier and the confinement
mode in that order, then reads the whole selection back before its
first prompt; `fast` is sent as codex-acp's `fast-mode: on` and
`standard` as `off`. The binding is held for codex-acp 1.13.1 only:
another release of the adapter, an adapter with no measured speed
control, a model whose session lists no such option, or a read-back
that does not hold the value refuses before the prompt (`NIKA-1805`).
A tier no route can carry is refused before task 1 by `nika check` and
by the run (`NIKA-1800`): an API or local route, a seat without a
measured control, and an `infer:` task outside `protocol: acp`. The
`access_selection` receipt gains a `service_tier` block only when the
file declares a tier: `requested`, `option`, `offered`, `transmitted`,
`configured` and `configured_source`, plus `turn` and `effective`,
which read `unknown` because no ACP answer names the tier a turn
carried or the tier that served it. Omitting the key is not
`standard`: nothing is sent, the route keeps its own tier, and the
file keeps its resume identity and its receipt. A resumed run whose
tier changed re-runs its `infer:` and `agent:` tasks. A child workflow
resolves its own declaration and never inherits its parent's tier.
`nika --sdk-identity` and a resident's `/health` list
`accessServiceTier` in `supportedCapabilities`: an engine without the
token refuses the key at parse, so a client requires it before it
admits a file that declares a tier. On `nika serve` the tier is a field
of the file, never of the request, and the per-call receipt stays in
the sealed journal. An editor served by an older `nika lsp` refuses the
new key and loses hover and navigation on the file until it is updated.
### Changed
- **An ACP task starts its adapter once.** For an agent application whose
identity is its `initialize` answer (codex-acp, claude-agent-acp,
opencode, copilot), every `agent:` task and every `infer:` task over ACP
used to start the adapter twice: once to ask `initialize` and stop it,
once for the session. The session's own first answer is now the one
judged, before `session/new`. A hosted codex seat saves one adapter
start per task, measured at about 1.6 s. The refusals are unchanged: an
adapter that does not answer `initialize` within 30 s, exits before
answering, answers as another program or sits outside its version pin
is unavailable (`NIKA-1803`) and is not retried. `nika doctor` and the
access rows still probe on their own.
- **The embedded language pack follows nika-spec `b4c692f`.** `SPEC_PIN`
and `crates/nika-pack/pack` advance together from `5cd95cd`, the commit
0.124.0 and 0.124.1 embed. The schema `nika spec --schema` prints, and
the one `nika check` validates against, gains
`run.access.service_tier` (`standard` · `fast`); spec 01 §run states its
rules and the coverage matrix lists its fixtures. The three status
amendments that name 0.124.0 and 0.124.1 as released ride along
(`QUICKSTART.md`, spec 00 and 07). `canon.yaml`, the examples, the
templates and the stdlib pages are unchanged.
### Fixed
- **`nika check --infer-permits` names the host an `exec:` URL reaches.**
For `exec: { command: ["curl", "https://example.com/"] }` it printed
`exec: ["curl"]` alone, and `nika check` then refused that very block
with `NIKA-SEC-004` (`exec URL host example.com is outside
permits.net.http`). A literal or `const:`-backed URL in an argv, or in a
shell line, now adds its host to `permits.net.http`, read from the same
tokens the check judges. When an HTTP client (`curl`, `wget`, `xh`,
`http`, `https`) gets only a computed argument, or a template decides a
URL's host, the block gains a review note naming the task and the
inferred net face is marked partial, instead of a block that checks
green and is refused at run. `nika check` also stops asking for
`"${{inputs.host}}"` in `permits.net.http` when a template written
without spaces fills an exec URL's host: that host is computed, and the
run judges it.
- **A refused seat no longer teaches a flag its own file refuses.** When a
seat fails at the call, the `access_refused` record on `task_failed`
names the next ready path and the flag that pins it. Under a declared
`run.access.via`, that next path could be a route the declaration
itself excludes: a machine with a ready `openai` key answered a refused
`via: codex` call with `"pin": "--access openai"`, a flag the same file
refuses before task 1. The record now names a next path only when the
declaration allows it, and names none otherwise.
- **A refused route declaration no longer suggests a pin that cannot help.**
When a run is refused before task 1 (`NIKA-1800`) and this machine has
a signed-in seat, `nika run` adds "or use a seat present on this
machine: `--access <seat>`". It also did so when the refusal was about
the file's own `run.access` declaration, for example a `service_tier`
its route cannot carry. Under a declared route, another seat is refused
by that declaration and the same seat changes nothing. The line is now
printed only when the refusal is about what the machine lacks, such as
a missing credential.
## [0.124.1](https://github.com/supernovae-st/nika/compare/v0.124.0..v0.124.1) - 2026-10-28
Nika 0.124.1 is the first maintenance release of the 0.124 line. On a direct seat route, without
`run.access.protocol: acp`, an `infer:` task with `vision:` files no longer runs without its
images: the run refuses it with `NIKA-432` before the seat starts, `nika check` blocks RUN READY
on the same code, and both name a route that carries the files; `temperature`, `max_tokens` and
`thinking` are still neither sent nor refused there. `nika run --dry-run` refuses what the run's
seat refuses. Where `nika check` said a mock rehearsal, or a file with no model route, dials
nothing, it names the tasks that can still reach the network; its estimate line adds the paid
media calls and the least they cost instead of reading `≤$0.0000`, and `nika arm` prints a cost
floor's refusal code once. `nika explain` answers the MCP pin refusals `NIKA-MCP-003` to
`NIKA-MCP-006` and leads `NIKA-PARSE-001` with the causes its refusal names, a file saved with a
byte-order mark is framed on the right line, and `nika serve` lists the files a `for_each`
fan-out saved. The workflow language, the file format and the SDK protocol are unchanged.
### Changed
- **The SDK guard reads what the published `@supernovae-st/nika` 0.124.0 closes.**
`published_sdk.json` is re-read from that package's `dist`, and every closed set of 0.123.2
is unchanged. The one runtime change in that `dist` names three more native task frames
(`task_skipped`, `task_cancelled`, `task_cache_hit`) in a mapping that reads any other frame
as an engine event, so it closes nothing new.
### Fixed
- **`nika check` no longer says a mock rehearsal dials nothing.** With
`--model mock/echo`, the ACCESS row read `mock · never dials · nothing to
judge` and a file with no `infer:` or `agent:` task read `access ready n/a
· nothing dials`, while its `nika:fetch` reached the network. The mock row
now says `this model route never dials` and, under it, names the tasks
whose own effects can still reach the network (a fetch or webhook to a
real host, an MCP tool, a paid media call), judged on the PERMITS row; the
moot line reads `access ready n/a · no model route` and names them the
same way. Both lines name three tasks and count the rest (`· +17 more`);
`--json` lists every one as `verdicts.network_tasks`, and the TUI's ACCESS
READY reason no longer says `nothing dials`.
- **A workflow saved with a byte-order mark is framed on the right line
(#1608).** The parser strips a leading UTF-8 byte-order mark, but every
span then indexed the stripped text, 3 bytes left of the file `nika check`
frames: an unknown key at column 1 was framed at the end of the line above
it (`5:24` instead of `6:1`), and the `--json` spans, trace and debugger
lines carried the same shift. Spans now index the text the caller passed
in, byte-order mark included. The frame and the browser checker's `col`
count line 1 from the character after the mark, as an editor shows it, so
a finding on line 1 keeps its column (`1:1`, as without the mark). A file
without one is unchanged.
- **A direct seat refuses an `infer:` with `vision:` instead of running it
without the images.** On a seat route without `run.access.protocol: acp`
(Codex's `codex exec`, Claude Code's `claude -p`, Grok Build, GitHub
Copilot CLI), the files were judged and read, then never sent: the command
carries the prompt, system and model (and a `schema:`, except on Copilot),
never an image. The run now refuses the task with `NIKA-432` before the
seat is spawned, as the ACP route refuses a `vision:` URL, and `nika
check` blocks RUN READY with the same code. Both name `run.access: { via:
<seat>, protocol: acp }` where that seat's ACP one-shot carries `vision:`
files and the rest of the task; a task that also sets what that session
refuses (`temperature`, `max_tokens`, `thinking`, a `vision:` URL) is
pointed at an API route, with those fields named. `temperature`,
`max_tokens` and `thinking` are still not sent on the direct route.
- **`nika run --dry-run` refuses what the run's seat refuses.** An `infer:`
task on a Codex or Claude Code seat over ACP that sets `max_tokens`,
`temperature`, an enabled `thinking` budget or a `vision:` URL, an
`agent:` with a `tools:` whitelist on a seat, and an `agent:` declaring
`images:` with no seat previewed as `dry-run · plan only`, then the run
refused them before anything was sent (`NIKA-432`, `NIKA-465`). The
preview now refuses with the same lines `nika check` blocks `run ready`
on, and exits 2 like its other file refusals.
- **`nika explain` answers the MCP pin refusals a run sends you to (NIK-254).**
A run refused with `NIKA-MCP-006` printed `fix: nika explain
NIKA-MCP-006`, and explain answered `unknown code` for it and for
`NIKA-MCP-003`, `004` and `005`, the other codes the pin store mints. Each
now says what failed and the remedy the refusal prints: re-approve a
drifted server deliberately (`nika mcp approve <server>`), restore or
re-enroll an untrusted `.nika/mcp_pins.json`, check the OS sandbox an MCP
server is confined by, approve the server a tool belongs to. The MCP
`nika_explain` tool answers the same.
- **Paid media no longer reads as free or doubles its code on the cost
lines.** `nika arm` printed `NIKA-1709 · NIKA-1709 · refusing to start: …`
for a beat a cost floor refuses, a paid media call included, and broke the
line before `(budget_floor)`; the readiness blocker now carries the
sentence alone, the code once on its own field, in the text and in
`--json`. The audited line of `nika check`, and the green answer of the
MCP `nika_check` tool, read `est out ≤$0.0000` for a workflow whose only
spend is a priced media call; they now add the calls and the least they
cost (`+ 1 paid media call from $0.0006`), counted as the `--max-cost-usd`
floor counts them: a literal `for_each` multiplies its call, while a call
behind a `when:` gate, over an expression `for_each` or made by a child
workflow is named as one only the run counts. Metered calls are named
beside unpriced ones.
- **`nika explain NIKA-PARSE-001` names the causes the refusal names
(#1608).** Its checklist sent a `prompt: Note: here` author looking for a
missing `:` when the line holds one too many. It now leads with the causes
the refusal itself names on the pointed line: a `: ` inside a plain value
(quote the whole value, use a block scalar, or put a key of its own on its
own line), an unquoted `${{ … }}` inside a flow mapping (quote it), and a
`{` or `[` the file never closes; then a stray tab, unbalanced quotes, a
key missing its `:` and a de-commented modeline.
- **`nika serve` lists the files a `for_each` fan-out saved.** A job whose
`for_each:` called `nika:chart` once per item wrote each item's file, yet
`GET /v1/jobs/{id}/artifacts` listed only the single calls' files: the
manifest read the tool from an `invoke · <tool>` note, and a fan-out's
note is `for_each · <n> items`. A fan-out's tool is now read from the
task's own journal: the one tool every tool-plane permit decision of the
task allowed, when every item settled ok, no agent frame came from the
task and its value holds one answer per item. Each item's file is listed
with its journaled size and digest and downloads like any other. A
fan-out with a recovered or failed item still lists none of its files.
## [0.124.0](https://github.com/supernovae-st/nika/compare/v0.123.2..v0.124.0) - 2026-10-27
Nika 0.124.0 opens the 0.124 release line with media capabilities that workflows need, so a
contact sheet or a narration no longer needs `exec magick montage` or `exec /usr/bin/say`:
`nika:image_compose`, the 30th builtin, lays 1 to 64 PNG images out as one grid, row or column,
and `nika:tts_generate` speaks with no key, offline, through `provider: system`. An `agent:` task
on a Codex seat hands the images it generates to the next task as values (`images:`, saved inside
`permits.fs.write` from the bytes its own session received). Over ACP
(`run.access.protocol: acp`), an `infer:` task on a Codex or Claude Code seat carries a `schema:`
and its `vision:` files, and `nika check` blocks what the seat cannot carry instead of passing
what the run refuses; on a direct seat route, without that protocol, the run still sends no
`vision:`, `temperature`, `max_tokens` or `thinking`, and `nika check` does not say so.
`nika doctor` and `nika welcome` name the `nika` that is running and every other copy on `PATH`,
and the first parse refusals a newcomer meets teach the shape they refused and point at the line.
Some workflows that 0.123.2 checked clean or ran unattended are now refused or paused.
`NIKA-SEC-009` follows untrusted content through a child workflow call and through a file a
builtin reads; a blocking `nika:prompt` that dominates the path still clears it, but a gate
inside the child does not count for its caller: put the gate in front of the call.
`NIKA-SEC-014` holds a confirm gate in front of a child workflow that acts: a bare
`after: { ask: success }` edge still runs the child on « no », so bind the answer on the call
(`with: { go: "${{ tasks.ask.output }}" }` and `when: ${{ with.go == true }}`), as an inline
effect already needs. On the `openai` provider, `nika check` refuses a `temperature:` on
`gpt-5`, `gpt-5-mini`, `gpt-5-nano` and the o-series, which accept only the default: drop it
there. On a Codex seat, a command that asks to leave its sandbox always pauses for you, under
`permits.exec: true` too, and a turn Codex reports as failed fails its task instead of
succeeding with the error as its output; what Codex does without asking, such as opening pages
with its hosted web tool or calling MCP tools marked read-only, is stated by `nika check`, not
bounded by `permits:`. On a seat, one `--answer` now decides one ask per task: an attempt that
`retry:` replays pauses on its ask instead of reusing the answer.
A `vision:` file is now a read inside `permits.fs.read`, a breaking change. 0.123.2 sent any file
an `infer:` task named to the model provider, under any `permits:` or none, and read a relative
path from the process directory. The run now reads each file under the workflow's own boundary,
on its resolved path, before any provider call, and fails `NIKA-SEC-004` outside it. `nika check`
refuses an ungranted literal path (`NIKA-AUTH-006` with no `permits:` block, `NIKA-SEC-004`
outside a declared one), and refuses a computed path, or a computed `images: { save: }`, under a
boundary that grants no path at all (no block, `permits: {}`, an empty `fs.read` or `fs.write`).
Grant each file in `permits.fs.read`: `nika check --infer-permits` prints the block for a literal
path, but for a computed one only a note and a partial `fs`, so the grant must cover every file
the run may name, for example `fs: { read: ["./in/**"] }`. A `source: url` entry needs no grant,
a secret in a vision entry now needs `egress: [{ to: "infer" }]` like one in a prompt, and
`NIKA-SEC-009` follows untrusted content that picks a vision file, as for a `nika:read` path.
A run under a cost cap now bills paid media at dated public prices and refuses what it cannot
price, a breaking change for capped runs, scheduled beats included: a beat always runs under
its `plafond:`, and a project `ceiling:` caps a plain `nika run`. Under a cap,
`nika:image_generate` on OpenAI `gpt-image-2` or one of the five Gemini image models the catalog
prices, and `nika:tts_generate` on OpenAI `tts-1` or `tts-1-hd` or a priced ElevenLabs model, are
floored at launch and billed against the cap; OpenAI's `gpt-image-2.5-sunburst` and
`gpt-image-2.5-flare` publish token rates only, so they are billed from the usage they report and
refused at launch only under a zero cap. 0.123.2 ran these OpenAI, Gemini and ElevenLabs calls
with no bound (xAI images were already floored and metered). `nika check` shows each paid media
call's floor, rate and price date, or says it is `METERED`, `UNPRICED` or `DECIDED AT RUN`. A
paid call the catalog cannot price is refused before any request, with `NIKA-1709` at launch, or
with `NIKA-1704` when only the run decides its provider or model or an `agent:` makes it through
its tools: OpenAI's default voice `gpt-4o-mini-tts`, `gpt-image-1.5` and `gpt-image-1-mini`
(which OpenAI shuts down on 2026-12-01), and any model id the catalog does not list. `nika arm`
shows a beat on one of them UNREADY. For speech, set `model: tts-1` or `tts-1-hd` (OpenAI shuts
all three of its speech models down on 2027-01-06), use a priced `elevenlabs` model, the keyless
`provider: system` voice, which bills no provider and runs under any cap as unpriced local work,
or `local` / `mock`; for images, use `gpt-image-2` (the default) or a 2.5 model. A paid call the
catalog cannot price runs only with no cap, and the settlement then counts it unpriced.
The file format and its nine top-level keys are unchanged and the SDK machine protocol stays 2;
the language gains `nika:image_compose`, `provider: system` and the `images:` field of `agent:`.
0.124 is the current release line. Work stays focused on maintenance, security and the media
capabilities workflows need; native video is planned for 0.125. Known limits are listed in plain
words, each with a way around it:
[Known limits in 0.124](https://docs.nika.sh/changelog/known-limits). In short: 0.124.0 makes no
video; `nika:image_compose` reads PNG only; the system voice needs macOS `say` or Linux
`espeak-ng`; only a Codex seat delivers images to `images:`; a seat carries no `temperature`,
`max_tokens` or `tools:` whitelist; the VS Code extension stays at 0.120.
### Added
- **`nika doctor` and `nika welcome` name which `nika` is running.** A
shadowed install used to read healthy: no path, no installer, no other
copy. Doctor now prints the running binary's resolved path, version,
commit and installer (read from the path: `install.sh`, `homebrew`,
`npm-global`, `npm-payload`, `cargo`, `nix`, `release-dir`,
`source-build`, else `unknown`), says what typing `nika` runs, warns
when that is another build, and lists every other `nika` on PATH with
the version it reports. Each copy is asked `--version` under a
two-second deadline: one that hangs is reported as unknown, one that
fails warns, neither is fatal. A relative or literal-`~` PATH entry is
never executed, and a copy that is asked runs from `/` with no
credential in its environment and no relative PATH entry; its whole
process group is killed after the answer. A removal is suggested only
for a known install layout. `--json` adds `running`, `others[]` and
`others_truncated`; `nika welcome` says the same in one line under
`this binary` and carries the same keys.
- **An agent application's images are task values.** An `agent:` task on a seat can declare
`images:` (spec 02 §Harness images): `.output` becomes `{ message, images }`, each image with
its `index`, `path`, `sha256`, `size_bytes`, `mime_type`, `width` and `height`, and
`images: { save: <path> }` has Nika write the received bytes through `nika:write`, inside
`permits.fs.write`: every path the save can take (image *n* at `-n`, up to four) is judged
before any file is written, and a save rides the same-path law (`NIKA-SEC-012`) like a
`nika:write`. `check` types `.output` as `{ message, images }`, so
`.output.images[0].path` and `.output.message.<key>` resolve and a schema key read at the
top is refused. Only the bytes sent on the task's own session count: the path an
application reports (Codex's `~/.codex/generated_images`, which every session shares) is
never opened, so a workflow no longer asks Codex to copy its image and cannot pick up another
session's. A save with no image received fails the new `NIKA-AGENT-007`. The image's
`agent_image_observed` frame records the `saved_path`, and `nika serve` lists the file among
the run's artifacts with the received digest. On an API or local model, `check` and the run
refuse `images:`.
- **`nika:image_compose` assembles several PNG images into one.** The 30th
builtin (stdlib §Media graduate #5) lays an ordered list of 1 to 64 inputs
out as a `grid` (columns and rows derived from the count when absent), a
`row` or a `column`; each image keeps its own size, is scaled to fit its
cell with the aspect kept (`fit`), or is scaled and center-cropped to fill
it (`fill`), with `gap`, `padding` and a `background` colour. Like
`nika:image_fx` it is pure, zero-dependency pixel work: the same input
bytes and args give the same PNG bytes, the `image_compose/v1` recipe rides
the tEXt chunk, and the output carries path, sha256, width and height,
never bytes. Every input is an `fs.read` that `nika check` judges, infers
and counts as a use, and `out:` an `fs.write`; caps on each input's side,
the summed input pixels, each scaled `fit`/`fill` box and the canvas fail
`NIKA-BUILTIN-IMAGE_COMPOSE-005` from the PNG headers before any
decompression (an input count outside 1..=64 is an argument, `-001`), an
input with a second IHDR is `-004`, and each of the six codes has its
`nika explain` lesson. Its inputs are file reads the `NIKA-SEC-009`
content flow follows, like `nika:image_fx`'s. A contact sheet no longer needs `exec magick
montage`.
- **`nika:tts_generate` speaks with no key through `provider: system`.** The
host's own synthesizer, offline: macOS `/usr/bin/say`, or Linux
`espeak-ng` found in an absolute directory of the engine's `PATH`. The
engine starts it itself as part of the builtin, with the text on standard
input, so the workflow needs no `permits.exec`. The text is spoken as
text: every `[[` that `say` reads as an embedded command (`[[volm 0]]`
would mute the file) or `espeak-ng` as phonemes is broken by a space, and
the audio is capped at 64 MiB. The WAV still lands under `output_dir:`
through `permits.fs.write`. `nika check` names the program in its plan and
refuses a literal `model:` other than `say`/`espeak-ng` or a voice that is
not a host voice name, as the run does. Every start is journaled as a
`permit_checked` frame on the `builtin` plane, and the cost is reported as
unpriced local work (`cost_usd: null` · `cost_unpriced: local_model` on
the output and the task's frame), never as priced or free; only this
builtin can label the run's cost that way. A named voice must be installed
(`say` would otherwise read with its default voice), and a host with no
synthesizer refuses `NIKA-BUILTIN-TTS_GENERATE-002` naming what is
missing, with no fallback to another provider. A narration no longer needs
`exec /usr/bin/say`.
### Changed
- **Capped runs bill common paid media at dated catalog prices, and refuse what the catalog cannot price, beats included.** A beat always runs under its mandatory `plafond:`, and a plain `nika run` in a project with `ceiling:` runs under that cap. Under a cap, `nika:image_generate` on OpenAI `gpt-image-2` (the default) or Gemini (`gemini-3.1-flash-image`, the default, `gemini-3-pro-image`, `gemini-2.5-flash-image`, `gemini-3.1-flash-lite-image`, `gemini-nano-banana-2.1`) and `nika:tts_generate` on OpenAI `tts-1` or `tts-1-hd` or on ElevenLabs (its models mapped to the price page's families) now run, floored at launch and billed against the cap; OpenAI's `gpt-image-2.5-sunburst` and `gpt-image-2.5-flare` publish token rates only, so they run billed from the usage they report, refused only under a zero cap. 0.123.2 ran these OpenAI, Gemini and ElevenLabs calls with no bound (xAI images were already floored and metered). A paid call the catalog cannot price is refused before any request (`nika arm` shows the beat UNREADY with NIKA-1709): OpenAI's default voice `gpt-4o-mini-tts` (no per-call price is published and its speech response reports no usage), `gpt-image-1.5` and `gpt-image-1-mini` (shut down by OpenAI on 2026-12-01), and any model id the catalog does not list; the refusal names every model the catalog prices. A beat on the default OpenAI voice is therefore UNREADY: set `model: tts-1` or `tts-1-hd` (OpenAI shuts every speech model down on 2027-01-06), use a priced `elevenlabs` model, the keyless `provider: system` voice (it bills no provider and settles as unpriced local work), or `local` / `mock`. Only a run with no cap at all makes a paid call the catalog cannot price.
- **The SDK guard reads what the published `@supernovae-st/nika` 0.123.2 closes.**
`published_sdk.json` is re-read from that package's `dist`. Every closed set of 0.123.1 is
unchanged, and one is new: an artifact manifest's `verdict` must be `sealed` or `anchored`,
the two values `GET /v1/jobs/{id}/artifacts` serves. The manifest and its entries are read
open otherwise, so a later key rides through.
- **The embedded language pack follows nika-spec `5cd95cd`.** `SPEC_PIN`
and `crates/nika-pack/pack` advance together from `c5dcd97`, the commit
0.123.2 embeds. nika-spec#397 (`aac8de5`) adds the contract of
`nika:image_compose` and the keyless `system` voice: `canon.yaml` counts 30
builtins, the schema's builtin enum and the stdlib gain
`nika:image_compose`, its caps list each input side, the summed pixels,
each scaled box and the canvas, the `system` voice speaks its text as text
with its audio capped at 64 MiB, and the `image-compose-sheet` example
joins the pack. nika-spec#399 (`ebc6448`) makes the images an agent
application delivers on a seat a value of the `agent:` task: the schema
gains `images: { save: }`, chapter 02 its §Harness images, chapter 17 the
`saved_path` of a saved image and the error registry `NIKA-AGENT-007` (112
codes), and a seat's `infer:` meets a JSON or JSON Schema need when the
engine validates its answer. nika-spec#400 (`479b0bc`) makes an `infer:`
`vision:` file a read under `permits.fs.read` (`NIKA-AUTH-006` under an
absent block, `NIKA-SEC-004` outside a declared one, and a refusal at
check, whatever the path's form, under a boundary that admits no read; a
`source: url` entry needs no grant), makes every `vision:` entry a
provider-egress sink of its task (`NIKA-SEC-006`, `NIKA-SEC-009`), and
bounds an `images: { save: }` to four files and names it, with its `-2` to
`-4` siblings, in the same-path law (`NIKA-SEC-012`). nika-spec#401
(`5cd95cd`) lets the `cost_usd` of `nika:image_generate` and
`nika:tts_generate` come from the engine's dated price catalog, marked by
`cost_basis` with the rates' date in `cost_as_of`, and keeps it `null`
when neither the provider's report nor the catalog yields a figure.
### Fixed
- **`nika check` accepts the `$key` variables `nika:jq` binds from its
input.** At run, every key of an object `input:` is a jq variable, so
`input: { rows: [...], batch: B1 }` with `$rows | map(. + {batch: $batch})`
works, but the check refused it with NIKA-VAR-005 « undefined filter or
variable `$rows` » and the documented form could never run. The check now
binds the same names; a templated `input:` leaves an undefined `$name` to
the run, and any other undefined name still refuses.
- **The first parse refusals a newcomer meets teach the shape they
refused.** A scalar `infer:`, `invoke:` or `agent:` body (`infer: "say
hi"`, `invoke: nika:jq`) now reflects it into its mapping (`infer: {
prompt: "say hi" }`, `invoke: { tool: nika:jq, args: { … } }`, or
`invoke: { workflow: ./child.nika }` for a child workflow); a bare builtin
name is taught under its prefix (`invoke: fetch` → `tool: nika:fetch`). An
untyped `inputs:` entry names the typed form it meant (`topic: { type:
string, default: "x" }`, `tags: { type: { array: string }, default: [...] }`
for a list) and the ten primitives. A builtin written as a task key
(`fetch:` or `nika:fetch:`) says it is called through `invoke: { tool:
nika:fetch, args: { url: … } }`. The codes are unchanged (`NIKA-PARSE-019`,
`NIKA-PARSE-005`).
- **A YAML syntax refusal (`NIKA-PARSE-001`) names its site and its usual
cause.** `prompt: Note: this has a colon: here` and `infer: {prompt: ${{
inputs.t }}}` printed one raw sentence with no line frame, and the `--json`
finding had no span. The refusal now carries the position the YAML scanner
stopped at (where the content ends, when the scanner ran out of input), so
`nika check` prints the `-->` frame with its caret and the `--json` parse
finding carries `span: {start, end}` like a CONFORM finding (every
span-bearing parse refusal does). A colon inside a plain value is told to
quote the value or use a block scalar, or to nest a key that meant to be
one; an unquoted `${{ … }}` inside a flow mapping is told to quote the
expression; a flow mapping or list the file never closed is told so.
- **A bare `{{ name }}` that names a declared input or a `with:` key draws
the `[silent-literal]` hint.** `{{ inputs.name }}` was already named as
literal text, but `content: "Hello {{ name }}"` passed `nika check` in
silence and the run wrote the braces. The hint now names the reference it
meant: `${{ with.name }}` for a `with:` key of that task, otherwise
`${{ inputs.name }}`. A bare word that names neither stays silent, and
so does a dashed name, whose dotted reference would not parse.
- **`nika welcome` no longer calls an API key « ready now ».** The key rung
read `OPENAI_API_KEY is set · ready now · billed per run`, but nothing on
that path checks the key, and a revoked one answered 401 on the first
paid call. The rung now says the variable is set and is not checked until
the first call; `welcome --json` carries the same reason.
- **A task-level key from another workflow dialect names the verb that owns
it.** `prompt:`, `max_tokens:` and `system:` written straight on a task now
point to `infer:`; `http:`, `url:`, `method:` and `request:` to `invoke:`
with `tool: nika:fetch`; `command:`, `shell:` and `script:` to `exec:`
and `permits.exec`; `tool:` and `args:` to `invoke:`. The field list still
follows, so `check --fix` and the editor repairs read it as before. `nika
explain NIKA-PARSE-005` now teaches that list and those mappings instead of
sending the author to `nika spec --schema`.
- **`nika serve` says so when the project arms nothing.** In a project whose
`nika.yaml` carries no `arm:` entry, `serve --once` exited 0 in silence,
`serve --dry` never returned and a bare `serve` waited without a word.
`--once` and `--dry` now print the sentence `nika arm` prints, plus the
`--bind` command and the token recipe for running workflows over HTTP,
and exit 0. The resident prints the same lines once on stderr and keeps
watching `nika.yaml`, so arming it later still takes effect. The token
recipe, here, in the `--bind` refusal and in `nika doctor`, now creates
`.nika/` before it writes the token, so it works in a project fresh from
`nika init`.
- **`nika check` judges a declared route over this machine's access rows.**
A file that declares `run.access` (a `via:` or a `protocol:`) and has no
`infer:` or `agent:` task was judged over no rows, so ACCESS said the binary
was built without agentic CLI adapters and `run ready` was refused, while
`nika run` resolved the same route and finished. Check now also probes for
a declared route, as the run does, beside a task that dials a model and an
`--access` pin.
- **`nika check` no longer calls the members of a folded group dead spend.**
A task that declares `group: <name>` is read by every `${{ group.<name> }}`
fold, but the `[dead-spend]` hint only counted `tasks.<id>.output`
references and told the author to remove each member. The hint now reads
group membership with the same law the DAG uses to fold.
- **An image save names at most four received images.** The four-image
bound counted path-only reports, so one delivered image beside four
reported paths was refused; and the agent verb wrote a fifth image from a
lent backend at a `-5` path no check judged. The bound now counts images
whose bytes arrived, and the writer refuses a fifth before any write.
- **A turn Codex could not complete no longer settles as a success.** codex-acp returned a
failed turn (a 400 `model_not_found`, a retry limit exhausted on 429) as answer text and
`end_turn`, so an `agent:` or `infer:` task on the Codex seat succeeded with the error as its
output. Nika now advertises the `sessionFailure` extension, which codex-acp and
claude-agent-acp implement, and the failure the adapter then reports fails the task with its
own error: transient (`NIKA-1804`) when the adapter offers a retry, an expired sign-in when it
asks you to sign in again, a refusal (`NIKA-1805`) otherwise. `nika explain` now describes
both codes for such a failed turn. An adapter without that extension still returns its error
as text.
- **`[jq-as-map]` no longer fires on a binding whose scope has ended.**
`map(. as $r | {n: ($r.a | length)})` binds each element, so the map that
encloses the binding maps what it was written to map, and so does a later
map of its output (`map(. as $r | …) | map(.n)`); the run's result was
right. The hint only checked that a `. as $name` binding and a bare `map(`
both appeared; it now counts a bare `map(` only inside a dot binding's
scope, which ends at the bracket that closes the construct the binding sits
in, so `. as $c | map(.n)` still draws it.
- **`scan` and `IN` in jq expressions give jq's results.** `scan` with a
regex that has capture groups returned each whole match where jq returns
the captures (`"abab" | [scan("(a)(b)")]` gave `["ab","ab"]` instead of
`[["a","b"],["a","b"]]`), and `IN(...)` was an undefined filter. `IN/1`,
`IN/2` and `scan` over a match where every group takes part now follow
jq 1.7 in `nika:jq`, output bindings, `nika check` and the compile
verifier, which load the same definitions. A group that takes no part in
a match (`a(b)?` on `a`, one side of `(a)|(b)`) still differs: jq yields
`null` for it, while this engine leaves it out, and yields the whole match
when no group took part.
- **`--max-cost-usd` and the run settlement now see paid media spend.** `nika:image_generate` and `nika:tts_generate` on OpenAI, Gemini and ElevenLabs billed their provider and reported no `cost_usd`, so a capped run never charged them and the settlement left them out (a run whose only spend was such a call settled `unmetered`). The engine now carries a hand-sourced media price table apart from the models.dev snapshot: each row names its provider, model, unit (per image by size class and quality, per million characters, or per million tokens of text input, image input, image output and text and thinking output), USD rate, official source page and the date it was read (2026-10-27). The builtins bill every priced call from it, each response on its own: at the token rates times the usage it reported, at the per-image price times its images when an image response reports no usage (the dearest row over what the request leaves open, such as `quality: auto`), or at the per-character rate times the characters sent, and say how on the output and the manifest (`cost_basis`, with the rates' date in `cost_as_of`); xAI images keep the spend xAI reports (`provider_reported`). A paid call that fails after its provider billed keeps that spend: a Gemini `n:` batch whose later request fails keeps the bill of the images already rendered, a render that fails after its answer keeps its bill, and a request that broke after it left is counted unpriced. The run ledger debits all of it as it debits a model's, per attempt, so a retry is judged against what is left. Under a cap every paid media call is judged on its rendered arguments before its request, a `${{` in a rendered text being text the provider bills: a provider, model, `quality:`, `size:`, `n:` or `text:` only the run decides refuses NIKA-1704 when the catalog cannot price the call or its floor exceeds what the cap leaves. An admitted call holds its floor until it returns, so the items of a capped `for_each` fan-out, parallel or not, cannot launch past the cap together; a call dropped in flight (a `fail_fast` sibling, a `timeout:`) is counted unpriced and its floor stays consumed; once a paid call billed without a figure, a call billed only from its usage is refused under the cap. An `agent:` loop judges its media tool calls the same way against what its task's remainder leaves after the loop's own model turns, its returned tools and the floors its concurrent calls hold, so a model batching image calls cannot spend one remainder twice; concurrent agent tasks each judge against their own snapshot, as text siblings in flight do. A paid call the catalog cannot price runs only with no cap and is counted in `spend.unpriced_calls`, so the settlement reads `unpriced` or `partially_priced`; a child workflow's unpriced calls reach its parent's settlement, and a grandchild's unpriced media is refused at the root's launch. The run's prologue records the table (`media_pricing`, beside `pricing` and `admission_pricing`), and `nika trace verify` refuses the cost replay of a journal that pinned other media rates. `nika check` names each paid media call on its COST rung with its floor, the rate and unit that set it and the table's date (METERED for a call billed from its usage or, for speech, per character of the text the run renders; UNPRICED; or DECIDED AT RUN), including a composed child's and the paid media an agent's tools can reach, and lists `unpriced_media_cap`, `priced_image_over_cap` and `priced_speech_over_cap` in `failure_plan`.
- **`nika check` refuses a `temperature:` on `openai/gpt-5`, `gpt-5-mini`,
`gpt-5-nano` and the o-series.** These models accept only the default
temperature, and OpenAI answers any other value with HTTP 400 at run
(« Only the default (1) value is supported »), while check showed
`capacity fit ✔`. The catalog now records that GPT-5, GPT-5 mini and
GPT-5 nano (and their 2025-08-07 snapshots) take no temperature, as it
already did for the o-series, and the fixed-sampling finding covers
OpenAI's own API beside Anthropic's for the models the catalog lists. A
Session label call on these models, a dated snapshot included, no longer
sends a temperature. The finding does not yet name a dated snapshot id
(`gpt-5-mini-2025-08-07`), which the catalog's model list does not carry.
OpenRouter and other compatible routes, which drop the control, and the
later dot variants (`gpt-5.1` onward) are unchanged.
- **An oversized PNG input is a size cap, and a PNG has one IHDR.**
`nika:image_fx` refused an input over 16384 pixels on a side as
`NIKA-BUILTIN-IMAGE_FX-004` (corrupt, re-produce it); the stdlib files that
input cap under `-005`, which it now returns. The PNG decoder behind
`image_fx` and `image_compose` now refuses an IHDR that is not the first
chunk, or a second IHDR, so the size a pixel budget judged from the header
is always the size decoded.
- **`check` and the run agree on what a Codex or Claude Code seat can carry.** `check` said
`run ready` for `schema:` and `vision:` on a seat while the run refused them. An `infer:`
task over ACP now carries a `schema:`: the prompt carries the schema and Nika validates the
answer, and each repair is a new one-turn session that receives the earlier answer and the
errors (`NIKA-INFER-002` after the budget). Its `vision:` files ride as ACP image blocks when
the application declares image prompts at `initialize` and the file carries an image
signature. An `agent:` task's `schema:` is validated on the seat's final message, with no
repair turn. What a seat still cannot carry (`temperature`, `max_tokens`, an enabled
`thinking` budget, a `vision:` URL on `infer:`, a `tools:` whitelist on `agent:`) is a `check`
blocker, and `check` no longer suggests `max_tokens` or `max_tokens_total` on a seated task.
- **A `--var` refusal names its code in the `--json` envelope.** An unknown
input, a value its declared type refuses and a malformed pair reached
`nika run --json` callers as `{"error":{"code":null,…}}`, while the
`--inputs-json` channel and the server say `unknown_input` and
`input_type_mismatch`. The envelope now carries `unknown_input`,
`input_type_mismatch`, `malformed`, `env_unset`, `env_undeclared_in_ci` or
`env_name_missing`; the sentence on stderr and exit 3 are unchanged.
- **A streamed WAV reports its real duration.** `nika:tts_generate` read a
streamed WAV header's placeholder `data` size (`0xFFFFFFFF`) verbatim, so
an OpenAI narration reported `duration_ms: 89478485` (24.8 hours). The
size is now clamped to the bytes actually present, which also gives
`espeak-ng --stdout` audio its true length.
### Security
- **`NIKA-SEC-014` holds a confirm gate in front of a child workflow call.** The consent check only looked at egress tasks written in the workflow itself, so a gate that the trifecta credited in front of an `invoke: workflow:` call was never checked to stop the child's effect on a « no » answer. `nika check` and `nika run` now judge a call as the egress-capable task its child (or a workflow below it) reaches, whatever content reaches that task: every route from a confirm gate to the call must close on a refusal, as for the inline form, and the finding names the call. A confirm gate inside the child owns the effect behind it, as inline. When only a child gate the check cannot decide from the caller stands before the effect (a `when:` on an input the caller may bind to its answer), the call gets the advisory hint instead of the refusal. A child the check cannot resolve counts as acting. `check --json` names the child task under `composed_calls[].effect`, with `effect_conditional` for the advisory case.
- **A Codex command that asks to leave its sandbox now always pauses.** In `read-only`, Codex runs
ordinary commands in its sandbox without asking, so every command it asks about asks to run
with no sandbox at all (`require_escalated`). Its ask carries the command as a string and the
reason in `_meta.permission.description`, which Nika read as `execute · <prose>`: under
`permits: { exec: true }` it was allowed, and Codex fetched a site outside its sandbox. It now
pauses for you on the exec plane (`sandbox · command`) whatever `permits.exec` grants, with the
command and the reason in the question and in the witness. Every pause question now names the
command and the reason an ask carries unless its title shows it, on one line a harness cannot
steer: control characters become spaces and invisible ones (direction overrides, zero-width
characters) are removed, so a Claude Code title, which is the model's own command, can no
longer erase itself on your terminal. A long command keeps its end in view (its beginning and
end around the bytes left out and their SHA-256; whole up to 16 KiB in the witness), and a
sandbox widening's witness names the command it is for. One `--answer` now decides one ask for
the task: an attempt that `retry:` replays never reuses it, where a typed 429 failure could
otherwise replay the session and approve a different command with the same answer.
`nika check`, `nika explain harness` and docs/usage/harness-agents.md now also state what Codex
does without asking: it calls its MCP tools marked `readOnlyHint`, opens pages with its hosted
web tool, writes its own `~/.codex`, starts every configured MCP server and applies your global
`~/.codex/AGENTS.md` and plugins.
- **`NIKA-SEC-009` now judges a child workflow through the call that runs it.** A parent that
read a private file and sent data out passed `nika check` and ran when part of the flow sat
in a child reached through `invoke: workflow:`, while the same tasks written inline were
refused. `nika check` and `nika run` now judge each call as the tasks it stands for, through
every workflow below it. The call is a source of untrusted content when content the child
brings in (a `nika:fetch`, an `exec:`, an `mcp:` tool, an `agent:` whose tools admit one)
reaches its outputs or a file it writes. It is an egress task when untrusted content reaches
an egress task inside it: content the child brings in, the call's arguments, or a file
written before the call that a child `exec:` reads. The parent is refused unless a blocking
`nika:prompt` of its own dominates the path, as for the inline form; a gate inside the
child is not credited to the caller. The finding names the child task behind the call. A
child the check cannot read, parse or resolve counts as all of these (fail-closed).
`check --json` lists every direct call under `composed_calls`, resolved or not, and the
runtime's report check re-derives the trifecta over the same calls. `NIKA-DRIFT-001` no
longer tells you to remove a grant a child needs (its program, its tools, the files and
hosts its own `permits:` lets its subprocess reach); removing it failed `NIKA-COMP-002` or
the child's run. A check of the source alone (an editor, the MCP `nika_check` tool) reads
no child: its trifecta judges the parent's own tasks only, and its `NIKA-DRIFT-001` makes
no `permits:` claim on a file that calls a child.
- **`NIKA-SEC-009` follows untrusted content through a file a builtin reads.** A gate could clear the task that saved untrusted content to a file, and a `nika:read` of that file then fed an ungated egress: the trifecta counted only an earlier `exec:` as a reader of such files, so `nika check` passed the workflow. A builtin read (`nika:read`, `nika:grep` (its default root `.` included), `nika:glob`, `nika:edit`, a `nika:decide` bundle, the files `nika:image_fx`, `nika:chart` and `nika:image_generate` read, a `nika:fetch` multipart upload) of a path the write may have reached now carries the content on, inline and inside a child workflow, and, when the write's path is literal, whatever the order: a read in the writer's own wave, or before it in the run (the next run reads the file), counts too. Paths compare lexically: folded, case-blind, a segment past ASCII may be any segment, `/private/tmp` is `/tmp`, and a templated path may be any file. A read whose path cannot reach the written one stays clean.
- **A run refuses a report that judged no child for a workflow call.** The runtime's report check re-derives the trifecta over the child calls the report resolved, and a call missing from that list counted as having no ingress: an embedder pairing a plain `nika_check::check()` with `Runtime::run` and a child runner could run a fetching child into an ungated egress. A call the report does not list for its task and target now counts as unresolved, and the run refuses with `NIKA-1707`, naming it. `nika run --task` re-checks the kept tasks with their children, so a scoped run of a composed workflow keeps its calls resolved.
- **A `vision:` file is a read inside `permits.fs.read` (breaking).** The
`infer:` verb opened every `vision: [{ source: file }]` path itself, on
every route (API and ACP; `nika run`, serve, Session, child workflows,
`nika test` and rehearsal rooms), with no `permits` check: an absent block,
a narrower `fs.read` grant, a symlink or a `..` did not keep the file from
the model provider, and a relative path was read from the process
directory instead of the run's root. The run now reads each file through
`nika:read` under the workflow's own boundary, on its resolved path,
before any provider call: outside it the task fails `NIKA-SEC-004` (one
`permit_checked` fs frame), and a granted file that cannot be read fails
with the read's own code (`NIKA-BUILTIN-READ-001` when no file is there,
`-002` otherwise) instead of the internal `NIKA-432`. `nika check` judges
a literal or bare-`const` path (`NIKA-AUTH-006` with no `permits:` block,
`NIKA-SEC-004` outside a declared one), and refuses a computed path, or a
computed `images.save`, under a boundary whose side grants no path at all.
`--infer-permits` prints the grant, the drift scan counts it as used, a
child's vision reads join `NIKA-COMP-002`, and the check card no longer
calls such a body pure compute. The `vision:` entries are sink fields of
the task, on check and run alike: a secret in a vision URL or path is the
`infer` leak (`NIKA-SEC-006` unless `egress: [{ to: "infer" }]`), and
untrusted content that picks an entry reaches the answer, so the trifecta
(`NIKA-SEC-009`) follows it as it follows a `nika:read` path; the file
channel follows a vision read of a file an untrusted writer left. A
`source: url` needs no grant: the provider fetches it. Migration: grant
each vision file in `permits.fs.read` (`nika check --infer-permits` prints
the block); embedders hand the verb bytes they read (`VisionPart::Loaded`),
never a path.
## [0.123.2](https://github.com/supernovae-st/nika/compare/v0.123.1..v0.123.2) - 2026-10-26
Nika 0.123.2 is the second maintenance release of the 0.123 reference line. An `agent:` task on
a harness seat (Codex, Claude Code, Gemini CLI) is now held to the workflow's `permits:` as far
as the seat allows: Nika selects the seat's asking mode, judges every ask against your grants and
pauses for network, sandbox-widening and the harness's own MCP approvals, and `nika check` says
what the seat still does without asking. `nika serve` can return the files a settled job made,
and verifies journals sealed with an explicit run key pair. A mock rehearsal no longer calls
itself offline when a step can reach the network, and `crypto-bigint` leaves its yanked release.
The workflow language, the file format and the SDK protocol are unchanged.
### Added
- **`nika serve` downloads the artifacts a settled job's sealed journal
records.** `GET /v1/jobs/{id}/artifacts` lists them, bound to the job's
receipt: each file `nika:chart`, `nika:image_generate`,
`nika:tts_generate` or `nika:image_fx` saved inside the project, and each
image an agent's harness sent, with its size, SHA-256 and media type.
`GET /v1/jobs/{id}/artifacts/{name}` streams one, with `Content-Length`
and `Nika-Artifact-Sha256`. Both need the bearer token, answer only once
the job settled (`409 job_not_settled`), and serve nothing unless the
journal verifies sealed and intact with the receipt's chain head. A
name is looked up in the manifest, never on disk, and the file opens
below the project root with no link on the way. One artifact is served
up to 256 MiB. Bytes that no longer match the journal are refused
(`409 artifact_changed`), and a file changed mid-stream never arrives
whole. Health advertises `artifacts`; ADR-117 records the decision.
### Changed
- **`crypto-bigint` moves from the yanked 0.7.6 to 0.7.9.** Upstream
yanked 0.7.6, 0.7.7 and 0.7.8 for a breaking change, so `cargo deny
check` refused the lockfile and blocked every push. The crate reaches
the build through `p384`, which verifies the Sigstore timestamp
authority's ECDSA P-384 leaf in trace anchoring. 0.7.9 keeps the same
dependency set and has no build script or procedural macro.
`Cargo.lock` and `fuzz/Cargo.lock` move together.
- **The SDK guard reads what the published `@supernovae-st/nika` 0.123.1 closes.**
`published_sdk.json` is re-read from that package's `dist`: machine protocol 2, a scheduled
origin's `missed_first` and `missed_count`, `choice` questions and the three compile cognitions,
on both compile generations. It still recorded 0.120.3 on protocol 1, so its test passed on a
moved clock; it now compares what the widest resident serves with what the published SDK reads.
- **The embedded language pack follows nika-spec `c5dcd97`.** `SPEC_PIN`
and `crates/nika-pack/pack` advance together from `6390b08`, the commit
0.123.0 and 0.123.1 embed. The examples and templates that `nika try` and
`nika compile` serve no longer call a mock rehearsal offline: the mock
replaces the model, not the network, so a fetch step still reaches its
host (#1605, the pack half of #1833). `release-notes` no longer drafts
notes from a sample log when `git log` fails (the sample is now an
explicit `--var rehearse=true`), and `pr-review-fanout` closes its fan-out
on a « no » at the gate instead of failing. The spec chapters and stdlib
pages carry current model ids and the 0.123 reference-release amendments.
The schema and `canon.yaml` are unchanged.
### Fixed
- **`nika explain NIKA-SEC-004` and `NIKA-SEC-005` say a redirect is held to the same
boundary.** `nika:fetch` follows a 3xx itself and judges every hop like the URL in the file:
a hop onto a host outside `permits.net.http` is SEC-004, a hop into loopback, private,
link-local or metadata space (or onto an undeclared loopback name) is SEC-005. Neither page
named a redirect, and the SEC-005 closer promised that `nika check` catches it before a run;
it now says that check catches a literal URL and that a hop is judged at run.
- **`nika explain NIKA-BUILTIN-JQ-001` names `decode: json` for `exec:` stdout.** A command that
prints JSON hands `nika:jq` a string, and the page taught only the `nika:read` + `fromjson`
path for files. It now names the exec side's explicit decoder,
`exec: { command: [...], decode: json }` (`jsonl` for one value per line), whose output is
the parsed value.
- **`nika explain NIKA-SEC-001` names the OS sandbox.** A confined `exec` child refused at run
reads « seatbelt refused the confined process », and the page it points at named no
sandbox. It now says that `exec` runs inside the sandbox built from `permits:` (Seatbelt
through `/usr/bin/sandbox-exec` on macOS, bubblewrap through `/usr/bin/bwrap` on Linux, the
backend `nika doctor` reports). It also says that a denied exit is classified as that
sandbox's refusal from its stderr text alone, so a program's own permission error (a file
the OS refuses inside the grant) reads the same, and the file's own permissions are worth
checking too.
- **An unresolved `${{ fetch.output }}` now teaches the task-output form.** When the root of a
NIKA-VAR-001 reference is a declared task id, the finding says so and names
`tasks.<id>.output` (bound in `with:` on a body surface), and `nika explain NIKA-VAR-001`
teaches the same `with:` binding. Neither surface names `config:` any more: the nine-key
envelope refuses that key (NIKA-PARSE-005), so a typo'd root is no longer offered it.
- **`nika check --help` says what `--fix` really applies.** It read « Apply typed rename
repairs and re-audit », while the repair ladder also hoists `with:` bindings (NIKA-VAR-021)
and migrates retired forms (`needs:`, `depends_on`, `vars:`, a string `exec:` …). The help
now names the three repair families and says that other findings stay the author's, and the
`○ FIX` line printed when nothing applied names the same families instead of « typed rename
suggestions only ».
- **`nika try` no longer calls its mock rehearsal offline, and names the hosts it may reach.**
The mock seat replaces the model only: `nika try snippets/use-a-tool` still GETs
`https://nika.sh`, and `snippets/weekly-radar` and `ceo-monday-brief` reach `hnrss.org` and
`hn.algolia.com` once their gate is answered. The `nika try` help, the first-run shelf heading,
the `--all` listing, the rescue tips, the `try` door blurb, the `model` glossary entry, the
`MODELS` row of `nika check`, the `NIKA-INFER-001` lesson and the provider connection hint now
say what is true (« on the mock model by default: zero keys, no model call; a step that fetches
still reaches its host »), and the rehearsal line of `nika explain <file>` names the declared
hosts (`# mock model · zero keys · may still reach nika.sh`). Before a mock rehearsal runs,
`nika try` prints `may reach: <hosts> — the mock replaces the model, not the network` from the
workflow's `permits.net.http`; a workflow that declares no host prints nothing new, and
`--quiet` keeps its compact card. The network itself is not blocked.
- **`nika explain NIKA-PARSE-019` teaches the `permits.net` mapping.** A host list
(`net: [api.example.com]`) refuses with « `permits.net` must be a YAML mapping », and the
page answered with `tasks:` and `exec:` forms only. It now teaches the shape the parser
accepts, `permits: { net: { http: [api.example.com] } }`, ahead of the exec lesson: list the
host, not the URL (an `https://` entry is refused), and the one `http:` key covers `https://`
URLs too.
- **`nika doctor --ping` no longer calls a web page on `:8080` a ready engine.** Its model-list
sweep asked `…/v1/chat/completions/models`, a 404 on every engine, and the ping row said
`ok … listening` for any open port. The sweep now asks the chat route's sibling,
`…/v1/models`. Only an answer that is clearly not an API (a 2xx body that is not JSON, such as
a web app's page) drops the `ok`: the row then reads « accepts connections but answers a page,
not an API — not a model server », and `--json`'s `adoption_state` no longer counts it as a
reachable engine. A 401 or 403 is named `key-protected`, a 404 `no /v1/models route` (the
`nika model serve` sidecar), and a timeout keeps the connect verdict. Ollama's
`{"object":"list","data":null}` reads as an empty model list.
- **A `nika try` rehearsal that pauses at a gate names the try form that finishes it.** The
paused card said `nika run --answer approve=true FILE`, a file the verb discards when it
returns. Under `nika try` it now says `nika try <slug> --answer approve=true` (the answer is
pre-seeded on a fresh rehearsal, the form `nika try --help` documents), nested slugs such as
`snippets/weekly-radar` included. It carries the rehearsal's own flags as typed (`--var`,
`--model` with `self` kept as `self`, `--access`, `--max-cost-usd`, `--resume` and the answers
already given), so the pasted line reruns the same rehearsal, and the rehearsal card names
that slug too (`rehearsal · snippets/weekly-radar`, no longer the room's folded stem). A
workspace `nika run` keeps its own card.
- **A recorded run's card names `nika trace verify`.** The run footer taught
`nika trace outputs` and never the proof itself. When the run recorded a trace, the card
(and the piped close) now lists `verify it: nika trace verify` beside `wrote .nika/traces`:
zero-arg, the verb judges the workspace's latest trace and prints the head the `trace:`
line names. The note is short enough that the card keeps its width.
- **The ADR index carries `amends`.** `docs/adr/README.md` says `docs/adr/index.json` holds the
full relation graph, but `scripts/adr/generate-index.sh` emitted every edge except `amends`,
so the twelve ADRs that amend another (ADR-154 amends three) read as amending nothing. The
generator now emits it, the index is regenerated, and a hygiene self-test pins the edge.
- **Release tarballs record root ownership.** The v0.123.1 archives recorded the build runner's
own uid (1001 on the Linux legs, 501 on macOS), so `tar -x` run as root in a rootless
container failed to restore an unmapped owner. The packaging step now writes every member
as 0:0 by number (GNU tar `--owner=0 --group=0 --numeric-owner` on Linux, bsdtar
`--uid 0 --gid 0 --numeric-owner` on macOS); the archive contents, the checksum step and the
replay of existing assets are unchanged.
- **`nika serve` attributes the journals it seals with an explicit run key
pair.** A resident started with `NIKA_RUN_KEY_FILE` and `NIKA_RUN_PUB_FILE`
naming a pair outside `~/.nika/keys` sealed its jobs with that pair, but
`GET /v1/jobs/{id}/trace/verify` called the seal unattributable and both
artifact routes refused the job with `409 artifact_journal_unverified`.
The verifier now weighs that pair's public half after `--key` and before
`~/.nika/keys/run-signing.pub` and the retired-key ledger, the order the
signer takes them, so `nika trace verify` also attributes such a journal
without `--key`. A host that names no pair verifies as before.
- **The `nika serve` OpenAPI document declares what the server answers.**
The trace verify response's `tier`, the 412 schedule error's
`error.currentRevision`, the schedule 422 `{findings}` body (kept as the
published SDK reads it) and the 404 of a by-name `POST /v1/check` and
`POST /v1/jobs` were sent but undeclared. `GET /v1/workflows/{name}` now
says a name containing `/` is sent with literal separators, and the
cancel route states its fixed 5-second grace before a run is
interrupted.
- **The Session guide says what 0.123.1 suggests on an unsigned run.** It still named 0.123.0's
`nika sign`, which signs workflow files; `/proof` and `nika trace verify` now point at the run
key, `nika key init`, which seals later runs.
### Security
- **An `agent:` task on a harness seat is held to the workflow's `permits:` as far as the seat
allows, and `nika check` says where that stops.** On codex-acp the session kept the adapter's
default `agent` mode, whose own reviewer approves most actions, so a Codex task wrote `./out`
and read files outside the project without a single `permit_checked` frame. Nika now roots the
session in the admitted project (never the server's or the shell's directory), selects the
seat's measured asking mode (codex-acp `read-only`, claude-agent-acp `default` instead of your
`auto` default, gemini-cli `default`) and reads it back before the prompt or refuses to start,
never picks a mode that approves on its own, and ends the session if the mode moves mid-turn.
The mode, every ask and every refused client `fs/` or `terminal/` call are journaled as
`permit_checked`, also when the run pauses on the ask (a paused trace used to carry the
question and no decision). Asks are judged in the project's own terms: an absolute path the
harness names now matches `./out/**`, and every path is judged where the operating system
lands, walked through each symlink (a dangling one included) and each `..`: `out/link/../x`
with `out/link` pointing outside the project was judged as `out/x` and allowed; a path that
cannot be resolved now pauses. An ask for network access pauses for you even under
`permits.exec: true`: codex-acp's SOCKS approvals (`socks5Tcp network access to <host>`) carry
no URL and were answered `allow_once` as plain commands. So does a command asking the harness
to widen its own sandbox, which `exec: true` also answered (codex-acp `additionalPermissions`,
Claude Code `dangerouslyDisableSandbox`). What the mode still leaves to the harness is printed
on every such task as a `[harness]` hint (`nika explain harness`,
docs/usage/harness-agents.md): Codex, for one, reads any file and writes the project, `/tmp`
and `$TMPDIR` inside its own sandbox even with no `fs.write` grant, and a Codex command that
`permits.exec: true` admits after it asked to leave that sandbox runs with none.
`NIKA-DRIFT-001` no longer tells you to remove a `permits.exec` or `permits.fs` grant that the
permission bridge judges.
- **A Codex MCP tool approval no longer rides `permits.exec: true`.** codex-acp asks to approve
its own MCP tool calls as a command ask with no command, which `exec: true` answered
`allow_once`; it now pauses for you on the tools plane, because `permits.tools` names your
workflow's MCP tools, not the harness's.
## [0.123.1](https://github.com/supernovae-st/nika/compare/v0.123.0..v0.123.1) - 2026-10-25
Nika 0.123.1 is the first maintenance release of the 0.123 reference line. It carries a security
update (`crypto-bigint` 0.7.6, RUSTSEC-2026-0387) and corrections to what Nika tells you on your
first runs: the welcome screen no longer advertises a hosted Nika Cloud, which does not exist;
access errors, hints and help name commands and steps that exist; `nika init --example` copies the
files its lesson reads; and a founded project keeps the `nika serve` token and job state out of
Git. The workflow language, the file format and the SDK protocol are unchanged.
### Changed
- **The repository's documents describe the released 0.123.0 and its maintenance.** The README,
`llms.txt`, the Session guide, CONTRIBUTING, SECURITY, ROADMAP and DIAMOND say that 0.123 is
maintained with security fixes, bug fixes and compatibility, with no hosted service and no 1.0
launch planned; SECURITY's code table lists only codes `nika explain` resolves, and it names the
MCP pin codes it does not. The serve operations page gives a first run that works (a project
`nika.yaml` it keeps when one exists, a served rehearsal workflow, a health and job request, the
served cancel route), `llms-install.md` adds the container and provenance paths and an
uninstall that names what `~/.nika` holds, and the README and the Session guide say that runs are
signed once `nika key init` minted a key. The README says what the TypeScript SDK's server door
changes (`allowInsecureHttp` for plain loopback HTTP, an `idempotencyKey` and no `maxCostUsd`),
and the Session guide lists the routes a Run admits. The README clips are captured again from
0.123.0, and a new `sdk-doors` clip shows the SDK's two doors (the bundled engine and
`nika serve`, with the two lines whose options change) from a capture of the published
`@supernovae-st/nika` 0.123.0.
### Fixed
- **The `AGENTS.md` that `nika init` writes no longer says the shipped binary cannot serve models.**
It said `nika model serve` « needs a `local-infer` build — the default binary prints the build
recipe », but release binaries are built with `local-infer`, and 0.123.0's `nika doctor` reports
« local inference built in ». The guide now says release binaries include it and that only a
source build without the feature prints the recipe.
- **`nika arm --emit --mode system` states its limit instead of a coming release.** It refused with
« la portée root arrive avec serve (W5) », promising a release. It now says « la portée root n'est
pas émise » and that units go to the operator's session (LaunchAgents · systemd --user), with the
same exit 2.
- **A project without `nika.yaml` is pointed at plain `nika init`.** `nika arm`, `nika arm fire` and
`nika serve` answered « fix: `nika init --project-file` lays a commented starter », naming a hidden
flag and leaving out what init writes. Plain `nika init` has laid that starter by default since
#1283 (the flag only keeps older scripts working), among the project's starter files (agent
briefs, hooks, editor and MCP wiring, a first workflow). The hint now says « fix: `nika init`
writes the project's starter files, a commented `nika.yaml` among them (existing files are
skipped) ».
- **Unsupported schedule policies state the present limit, not a future release.** `nika arm fire`
refused `chevauchement: remplacer`, `après_saut: à-complétion`, `manqué: rattraper` and
`décalage:` with « arrive avec serve v0.2 », the unit emitter said the same, and `nika arm` read
« not supported by this firer (arrives with serve v0.2) ». No such release is planned. The firer
now says « ce tireur ne tient pas cette politique » (this firer does not keep this policy), then
what it does today, as before. For the two catch-up policies, `rattraper` and
`{ rattraper: N }`, which the resident `nika serve` already keeps, the refusal adds « `nika serve`
la tient » and `nika arm` adds « `nika serve` keeps it ».
- **A home or project path through a symbolic link is refused by name.** Nika opens its owned
directories (the Session history under the home, among others) through real directories only. A
path with a symbolic link on the way stopped the terminal Session with « Not a directory (os error
20) »; the refusal now names the link and the resolved path to use instead.
- **`nika catalog --tools` names a command that exists.** Its listing began « nika tools — 29
builtins » and ended « machine: nika tools --json », but `nika tools` is not a command and exits
2. The listing now names `nika catalog --tools`, and its footer points at
`nika catalog --tools --json`, the projection that carries the model-facing JSON Schemas.
- **`nika check --fix` no longer renames an argument when its value is the mistake.** A literal
outside a builtin's closed set (`nika:inspect` `view: record`) carried its nearest member as the
key-rename suggestion, so `--fix` rewrote the file to `records: record`. The nearest member is now
said in the refusal (« did you mean `records`? ») and `--fix` leaves the value for you to change.
- **`nika init --example` founds a project its lesson can run in.** The example lane copied the
workflow alone: 19 lessons that read files under `examples/fixtures/` (`csv-chart-report`,
`meeting-actions`, …) failed their first suggested run with « file not found », and
`10-compose-pipeline` founded without the child it invokes, so its own audit refused
NIKA-COMP-001. Init now stages what `nika try` stages: the fixtures under the project root, where
runs resolve them, and composition children beside the lesson in `workflows/`. Each staged file
is a `created` row, and an existing file is never overwritten.
- **`nika init` keeps the `nika serve` token and job state out of Git.** The generated `.gitignore`
covered only `.nika/traces/`, while every `nika serve` surface teaches minting the HTTP Bearer
token at `.nika/serve.token` and the resident keeps job events and results under `.nika/serve/`.
In a founded repository `git status` listed the token as a file to commit. Init's ignore section
now covers both, as does the same adds-only section `nika compile` writes beside a workflow, and
`NIKA.md` says so. Repositories founded earlier keep their section; add the two lines by hand.
- **The agent kit links the `nika arm` reference instead of a guide that was never published.** The
operating and migration skills, the language rule, the session map, the kit README and the
check-on-edit message on a `nika.yaml` finding pointed at docs.nika.sh/guides/projects-and-schedules,
which answers 404; they name docs.nika.sh/reference/arm, and `nika init` lays the corrected files.
The operating skill and the project reference also say what 0.123.0 fires: `sauter` and
`rattraper-une-fois`, with `rattraper` and the bounded `{ rattraper: N }` read UNREADY. The Cursor
manifest lists `/compile` where it still named the retired `/new`.
- **The agent kit separates signing a workflow from sealing a run.** The operating skill's audit
checklist asked for a run « signed (`nika sign`) », but `nika sign` binds a workflow file to its
author, and only the run key `nika key init` mints seals a journal. The checklist now asks for
both, each with its own command. The portable plugin manifest's description opens with the
README's « Say it once. Keep it as a file. » instead of the retired intent-as-code pitch.
- **The editor's `secrets` hover names the sources that work.** `nika lsp` described `secrets:` as
« Masked, vault-backed references ». Spec 01 §secrets withdrew `vault`, and no engine resolves it.
The hover now says the references point at an environment variable (`source: env`) or a file
(`source: file`).
- **A registry child is refused as a present limit, not a coming lane.** `nika check` and the run
both refused a pinned `registry:owner/name@1.2.0` child with « registry child execution lands with
the registry lane », which promised a release. They now say « this engine does not run registry
children; run a filesystem child (`./child.nika`) », still as NIKA-COMP-001 and in the same words
at check and at run.
- **`nika trace verify --help` lists every exit the verdict returns.** The help named exits 0, 2 and
3, while a journal that never reached a terminal frame (a run in flight or a writer that died)
exits 5 INCOMPLETE. It now lists 5, names buried seals and replay divergence under exit 2, and
counts its four named refusals as four.
- **`nika try` no longer hands over a command for a file that is gone.** After a rehearsal, the run's
closing line said « explore: nika run /var/folders/…/nika-try-01-hello/01-hello.nika --json > … »,
but the rehearsal room is deleted when `try` returns, so that command could never run. A try
rehearsal now ends on its own line, which names the example and `nika compile --list`; workspace
runs keep their explore line.
- **Refused type names state their status instead of a coming wave.** A `returns:` or `type:` naming
`result`, `artifact`, `secret` or `money` was refused with « is reserved — lands with outcomes
(W5) » (or W4, W-DEC). Spec 09 keeps only `artifact<mime>` reserved, never as a delivery claim,
and withdrew the other three from the grammar. The refusal, still NIKA-TYPE-001, now says « is not
a type — reserved by the spec and not implemented » for `artifact` and « withdrawn from the type
grammar (spec 09) » for the others.
- **An unsealed journal now points at the run key, not at `nika sign`.** `nika trace verify` said
« `nika sign` seals future runs » and the Session's `/proof` said « `nika sign` signs future runs ».
`nika sign` binds a workflow file to its author; it never seals a run. Both now say that a run key
(`nika key init`) makes later runs seal, which is what turns the next journal SEALED.
- **Secret teachings no longer offer the withdrawn `vault` source.** The refusals for a missing or
unknown `source:` listed « vault · env · file », and the `secrets-store` hint said a `vault`
secret would work « until vault resolution ships » and fail with NIKA-1702. Spec 01 §secrets
withdrew `vault`, and the run reports the failure as NIKA-VAR-001. The teachings now list `env`
and `file`, and the hint says `vault` is withdrawn from the contract and names NIKA-VAR-001. A
`vault` secret still parses as before.
- **The welcome screen and the access errors no longer advertise a Nika Cloud.** `nika welcome`
said « Local first. Cloud when you want it. » and « Coming: Nika Cloud · our models, our tools, your
workflows online. », and its envelope listed an unavailable `cloud` rung; access errors told the
person to « pick Nika local / Nika Cloud ». There is no hosted Nika service: the screen now opens
with the README's « Say it once. Keep it as a file. », the cloud rung is gone, and the errors
point to an API key or a local engine (`nika doctor --verbose` lists them). The featured local
model is described as open weights that need no API key, not as « ours » or « free » (a local
model is unpriced).
### Security
- **Big-integer arithmetic is past its October constant-time advisory.** `crypto-bigint` 0.7.6
keeps subtraction, zero counting, division and Montgomery multiplication free of secret-dependent
branches (RUSTSEC-2026-0387), in the workspace and fuzz lockfiles.
## [0.123.0](https://github.com/supernovae-st/nika/compare/v0.123.0-preview.1..v0.123.0) - 2026-10-23
Nika 0.123 is the open-source reference release of Nika: the Rust engine, the `nika` command
line and its terminal workspace, and a TypeScript SDK (`@supernovae-st/nika` 0.123.0) that runs
`.nika` workflows from an application. After this release the project is maintained: security
fixes, bug fixes and compatibility. No hosted service is planned.
What it does:
- **Say it once, keep it as a file.** Describe an outcome in the terminal workspace (`nika`);
Nika asks only for what is missing, writes a `.nika` workflow, checks it and, where it can,
rehearses it on a copy of your files before you save or run anything.
- **Audited before it runs.** `nika check` reads the whole file first: the effects it may
perform and nothing else (`permits:`), where secrets go, literal credentials, cost estimates
and the lethal trifecta, and it names the repair where it knows one.
- **Metered and recorded while it runs.** `--max-cost-usd` caps a run, every run is recorded
under `.nika/traces/`, and a run paused at a human gate resumes with the person's answer.
- **Scheduled by your own machine.** `nika arm` emits launchd or systemd units, with an env file
for the provider keys a scheduled run needs.
- **Your choice of model.** The workflow file names its model, and Nika never substitutes
another one silently.
Known limits are listed in plain words, each with a way around it:
[Known limits in 0.123](https://docs.nika.sh/changelog/known-limits). In short: a rehearsal
does not call models and replays only plain GETs of pages already observed; writing a workflow
with a large model can take minutes; the terminal mixes English and French lines; the VS Code
extension stays at 0.120; scheduled runs need your keys in an env file.
The changes since the 0.123.0-preview.1 prerelease follow; those from 0.122.0 to that
prerelease are in its own section of this changelog.
### Added
- **An ACP agent can lead a Session conversation with Nika's tools alone.** The harness opens
one persistent session per conversation (`SpawnedHarness::converse`) and sends one prompt per
user entry; the agent keeps the history and runs its own loop with no turn limit. Claude Code
gets the audited one-shot surface without `maxTurns` (no built-in tools, strict MCP, no
settings, plugins, skills or agents, nothing persisted, summarized thinking) and exactly one
MCP server, Nika's; Codex mounts it from its configuration, read back before the spawn with
every other server disabled. The transport follows the agent's advertised HTTP MCP capability
and is recorded; an agent that cannot mount what Nika offers is refused, never sent down the
one-shot path. Only Nika's own tools are allowed, once; every other permission is rejected and
recorded. Stop sends `session/cancel` once per turn, and each turn ends with its activity
record, where tool frames are expected activity.
- **An admission tariff can name the dated models its provider serves, and a
cache-write rate.** `nika/inference-admission@1.2` adds two optional fields.
`served_models` lists the models the provider's own documentation names as
served for the selected model (a dated snapshot its replies report): a reply
naming one of them answers that same route at settlement, in a restored
checkpoint and in the per-dispatch record, and any other name leaves the
charge unknown. `cache_write_nano_per_token` prices input written to the
provider's cache, and a write rate above input reserves the whole context as
written; a tariff without it leaves a call that wrote the cache unknown, now
at settlement as well as in the per-dispatch record. No current row uses
either field. Pricing observations name `table_schema` `@1.2` and a
`cache_write_rate`, and records written under `@1.1` still read.
- **Claude Sonnet 5.5, Opus 5.5, Haiku 5.5 and Fable 5.1 are admitted on the direct API.**
Each row reads Anthropic's pricing page of 2026-10-15: input, output, cache hit and
5-minute cache write, a 1M window (Claude Haiku 5.5 up to its 100,000-token lower
tier) and 128K output. The model choice offers the four first for an Anthropic key,
Sonnet 5.5 leading, each with its admitted output price. A Session allowance still
refuses a Claude author until the Anthropic wire settles bounded calls: a qualified
route is never sent in another wire's shape.
- **Anthropic requests ask for prompt caching.** A conversation, or a request with tools,
now carries the automatic cache marker (five minutes) and a breakpoint on its last tool,
so a later turn reads its prefix at a tenth of the input price. A one-shot request sends
no marker, since a cache write costs a quarter more than the input it holds, and a
request that names its own `cache_control` keeps it unchanged.
- **`nika arm --emit <OS> --beat <label>` emits one schedule's unit.** With `--write`, only that
beat's unit is written, and every other beat's unit stays as it is. Another beat that could not
arm no longer refuses the emission. An unknown label refuses with the labels `nika.yaml` gives.
- **An authoring call's receipt says how many requests it sent.** Each
authoring call in a compile's receipt records `requests_sent`, read
from the provider's own record of its dispatches: a call that left
twice says two, a call its deadline cut says one, and a harness route,
which counts its own invocations, says nothing.
- **A kept run says whether it ran a component of saved bytes.**
`KeptRun::component_run` reads a journal's frames of the run's own execution:
its one start must name the hash of the exact saved bytes, and each of the
component's tasks is settled by that execution's frames alone (`completed`,
`skipped`, `failed`, `recovered`, `cache_hit`, … or `absent`). The run's exit,
the last start a journal holds and a child run's task of the same name never
stand in for it; what the frames cannot attribute is `Unknown`, with why.
- **A new conversation in the same project.** `/new`, and `SessionRuntime::begin_new_conversation()`
for hosts, seal the current conversation beside its journal under a dated name and start the
next one empty. Nothing is deleted, and its receipts and any unknown charge stay on record. The
project's workflows, schedules and runs, the chosen intelligence and a paused run's gate carry
over, and the Session says so. Nothing begins while a decision waits for the person or a run is
under way. A restart resumes the new conversation.
- **A run is told the instant it serves.** `${{ run.scheduled_for }}` reads the slot a
`nika arm fire` or a `nika serve` schedule fired the run for, the instant
`nika test --at <instant>` names, or else the run's own start, as RFC 3339 UTC. The
run's opening frame journals it with `scheduled_for_source` (`slot` · `at` · `start`),
and a child workflow the run calls serves the same instant. Under `manqué:
rattraper-une-fois`, `${{ run.missed }}` reads the silence a catch-up covers, `{ first,
last, count }` (journaled as `missed`), and null otherwise. A `nika serve` catch-up's job
carries that silence on its schedule origin as `missed_first` and `missed_count`.
- **A run's result says the ceiling it was admitted under.** The result of a run said its cost
but not the most it was allowed to cost, so asked whether her $0.10 ceiling held, the
conversation answered that it could not tell which ceiling applied and pointed to the trace
file. The result now says « ceiling · $0.10 · the most this run was allowed to cost », read from
the run's own start frame, and the conversation is told the same.
- **A Session can speak the host's language before the person writes.**
`SessionRuntime::with_initial_language` takes the host's guess from its locale. The restored
notice and `/status` then read in French under `fr_FR` from the first screen. A line the
person types, or one a restored conversation kept, still decides. The restored notice is now
said in the restored language. A turn that parks with nothing to ask says how to go on,
rather than showing an empty question.
- **A knowledge release can carry a block proven on recorded worlds.**
Profile r2 admitted a block only as `CHECKED`, so a block whose
outcome was qualified on recorded worlds could not say so. A block may
now claim `OUTCOME_PROVEN`. Its check receipt stays bound as before,
and an `outcome_receipt` binds the same bytes, the target's
specification and the target binary to its worlds: at least one, each
judged `OUTCOME_CORRECT` and run at least once. Releases that do not
claim the level are admitted unchanged.
- **`manqué: { rattraper: N }` fires the N latest missed slots.** A `nika serve` project beat
declared with the bounded catch-up fires up to the N latest missed slots, one run each,
oldest first, each run told only its own slot; the older slots are skipped first, journaled
once. `nika check` reads the form (N a whole number from 1, `{ rattraper: 1 }` being
`rattraper`); `nika arm` refuses it by name, as it refuses `rattraper`: catch-up is served.
- **The catalogue lists current Anthropic, OpenAI, DeepSeek, xAI and
OpenRouter models, each with the source and date of its limits.** Claude
Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 (1,000,000-token window,
128,000 output tokens), GPT-6 Astra, GPT-6.1 Sol and GPT-6 Luna (1,050,000
and 128,000), DeepSeek V4 Pro (1,000,000 and 384,000), Grok 4.7 and Grok 4.3
(500,000 and 1,000,000-token windows; their output caps are OpenRouter's
listing, xAI publishes none), and the five admitted OpenRouter routes at the
smallest window and output of their endpoints. Every admitted route now
reads a dated window and output cap. Claude Haiku 4.5's output cap is
corrected to 64,000 tokens (it read 8,192), from Anthropic's model page.
- **The provider catalogue records where a model's limits were read and when
its provider retires it.** `nika/llm-providers@1.2` lets a model row carry
`limits_source` with `as_of`, and a `lifecycle` (`retires_not_before`,
`successor`, `source`, `as_of`) whose successor is another model of the same
provider; `nika_catalog::model_facts` returns them. Two nicknames may name
one model only as aliases with identical limits and facts. Rows without
these facts keep their exact shape.
- **`nika-catalog` states a model's output facts.**
`model_output_limit` returns the output limit the catalog records with
where it is recorded, `generated_cap` the cap a caller generates for a
reasoning model within that limit, and `output_caps` the facts an
authoring seat reads before it writes (reasoning recorded or not, the
documented effort levels, the suggested cap). `nika-compile` now reads
them there; no record changed.
- **The catalog records how a model reasons, and what a `.nika` file can do about it.**
`ModelCapabilities.reasoning_facts` says, per capability rule, whether the model reasons
when the request names no reasoning control, whether that reasoning is spent inside the
output cap, and how a `.nika` file turns it off on this engine (`ThinkingOff`: by default,
a zero thinking budget, not reached, or never). DeepSeek Flash and V4 Pro, Gemini 2.5
Flash and Pro on the Gemini API, and Claude on Anthropic's API carry recorded facts; every
other model stays unknown rather than assumed. Tests pin the facts to what each wire sends.
- **A conversation's workflow can be verified like any compiled one.**
`verify_document` verifies a document a Session conversation wrote
exactly as the compiler verifies its own candidates: the fidelity laws
over the world the host observed for the request, the core's finish, a
run in the room when the host lends one, then the whole-request verdict
of the permitted judge (a decision model, else the authoring model),
never asked again on bytes it rejected. It is ready only on that
verdict: a doubt nothing located is held, a located defect keeps the
workflow as the preview with what to repair, and with no judge the
request stays pending.
- **A dropped value needs words that remove it, as the judge reads them.**
When a conversation's revision drops a value an earlier proposal bound
(a source, an output), the Session cites the person's words that remove
it. Those words were only checked to be the person's: « I wanted
today's articles » could drop a source no one asked to change. The
verifier now asks its judge, for each such claim, whether the words
remove that value; a claim it does not confirm keeps the workflow from
being proposed, and the conversation is told which value to keep or
ask about.
- **The decision seat's journal says how long each request took.** Every
System One request that left, a single question or one request of a
batch, now records `elapsed_ms` in the Session's decision-seat
observation (`nika/session-decision-seat@2`, additive): the time its
transport took, measured by the host. A request never sent records none.
- **`nika doctor --json` states its scope.** The report carries
`"scope": {"mode": "audit-wide"}`: it audits the machine as a whole. A
question about one workflow stays with `nika check`.
- **A draft read whole runs at its model's documented low effort.** When
every model step of a request is a draft (write, summarize,
translate), the reader admits the request whole with no constraint
beside it, and the model's exact catalog row documents a `low`
reasoning effort (DeepSeek Flash and V4 Pro today), the compiled
workflow declares `run.reasoning.effort: low` and the proposal says
so. On Flash, one measured 16.7k-token summary spent 83% of a
16,384-token cap reasoning at the default effort and 69% at low; at
the default effort the same step had ended with no answer. A
candidate that already declares an effort or a route keeps it, and a
model-written candidate gains one block, never a reformatted file.
The level reaches only the model's exact direct route: behind a
base-URL override (a gateway), every request that carries it is
refused with nothing sent.
- **What a workflow reaches reads in French.** A French proposal card showed « à l'exécution ·
model output estimate · unbounded: `grade` declare(s) no `max_tokens`; Run admission is
separate » and « connected: anthropic/claude-sonnet-5-5 (remote model) ».
`nika_display::check_render::review::effect_row_fr` says each row `effect_rows` writes in
French, and `World::summary_fr` the reach summary. Paths, hosts, tools, models, tasks and
amounts stay as written. A row they do not know is returned as it is.
- **`egress/001` warns about a private key in a prompt.** A `prompt:` or
`system:` text holding a PEM private-key block (a line
`-----BEGIN … PRIVATE KEY-----`) is a credential bound for the model
provider: the linter flags it and the check verdict lists it in
`hints[]`, on inline text and on the text of a prompt file alike, where
the door read that file. It refuses nothing and never repeats the text;
a secret the provider needs belongs in `secrets:`.
- **A finding has a canonical key a host can compare across revisions.**
`fidelity::keys::finding(kind or target, message)` keys a finding on
its kind, its law's head and the subject it quotes, for example
`conversation:invented-literal:./news/digest.md` or
`semantic_verification:write the digest`. A Session can tell whether
the same defect still stands on a new revision without comparing
prose: rewording or an appended remedy keeps the key, and another
subject or another law changes it. No finding's target changes.
- **A harness authoring call's record tells thinking from answering.** The
`activity` of a terminal ACP authoring record now also says when this
session's first thought and first answer arrived (`first_thought_ms`,
`first_answer_ms`) and which update it received last, by its closed word
and time (`last_update`), each null before any. A receipt can now say a
call was silent until its first answer, or that its last update before a
silence was a thought. The fields are additive; counts, deadlines and
acceptance are unchanged, and no frame text is kept.
- **Hosts name a zone as the Session does.** `nika_session::zone_words` (« heure de Maurice »,
"Mauritius time") and `nika_session::zone_place` (« Maurice », "Mauritius") give a host the
words the Session itself uses for a schedule's zone, so a run list and a cadence never name the
same zone two ways.
- **Hosts read a declaration's schedule in words.** A candidate that declares a schedule in
`nika.yaml` carries `Candidate::schedule` (`ScheduleWords`) in the person's language: when it
runs, what a missed run does, and its ceiling per run. A host shows these words instead of
the cron. The work snapshot's compile draft is kept only while one of that compile's own
questions waits, so a host never shows a draft beside another question.
- **A declared schedule can be installed on this Mac from the conversation.** Once a schedule
lands in `nika.yaml`, the terminal Session asks « Installer le déclencheur sur ce Mac ? ». The
question names the workflow, its cadence and zone, the missed-run policy, the ceiling per run
and the effect (one launchd unit run by the program that wrote the workflow). « Installer et
activer » writes the unit as `arm --emit launchd --write` does, loads it and says when it runs
next. « Garder déclaré seulement » installs nothing, and « désactive-le » unloads and removes
it. Nothing is installed without that yes. Hosts other than the terminal on macOS keep the
schedule declared, as before.
- **The embedded knowledge release is r3, the Foundry enrichment.** It adds 16 families,
14 facets, 23 packs and 83 patterns written in the studio's own words. A family now
recommends its packs, so the door's family, pack, pattern and block path reaches the
qualified blocks (`feeds-items-on-local-date`, `json-threshold-report`).
The r2 release it replaces stays embedded for earlier pins, which reopen its exact bytes.
- **`nika-mcp` serves a Session's tools to the agent leading its conversation.**
`conversation::ToolServer` lists the session's `nika/author-tools@0` definitions and relays
each call of a listed tool to the session over Streamable HTTP, bound to `127.0.0.1` on an
ephemeral port with a bearer minted for that conversation alone and always required. A tool the
session does not list never reaches it; a failed call returns `isError: true`. Connections are
served side by side under the same bounds as `nika mcp --http`, and the session receives one
call at a time. The server interprets nothing and grants nothing. A stdio bridge to the same
server exists as a library; no CLI starts it yet.
- **A pinned `mcp:` step is judged against its approved schema.** Once the
project registers the server, approved its pins under the command,
arguments or URL the registry names now, and the pins hold the tool,
`nika compile` judges an `invoke:` step's literal arguments against the
pinned input schema (an argument a template builds is wired, the run's to
judge) and records each verdict on `decision.mcp`, without reaching the
server. A registry re-pointed after its approval keeps the hold until the
server is approved again, as the run's own verification requires. The
served compile door judges the same step on the server project's pins and
records the verdict, but keeps the step held: that server runs no MCP
step. Any other answer keeps the source-only hold, in words that say what
to do; permits stay the only authority over what a step may do.
`nika check` advises, without blocking, on a registered server whose pins
were never approved or do not hold the tool.
- **An agent can list the models each role can use and have its pick
validated.** `nika_providers::model_choice` keeps separate offers for
the Run, Author and decision roles, built from this machine's routes
and the catalogue, and answers a selection with one exact target, a
delegated pick with its rule, a targeted choice, or an exact refusal
with real alternatives. No id is invented and no closed provider,
route or protocol is changed.
- **`nika check` names a model's published retirement floor and its
successor.** A workflow on `anthropic/haiku` or Claude Haiku 4.5 still
checks clean, with one advisory line under the green MODELS line: Anthropic
does not retire it before 2026-10-15, and `claude-haiku-5-5` is available to
name. The nickname keeps its model; nothing switches silently.
- **OpenAI's GPT-5.2, GPT-5 mini and GPT-4o mini are admitted on the direct API.**
Each standard-tier row reads its model page of 2026-10-12: the input, output and
cached-input prices, the context window and output cap, and the dated snapshot
OpenAI's replies report (`gpt-5.2-2025-12-11`, `gpt-5-mini-2025-08-07`,
`gpt-4o-mini-2024-07-18`), which settles the same route. The pages list no
cache-write price, so a call that wrote the cache stays unknown. GPT-6 direct
rows are not admitted.
- **An OpenAI conversation keeps its prompt cache together.** A request can name the conversation
it continues (`InferRequest::cache_key`, a hash of the conversation's id); the OpenAI wire sends
it as `prompt_cache_key`, so one conversation's calls route and account their cached prefix
together. Other providers ignore it, and a caller's own `prompt_cache_key` stays unchanged.
- **Five paid OpenRouter routes are admitted.** GLM 5.3 Flash, MiMo V2.6 Flash, Step 5
Preview, HY4 Preview and GPT-6 Luna have exact admission tariffs read from
OpenRouter's endpoint listings of 2026-10-12: the highest price of any endpoint, the
smallest context and output limit, and, for GPT-6 Luna, an input bound below its
long-prompt surcharge. A Run no longer needs an unknown-cost choice for them, and a
Session can reserve them.
- **`prompt:`, `system:` and `schema:` can name a project file, and
`nika check` judges it.** `{ file: <path> }` resolves from the folder of
the workflow that names it, stays inside the root workflow's folder, and
is read only inside that workflow's `permits.fs.read` boundary, never
through a symbolic link: a broken path law is `NIKA-FILE-001`, a missing
file `NIKA-FILE-002`, more than 65,536 bytes or bytes that are not UTF-8
`NIKA-FILE-003`, a schema file holding no JSON Schema `NIKA-TYPE-007`.
The text or schema read then meets every law the same field written
inline meets, in a composed child too, where each finding names the
child, task, field and path. A door without the files names them as
unread (`judged.project_files: false`). No run carries project files
yet: `nika run`, `nika test` and a captured world refuse such a workflow
before any task, and the runtime refuses a file no compose step read,
at launch and at the `infer:` or `agent:` dispatch.
- **An unstable build trims the authoring prompt for evaluation.** The
`unstable-prompt-trims` feature of `nika-compile-seats`, off by default,
makes the document door show the language's schema without its
descriptions (29,749 bytes compact, 9,705 without them). It also narrows
the callable contracts to what the reader's typed plan reaches: `fetch`
only for a fetched, looked-up or explored source or an outbound effect,
`convert` for a computation, `prompt` for an approved effect, `notify`
for a sent one. A default build sends the whole prompt, as before; the
feature exists so an evaluation can measure authoring quality with and
without the trims.
- **An Anthropic key can be checked on request, without running a model.** `check_key` (and
the Session's `check_api_key`) sends one GET of Anthropic's model list with the key, its
API version and the endpoint a run would use (`NIKA_ANTHROPIC_BASE_URL` honored). It
answers valid, refused (401 or 403, in the provider's words with the key withheld),
unreachable or unknown, spends no token, never follows a redirect or retries, and runs
only when a person asks. Another provider answers unknown without a request: an
OpenAI-compatible model list may answer without a key, so it would prove none.
- **A trial run shows what each step received and returned.** A host's
rehearsal report can carry each task's flow, the items it received
and returned. The verifier is shown it with the trial's outputs, a step
that returned nothing from a non-empty input marked, so a filter that
kept no item of today's news is a fact the verifier weighs; a host's
card can show a passing trial's flow.
- **A rehearsal runs `nika:validate`.** A candidate that checks data against a JSON
schema is tried in the room before its proposal, instead of being refused as a
tool the rehearsal does not know; the check reaches no network and no file.
- **`nika check` reads the facts of the run.** `${{ run.scheduled_for }}` and
`${{ run.missed }}`, the closed set of facts the engine tells a run, resolve in
every value position and in `when:` and never make a data edge. Any other
`${{ run.X }}` is an unresolved reference (`NIKA-VAR-001`) whose message names
the two facts; a near miss (`run.schedule_for`) is offered as its rename, a
typo'd root is offered `run`, and `${ run.missed }` or
`{{ run.missed }}` without `${{` is caught as for the other namespaces.
- **A schedule's history says its firer, its last skip's reason and its catch-ups.**
`ScheduleHistory` now says whether a resident `nika serve` holds the project's server lease
(`serve_running`), and when the unit was last disarmed and how many runs fired since
(`last_disarmed_at`, `fired_since_disarm`). `LastDecision` now carries a skip's reason
(`missed:1`, `overlap`) and the slots a catch-up run answered. Both types have `new()`
constructors. The unit's presence is asked of the Session's trigger host.
`nika_cadence::ledger::decisions` returns each decision with its instant, reason and slots.
- **Activation offers its ceiling and missed-run policy as choices.**
`SessionRuntime::waiting_activation_choices` states the activation's
finite question (its sentence and one short line, its options, and
whether another typed value is admitted). The ceiling per scheduled run
offers 0.10, 0.25, 0.50 and 1.00 USD, recommending the smallest that
holds about three times the workflow's last observed run (its trace's
total) and 0.25 when none was observed, with why in the label. Another
amount is still typed, the same grammar validates every answer, and no
amount is ever applied without the person's answer.
- **The selected intelligence leads the Session's conversation.** With an API or a local route,
a person's line now goes to one loop over that model and the Session's tools
(`nika/author-tools@0`): it reads the project and the language, writes and edits the candidate,
asks several questions together with their own identities, and proposes. Every value the
candidate binds carries its provenance (named, delegated, derived, answered, offered or kept)
checked against the person's own cited lines; a value they saw bound is never dropped
silently; a reopen keeps the values and their provenance, never a question or a proposal.
Saving and running stay the consent door's acts: the person's words open it only for the exact
candidate they were shown, and a candidate whose effects changed waits for their consent.
`NIKA_SESSION_DRIVER=rounds`, read when a host door opens the Session, keeps the round driver
during the transition.
- **A Session host answers one question by its identity.** Both Session doors take `answer`
(`command`, `snapshot`, `question`, `line`) and `delegate` (`command`, `snapshot`,
`question`) and advertise `sessionQuestion`: a client that read several questions asked
together, or the choices a proposal shows, binds its reply to the one it names and the others
keep waiting, where a plain line answers every question whose offers it names. The host takes
the identity only among those the current snapshot asks (`unknown_question` otherwise, with
nothing sent), and `delegate` leaves that choice to Nika as the person's act, never as words.
- **Every model and tool call of a Session turn reaches the host.** The
conversation's model requests are activity frames naming their rank in
the turn, the route asked, when they left, how long they took and the
tokens the provider reported; tool calls also name when they began and
their reply's size. Neither carries a prompt, an answer, arguments or a
reply.
- **A conversation's candidate is tried before it is shown.** Each public
GET source the candidate names is observed once for the conversation
through the guarded observer (no credentials, no private address,
bounded), and the observed room replays those pages to a run of the
candidate where nothing leaves: a GET of exactly an observed address is
answered with its page, every other request is refused. A step the trial
cannot run (a model step, a request other than GET, an effect outside
the room) is named, never run as itself. A failed trial, or a filter that
keeps nothing of the items it read, goes back to the author with its
facts before any proposal; a passed trial is bound to the proposal, which
says what it ran on and what it did not run.
- **A Session conversation proposes only a workflow its judge found
ready.** Every `verify` and `propose` of the conversation's candidate
now faces the verification a compile round of the same request faces:
the laws over the files the request names, then the whole-request
verdict of the Session's judge (its decision model, else its authoring
route; a seat's agent judges through its own one-shot). Anything short
of ready goes back to the author with the findings, and nothing is
shown. A verdict that declined some bytes is kept for the conversation
and never asked again on those bytes, and a ready verdict on unchanged
bytes, words and selections is reused.
- **A conversation's run can be steered, stopped and watched while it works.** The Session
host (`nika/session-host@1`, both doors) gains `steer` and `follow_up`: a line sent while a
turn of the conversation runs is queued with its identity (`l1`, `l2`, …) and shown in
`busy.queued`, then enters after the calls under way (an ACP agent is asked once to stop) or
when the run would end, as the person's next cited line; a line no run reads is refused,
never kept for later. Stop of such a turn now settles as the typed outcome `stopped` (how it
reached the intelligence, the lines returned unsent, the draft kept). Each tool the
conversation's intelligence calls reaches the event stream as typed activity (call, tool,
started/finished/failed, elapsed), never its arguments. On `nika/session-work@0`, additively:
`queued`, and a `choice` on each offered model this machine's inventory knows (route,
billing, list price).
- **One edit call, several passages.** `candidate_edit` takes `edits`, a list of old and
new passages applied as one revision: each old passage is found exactly once in the
candidate and none overlaps another, so an author repairs several places in one call.
- **A failed run goes back to its author.** When a run the person asked
for fails, the author receives the run's facts and proposes the repaired
revision with what changed; a complaint about a run's result starts the
same repair. The repaired revision runs again only on the person's words
or consent. A revision that replaces a value the person saw bound says
so in words.
- **A failed tool step says why.** The Session's activity frame for a
tool call that failed carries the tool's reply (`tool.reply`) as a host
may show it: its obvious secret shapes redacted first, on one line, at
most 512 bytes. A call that started or finished carries none, and no
frame carries a call's arguments.
- **A gate shows its values, structured.** A run paused at a
`nika:prompt` gate journals the values its message renders as one
object (`values`), bounded, and names the keys it withheld with why
(`withheld`: a value derived from a secret, one past the bound, one
that did not resolve). A Session's gate outcome carries both beside
its text, so a host or a consent review binds the exact values shown.
- **The Session host's activity frames say what each finished call cost.** An activity frame's
`call` now carries `cost` once the call returned, as the Session settled it:
`{"kind": "priced", "nano_usd": "120000000"}` (nano-USD at list price, as a decimal string: an
estimate, never an invoice) or `{"kind": "unpriced"}`. It is absent while the call runs, when
nothing settled, and when its producer states nothing. A host can sum the priced calls for a
live preparation cost, as the terminal does. The published contract now describes the frame's
`call` (`SessionCall`) and its `cost` (`SessionCallCost`).
- **Intelligence options state each model's prices, context and lifecycle.**
Every option naming a model now carries typed facts for any host:
`ModelFacts` gains `input_usd_per_million`, `context_tokens` and `legacy`
beside the output price, from the route's qualified tariff or else the
catalogue's list row on a metered route. Unknown stays absent, never
zero; nothing is selected or called by stating them.
- **The intelligence picker recommends one model.** While the choice is
open, `Work.intelligence.recommended` names the option recommended for
the conversation: the operator's saved default when this machine serves
it, else the default model of the first configured provider in catalogue
order (an Anthropic key yields Claude Sonnet 5.5). It selects nothing.
- **The bytes a verification judged are kept.** A conversation's Session keeps each
candidate its verifier judged, redacted and bounded, in its history as
`verified/<sha256>.nika`, the digest its verify record names, so a hold can be
checked after the candidate is rewritten.
- **A proposal shows its open choices with their defaults.** When the author
can recommend a value the person left open, such as the run model or an
output folder, it writes the recommendation in the candidate and proposes
at once, showing the choice with its default and the other options
instead of asking first. The person's yes keeps every default, bound as
the option they picked with that line; naming another option binds it and
the author revises; nothing is saved while a choice the proposal does not
show is open. The author tool contract is `nika/author-tools@1`.
- **After a result, an optional « what would you like to adjust? ».** When a run of the workflow
the conversation proposed succeeds and writes its result, the terminal offers, at the free
prompt, the dimensions that workflow's own tasks have (depth, length, topics, sources,
presentation), « Nothing to change » and « Later ». Picking dimensions asks Nika to ask only
what is still missing and propose an amended workflow, approved as any other; the run of a later
revision of that same request then offers one neutral opinion (suitable for this use, still
needs changes, wrong result, not evaluated yet, later), with no default. Only a choice selected
in the offer answers it, and only while nothing else waits: words typed instead always go to the
conversation, whatever they read as; asking for a run or the next run closes the offer, the same
workflow bytes are never offered twice (a reopened conversation included), and Esc answers
nothing. An answer changes nothing by itself: it is kept with the exact request, revision, run
and files it is about, never as a permission to save, run or send.
- **A conversation can switch its decision seat and save its default.**
`choose_decision` makes Jev (only the seat the host named) or the author
alone consult the next turn's finite choices; the author alone clears a
refusal the earlier seat caused, and an unavailable Jev is refused with
its reason. `save_operator_default` writes the kind, model, decision seat
by name and time to `~/.nika/session-intelligence.json` atomically, never
a key; a default saved deciding alone opens new conversations that way.
- **A Session Stop now reaches the Run it handed off.** While a `save & run`
or a run line executes through the native door, a `stop` command asks that
Run to stop at its next wave boundary, as a first Ctrl-C does: the work in
flight completes and is counted, the next tasks are cancelled and the trace
seals one cancelled terminal. A Stop that arrives before the run child
exists is applied when it starts (before the spawn, nothing starts). The
receipt says `stop_requested` until the Run takes the signal and
`run_stopping` once it has; the snapshot's phase reads `stopping`; the
result adds `run_stopped` when the trace sealed, `run_aborted` when the Run
ended without sealing it, and the work snapshot's run gains `sealed`, so an
interrupted run that stopped is told from one cut mid-flight. A replayed or
second Stop sends nothing more and never escalates to an abort. A door that
cannot stop its runs (the resident's job door, for now) still answers
`run_underway`.
- **The last run a Session keeps names its job, its receipt, its outputs and its verified seal.**
For a run `nika serve` ran, the kept run now records the job it ran as, the receipt and the
declared outputs that job serves (kept whole up to 16 KiB encoded; larger ones stay with the
job, at `GET /v1/jobs/{id}`), and whether `nika trace verify` accepts the journal's
`run_sealed` record and its signature (`seal_verified`, `signed`), never a seal read from the
receipt's chain head. Forward
compatibility: these parts are written only for served runs, and an engine older than this one
reports such a kept run as unreadable and keeps its bytes unchanged, so nothing is lost; the
kept value of a native run is unchanged.
- **Both Session doors advertise `sessionSteering`.** The native identity
(`nika --sdk-identity`) and a resident serving Sessions (`/health`) name
the word beside `sessionHost` and `sessionIntelligence`, so a client tells
a door that takes `steer` and `follow_up` from an older one, and refuses
those lines itself, before it sends one.
- **One contract for the tools a Session serves.** `nika_session_change::tools` types the
`nika/author-tools@0` tools once for Nika's own agent loop and for an ACP agent reaching them
over MCP: the 17 fixed tool names, `ToolDef`, `ToolCall` (with the caller's tool-use id),
`ToolReply` (text, failure, and whether the turn ends on it) and the `SessionTools` trait the
Session implements. It grants nothing: saves, runs and consents keep their own doors.
- **A proposal's activity carries its original verification records.**
The activity of a successful `propose` tool step carries a separate,
versioned `tool.verification` receipt: the compiler's semantic
verification records for the candidate it shows, unchanged, with the
candidate's digest and whether this proposal reused an earlier
verification. A receipt the secret-shape filter would alter is withheld
whole, and no other tool step carries one.
- **The work snapshot carries a conversation's values, delegations and questions.**
`nika/session-work@0` gains, additively, `bindings` (each value, its role and its provenance
with the person's citation and words), `delegations` and `questions` (each with its identity,
open or waiting for its prerequisites, and the concrete values its options carry), and
`waiting.kind: questions` when several questions are asked together. A snapshot without a
conversation keeps its bytes.
- **A Session snapshot says how long its knowledge and trial stages took.**
`work.authoring.stages` now carries, as the compile's decision record
states them, the wall time of the Foundry knowledge qualification
(`qualification_ms`) and each trial of a candidate in order (`trials`:
how far it went, `elapsed_ms` and the host's `runtime_bound_ms`). The JSON
door and `/v1/sessions` carry it unchanged; a time the record does not
state is null, nothing is summed, and no output or failure text passes.
- **A System One request can write its options in a chosen order.** `system_one::body` serializes
a request with each question's criteria in an `Order`: `Alphabetical` (today's bytes exactly),
the question's own order, or its mirror, the pair a both-order reading averages. Nothing sends
it yet: requests keep today's alphabetical order until the both-order setting lands.
- **The Session reads each schedule's history, and journals its removal.**
`SessionRuntime::schedule_history()` returns every schedule `nika.yaml` declares, with how many
runs its arm ledger records as fired, skipped, paused, failed or disarmed, its last decided slot
and whether this Mac holds its unit. Reading it creates, locks and repairs nothing.
« désactive-le » now journals a `disarmed` decision in the schedule's ledger and says so.
Installing a trigger from the conversation installs that schedule's unit only: a schedule kept
declared stays without one.
- **The Session says its own sentences in the person's language.** Every French-speaking person
met English questions and cards between the author's French answers. The Session now reads the
language of the person's own lines, English or French, and asks the activation questions (time
zone, missed run, ceiling), proposes the declaration and says what saving and declaring did in
it. A host reads it (`SessionRuntime::language`) to say its own words in it too.
- **The activity card a turn leaves in the workspace now uses the language you write in.** In
a French conversation it reads « Terminé · en moins d'1 s · 1 appel auteur · routage de la
conversation et appels du service de décision non comptés ». Its phases (« Compréhension »,
« Génération », « Réparation ») and its call row (« appel 1 · planifier le workflow · demandé
deepseek/v4 · revenu après 3 s ») follow too. What the Session reported, the model and the
times stay as they are. The card's text from other doors is unchanged:
`ActivityCard::lines_in` is a new variant that takes the door's words, and `lines` still
says English.
- **The workspace's project list and header now use the language you write in.** In a French
conversation the aside reads « Projet » and « Nika · Fichiers ». It names « cette
conversation », titles its groups (« FICHIERS », « MÉMOIRE », « RÉGLAGES ») and states a
workflow's verdict in French (« 2 constats », « non conforme », « run échoué »), and so are
its counts, footer and notes. The header says « projet local », « pas de git · pas de
nika.yaml » and « Préparer avec : » before the selected intelligence, or « non choisie ».
Project names, paths, hosts and the intelligence's own words stay as they are. A click on
« Fichiers » turns the list as « Files » does.
- **A finished run's card lists its steps, models and files.** Below a run's story, the
terminal's card now opens on how the run ended in the theme's own state glyph (`✔ Ran in
16.5s · 14 tasks · $0.034 · 1 model call`, `✖ Failed after …`), then lists each step
with its glyph, wall time, model and cost (`✖ notify · failed: the webhook answered 500`
in the failure colour, beside its words; `⊘ tidy · not run`), what each model cost and
how often it was called, and every file the run reported writing with its path
(`→ ./out/digest.md (write)`). A long run lists its first twelve steps and every
failure, then counts the rest; unknown times, models or costs are left out.
- **One live line says what Nika does while it prepares or runs.** In the terminal, a turn
that prepares reads `Preparing · 45s · 12 author calls · Ctrl+C stop`, then the Session's
own phase words; a workflow the turn runs reads `Running veille-tech.nika · 12s · 6/14
tasks · now: pick (claude-sonnet-5-5) · $0.012 of $0.25 · Ctrl+C twice leaves` (a Run is
never stopped there). Its card opens on how it ended: `Ran in 16.5s · 14 tasks · $0.034 ·
1 model call`. Outside the workspace, whose activity card already says it, a preparation
that called its author ends with `Prepared in 2m 10s · 35 calls · $0.91`. Every fact is
one the terminal observed (typed calls, the run's frames, its clock, the Session's known
cost estimate after the turn); an unknown one is left out, never a placeholder, and the
facts refresh at most twice a second. Hosts can report typed activity with
`Seen::activity` and the spend with `Conversation::spend`.
- **A question that recommends an answer needs no typing.** The terminal opens such a
question with the recommendation selected (each recommended answer checked in a multiple
choice), marked `recommended`, so `Enter` alone accepts it; nothing is sent before that
`Enter`. A « Let Nika choose » row follows the offers and leaves the choice to Nika as the
person's own act, never words: the Session binds the recommended answer as delegated,
whatever another answer's label reads, and on a choice a proposal shows that keeps the
request's save and run. The own-reply field stays. Decisions, settings and the knowledge
choice keep opening on nothing.
- **A source's address reads beside its name in a question's options.** When an option's
label names a site or a data source and then its address after « · » (`TechCrunch ·
techcrunch.com/feed`), the terminal paints the name first and the address dimmed in one
column aligned over the list, beside `recommended` where the author recommends it; a
narrow row cuts the address with a visible mark before it ever cuts the name. The ASCII
glyph column keeps every fact.
- **`PgUp` and `PgDn` page through a long choice list.** In a question,
decision or `/intelligence` list they move the selection eight rows at a
time and stop at either end; the list follows the selection page by page.
- **Reception proofs for the choice surfaces.** The TUI prototype takes
`--demo-surface intelligence|multi` and says back what it receives, and
`qa_surfaces` proves at 80x24, 120x40 and 180x48 that a long model list
pages, filters and applies the selected row, that `Esc` steps back
without answering, and that a multiple choice validates with `Enter`.
- **Question options are counted, checked together and picked by their digit.** In the
terminal, each of a question's first nine options shows the digit that selects it: with
nothing typed yet, pressing that digit moves the selection there and sends nothing (once
words are typed, digits are words, and the `/intelligence` list keeps digits for its
expert route). A multiple choice counts `N of M selected` in its title, `Ctrl+A` checks
every answer (or none when all are), `Space` still toggles one and `Enter` validates the
checked set. The Submit row no longer repeats the count.
- **A decision's question and acts now use the language you write in.** In a French
conversation the proposal's decision asks « Que faire de cette proposition relue ? » over
« Enregistrer le workflow », « Enregistrer et lancer une fois », « Écarter cette
proposition » and « Examiner les changements exacts ». The same holds for a run's cost
decision (« Continuer ce run une fois ? », « Lancer une fois », « Annuler ce run »), a gate's
acts, a schedule's and a run ceiling's way out, and the recovery of a drifted workflow. Every
act keeps its key, so `yes`, `no`, `save & run`, `/show` and `details` answer as before. The
Session's own questions and options stay as it says them, and English stays the default.
- **Digits select a decision's act in the terminal.** In a decision list (Save · Save and
run once · Discard · Inspect, a gate's answers, a missed run's handling), each act now
wears the digit that selects it, and pressing that digit with nothing typed moves the
selection there and does nothing else; only `Enter` acts, once. A decision still opens
with no act selected, and the `?` keys list the digits beside it.
- **The workspace says what a schedule's trigger fired, and the rail reads « Actif ● » only once
a fire is proved.** At the opening and after each turn, the workspace reads the Session's history
of each declared schedule's arm ledger (`SessionRuntime::schedule_history`, a read only) and
says, once, what changed: « `compiled-workflow.nika` a tourné tout seul : 1 run par son
déclencheur », its last slot with its exit and its trace, a slot caught up late (« son créneau
… a été rattrapé à … »), and the slots skipped, the runs a gate parked and the failures. The
list counts the runs fired under their workflow (« 1 run déclenché · 10-16 06:00 ✔ »). The rail's
activation field stays declared (◐) until a fire of an installed, active trigger is proved,
then reads ●, and goes back to declared once the trigger is removed.
- **Card names, band titles and the `?` keys overlay now use the language you write in.** In a
French conversation a refusal's card reads « Impossible de continuer », your own messages
are labelled « Vous » and a result « Nika · Résultat ». A decision's band is titled
« Décision · obligatoire », and a question's card names the answer it takes (« Une réponse
en texte »). The keys overlay lists every key in French under « Conversation », « Décision »
or « Pendant que Nika travaille », its reader frame included. The Session's words inside
every card stay as written, and English stays the default.
- **A held message shows its way on.** When the knowledge your
configuration names is refused, the message Nika holds stays in view
with the refusal and its two acts: `/knowledge embedded` resumes it
once with the knowledge built into Nika, `cancel` drops it.
- **The TUI's hint row now uses the language you write in.** When the Session reads French in
your lines, the keys under the composer are shown in French: « ↑↓ choisir · Entrée valide »
at a decision, « Entrée répond · annuler l'abandonne » under a typed question, and
« oui + Entrée : enregistrer · non : annuler » at a proposal. The same goes for the notices a
key leaves (« rien n'est envoyé · tapez oui ou non… »), what a second `Ctrl+C` does, a
proposal's standing and the composer's caption. The words the Session reads when you type
them keep their form: `oui`, `non`, `annuler`, `details`, `save & run` and the slash
commands. The Session's words, your words, values, paths and model ids are never rewritten.
English stays the default and the fallback, and an ASCII screen shows the French without
accents.
- **The whole-words reader opens the block at the reading position.**
Scrolled back in the workspace conversation, `F2` (and its palette
entry) opens the Session's own words of the newest block shown there
short of whole: a summarized refusal, the current proposal, the question
its card carries. A plain press on such a block opens the same words:
the key and the pointer read the rows painting uses. Closing returns
the exact rows, the draft and the keys. At the latest rows the question
waiting comes first, then the latest such block, as before.
- **A fire while the workspace sits open shows without a keystroke.** About once a minute, while
no turn or run is busy, the TUI reads the Session's schedule history again (a read only) and says
a schedule's new fires as it says them after a turn: once, with the rail. Before, a fire while
the TUI stood open stayed unsaid until the person typed something. `Options::idle_every` sets
the period (`None` never asks); a conversation answers through `Conversation::idle`.
- **The workspace installs a declared schedule's trigger on your yes (D1).** On a Mac, in your
own terminal, once « active-le » has declared a schedule in `nika.yaml`, the workspace asks
« Installer le déclencheur sur ce Mac ? » with two rows, « Installer et activer » (recommended)
and « Garder déclaré seulement », under the Session's report of what it wrote. « oui » writes
and loads the launchd unit run by the program you use and says when it fires next (« Installé
et actif · … prochain run : … »); « désactive-le » removes it. Nothing is installed without that
yes; any other door, and every test, keeps the schedule declared and names the command.
- **Type a model's name anywhere in `/intelligence`.** From the home
screen or the connection list, typed words now also find models by
their display name under every connection (`son` finds Claude Sonnet
5.5 under Anthropic), and `Enter` applies the one selected.
- **`/intelligence` opens on a short home screen.** The recommended model
comes first and is selected, so one `Enter` applies it; then keep the
current choice, other choices (every connection and its models), and the
Author, Decision and Run roles with their values, each row opening its
change: Jev or the author deciding alone for decisions, and the model for
the next workflows' AI steps, told to the author. `Set as default` keeps
the choice for new conversations. `Esc` steps back one list at a time.
- **`/intelligence` names the key a provider uses and can test it.** API
rows show the variable that supplies the key (`key: ANTHROPIC_API_KEY`)
and warn when two set variables differ, naming the one Nika uses. A
`Test the key` row on the current or opened API connection sends one free
model-list request only when you pick it and shows the verdict inline
(valid, refused, unreachable, unknown); the key is never printed.
- **Models in `/intelligence` read by name, with prices and context.** A
model row now shows a display name (`Claude Sonnet 5.5`, `GPT-6.1 Sol`,
`DeepSeek Flash`) and, in a quiet aligned column, its current and legacy
marks, its input and output prices per million tokens when known, its
context window and a key it still needs. Typing filters on these words
too; the key sent is always the exact model id.
- **Type to filter the `/intelligence` lists.** Typed words narrow the
rows to those holding every word, case aside; the first match is
selected and `Enter` applies it. Words that match no row are sent as
typed, so `/intelligence 2 provider/model` still works for an unlisted
model. Questions and decisions keep their own reply field unfiltered.
- **The live line shows what preparation costs while Nika works.** While Nika prepares, the line
now sums what the returned author calls cost as the Session settled them, at list price:
`Preparing · 45s · 12 author calls · ~$0.12 · Ctrl+C stop`. A call that returned without a
price is counted beside the sum (`~$0.12 + 2 unpriced`); while no call is priced, no cost is
shown at all. A finished call's activity mark now carries its settled cost (`CallCost`), read
from the preparation scope's own journal of returned requests: an estimate, never an invoice.
- **`/new` and `Ctrl+N` begin a new conversation in the workspace.** Through the Session's own
door (`begin_new_conversation`): the conversation is kept as it was, the opening is painted
again (the banner, then the Session's words of what carries over: the project's workflows,
schedules and runs, a gate still waiting), and the prompt is free. While a decision waits, the
Session refuses in its own words (« une décision vous attend encore … ») and the decision is
shown again. `/new` is listed in the palette before `/quit`, and `Ctrl+N` among its keys.
- **The notices around your own words now use the language you write in.** In a French
conversation a draft kept aside reads « votre brouillon « … » · il est dans la zone de saisie,
non envoyé », a correction « correction en attente « … » · la préparation s'arrête, puis elle
est envoyée », a command typed while Nika works « Nika continue · /intelligence attend votre
tour », and the decision card says « cette proposition n'a pas changé depuis votre dernier
message ». What an earlier session kept reads « dernier run ✓ sortie 0 dans une session
antérieure » on the status row (« Run ○ (précédent ✓) » on the rail), with its kept line and
its earlier context, and the conversation panel names « Message pour … », « Joint : … ». Your
words stay exactly as typed, a brace included. For other doors, nika-display adds
`correction_queued_in` and `correction_unsent_in`, and nika-tui-view `Thread::speaking`;
English is unchanged.
- **The palette and the slash list now use the language you write in.** In a French
conversation each command and view key says what it does, where it applies and its one
sentence of help in French (« /status · Où vous en êtes », « Ctrl+L · Redessiner l'écran »),
and the chooser's own words follow too: the band's title, what waits (« Une proposition attend ·
aucune commande n'y répond »), its keys (« Entrée envoie /show · ↑↓ choisir · Échap masque
la liste ») and the search prompt « commandes › ». Command names stay as you type them, and a
French word finds the command it describes while the English search words still work.
- **The prompt beside the line and the own reply's placeholder now use the language you write
in.** In a French conversation a proposal's line reads « Enregistrer ? › » and a question's
« réponse › », and the field beside a choice invites « ou tapez votre propre réponse »,
« ou tapez oui ou non » or « tapez pour filtrer ». The line keeps the cells the longer
prompt leaves, and a field already on screen changes its placeholder at once while keeping
what you typed. `action ›` and `nika ›` read the same in both languages.
- **`?` shows the keys of what is on screen.** With nothing typed, `?` opens a read-only
frame listing the keys of the surface in view (the conversation, the intelligence list,
a question, a decision, or the work under way), its hint row's own words first, then the
keys that always hold and the view keys the palette offers now, from the same catalog.
`Esc`, `Enter` or `?` closes it with everything as it was; once words are typed, `?` is a
word. The stage line ends with `? for shortcuts`, the palette lists it, and the ASCII
glyph column reads the same keys.
- **`PgUp` reads the conversation behind a question's or a decision's card.** On a full screen the
card covers the conversation down to the box, and a mandatory decision left no way to read the
activity behind it before answering. `PgUp` now steps behind the card: the conversation shows
and scrolls with the page keys and the arrows, the hint says « PgUp/PgDn scroll · any other key:
back to the card », and any other key brings the card back as it was, answering, selecting and
typing nothing. The card's hint names the key (« PgUp: history » where the row holds it), and so
does `Esc` with nothing left to undo. A decision still opens with nothing selected; a question
still opens on its recommended answer.
- **The request beside its question.** A typed choice shows what the
Session keeps of your request, its goal and the questions still open,
above the question, so nothing already understood has to be typed
again.
- **A proposal's review card now uses the language you write in, around the Session's own
facts.** In a French conversation the card reads « crée compiled-workflow.nika (33 lignes) »,
« remplace … · sur les octets attestés … », « révisé sur place · … » and « run demandé ·
report.nika une fois · plafond $0.25 ». Its headings follow (« à l'exécution · », « atteint,
comme déclaré · », « répétition · aucune liée à cette identité »), and so does its identity
on the border (« proposition 74189e5877e0 · F2 : tous les mots ») and the faces' first rows.
What the Session's audit says a workflow reads, writes and reaches, and its rehearsal words,
stay as the Session says them. On an ASCII screen these French words lose their accents.
- **A proposal's check face details the compile record of its bytes.**
Below the static check, the object now shows how the proposed bytes
were made (the mode, their sha256, the base a revision rests on, what
changed and the preservation claimed) and every admitted component
they hold, with its version, release and admitted-file digests, its
bindings and what the bytes show of it now. `invoked` is admitted
reuse like `expanded`; only a revised, absent, unreadable or
unwitnessed component asks for attention.
- **A run's ceiling is chosen from rows.** When the Session asks the
ceiling of a run it cannot prove one for, the TUI shows the amounts as
rows, the recommended one preselected with why, another amount typed
beside them and a way out; the transcript keeps the save report and
the one short question.
- **The run in view now speaks the language you write in.** In a French conversation its
faces read « [run] sorties fichiers preuve », its standing « un nouveau run · terminé ·
réussi » (on its pinned row too), and the facts above its graph say the bytes it binds
(« graphe · les octets … sur lesquels il a été demandé, tels que le run les nomme »), how far
it went (« en cours · 4 tâche(s) vue(s) »), its settlement (« clôture · normal · 4/4 tâches ok
· 9857 ms »), its stream (« flux · 19 événements et la clôture, entiers ») and its evidence
(« attestation · scellée, comme le run l'a déclarée · la preuve est la vérification propre de
la trace »). Its graph counts in French, its tasks wear « ✔ réussie » or « ○ planifiée », and
the list says « tâches · ↑↓ choisir · Entrée détails ». The workflow, the cause, counts, times
and digests stay as they are; a task's detail and the outputs, files and proof faces still
read English. English is unchanged.
- **A settled run's story folds to one row.** In the workspace
conversation, a run's story is folded once, when it is committed and
the shell observed its run typed, whole and settled (never paused), to
one quiet row: the run's label, its state and `F2`. The fold is never
recomputed: a later block never folds or unfolds it. `F2` at the reading
position, or a press on the row, opens the whole story; an untyped or
incomplete run keeps its words.
- **A run's story now speaks the language you write in.** While a workflow runs from a French
conversation, the busy row and the activity card read « en cours · meteo-maurice-quotidienne »,
« ↺ today · depuis le cache », « ◇ en pause · `approve` vous attend » and « succeeded · 4/4
tâches · 9857 ms », and the story the transcript keeps reads the same. The workflow, the
tasks, the engine's status word, counts and times stay as they are. For other doors, a
`RunSink` gains a `say` method that defaults to English, so `nika`'s plain output and every
existing sink are unchanged.
- **The stage line, the live line and a run's card now use the language you write in.** In a
French conversation the stage line reads « Brouillon ✓ · Enregistré ✓ · Vérifié ✓ · Actif ○ ·
Exécution ○ » beside « ~$0.31 dépensés » and « ? pour les raccourcis ». The line under work
in progress says « Préparation · 45s · 2 appels auteur · Ctrl+C arrête » or « Exécution de
veille-tech.nika · 2/3 tâches · en cours : write · $0.012 sur $0.25 », and a run's card opens
on « Échoué après 16.5s · 3 tâches exécutées · 1 échouée ». Times, amounts, file, task and
model names keep their form, the Session's phase words stay as it says them, and English
stays the default.
- **The stage line carries what the session spent so far.** After its stages (`Draft ✓ ·
Saved ○ · …`), the terminal now adds `~$0.62 spent`: the Session's known estimate of
this session's preparation plus the metered spend of the runs it observed. Nothing is said
while the spend is unknown, a row too short for it drops it before the stages give way,
and the model and decision seat keep their one home, the header.
- **`/welcome` brings the welcome back.** Once the first exchange took its space, the welcome and
the example it offers (« Try: aggregate-by-key ») could not be read again. `/welcome`, listed
with the commands, now brings it back as the object in view beside the conversation until
another object takes its place; outside the workspace its words are said in the conversation.
Nothing is sent to the Session.
- **The butterfly is drawn in braille.** Outside the ASCII column the
welcome's mark takes Unicode braille renditions, 2×4 dots per cell, read
from the same samples of `media/brand/nika-logomark.svg` as the ASCII
density ramp, for the same five sizes; the reveal lights its dots in the
same five frames. A test now regenerates every rendition from the pinned
mark and fails on any difference, so a handful of ramp cells moved one
step to the reproducible sampling.
- **A workflow's faces now speak the language you write in.** In a French conversation the
object pane names its faces « source plan graphe vérification », and says the facts above the
face (« workflow Nika · source · 1.9 Ko · identité … · 34 lignes »), what it cut (« 7 lignes
plus larges que 65 colonnes, coupées au bord »), the plan's waves, the graph's summary
(« Définition · 4 tâches · 3 arêtes ») and the check's reasons (« la définition est
conforme »), as well as the compile record a proposal binds to its bytes and the rows around
an opened file (« les octets de ce fichier …, tels que lus en dernier · r le relit »). The
object's own action reads « [+] Agrandir · F4 », and a click on a face's tab finds it by the
word painted there. The bytes, the engine's findings, the check's layer names, digests and
values stay as they are; English is unchanged, except that the check's closing reminder
now wraps rather than being cut on a narrow pane. For other hosts, `Canvas::speaking`,
`Inspected::speaking`, `Checked::importing` and `Face::label_in` are new additive calls.
### Changed
- **An Anthropic key starts on Claude Sonnet 5.5.** The catalog's Anthropic default and
the first-run suggestion for an Anthropic key name `claude-sonnet-5-5`, so a person who
picks Anthropic without naming a model runs it; an explicit model keeps running as
named.
- **The Anthropic nicknames name the latest Claude models.** By maintainer decision of
2026-10-15, `anthropic/sonnet` runs Claude Sonnet 5.5, `anthropic/haiku` Claude Haiku
5.5 and the new `anthropic/opus` Claude Opus 5.5, each with that model's 1M-token
window and 128K output cap; the catalog's cheap Anthropic model is Claude Haiku 5.5.
`anthropic/claude-sonnet-4-6` and `anthropic/claude-haiku-4-5-20251001` keep running
as named, the latter with its retirement advice.
- **The conversation's author offers only what one workflow can carry.** Its instructions now say
that every proposal is verified against the whole request, so splitting the work across several
workflows is no option; that an option dropping a file says so in its label and one asking for
an approval says who approves before what, so picking it settles the question; and that a
refusal is told in plain words, never by a finding's name.
- **The workflow author shapes its choices for picking, not typing.** The author's
instructions and the `ask` tool now say: whenever an answer is among known alternatives,
give two to seven native options rather than a free-text question, with `multi_select`
when several may apply; label an option that names a website or a data source with its
display name, then « · », then its domain or feed address (`TechCrunch ·
techcrunch.com/feed`); whenever a default can stand, show the choice in the proposal's
`open` with exactly one recommended option. The assumptions-first rules are unchanged: a
default is still never asked, and a question asked with `ask` recommends nothing.
- **The author waits for a fan-out and keeps independent items.** Its card showed `fail_fast: true`
as the one form of a fan-out, and never said that a fan-out over an empty collection is skipped
with a `null` output: an inbox workflow failed with NIKA-VAR-006 on an empty folder, its steady
state, and one failed summary of two lost both. The card now shows the plain fan-out, has every
task that reads a fan-out's output wait for it with `after: { <id>: success }` (an empty
collection then ends the run with nothing to do), and, when each item stands alone and the
request does not want all or nothing, keeps the other items with `on_error: { skip: true }`.
- **Provider defaults and nicknames follow the served model ids.** OpenRouter uses
`anthropic/claude-sonnet-4.6` and `anthropic/claude-haiku-4.5` with their sourced
limits; `deepseek/chat` aliases `deepseek-flash`, while the explicit retired wire
id remains unchanged. OpenAI defaults to `gpt-6.1-sol` and `gpt-6-luna`; xAI
defaults to `grok-4.7` and `grok-4.3`, with the first-run suggestion naming
`xai/grok-4.3`. Retired grok-3 prices are removed, and the human catalogue lists
each aliased wire once. Groq defaults retain their models.
- **`nika catalog` lists providers in the spec's presentation order.** Within its
LOCAL and CLOUD sections the rows follow the vendored design tokens'
`presentation.providers_order` (local engines first, then Mistral, Hugging Face,
OpenAI, xAI, Anthropic, DeepSeek, Google, Groq, NVIDIA and OpenRouter), the
order the language server already teaches; a provider the tokens do not name
comes after every named one, by id. The command keeps no order of its own.
- **The TypeSafe decision seat lives in a size-cap member.** `nika-cli-host-typesafe` now holds
the System One seat (its transport, the Session's journaling seat and the batch exchange);
`nika-cli-host` re-exports it whole as `compile::typesafe`, so no consumer changes. The host
crate drops from 14,995 to about 14,076 production lines. The seat's key accessor now
returns a `Secret`, read only through an explicit `expose`.
- **Decision partitions retain the exact serialized request bytes.**
`Partition::begin` returns the optional carried request as `Vec<u8>` instead of
a JSON value. Callers must preserve those bytes rather than serialize a parsed
value again. Model-part qualification also exposes its typed findings; this
preview's public API change is not exclusively additive.
- **A decision batch is not refused twice at a size it already learned.**
When the System One decision service refuses a request of several items
for capacity, the rest of the batch never sends a request that large
again: a waiting half as large as the refused request is split before it
leaves, in order, instead of being sent whole to be refused. The seat a
Session keeps also remembers what its batches learned, so a later
compile's batch starts at the largest size the service answered, below
every refused size; a batch that never met the capacity teaches nothing,
and nothing persists beyond the process. Each journaled request split
before it left names that bound (`split_below`). Single items, order,
answers and the rest of the journal are unchanged.
- **`nika doctor` says "unknown" when it could not see.** A harness seat
whose login check did not answer in time was a warning; it is now its own
level, `unknown` (glyph `?`), counted apart in the verdict line and in
`--json` as `summary.unknown`, never as ok, warn or fail. The exit code
is unchanged: an unknown row never makes `nika doctor` exit 3.
- **Groq's catalogue row lists its self-serve gpt-oss models, and its defaults
move to them.** `groq/gpt-oss-120b` and `groq/gpt-oss-20b` name
`openai/gpt-oss-120b` and `openai/gpt-oss-20b` (131,072-token window, 65,536
output tokens, from Groq's models page of 2026-10-12), and Groq's default and
cheap models move to them from the Llama rows, which that page now lists for
enterprise contracts only. The Llama nicknames keep their models, and the same
ids stay usable on Scaleway, NVIDIA and local engines.
- **The knowledge door ranks families by the light French stems of their words.** A request and a
family now meet whatever the inflection: each informative word of both is read by a light French
stem (a plural, « -aux », then one inflectional or common derivational ending), so « importées »
meets « importer » and « feeds » meets « feed ». On 32 held-out French requests, the family each
belongs to now ranks first for 20 instead of 19 and is recalled for 31 instead of 28.
Patterns, blocks, examples and skills reached by their own text keep their words; each family
recalled still brings its packs' patterns and blocks, presented whole. The pack names its
builder `knowledge-door-v8`.
- **The embedded knowledge release is r3.5** (`knowledge-0.123.0-r3.5-foundry-20261016`). It adds
seven blocks for recurring jobs, each qualified in recorded worlds: an API value watched with a
state file that asks only on the first run and on a change, a report of the previous month on a
day-of-month cadence with amounts in integer cents, a folder translated file by file with each
failure named in a report, an inbox processed and emptied, a weekly reading list from feeds, a
daily news digest posted only on approval, and a CSV cleaned with the file line of every removed
row. Nine patterns (among them the run's spending ceiling, which lives in `--max-cost-usd`, the
project's `ceiling:` and a beat's `plafond:`, never in the workflow file), eight packs, two
families and French phrasings come with them, and the cadence knowledge reads a day of the month
(`TZ=<zone> 0 8 1 * *`). The r3.2 release it replaces stays embedded for earlier pins, which
reopen its exact bytes.
- **Machine protocol 2.** This engine reports `machineProtocolVersion: 2` on `/health` and
`nika --sdk-identity`.
- **Why.** Compile questions may be `choice`, which `@supernovae-st/nika` 0.120.3 refuses
mid-compile. From now on, any change to an object a published SDK reads closed raises the
protocol in the same commit, held by a contract test against that SDK's closed sets. That SDK
then refuses this engine at connect (`NikaCompatibilityError`), before any job is admitted,
never in the middle of a run. Use the SDK of the same release. It reads protocols 1 and 2, so
upgrade the SDK first, then the engine.
- **Resident stores.** A resident stamps its stores with the protocol once it holds the lease,
so stores a 0.123 resident wrote are refused by an older resident, by design. Before
upgrading, copy the state root (by default `.nika/serve`). To roll back, serve that copy, or
a fresh `--state-root`.
- **Doctor.** Its resident row fails for stores a newer protocol wrote, whether a resident runs
or not, and names those two ways out. It used to read a stopped newer resident's stores as
fine and suggest a restart this binary would refuse.
- **The models.dev pricing snapshot is refreshed to 2026-10-12.** It carries
729 rules (716 before): 38 rows arrive, among them Claude Sonnet 5.5 and
Haiku 5.5, GPT-6.1 Sol and Step 5 Preview, and 25 rows models.dev no longer
lists leave, among them `openrouter/qwen/qwen3.8-27b:free`, with the three
measured energy figures that rode on them. DeepSeek V4 Pro's snapshot price
moves from $0.435/$0.87 to $0.66/$1.98 per million tokens; its admission
tariff, at the peak rate, still prices its runs. DeepSeek's retired
`deepseek-chat` and `deepseek-reasoner` names, which models.dev no longer
lists, keep their `deprecated` status: priced for a recorded run, never
offered.
- **The knowledge door lives in a size-cap member.** `nika-onboard-knowledge` now holds the strict
admission of a Foundry knowledge release, the releases this build embeds, the catalogue and the
authoring pack, with their assets and pinned vectors; `nika-onboard` re-exports every item at
`knowledge::`, so no consumer changes, and keeps `knowledge::pin`. The onboarding crate drops
from 14,999 to about 11,090 production lines.
- **The access plan, the probe facts and route identity live in a size-cap member.**
`nika-providers-access` now holds `resolve_access`, `route_identity`, the probe's data types and
`canonical_provider`; `nika-providers` re-exports every one at its historical path, so no consumer
changes. The provider crate drops from 14,991 to about 12,800 production lines.
- **A rehearsal bounds the values a run builds.** A candidate whose templates
build a value larger than the rehearsal room holds is run, and stopped where
the value is built: the rehearsal reports a data bound naming the task and
the value's size, instead of refusing every amplifying template before any
run.
- **A served `rattraper` catch-up fires the latest missed slot once.** As spec 01 states, and as
the activation's one-ceiling worst spend assumes: a `nika serve` schedule under `manqué:
rattraper` (the job API's `catch-up`) no longer fires every missed slot, up to 10,000 runs. The
older slots are skipped, journaled once. A catch-up due while its schedule's previous run still
runs now waits for that run instead of being skipped as an overlap.
- **A new run reads earlier work lean.** When the person writes, a superseded candidate's
arguments and findings and a page read again later reach the model as one-line stubs;
the person's lines, the provenance replies and the current candidate stay whole, the
record is unchanged, and nothing before a replayed thinking block is edited.
- **A Session conversation is compacted within its route's window.** On
a route a tariff prices, the conversation's earlier entries are folded
into a summary before a request its route could not take, instead of
every request carrying the whole conversation. A route no tariff prices
keeps its whole conversation, as before.
- **The session's intelligence, reasoners, line router and authoring door
now live in the `nika-session-intelligence` member crate.** A structural
split (ADR-150) that brings `nika-session` back under its production-size
wall: every `nika_session::{authoring, intelligence, reasoner, turn}` path
and root re-export names the same items, and no behaviour changes.
- **A default picked at a proposal needs no author round trip.** When a
proposal shows its choices with their defaults, picking a choice's
recommended option (Enter on its preselected row, its key or label, or
`yes`) binds it and keeps the proposal shown, with its next choice or
its save decision, instead of asking the author to propose again. The
bytes, identity and card stay those the person saw. Another option, a
sentence, a changed candidate, the last pick of a proposal whose request
asked to save or run, and a conversation an ACP agent leads still reach
the author.
- **The author reads the language up front.** A new conversation's instructions carry
the engine's one-page language, its canonical fragments and output conventions as
reference, and an index of what `language` and `knowledge` hold, so a missing detail
costs one narrow lookup.
- **The observations of one message run side by side.** An observation reads one public
page outside the Session's lock, and consecutive ones run at once: three sources cost
about one page's time. Their replies keep the message's order; every other call stays
sequential.
- **A Session turn keeps one connection to its model route.** The
conversation's calls in one turn share one client and its runtime,
built at the turn's first call, instead of a new client per request;
each request is still admitted and can still be stopped on its own.
- **The author knows the run model's efforts.** The `models` tool names the
reasoning efforts each offered run model documents and its output limit, and the
author declares a `low` effort for tasks that only write or summarize text, so a
reasoning model's thinking no longer exhausts the answer's tokens.
- **A saved default naming Jev opens new conversations with Jev.** When
the operator default names a decision seat and the host names none, the
session builds that seat by name, its key read through the seat's own
boundary, and consults it while it can; with the key missing the seat is
shown unavailable and never blocks the author. The header now reads
`Decision: Jev 1.13.0` (or `Decision: same model`).
- **The properties you state are checked before an output is written.** The Session's
author is asked to check each property your words require of an output (an order, an
exclusion, a count or a total) with an assertion that the trial and every run evaluate
before the output is written; a candidate that breaks it fails its trial and is repaired
before you see it.
- **A line typed during a turn holds back only the calls that hand the turn over.** A
steering line waits before ask, propose and new_request; a candidate write or a read
the author already decided still runs, and the line enters after them.
- **A Session's last run names its job, receipt, outputs and the verifier's seal facts; `sealed`
keeps its meaning.** `work.run` gains `job`, `receipt`, `outputs` (kept whole up to 16 KiB
encoded, absent past it: the job keeps them) and `seal_verified` / `signed` as `nika trace
verify` attests them for the run's journal, absent when no verifier judged it. The Session
judges a native run's local journal when it observes the run and holds those facts beside its
kept value, which stays the one an earlier engine reads; an opaque trace identity is never read
as a local journal. `sealed` still names what the Stop's story reads, the settlement's receipt
naming the chain head: a run that stopped at a wave boundary without a run key reads stopped,
never cut, and no verifier's judgment is read from it.
- **The author writes and proposes in one message.** A write's reply now
carries Nika's check findings, the laws over the person's words and
the values it dropped, so a candidate needs no separate check or verify
round before it is proposed; a shown proposal ends the turn instead of
costing one more model call; `compose` is lent the pinned release's
components and the release is admitted once per turn.
- **The size walls are 25,000 production lines per crate and 2,500 lines per file.** The CI
ratchets and the pre-push gate now block a crate above 25,000 production lines and a source
file above 2,500 lines; the hygiene dashboard warns, without blocking, above 15,000 and 1,500
and names the crate closest to its wall, and a file with a `LOC-EXEMPT` marker stays capped at
5,000 lines. One ceiling applies to every crate: the native renderer's 19,000-line exception
is absorbed. ADR-154 records the decision and amends ADR-004, ADR-023 and ADR-143.
- **A conversation's document needs an executable check for each count,
total, order or cut its request states.** A document the Session's author
wrote was READY although a typed property the request states had no check
any run evaluates, so its trial proved nothing about it. The conversation
door now holds such a document before its trial and its judge, INCOMPLETE,
naming each property with the request's words and the check to add: a
`nika:jq` boolean asserted by a `nika:assert` that reads it through
`with:` and quotes those words, the write waiting for it. A bound on text a
model writes is not asserted and stays the judge's. No model is called to
decide it, and the other doors follow once their author prompts teach the
same checks.
- **`/status` says first whether the saved workflow is scheduled.** A person asking « is my Monday
recap on? » got the intelligence, the seat, the knowledge and the money lines and no word about
the schedule. `/status` now opens with the workflow this session saved, whether a schedule is
declared for it and whether a firer is proven on this machine, and how its last run ended.
- **The conversation's author speaks as Nika and leaves the Session's acts to it.** The author's
instructions now say to speak as Nika in the first person. Installing, removing, activating
and deactivating a trigger belong to the Session: the author never claims them, nor advises a
terminal command or a hand edit of `nika.yaml` instead. They also cover plain text without HTML
entities, and « montre-le-moi avant de l'enregistrer » as a request to see the proposal. The
author never advises a stronger model unless the verifier asks for it, and a recommendation
never contradicts the person's explicit requirement. Each source of a multi-source result
recovers on its own, and a thinking model's `max_tokens` leaves room for its thinking.
- **Schedule questions show their options once.** The missed-run policy
and the ceiling per scheduled run are chosen from rows the Session
states, the recommended amount preselected and another one typed
beside them; the transcript keeps the question's sentence and one
short line instead of repeating the options as text. A host without
rows still reads every option as text.
- **A chosen command runs once.** `Enter` on a command in the palette
or the slash list runs it once, your unsent message left as it was;
`Tab` still only inserts it. A repeated `Enter` after choosing or
answering counts once, and queues nothing from a message waiting out
of view.
- **One surface above a fixed composer.** The command palette, the slash
list and a typed question's offers open as one opaque surface above the
composer. The composer and the transcript keep their rows and the reading
position: opening, searching or closing it moves nothing, and nothing
hidden under it takes a key or a click. A typed choice keeps your unsent
message out of view, untouched, and gives its own reply a field of its
own; the arrows choose whatever that message holds.
- **A decision names the act the Session recommends.** Its rows now say « recommandé » /
« recommended » beside that act, as a question's rows already did: the trigger's install marks
« Installer et activer », or « Garder déclaré seulement » when a scheduled run would not see the
workflow's keys, and a run's or a schedule's ceiling marks its recommended amount. A decision
still opens with no act selected, so Enter consents to nothing by itself; a decision the
Session recommends nothing for, such as a save, marks nothing.
- **The workspace wears electric blue.** The brand, focus, selection,
primary action and interaction chrome take the electric-blue accent
and selection fill of the ratified design, replacing violet. Success,
caution, failure and each verb keep their own hue; `NO_COLOR` still
carries no hue and the ASCII column keeps its marks.
- **`Esc` steps back and never answers a decision.** In a choice list it
first empties what you typed, then the selection; on a Save, cost, gate
or other decision it then sends nothing and says so, so its discard or
cancel row is chosen only with `Enter`. In `/intelligence` it returns
from a connection's models to all connections, and at the top keeps the
current choice.
- **A connection's models open on the current generation.** In
`/intelligence`, earlier generations, dated snapshots and legacy models
now fold behind one `Older models (N)…` row, so Anthropic opens on its
Claude 5 models; the model in use and the recommended one always stay
in view.
- **Reopened, `/intelligence` opens on `Keep current`.** Opening the picker again selected
`Recommended for you`, so a reflexive `Enter` replaced the author in use with the
recommendation. Once an intelligence is chosen, the picker now opens on `Keep current ·
<model>` and one `Enter` keeps it; where a first choice is required, the recommendation is
still selected and one `Enter` applies it.
- **The intelligence guide no longer teaches a typed route.** `/intelligence`
and an opened connection now say only to choose below; every model this
machine offers is a row. Typing `/intelligence 2 provider/model` still
works as an expert fallback.
- **`Enter` validates a multiple choice.** Space checks or unchecks an
answer; `Enter` on any row now sends every checked answer at once instead
of toggling the row under the cursor. With nothing checked it sends
nothing and says that Space checks an answer.
- **The lifecycle rail names its stages in the Session's words, in the person's language.** The
workspace now asks the Session for the rail in the language it reads from the person
(`nika_session::Lang::rail`: « Brouillon ✓ · Enregistré ✓ · Vérifié ✓ · Actif ◐ · Run ○ ») and
paints it as it comes, instead of translating the Session's English stage names word by word
with its own table (which said « Exécution » where the Session says « Run »). A first
question's or a waiting proposal's own rail still gives its row to the card in either language.
Under the ASCII glyph column the Session's words keep their accents, as its other words do.
- **The connections you can use now come first in « Choose an AI connection ».** Within each
access (agent accounts, APIs, local servers), the connections usable now lead: an API whose
key is present, an account or a local server the Session detected that can write workflows.
The rest keep their order below, and every other order is unchanged. With only
`NIKA_ANTHROPIC_API_KEY` set, Anthropic is now the first API row, where it stood twelfth under
eleven « key required » rows. Nothing is probed to tell.
- **The workspace reads the Session's schedule histories as they are.** `ProjectView::schedules`
now holds `nika_session::ScheduleHistory` itself (built with its `new`), and the TUI's own copy
of it (`ScheduleView`, `SlotView`, both added in this preview) is gone. Whether this machine
holds a schedule's unit is the lent trigger host's answer (`TriggerHost::installed`), so a test
host proves the rail's « Actif ● » without touching launchd. Nothing a person sees changes.
- **A stopped call wears the cancelled mark, not a failure's.** In the activity card a call the
person stopped read « ✖ call 1 · … · stopped, a charge may be unknown », the glyph the run views
use for a failure. It now reads « ⊘ call 1 · … », the theme's cancelled mark (« - » in ASCII):
a stop is the person's own act, not a failure.
- **A typed « je voudrais l'enregistrer et le lancer » at a shown proposal is pinned through the
TUI.** The save decision takes the person's words through its bound door: the Session saves the
workflow and hands its run over, and the door runs it once. A test now holds that journey end to
end in the TUI, nothing run before the door performs it.
- **The welcome names the Session's own demo.** « Try: aggregate-by-key » on the welcome screen
now reads the Session's `nika_session::WELCOME_DEMO`, the demo it gives a person who asks for
« l'exemple de l'écran d'accueil »: the screen and that answer cannot drift apart.
- **The welcome's butterfly wears the brand's ice.** Where `COLORTERM`
proves 24-bit colour the mark takes the logomark's own ice, and while it
is revealed what each frame has just lit glows until the last frame
settles it. A 256-colour terminal gets the nearest xterm indices, the 16
ANSI slots bright cyan and bright white, `NO_COLOR` no hue; reduced
motion draws the settled mark at once.
- **The wordmark locks up with the butterfly.** The welcome's spaced
`N I K A` becomes the wordmark `nika.` under the mark, the name strong and
its dot in the exec hue, and the header opens with the same wordmark in
place of `◆ NIKA`. Both glyph columns draw the same characters; without
colour it is plain `nika.`, and every other welcome word stays.
- **A workflow named exactly `v1`, `v2`, … must be renamed.** `nika:`
names the file, and `v1` is the retired schema tag it once held. A
workflow named that way is now refused at parse (`NIKA-PARSE-003`) with
a remedy that says so, as `nika.yaml` already refused it. Only the
whole marker is refused: `vault`, `v2ray` and `v1-migration` stay
ordinary names.
### Fixed
- **« active-le » on a schedule declared earlier offers its trigger.** On a Mac whose terminal
Session installs triggers, activating a schedule `nika.yaml` already declares no longer stops at
« déjà déclaré · pas prouvé actif ». When no unit is installed, Nika asks « Installer le
déclencheur sur ce Mac ? ». When the unit is there, it says the schedule is active and when it
runs next. A line asking for another cadence still points to the entry to change.
- **The activation honours « choisis pour moi », says when a schedule is already declared, and
never runs instead.** On the activation's questions, « choisis pour moi » or « fais au mieux »
was dropped or refused (« is not a positive amount in USD »): it now takes the run-once policy
and the recommended ceiling, both shown in the declaration consented to. Activating a workflow
already declared said so only after asking the missed policy and the ceiling again; it is now
said at once. « Vous pouvez l'activer pour moi, directement ici ? » ran the workflow: a filler
around the activation words keeps them the activation, and a line about activating or
scheduling never reaches Run. A green run that recorded no priced cost is still a run observed:
the ceiling says it cost nothing priced, not « no run of this workflow observed yet ».
- **A second schedule joins the `arm:` list Nika wrote.** Activating a second workflow of the
same project was refused with « Nika does not rewrite a list it did not write », about the
`nika.yaml` Nika had written minutes earlier, and the person was sent to edit YAML by hand. An
activation now appends its entry to an `arm:` list that ends the file; a list followed by other
settings is still the person's to edit, and a workflow already declared keeps its entry.
- **An agent no longer runs the calls of a message cut at its output limit.** Each call of
such a message is answered « not run: your message was cut at the output limit; re-issue the
call with complete arguments » and the loop goes on under the task's own limits, as the
Session's agent does. A turn the provider interrupted on its side is never acted on: the task
fails `NIKA-INFER-001`, transient, naming the provider's own word.
- **An answer the provider cut at its output limit now fails instead of passing as the
output.** An `infer:` task whose provider stopped at `max_tokens` used to return the
truncated text as a success (a model that reasons by default can spend most of the cap
on reasoning: 4,096 of 4,096 output tokens, 3,860 of them reasoning, the answer cut
mid-sentence). It now fails with `NIKA-INFER-005` (not transient), before any `schema:`
validation or repair. The run record's `details` carry the text received
(`partial_output`) and the provider's stop reason, and an `on_error:` recovery can use
them. An agent's final message cut the same way fails with `NIKA-AGENT-006`. A blank
cut answer keeps its earlier failure: `NIKA-INFER-004` on a text task, the schema check
that names the token limit under `schema:`.
- **Anthropic calls are priced again.** Every Anthropic reply now carries `"speed": "standard"` in
its `usage` object, a key the completeness rule did not know, so each call counted as « provider
did not report usage »: runs on Claude showed « cost · UNKNOWN · 1 unpriced call(s) » and no
allowance settled them. The standard speed is now a known axis beside the standard tier and
global inference; a fast reply, billed at another rate, still leaves its call unpriced.
- **« Active-le tous les matins à 7h » activates that schedule.** An activation line that states
its cadence re-proposed the saved workflow, then asked for the English word « activate ». The
cadence it states is now the schedule declared, and a plain « activate » after a workflow saved
without one takes the cadence the person's latest line stated (« qu'il tourne toutes les
heures »). The activation questions read the person's words: a city for the time zone
(« Paris », « heure de Paris »), a sentence for a missed run (« le faire une fois quand il se
rallume »), an amount with its currency (« 0,10 $ ») and a choice left to Nika after another
clause (« je ne comprends pas, choisissez pour moi »).
- **« Active-le tous les matins » activates the saved schedule.** An activation word with only
the period the saved schedule already runs on reached the author, which proposed the saved
workflow again. It now opens the same activation « active-le » opens; another period is a change.
- **An activation question takes its recommended option in words.** « le recommandé, celui à
10 cents » was refused as no amount in USD, and « comme la dernière fois, vous choisissez » was
not one of the three, while « comme vous voulez » was. The recommended option, a choice left
to Nika in other words, and one amount said with its unit inside a sentence are now read.
- **An answer at a gate is recorded when the paused run spent part of its ceiling.** A run that
paused at a `nika:prompt` gate after a priced step resumes under what its ceiling leaves, and
its admission priced the whole workflow again, the finished step included. « non » and « oui »
were both refused (`NIKA-1709 · … unavoidable cost floor $0.196608 exceeds --max-cost-usd
$0.195214`), the answer was never recorded and the run stayed paused, from the Session, the
terminal and `nika run --resume <trace> --answer <task>=<value>` alike. The CLI preflight and
the runtime's own launch gate now price the work still to run: a task the resumed journal
completed is a cache hit and costs nothing, with any child workflow or priced builtin it
calls, and a branch the answer closes was already priced closed. A step edited since the
pause runs again and is still priced by the runtime's gate. What still runs is judged as
before, and a fresh run's floor is unchanged.
- **A question Nika asked no longer blocks a proposal that shows it.** When the author asked a
question and then proposed with that same choice shown on the card, `yes` was held (« Nika asked
`topics`, which this proposal does not show ») and the asked question could not be answered while
the proposal waited. The card's choice now is that question: `yes` keeps its default, another
option revises. A question the card does not show waits first and is answered by its own
identity (that reply never names the workflow's file); a proposal shown before that answer is
revised before it saves.
- **A reasoning effort the model does not offer is a finding, not « clean ».** A workflow
declaring `run.reasoning.effort: low` for a model whose catalog lists no effort level passed
« check · clean ✔ » and « Vérifié ✓ ». Every run of it was then refused before its first task
(NIKA-1800). The Session's check now reports it as a finding, so such bytes are neither shown
as ready nor saved as clean. What only this machine lacks, such as a seat not installed, stays
a run-time matter.
- **An empty product is said in the run report.** A run reported « réussi » with « produit ·
./journal.md (0 B) » among its green ticks, after moving the three messages it read to
`traites/`. The report now says when a file it wrote is empty, and how many files the run
removed beside it, so the person checks the result before running it again.
- **An HTML feed title reads as its words.** A feed title of type `html` (Atom `type="html"`, as
The Verge publishes) kept its markup: a reading list showed « Bluesky’s AI tool ». The
`feed` mode now decodes its entities and drops its tags for the feed's and each item's title; a
plain title, the summary and the content are unchanged.
- **An identical restatement no longer piles up in the request.** A retry or a restatement sent
twice joined the request each time: « Original request: » four times, three identical
corrections, and a judge question large enough to overflow its context. A correction whose
words equal the request's last ones now leaves the request as it was.
- **An installed trigger whose runs would miss their API keys is never said to run on its own.**
After the activation card had warned that a scheduled run would not see the person's keys, the
install still answered « Installé et actif · tournera tout seul ». It now says the trigger is
started, that every scheduled run fails until it sees the named keys, and how to give them: a
file of `KEY=value` lines passed to `arm --emit launchd --env-file <file> --write`.
- **An answer the provider interrupted says how to go on.** « the provider interrupted the
answer: nothing of it was kept or run » came back three times after 2 min 05 s with nothing
saying whether to retry, rephrase or change the model. It now says the provider ended the
answer with an error, that nothing was kept or run, and to send the same line again or choose
another model with `/intelligence`.
- **A GPT-6 call that wrote the prompt cache is priced.** OpenAI reports the input it wrote to
the prompt cache beside the cache read; Nika read any write as an unknown billed axis, so such
a call stayed unpriced. The write now counts within the input, at the cache-write rate the
GPT-6 pages state; on a model whose page lists no write rate, the call stays unpriced.
- **An authoring call whose recalled knowledge exceeds what one request carries is still sent.**
Bounded inference sends at most 1 MiB and refuses a larger request before any byte leaves, so a
large knowledge pack lost the whole call and nothing was authored. The document, sketch,
revision and Plan doors now weigh their opening first: past the capacity less an eighth kept for
the rounds that follow, the pack's references are shown by their first line from the end of each
kind's order, the same share of every kind, until it fits. Each says so in its text, and its
receipt keeps the whole entry's size and digest (`first_line_only`). The request, the card, the
callable contracts, the language and the component list stay whole, and an opening that fits is
sent unchanged.
- **An OpenRouter reply is priced again.** Every paid OpenRouter reply read as an unknown cost
(« cost · UNKNOWN · 1 unpriced call(s) ») because it carries the upstream provider's cost
beside its own charge, which Nika took for a BYOK bill. A non-BYOK reply now settles at the
charge it reports, its breakdown read for its shape only: `z-ai/glm-5.3-flash` costs
$0.000078 for a one-word reply instead of an unknown amount.
- **An unbounded estimate names what bounds it not.** « model output estimate · unbounded: a
token or iteration bound is missing » stood beside a declared `max_tokens: 4096`, when only
the number of items a step fans over was unknown. The line now names the tasks that declare
no `max_tokens`, the tasks that run once per item of a list known only at run time, and a
composed workflow with an unbounded step.
- **An unpriced call says why.** Every unknown cost read « a route with no price table (a local
model is unpriced, never free) », said of OpenRouter and Gemini routes, remote and billed. The
run result now names the reason its frames recorded: no verified tariff for the route, usage
its provider did not report completely, a subscription seat, rejected usage or a local model.
- **An approval stated before an effect is a gate without an asking verb.** The reader saw a human
gate only through an asking verb or an « after my approval » shape, so « je veux une validation
humaine avant d'écrire jobs-<date>.md » or « je valide chaque run avant l'écriture de … »
stated none, and the « écrire jobs.md » such a gate holds was no write without the word
« fichier ». A person's approval that a `before` closes now gates the effect named after it,
and inside a gate a form of writing followed by a file it names is the write. A denied approval
(« pas de validation avant … ») and a validation step no person performs (« a schema
validation before writing ») stay none, and a person turning down a proposal (« non, je veux
plutôt écrire le total dans ./out/total.txt ») bans nothing.
- **`nika arm --emit` no longer arms a beat that cannot run.** `nika arm` reported a beat
UNREADY (a required input with no binding source, a missing workflow, an unpriced
model), yet `--emit` printed its unit and `--write` installed it, so the OS would fire
a run that could only fail. The emission now refuses while any active local beat is
UNREADY, naming each one with the blockers `nika arm` shows: fix the beat, or suspend
it with `actif: false`. A beat whose only open question is an unknown (a child
workflow whose model only the run decides) now reads READY, as `nika arm fire` fires
it, and `nika arm` still names the unknown.
- **`nika arm --emit --write` retires the units of beats `nika.yaml` no longer arms.** A beat
removed from the registry, renamed, or moved to the cloud kept its OS unit, which went on
waking `nika arm fire` for a beat that no longer exists; neither `--write` nor a dry
emit mentioned it. Now `--write` syncs the units with the registry: each of this
project's units whose beat is no longer armed is named with the command that unloads it,
then its files are removed. Loading and unloading stay your gesture, as before. A dry
`nika arm --emit` lists what `--write` would retire. Another project's unit, and a unit
without this project's GENERATED mark, are never touched.
- **Two projects that arm a beat of the same name keep their own OS units.**
`nika arm --emit … --write` named a unit after the beat alone
(`nika.arm.daily`). A second project's `--write` replaced the first
project's unit, `nika arm disarm daily --write` in one project deleted the
other's, and a flagless `--write` could copy another project's env-file
wrap, so its runs sourced that project's keys. Unit names now carry the
project's key (`nika.arm.<key>.daily`, the start of the project root's
fingerprint), and neither the emit nor disarm reads, replaces or removes
another project's unit. Disarm now prints `launchctl bootout
gui/$UID/<label>`: the printed path form failed once the plist was gone, so
the job stayed loaded until logout. Units an older engine wrote are
reported with the commands that retire them; re-run `nika arm --emit
<target> --write` in each project. A project whose `.nika/arm/env-file`
already names another project's file keeps it until you pass `--env-file`.
- **A failed authoring call names the provider's status again.** A dispatched call that fails
carries its money observations, and the authoring boundary judged that wrapper instead of the
error inside it: a busy seat's 503, now ended at once instead of re-sent, read only « the
authoring provider call failed », and an observed admission refusal lost its type. The document
now says « the authoring provider answered HTTP 503 », so the caller knows a retry may help.
- **The author reads the Session's spend so far with each line.**
QA gauntlet q09: asked what preparing had cost, the author answered
« je n'ai accès à aucune mesure » while `/status` said $2.32. Once a
preparation request was observed, each line the person types now
reaches the author with the spend as `/status` says it, ahead of the
run and recovery notes. An agent leading over ACP, whose subscription
the Session never meters, reads none.
- **« quoi ? » reaches the conversation.** With no recovery to tell, a bare « quoi ? » or
« hein ? » answered with the last run's fact, « no run yet under this root (no trace in
`.nika/traces/`) », beside a question the person could not read. It now reaches the
conversation, which answers what the person did not follow. « what happened? » still reads the
last run.
- **A bare yes answers the author, never runs the workflow.** « Oui, allez-y, merci. » to the
author's offer to prepare a change ran the saved workflow and overwrote its report. A line made
only of a yes, a go-ahead and courtesy now goes to the conversation, which knows what it
offered.
- **A behaviour defect is said in words.** When a rehearsal of the draft gave a result the request
does not ask for, and no repair followed, the conversation showed the repair's raw record
(`{"kind":"behaviour_failed",…}`) under « what stopped it ». It now says what the rehearsal
showed against what was asked.
- **A choice or decision card no longer hides what Nika just said.** The card covers the
conversation, so a reply written after your last message, such as why a requested change could
not be made, was only found by scrolling once the card had closed, and the save decision came
back looking as if nothing had happened. The card now opens on Nika's words since your last
message: the whole reply where it has room, otherwise its first row, marked as cut, then its
latest lines, with Markdown marks read as words. A decision on a proposal shown before your
last message says `this proposal is unchanged since your last message`; an act you picked on
the card, such as « Inspect the exact changes », does not count as a message.
- **A recommended ceiling reads as a multiple of one run.** « $0.10 · covers 10 runs like the last
one » read like a budget for ten runs. It now says « 10 × the last run ($0.0094) », in French
for French speakers.
- **Naming the ceiling without an amount no longer refuses the line.** « Le plafond, je le donnerai
au moment de lancer » or « quel est le plafond ? » was refused as an invalid amount, and nothing
was prepared. Such a line now states no ceiling. A bare « …, budget » after the work and a
malformed amount (« plafond -1 », « budget NaN ») are still refused.
- **A ceiling offer counts the runs it holds.** The recommended ceiling read « $0.10 · covers at
least 3 runs like the last one, $0.0015 » for an amount that holds 66 of them. It now says how
many: « covers 66 runs like the last one, $0.0015 each ».
- **A run's ceiling now reads the same way wherever it appears.** Before a run, the
Session wrote « ceiling $0.1 (saved with the workflow) » and the line below it wrote « ceiling
$0.10 ». A ceiling under a cent showed as « $0.00 », which is wrong. Every surface that names a
ceiling (the run's announcement, the proposal's asked run, the project facts, a run's result,
`nika check` on a project) now writes the amount as you set it: `$0.10`, `$0.25`, `$0.001`,
`$2`. Every significant digit is kept, cents show wherever there is a fraction, and no amount is
rounded to zero.
- **A change said beside a rerun is made first.** A line with a rerun word and an amount was read
as « rerun the saved workflow under this ceiling » whatever else it asked: « passe l'étape IA du
commentaire sur Claude Sonnet, et relance avec le même plafond de 0,10 $ » reran the Haiku
workflow at once and overwrote its report, the model change lost without a word. The shortcut
now applies only to a line that says nothing but the rerun and its ceiling (« augmente le budget
à 1 $ et relance », « relance-le, max 0,50 $ »); a line asking a change beside it reaches the
usual reading, which prepares the change.
- **A choice left to Nika is said in the person's language.** « Laisser Nika choisir » recorded
« I leave the choice to Nika. » as a French person's words. It now records « Je laisse le choix
à Nika. » for a person who writes French.
- **A revision's circumstance is judged with its clause.** « Dans rapport.md, pour les doublons,
mets l'email … » was cut at its commas and refused because « Dans rapport.md » and « pour les
doublons » were judged as missing requirements of their own, under a « Change: » label the
person never typed. A short phrase that opens with a preposition and ends at a comma now joins
the clause it frames, and the refusal no longer quotes the label.
- **A long conversation keeps the person's own words.** When a conversation outgrew the
model's window, its earlier part was folded into a summary that, by design, is not the
person's words. The author then had nothing of theirs it could cite: it asked four times to
restate a request that was still on the screen, and refused to bind an authorization given
earlier. A summary now comes with the person's own lines it folded, verbatim, each with its
citation. An old line too long to fit is given by its opening words.
- **A cost ceiling says when a model reasons beyond `max_tokens`.** On a route whose reasoning
no request bound holds (xAI's Grok models), check said « ✔ COST $0.0123 – $0.0123 worst-case
output ceiling » and « model output estimate ≤ $0.0123 ». Runs then cost $0.016 to $0.021. The
line now reads « visible-output ceiling · reasoning of `task` billed beyond it ». The proposal's
money rows name each such task and model. Run admission already reserved the whole generation.
- **A number that counts or ranks something is no ceiling.** A run line was refused whenever it
held a number without a currency: « J'ai déposé 3 messages dans inbox/, relance-le stp » and
« relance-le une 2e fois stp » met « that number has no explicit monetary meaning ». A number
followed by what it counts (« 3 messages », « 2 fois ») or with an ordinal ending (« 2e »,
« 3rd ») now states no money; a bare number still asks for its currency.
- **A request to create or copy a workflow is shown before anything is written.** The
conversation's model could treat a request such as « Crée-moi un nouveau workflow X, identique
à Y » as the person's consent, and the Session then saved a workflow nobody had seen. A workflow
the person has not seen is now saved on their earlier words only when those words say save or
run (« enregistre-le », « puis lance-le », « save it »). Otherwise the proposal waits for their
yes. Their consent to a proposal already shown is read as before.
- **A CSV workflow is rehearsed.** A rehearsal refused every `nika:convert` step: « task
parse_perso runs nika:convert, outside any bounded subset a rehearsal vouches for ». So no
workflow that parses a CSV was ever tried before its first run. The room now runs
`nika:convert` over its own copy, within the copy bound. Every value a room run builds is
bounded where it is built.
- **A day a noun names is no money.** « J'ai mis le fichier du 16 de côté. Relance-le. » was
refused as a ceiling without a currency, in English. A day of the month a noun names (« le
fichier du 16 ») is now data; a day said after a run verb (« relance-le le 16 ») is still asked,
since it may say when to run.
- **A day a past participle names is no amount.** « J'ai remis le 17 et mis le 16 de côté.
Relance. » was refused as a ceiling without a currency. A day after « remis », « mis »,
« gardé » or « déplacé » names what was moved, as « le fichier du 16 » does; « relance-le le 16 »,
which may say when to run, is still asked.
- **A day of every month is a monthly schedule.** « Le 1er de chaque mois à 8h », « tous les 15
du mois à 9h » and « every month on the 1st at 8 am » had no cadence reading: the Session
refused them as incomplete, and an author bound a daily 8:00 with a date check inside the
workflow, which lost every missed 1st. They now read as `0 8 1 * *` at the head or the end of
a request and in a schedule answer. A 29th, 30th or 31st, which some months lack, is asked
again with the last day of the month offered, never bound.
- **A schedule's declaration says no freshness of its sources.** Landing `nika.yaml` said
« source freshness not judged: it reads no project file », even for a workflow that reads
`releves/*.csv`. A set that lands no workflow runs nothing, so its yes now says nothing of
sources.
- **A declared schedule changes zone, or goes, in the person's words.** Four attempts to move a
declared schedule from Mauritius to Paris time reached the author, which cannot write
`nika.yaml`, and « retirez cette planification » was sent to « désactive-le », which removes a
trigger, never the declaration. « Mettez plutôt l'heure de Paris » now proposes `nika.yaml` with
that entry's cadence in the new zone, its hour, missed policy and ceiling kept, and the trigger
is offered again once it lands; « retire la planification » proposes the file without the
entry. A schedule this Mac fires is deactivated first. « l'heure de Paris » now reads as a zone.
- **A demo asked for by its name is its skeleton.** Only the bare name of the welcome's demo
(« aggregate-by-key ») reached its exact skeleton: « montre-moi cette démo aggregate-by-key dont
tu parles sur l'écran d'accueil » went to the conversation, which showed the workflow in the
chat, and « enregistre et lance » then failed because no request of the person's stood behind
it. A line that names one demo and only asks to see, try or run it now reaches its skeleton
with no model: its one open value is asked and its answer proposes the demo. A line that adds a
path, an address or a number stays the conversation's.
- **A discarded proposal never comes back.** After « Discard » of a revision over a saved
workflow, the next fix edited the discarded draft, which came back under another name as a
second workflow beside the saved one. A discard now leaves the saved workflow as it stands as
the base of the next change (no base when nothing is saved), and the author is told what was
discarded. The discard is said in the language you write in.
- **A failed step says why.** « proposing the workflow failed · 3 ms » now carries the reason
the run reported, on one short line.
- **A resume serves back a fan-out over its own `with:` and a group fold.** A `for_each` whose
collection read the task's own binding (`for_each: { items: "${{ with.ids }}" }`) was never
stamped, so `--resume` ran it live again: the content a gate had shown changed under the
answer, and every answer was refused NIKA-SEC-010. The stamp now resolves the collection over
the item-free `with:`, as the run does. A `${{ group.<name> }}` fold is stamped over its
declared members, without their durations, which the journal cannot serve back.
- **A fetch that sends says it sends.** A `nika:fetch` task whose method is POST, PUT, PATCH or
DELETE now reads « sends to the web » wherever the Session says what a task does. That covers
the gate's « un oui permet … » line. Before, a POST to a webhook read « fetches from the web ».
- **A file the person names alone is found where it is.** A request that names a file by its
name only (« si la note tarifs-2027.md échoue ») while the file lives in a folder
(`./notes/tarifs-2027.md`) was refused at every revision as « UNREALIZED PATH », even when the
workflow read every note through a glob. The observation now locates the one file of that name
under the project (hidden folders and build outputs aside), and a workflow that reads it where
it is realizes the name. Two files of that name, or none, leave it unrealized as before.
- **A file the person takes back is no longer owed.** Every file a request named stayed owed for
the whole conversation: after « ne lis pas profil.md, copie mon profil dans le workflow », each
workflow that no longer read the file was refused until the person restated everything. Their
latest words about a file now decide: a later line that stops it from being read or used, or an
offer they picked that says it is no longer read, withdraws the earlier mention, and naming it
again asks for it again. A line that only qualifies the reading (« ne lis pas data.csv en
entier ») or a condition (« si tu ne lis pas … ») withdraws nothing.
- **A folder named in words is held by the rehearsal.** « Les fichiers .txt du dossier inbox »
named no folder, because only path-shaped words (`./inbox/`) counted. The rehearsal then held
nothing, and the first run on the empty inbox failed. A folder introduced by its noun
(« dossier », « folder », « répertoire ») and a bare name is now copied into the rehearsal room
when the workflow reads under it. « Le dossier client » with no read under `./client/` stays a
case file. A named folder that is not there is skipped, as the real run finds it.
- **A folder you name as `copies/` is tried before « propre ✔ ».** « Dans le dossier copies/ … »,
« dans commandes/ … » or « du dossier notes/ » named no folder the rehearsal could hold, so a
workflow reading those files was proposed as clean without running a step on them, and its
first real run could fail on its own jq. Such a folder is now copied into the rehearsal room
where the workflow reads under it (within the room's copy bound, originals untouched), and the
steps after a model step that declares its answer's shape run on an example of that shape: a
wrong jq is caught before you are asked to say yes.
- **A proposal over a folder is really rehearsed, and a rehearsal that binds nothing is said.**
A Session proposal whose request reads a folder (« copies/ », « inbox/ ») was still shown
« propre ✔ » with no rehearsal: the world observed before the trial refused the folder as no
regular file, the trial's evidence was refused for having no receipt of the folder itself, and
the refusal was dropped in silence. The world before a rehearsal now holds each file of a
folder or a pattern the request reads, and of a destination folder, so the rehearsal is bound
to the copies it ran on; a source the workflow reads only under (« lis ./inbox ») is held as
the folder it is, even empty; a file the run writes that the request never named is bound as
the absence it was in your project. The proof then keeps what the request names and what the
run wrote: files arriving in or leaving an inbox between runs are no drift. A proposal whose
trial binds nothing says « No rehearsal bound to these bytes » and why.
- **A schedule in another zone than the Mac's can be armed with launchd.** launchd fires in the
machine's zone, so `nika arm --emit launchd` refused a cadence such as `TZ=Europe/Paris 0 8 * *
*` on a Mac set to another zone. The unit now wakes at each machine hour one of the schedule's
slots can fall on over the coming year (10:00 and 11:00 on a Mauritius Mac for 08:00 Paris,
summer and winter time). The firer reads every wake in the schedule's own zone: it runs the due
slot and leaves the other wakes not due, without journaling them.
- **A free « Oui » to the author's approval question is the person's approval.** When the
author asked « Veux-tu valider chaque envoi avant qu'il parte ? » and the person typed « Oui »,
that answer bound nothing, so the gate it approved still read as invented and the author asked
again (six times in one session). Such a yes now states the approval its question names, as a
picked option already did; a « Non », a qualified no (« oui, sans validation ») or a yes to a
question that asks no approval states none. Approval questions read their infinitive
(« valider », « approuver », « confirmer ») and the person glued to the verb that asks them
(« veux-tu », « dois-je ») or doing them (« par un humain »).
- **A French proposal card says its rehearsal in French.** The card beside a French proposal
read « répétition · Rehearsed on a copy of your files · nothing ran on the originals · … »,
while the proposal's own text said it in French. The rehearsal words a host draws now follow
the person's language as the proposal does.
- **A French card says what runs in French.** A proposal's run-time rows (« writes … », « pauses
for a human answer », the model estimate), the understanding note and the resume line were
English on a French card; they now read in French, every path, host, model and amount as
written.
- **A French session reads its proposals and its runs in French.** A French proposal still
read « money: $0.25 USD · session default · proposal/Run ceiling · … », « when it runs: » and
« apply this? (yes applies · …) », and its run « check · … · clean ✔ · ceiling $0.10 (saved
with the workflow) » and « run observed · exit 0 · succeeded ». A person who writes French now
reads them in French, every path, amount, identity and workflow byte as written. The identity
a consent answers is unchanged. The ceiling's source now says « saved beside the workflow »:
it is kept beside the file, never in it.
- **Asking one more change to a proposal no longer fails the turn.** The revisions of one
proposal drew on one rehearsal account across every turn, so after a few changes each new one
failed with « the session runtime failed: the shared turn rehearsal budget admits no further
world ». A line that answers no open question now starts a fresh account; answering a question
still continues the one it belongs to, and a suspended proof still waits aside.
- **A run's text result says its last line too.** The run card showed a declared text output
by its first line. A journal each run appends to read the first run's line after every run, as
if nothing new had been written. A text of several lines now reads as its first line, its last
line and how many lines it holds.
- **A workflow that keeps its own state is rehearsed, first day included.** A journal it appends
to, or yesterday's figure it compares with, was read in the rehearsal room as « read but not
observed », or refused as an absent input on the first day, so the whole rehearsal was dropped
and the first real run met the first-day case untried. A path the request names that the
workflow both reads and writes is now handed to the room as its state: copied as it is, or held
as the first day's absence, read back after the run, and its write advances the proof.
- **A jq step built from a `const:` value is compiled by `nika check`.** A `nika:jq` expression
holding `${{ const.NAME }}` was skipped as « built at run time », so a constant set unquoted in
the program (`${{ const.target_dir }} + "/"`, read by jq as names) checked clean and failed the
first paid run with « undefined filter or variable ». The check now builds such a program
exactly as the run does and compiles it; a template only the run can know is still left to the
run.
- **A judge reads the Foundry offers it weighs, not the whole catalogue.** Every verification
question carried each offered component with its contract, about 70 KB for fifty components.
The decision seat could not read the whole-request question, so nothing was judged on the
standard path. A question now carries in full the components the bytes hold; every other offer
appears by identity, title and status. Only a localization, which weighs every contract, is
sent them all. A seat that refuses a question as too large no longer advises a retry that
would ask it again.
- **A refused key check says what the provider said.** Testing a key the provider refused
answered with the hint a failed run prints for the command line (« NIKA-INFER-001 …
`--access <seat>` »). It now reads the provider's own words and its error's kind, the key
withheld.
- **A launchd unit given an env file passes its keys to the run.** `nika arm --emit launchd
--env-file <file>` sourced the file through `/bin/sh` without exporting it: the plain
`KEY=value` lines of a dotenv file never reached the firer, and every scheduled run that needs
an API key failed. `nika arm fire --env-file <file>` now reads the file itself (plain
`KEY=value` lines, an optional `export `, comments, blank lines, single or double quotes) and
fires the beat with those variables set, never printing a value; a file it cannot read, or a
line that is not `KEY=value`, refuses the fire before anything runs, naming the file and the
line. The launchd unit hands `--env-file <file>` to the firer in its `ProgramArguments`, with
no shell wrapper. Units written by an earlier engine are still recognized when they are
re-emitted or retired.
- **A line typed after a failed run's diagnosis is the person's own message.** When a run the
person's words authorized failed, Nika's note started a turn the model answered, but the
proposal it ran still counted as waiting: the person's next line was read as that proposal's
answer and moved into its reply, so the request ended on the model's own message. Claude Opus
5.5 refused it (« does not support assistant message prefill ») and the turn failed. Once the
model has answered such a note, nothing waits on the proposal any more.
- **A change that names the saved workflow's files revises it.** After a workflow was saved,
« Trie le rapport par ligne. » could be read as new work. It then got an English question about
the run model and an unrelated, paid `classify` workflow. A line that opens with a change (« trie
», « ajoute », « retire », « sort »…) and names a file the saved workflow reads or writes, by
name or without its extension (« le rapport » for `rapport.md`), now revises it. A line that
states the work again, or asks for a new workflow, still starts one.
- **A judge locates a missing part over each offer's purpose.** On the standard path, every
revision still overflowed the decision seat on one question: the localization of a missing part,
which was sent every contract of the catalogue. It is now sent each offer's identity, title,
purpose and status. The components the bytes hold stay whole.
- **A model's working notes are never shown.** A model that streams its reasoning as text (no
reasoning field) had its English working notes, turn ids and finding names shown in the
conversation and in a question card to a French person. The words beside a call are now shown
only when they are written in the person's language, without what a reasoning tag
(`</think>`, `</mm:think>`) closes or opens.
- **A model the person named is told how to state it.** A model the person named in words
(« Grok 4.7 (xAI) ») but stated by the author as typed (`answered`) was refused as words
« not in the person's line », though they were; Grok 4.7 retried other excerpts of the same
line again and again. The refusal now says the value was not typed and asks for it as
`named` with the same words.
- **A newline after a lone reference is written.** `content: "${{ with.d.version }}\n"` wrote
`3.15.0` without its newline, and `"${{ x }}\n\n"` lost both: a string holding one reference
and only whitespace around it was read as the reference alone, which keeps the value's type and
drops the whitespace. Only the reference alone keeps its type now; any other character around
it, whitespace included, is text. A `for_each` collection is still read whole when a block
scalar leaves whitespace around its reference.
- **A per-run budget never blocks a proposal.** « … chaque run doit coûter au maximum 0,10 $ :
budget max 0,10 $ par run » was read as an obligation whose quote spanned the model clause too,
and the first proposal was blocked twice for a ceiling no task can carry. An obligation whose
requirement states only the Run ceiling beside the candidate is now carried there. A copy of a
saved workflow also keeps the ceiling the person set for its next runs in the conversation
(« Le plafond est de 0,10 $ par run stp », then « identique à … »: the copy ran at $0.25).
- **An approval the person picks is their gate.** The approval laws read only the person's typed
words, so a gate they picked from a question (« Oui, je valide chaque run avant l'écriture de
jobs-<date>.md ») was refused as an invented gate, while the Rule of Two required that very gate:
a job digest that read the person's profile was refused after each of five such questions. The
laws now read the label of an offer the person picked themselves as their words: the gate it
asks for stands, and a candidate without it owes the approval. A choice left to Nika or a label
that denies the approval states none.
- **A proposal's choice takes the words you type beside it, and `cancel` discards the
proposal.** The card's hint said « or type your reply · cancel drops it », yet a sentence of
your own came back as « not taken · answer text restored for editing » after Nika had read and
answered it, and « cancel » or « annuler » were sent to Nika as one more answer, so the only
way out was to accept a change you did not want. Words Nika reads now stay said: its reply
shows above the choice, which keeps waiting. `cancel` or `annuler` on its own line discards
the proposal through the same door as « Discard this proposal », with no model call and
nothing written, and the hint now says `cancel discards the proposal`. An offer that is itself
named `cancel` still answers the choice.
- **A proposal of the saved workflow says nothing changed.** The saved workflow proposed again
byte for byte showed Save, Save and run once, and Discard. It now says nothing changed and how
to run or activate it, and no proposal waits.
- **The Session no longer crashes when the decision seat skips an oversized question.** After the
seat had refused one question alone for its context, a later, larger question was skipped
without a place in the request journal. The next request then settled at the wrong index and
the terminal app panicked (`batch.rs:121`). Every skipped question is now journaled as never
sent, in its own place.
- **A question the author asked owns its answer.** After an accidental « annuler », the author
asked when the journal should be written; « tous les soirs à 20h » then opened the activation
of the welcome example saved earlier, and its zone, missed-run and ceiling questions. A line
that answers a question the author asked now reaches the author; with no question waiting, a
schedule stated for the saved workflow still activates it.
- **A rehearsal says when what it read back came from steps it did not run.** A trial that
could not run three model steps still read back the file written behind them, « _Aucun article
sur les 7 derniers jours._ » three times, as if it were the result; the real run wrote 46
articles. A trial now says that what it read back of a file its unrun steps write never is what
a run writes.
- **A recommended run ceiling says why, never as a ratio.** The ceiling question of an activation
said « $0.10 · ~3× the last run, $0.0015 », read as $0.10 being three times $0.0015 (it is
about 67 times). It now says « covers at least 3 runs like the last one, $0.0015 ».
- **A run whose gate the person refused is never said to have succeeded.** After a « non » at a
gate, the observation line now says « exit 0 · stopped at your no » / « arrêté à votre « non » »
instead of « succeeded ». `/status` says « Last run · Done · stopped at your no » / « Dernier
run · Terminé · arrêté à votre « non » » instead of « le run a réussi »
(`RunFacts::refused_at_gate`).
- **A refused key reads plainly while a workflow is prepared.** When the provider refused the
key of an authoring call, the recovery card carried the hint a failed run prints for the
command line (« --model … --access <seat> »). It now says the provider refused the
connection's key, with the provider's own words, and to check it with « Test the key » in
`/intelligence` or choose another connection.
- **A turn whose proposal was refused says so, whatever the reply claims.** After Nika's check had
refused every draft of a turn, the author's own words reached the person as they were, and could
read « C'est enregistré et lancé » while nothing was proposed, saved or run. Such a turn now ends
with « Nothing was proposed, saved or run in this turn: Nika's check refused the draft. » (in
French in a French conversation), after the author's words. A turn that refused no proposal
carries no such line.
- **A refused revision says the proposal you saw still waits.** When Nika's check refused a
revision while an earlier proposal was still waiting, the turn ended with « Nothing was
proposed, saved or run in this turn », beside a proposal the person could still save. It now
says « This turn changed nothing: Nika's check refused the revision, and the proposal shown
before still waits for your answer. » (in French in a French conversation).
- **A rehearsal holds the folders and patterns the request names.** A request naming a folder
(« inbox/ ») was not rehearsed: the room refused the folder as no regular file, and
`nika:glob` was outside what a rehearsal runs. The room now copies a named folder with each
file under it, and makes an empty one empty: the empty day is rehearsed too. It copies a named
pattern by its matches. A rehearsal runs `nika:glob` and `nika:remove_file` over its own copy. A
listing that no named folder or pattern holds is refused before any room, as a read is.
- **A rehearsal names a web read as one.** A `nika:fetch` whose answer jq shapes was listed
among the steps not run as « a jq extraction » (« une extraction jq »), which reads as a jq step;
it is now « a web read through jq » (« une lecture du réseau filtrée par jq »).
- **A rehearsal is told in French to a person who writes French.** The proposal's rehearsal
lines (« Rehearsed on a copy of your files … », « tried on the pages observed at … », « not
run: the model step (…) … ») stayed English under French answers. They now read « Répété sur
une copie de vos fichiers … », « essayé sur les pages observées à … », « pas lancé : l'étape du
modèle (…) … ». Names, paths, excerpts and reasons stay as written. The proof keeps the engine's
own words.
- **A rehearsal tries the day a gated step is skipped.** A workflow could pass its check and
its rehearsal, then fail its first run with nothing new: a filter after a skipped step read its
null (« cannot use null as string », « cannot use null as iterable »). After its run, a
rehearsal now evaluates each such `nika:jq` step on that null, in its bounded jq process. It
fails with `NIKA-REHEARSAL-SKIPPED` only when the null alone makes the filter fail: the same
filter passes with a value in its place. The author then guards the step before anything is
proposed.
- **A rehearsal tries the steps after a model step on an example of its answer.** A trial ran
no model step, so it never ran the steps after one either. A workflow could pass its check and
its trial, then fail its first run in a step that read the model's answer, or a date beside it,
the wrong way. When a model step declares its answer's shape (`schema:`, `returns:`), a second
pass stands an example of that shape in for the answer, over the room's files, writing nothing.
A step behind it that fails on the shape (a jq type error, a binding that reads beneath a value)
fails the trial with `NIKA-REHEARSAL-SHAPE`. A failure of content (an assertion, an empty
filter) is the example's, never counted. Nothing this pass computes is shown as a result.
- **A file the run removed no longer reads « produced ».** The run result listed every path the
run was allowed to write as « produced », so the inbox files a run moved to `traites/` read
« produced · ./inbox/… ». A path that is not there now now reads « removed » when a
`nika:remove_file` step of the run took it away, « not there now » otherwise; the run's own
completed writes still read « produced ».
- **A reopened conversation checks its saved workflow again.** After a restart, the workflow
the conversation saved is checked again on disk, with the same audit the save ran and no model.
An unchanged clean file now stays « Vérifié » instead of « Vérifié ○ », and a file changed into
findings says so.
- **A reading that repeats a coverage row is said once, not repaired.** When the author's
reading of the request covered one span in two rows (« coverage[1].span · This identifier is
repeated »), the verification started a repair round with the model: 2.7 minutes, then 9.7, on
a slow model, before anything was judged. Rows for one span or one answer key are now merged,
their references united, before the reading is checked. Nothing the reading claims is dropped.
- **A candidate refused the same way under a new revision is told it repeats.** The author was
never told it was repeating itself when the same `candidate_write` came back refused with the
same findings, because each reply carried a new revision number: Grok 4.7 sent it some sixty
times. The revision is left out of the comparison, so the repetition and stall notes reach the
model from the second and third identical call.
- **A request over the model's context window is told apart from other refusals.** A
provider's documented overflow code (`context_length_exceeded`) used to be dropped, so
the failure read as a generic HTTP 400. It is now a typed context overflow that names
the model's window when the catalog knows it. On a wire without such a code (Anthropic),
a 400 `invalid_request_error` counts as an overflow only when Nika's own estimate of the
request reaches that window; the provider's wording never decides. The overflow is
never retried as transient, and 429, 503 and 529 stay transient rejections. An answer
that stopped because the context window was full (Anthropic
`model_context_window_exceeded`, Mistral `model_length`) stays cut, as before: never a
normal end, its tool calls never run. It now also carries a window-full marker, set too
when a provider reports more input than the model's window holds, so a conversation
can compact before asking again instead of re-sending the same full window.
- **« je voudrais l'enregistrer et le lancer » saves and runs the proposal shown.** Requests around
the save and run words are now read by the closed consent grammar, so they no longer reach the
author as new work: « je voudrais », « pouvez-vous », « can you », « I'd like to », with the
infinitive verbs « l'enregistrer », « le lancer ». A line that asks anything more keeps its
route.
- **A request to see the results never starts a run.** Asking to show, display
or read what a workflow produced (« Pouvez-vous me montrer le contenu des
trois traductions », « affiche le résultat », « lis-moi », « show me what it
wrote ») could be heard as a request to run it again: a metered run then
rewrote the very files the person wanted to read. Such a line now never
reaches Run, whatever a classifier hears in it, even beside a run verb; the
conversation answers it from the files or the last trace, and a run still
takes its own words (« lance-le ») (NIK-234).
- **A refused call names the amount it needs.** A model call whose reservation the stated
allowance cannot cover was refused with « remaining catalog allowance cannot cover the
full-context reservation ». It now names the reservation, what it covers (for Grok, its whole
generation, since a request cannot bound its reasoning), what remains of the allowance, and the
amount to state: « state at least $3.40 for this work, or choose a model whose call reserves
less ».
- **A result offer, a decision and a schedule are said once.** After a run, « What would you
like to adjust in this result? » was printed twice: in the run's words and on its own card. A
trigger's decision repeated « Install the trigger on this Mac? » under its own heading, and a
schedule read « every day at 9 · every day at 9 (Mauritius time) ». Each is now said once; words
the cadence reads differently stay beside it.
- **A resumed run keeps its ceiling.** A run that paused at a gate and was resumed without
`--max-cost-usd` ran its next leg unbounded (« budget: none journaled »). The run answered in
the same terminal got its whole ceiling again. Now both continue under what the paused leg's
ceiling leaves after its spend (`RunFacts::ceiling_left`), and the resumed leg journals that
budget. A ceiling stated on the resume still stands as given.
- **A run a gate split reports the ceiling it started under.** Its resumed leg records only
what the first leg left, so the report said « ceiling · $0.212482 » for a run started under
$0.25, and a ceiling stop compared the whole spend with that remainder.
- **A revision of a budgeted workflow is no longer refused for its ceiling.** After a workflow
saved with « 0,10 dollar par run » was run, every revision was refused: « the judge finds no
task performing » the budget. The person's later words for the same ceiling (« le plafond de
0,10 dollar s'applique au lancement du run ») were not the words beside the candidate, so the
judge was asked to find a task for a ceiling only the Run applies. Words that say only the
ceiling of the amount beside the candidate are now carried there, as the first statement was.
- **A run's card shows one task count and one cost.** The card under a finished run said
« ✔ Ran in 1.7s · 20 tasks · $0.005 » while the run's result beside it said « 9 tasks ran (6
from cache) · 2 skipped » and « cost · $0.0046 ». A run as small as « <$0.001 » read
« cost · $0.0004 » there too. The card now counts tasks as the result does: the tasks that
ran, those answered from cache, then those skipped, failed or never run. It also writes every
cost on the card at the result's precision (`$0.0046`).
- **A run line with courtesy is still a run.** « lance copy.nika s'il te plaît », « lance-le une
fois » and « relance-le tout de suite stp » were not read as run lines. They reached the
conversation instead, and in one case met an English reply. They now run as « lance-le » does.
« lance-le une fois par jour » still is not a plain run: it asks for a schedule.
- **A run names the items it recovered.** A site check listed `https://example.com`, a host its
workflow permits, and logged it as down with a code 0: the transport never dials a
documentation address (RFC 2606/6761), the step recovered the item, and the run report said
nothing of it. The report now names each item of a list that failed and was recovered, and
says when the address is one Nika never dials, in the person's language.
- **A run number of no amount is refused in French to a French person.** « that number has no
explicit monetary meaning … » now reads « ce nombre ne dit pas un montant … » in a French
conversation; the money record keeps the reason as found.
- **A run refused before it started says why.** A run the engine refused before its first step
(an access path refused, NIKA-1800, for an effort Gemini does not offer) showed only « exit 3 ·
refused by the environment »: its reason went to a discarded stderr. The run's lane now keeps
that reason and shows it as the run's line, never doubling a reason the stream already gave,
and the conversation hears it with the person's next line instead of guessing a cause.
- **A run says whether each step that sends sent.** The run card named the files a run produced,
but not whether its webhook or notification went out. Each step that sends beyond the machine
(a notification, a request other than GET) now reads « sent · `ping` · the step completed »,
or « not sent · `ping` · its condition was false » (cancelled, failed, served from the cache),
in the person's language.
- **A run now settles a crashed run's leftovers when it starts.** A run killed before its
end (SIGKILL, a machine that went down) left the files it created in place as if
whole, and the original bytes of the files it changed in `.nika/originals/`, where
nothing ever mentioned them. Each file a run first writes now leaves a small record
there, with the digest of a created file's bytes after each write. The next `nika run`
in the project settles a run whose journal shows it dead: a file it created moves to
`.nika/quarantine/<its run>/` only while its bytes are still the ones it wrote (a file
you changed since stays where it is), a file of yours it changed stays and its
original moves to `originals/`, and a `manifest.json` lets `--resume` of that run put
the moved files back. A run that is still alive, or whose state cannot be proven, is
never touched. Each action is one line on stderr.
- **A save and its checks are said in the person's language.** A French conversation read
« applied · wrote », « check · … · clean ✔ », « running … once · ceiling $0.25 », « (the
proposal still waits …) », the source freshness lines, the ceiling question after « save &
run » and every withdrawal in English. They are now said in French to a person who writes
French, and a rehearsal that read no file no longer prints an empty « les fichiers de l'essai
sont tels qu'il les a vus : (…) ».
- **A schedule question names its hour in French.** For a cadence with no stated hour (« toutes
les heures »), the French questions read « il fixe quand tombe the scheduled time » and « Si cette
machine est éteinte à the scheduled time ». They now say « l'heure prévue ».
- **A schedule stated for the saved workflow opens its activation.** After a save, « déclare-la
à côté, tous les jours à 8h heure de Maurice », « je voudrais que ce bilan se fasse tout seul
le 1er de chaque mois à 8h » or « ok tous les jours à 8h du mat alors » now open the activation
with that cadence, as « active-le » does. They used to reach the author, which tried to write
the schedule into the workflow and failed after several minutes. A zone the line names
(« heure de Maurice », « Europe/Paris », « Paris time ») answers the zone question, which is no
longer asked again. A line that also asks something else, or asks for a new workflow, keeps its
route.
- **A schedule's time zone shown as a choice is said to be the schedule's.** An author that
offered the schedule's zone among the choices beside the candidate was told to write it in the
candidate, which a schedule binding never is, and tried 35 times. The refusal now says to give
it as the schedule's `timezone` or ask it, and to leave it out of the shown choices.
- **Searching the intelligence list counts its matches, not every model behind it.** Typing a
name over the list of connections showed « 1 of 657 match » next to about 23 connections. The
657 counted every model of every connection, which the list keeps out of view until a search
finds them. Where a search can find such rows, the title now counts the matches alone
(« 1 match », « 17 matches »). A list that shows every row keeps « 4 of 30 match ».
- **An agent account that cannot write workflows says so before you choose it.** The
intelligence list offered « copilot · Agent account » with no warning. Once chosen, every
request failed with « authoring unavailable · seat `copilot` is not infer-grade for
json_schema ». The list now reads « copilot · Agent account · cannot write workflows here » on
the connection's row and on its own. The check is the same one applied once a seat is chosen:
one turn, no implicit tool, an answer checked against a JSON schema, and an observable model.
Accounts that can write workflows show nothing new.
- **The intelligence list's filled radio marks the choice in use, not the cursor.** Moving
through the list filled the radio of the row under the cursor, so « › (●) GPT-6 Astra » could
read as the current choice while Claude was in use. The filled radio now stays on the value
the list holds: the model in use, the decision seat consulted, or the model requested for
Run. The cursor alone shows the row `Enter` applies.
- **A schedule's zone, once settled, stays settled.** After a repair of the saved workflow,
« active-le tous les matins à 7h » asked the zone again. A revision that carries no schedule
now keeps the one asked for the workflow, zone included. « Choisis pour moi » at the zone
question takes this machine's zone and goes on; before, the Session demanded `Area/City`
syntax. A word that names no zone is refused in the person's language.
- **An OS unit left behind by an edited schedule is named and refused.** After a cadence
change in `nika.yaml` without re-running `nika arm --emit … --write`, the installed unit
kept waking `nika arm fire` at the old time; the beat answered « not due » every day and
nothing woke it at the new time, while `nika arm` still reported it READY. Each beat
unit now carries the revision of the schedule it was written for (the cadence, and for
launchd the machine zone it was rendered in) and hands it to the firer, which refuses a
wake from a stale unit with the re-emit that fixes it and fires nothing. `nika arm` lists
such units under « unités périmées », and also names a unit written by an older engine,
which carries no revision. Re-run the emit with `--write` once to give existing units
their revision.
- **An author stuck repeating one failing call is told to change it.** On Gemini 3.1 Pro the
author copied the note Nika leaves in its history in place of an earlier candidate's document
and sent it as a new candidate 80 times in 15 minutes, each refused « needs the complete
document in `source` ». That refusal now names the copied note and the two ways out (the whole
document, or `candidate_edit`), and a call that returns the same reply three times in a row is
told that repeating it cannot change the reply. Nothing stops the conversation: no quota.
- **A step whose output is over 1 MiB no longer ends its run's trace.** A fetched 454,763-byte
JSON page made a 1,180,930-byte `task_completed` frame: its `output` and its `outcome` each
carry the value, the journal refused the line (« journal line is 1180930 bytes; maximum is
1048576 ») and the trace stopped there, so the run could not be resumed. A frame over the
line bound now moves its largest values, one at a time, to `<trace>.values/<sha256>` beside
the journal until it fits, each field keeping the value's digest, and `spilled_fields` names
them. The chain commits to each digest, `--resume`, `nika trace show|replay` and the debugger
read the values back only when their bytes match it, and `nika trace verify` refuses a value
that is missing or altered. The line bound stays; trace retention removes the values with
their journal and counts them in its budget.
- **A decision seat that cannot hold a document no longer stops its proposal.** When the decision
seat's context is too small for the state a verification asks about, every question was sent,
refused, halved and sent again (about a thousand calls in two live runs) and nothing was ever
proposed. A question larger than one the seat already refused alone for its context is no
longer sent, and a verification the seat stopped is judged by the authoring model instead. The
proposal says which seat could not judge and which model did.
- **A choice gate takes the answer its resume line prints.** A choice gate whose choices spell
numbers (`choices: ["42", "17"]`) printed `--answer review=42` to resume, then refused that
very line: `42` was read as a number, not one of its strings, and a shell could not pass the
JSON string without a second quoting. An `input` or `choice` gate now takes the text typed
(`'"42"'` still gives its text); a word outside the choices is still refused.
- **A topic is selected by the model, never by keywords.** « les 5 top stories de la front page
qui parlent d'IA » was written as a fixed keyword regex: it missed AI stories that do not use
the words and kept stories that only mention them, so one story in five was wrong in every run.
The author is now told that selecting items by what they are about is a judgment: an `infer`
step returns the matching identifiers, and a deterministic step keeps them in the source's
order. A filter on words the person names exactly stays deterministic.
- **A trial names the steps it cannot run behind one it does not run.** A trial said « not
run: … the model step (select_top) … » and nothing of the steps that read it, which were not
tried either (the first run failed in one of them). Its words now add « nor what reads them: »
with those steps, in the workflow's order.
- **A Unicode regex class is found before the run.** `test("\\p{L}")` compiles as jq, then the
runtime's regex engine refuses it on the first item: a clean check was followed by a failed
run. The check now names the class and the way out (an explicit set such as
`[A-Za-zÀ-ÖØ-öø-ÿ]`), so the author repairs it before proposing.
- **The trigger install says when a scheduled run would not see the API keys.** A unit started by
launchd does not inherit the terminal's environment, so a workflow whose model reads its key
there (`NIKA_ANTHROPIC_API_KEY`, `ANTHROPIC_API_KEY`…) would fail at every scheduled run.
« Installer le déclencheur sur ce Mac ? » now names those keys, says the run would fail until
the project's units load a file that holds them (`arm --emit --env-file`), and recommends
keeping the schedule declared. Workflows with no model step, with a mock or a local model, or
whose units already load an environment file naming their keys keep the card as before.
- **A line that sets the ceiling moves it.** « Mets le plafond à 0,001 $ par run et relance »
was not read as money: only a ceiling phrase attached to the work (« avec un plafond de … »,
« budget max … ») was, so the run kept the first request's 0,10 $. A verb that sets the work's
limit (« mets », « remets », « passe », « set », « change », « je veux un budget de … ») now
states the ceiling, « à » and « to » link it to its amount, and the directive ends before the
conjunction that opens the next clause. A record's budget (« le budget du client à 500 $ dans
./clients.csv ») stays data.
- **A verification no longer reads the same request again.** Every verification in a
conversation asked its author to read the person's request, then asked for an audit of that
reading, even when the words had not changed. With Grok 4.7, those calls took 89 % of a
session's reasoning tokens. The Session now keeps, in memory, the last reading audited as
covered. The next verification of the same words, answers, model and prompts reuses it with
no call. The judge still judges every candidate, and a reopened conversation reads and audits
the request again.
- **A workflow named without its extension is the one run.** « je voulais relancer
journal-histoire-du-soir » ran the last saved workflow, journal-recopie.nika, twice. A line
naming exactly one project workflow by its name (a hyphen, an underscore or a digit in it)
now runs that workflow; two such names, or a plain word, name none.
- **A workflow may read back the file it keeps.** « Il garde le chiffre d'affaires du jour pour le
comparer au jour suivant » asks for state a next run reads. The output law refused any read of a
derived output, so every such workflow was refused and the author re-parsed an older report
instead. An output may now be read back by the workflow that writes it (`nika:read`,
`nika:grep`, `nika:glob`), and nothing else uses it. A read source that a pure step builds
addresses from is refused with the way to keep it: compose the base address in the `url:` of the
fetch that reads each item.
- **A workflow that keeps its own state runs again.** A rehearsed workflow that reads and writes
the same file (a log it appends to, yesterday's figure it compares with) could run once: every
later « lance-le » was refused because that file had « appeared » since the rehearsal, and the
only way out was the command line. The run's own completed write now advances that file in the
rehearsal's witness, like any file the workflow only writes. An edit by anyone else after the
run, or of a file the workflow only reads, still withdraws the rehearsal.
- **A workflow that also writes a file you never named runs again.** A site check that appends to
`statut.log` and keeps its own `etat_echecs.json` could run once: every later « lance-le » was
refused because `statut.log` had « appeared » since the rehearsal. One written file outside what
the rehearsal witnessed cancelled the whole advance. Such a file is now left out of the witness
(nothing witnessed it, so it can never drift), and the files the rehearsal bound advance as
before. A source edit, a foreign write, or a write outside the project still withdraws it.
- **A written text file ends with a newline.** Translated notes written from a model's answer
ended on their last character, while their sources ended on a line break. The author is now
told to end each written text with one line break.
- **« Oui, enregistre » saves.** A plain yes before the save verb (« oui, enregistre »,
« ok, save it and run it ») and a courtesy after it (« stp », « s'il vous plaît », « please »)
were not read as the consent they gave, and the proposal waited on. They are now read as the
closed save, or save-and-run, line they are; a line opened by another word keeps its route to
the author.
- **« oui, enregistrez ces changements » saves the card it answers.** Under a card whose option
reads « Enregistrer ces changements », the person's « oui, enregistrez ces changements » was
not read as a yes: it reached the author, which proposed something else, and the `nika.yaml`
was never saved. A yes may now name what the card shows: « ces changements », « these
changes », « ça ».
- **« Oui, activez-le » and « vous pouvez l'activer ? » open the activation.** Only an
imperative opened it: « oui activez le s'il vous plait » ran the workflow once more instead,
and « vous pouvez l'activer ? » became a change proposal, so a French person had to learn the
English keyword « activate ». A yes before the words, a space for the hyphen and the polite
question forms now open the activation, whose questions still decide everything.
- **A yes to the bytes a file already holds says nothing changed.** The landing said « applied ·
wrote » for a proposal identical to the saved workflow; it now says « applied · nothing
changed in … ».
- **A yes with its go-ahead consents to the proposal shown.** « Oui, allez-y », « ok vas-y
merci » or « yes, go ahead please » under a proposal card now save the bytes shown, as « oui »
does. They used to reach the author as new words and start a fresh revision. A line that adds
anything else (« oui mais change le titre ») still goes to the author, and a spending approval
still takes the strict yes.
- **A cloud inference call is no longer cut at 30 s by a deadline the workflow never declared.**
An `infer:` task without `timeout:` gave a buffered cloud call a total deadline of 30 s (300 s
for a local model), so a legitimate answer from a reasoning model or a long prompt failed with
NIKA-INFER-001 (provider 408) and the Run wrote nothing. Every buffered call without a task
`timeout:` now gets 600 s, local or cloud: the provider transport's own bound on a connection
that delivers nothing. A task `timeout:` still sets its own bound, streaming keeps its
idle-read guard, and the timeout message names the deadline that applied.
- **A call that wrote to the provider's prompt cache keeps its price.** Nika priced a call
that reported a cache write as unknown. It now prices the written tokens at the catalogue
row's own cache-write rate (the five-minute one, the only lifetime Nika asks for). A row
that states no write rate, a declared tariff and an admission tariff still leave such a
call unknown, as do two write meters that disagree.
- **« annuler » at a question the author asked drops it.**
Gauntlet wave 3: « cancel », « annuler » or « laisse tomber » typed at
the author's question reached it as one more answer. The hosts' one
cancel reader now drops the question with no model call: nothing is
bound, the author reads in one fact that the person dropped it, and
asking it again is refused. A cancel word the question itself offers
(an option « Annuler ») still answers it.
- **The native authoring card's example no longer caps its `infer`
step at `max_tokens: 600`.** The card's own rule says to write
`max_tokens` only when the person names a token bound and otherwise to
leave the default to the compiler, which sizes it once the workflow's
model is known; its example contradicted the rule, and an author that
copied the 600 left a model that reasons before it answers no room to
answer.
- **Carriage returns leave a model's call arguments.** GLM 5.3 Flash slipped carriage returns
between the words of its calls (`schedule\r_zone`, `Indian\r/Mauritius`) and copied its own
earlier calls with more of them each time: 115 calls were refused for a key or a time zone that
did not match. They are removed from every call's arguments before the tool runs and before the
call is recorded, so the model never reads them back.
- **`nika check` no longer says a run would fail for a model its catalog does not
list.** The advisory line under MODELS, the oracle's `models_catalog_warnings` and
the MCP `nika_check` now read: not one of the provider's models in this binary's
catalog snapshot, a typo or a model newer than the snapshot, and the provider
decides at run time. It stays a warning, never a refusal.
- **« Augmente le budget du run à 1 dollar » states a ceiling.** The money reader took the word
after « budget » as the amount, so « le budget du run à 1 dollar » was refused as an invalid
amount, and an unaccented « a » (« budget du run a 1 dollar ») stopped the reading too. The run a
ceiling bounds may now be named between the anchor and the amount (« du run », « of the run »,
« pour ce workflow »), and « a » reads as « à » before an amount.
- **`nika check nika.yaml` judges the beats as `nika arm` and `nika serve`
do.** It read the project file's shape only, so a beat whose cadence has
no `TZ=` zone, names an unknown zone, breaks the cron or readable grammar,
carries a `décalage` other than `hash`, a `jusqu_au` that is not a date or
a workflow path with a `..` segment passed `check` and was refused only
when someone armed it. `check` now applies the arming door's own laws
(the same function `nika arm` uses) and names every law broken, each with
its beat, its code and its fix; `--json` lists one finding per law.
- **`nika check` names a read that breaks when its producer is skipped.** A task
that can be skipped (a `when:` that can close, an empty `for_each`, `on_error:
skip`) hands its readers `null`, and a `with:` binding is evaluated before the
reader's own `when:`: `u: ${{ tasks.x.output.url }}` guarded by
`when: ${{ with.u != null }}` failed the run with `NIKA-VAR-001` while `check`
said « run ready ✔ » with zero hints. `check` now carries a `skipped-read`
hint for such a read, in a binding or in a `when:`, `for_each:` or verb field
stepping into a whole bound value that no `when:` rules out as null. The hint
names the reader, the read, the producer, why it can skip and the repair: bind
the whole `tasks.x.output`, test it in `when:`, then step into it in the verb.
Guards that hold once the producer skipped stay silent (`has()`, `!= null &&`,
the producer's own consent guard repeated), so does a task the skip cancels
or a verb read its `on_error:` recovers. Advisory: `is_clean` is unchanged,
and past the 2,000-task analysis cap the miss is stated.
- **The CI file ratchet honours `LOC-EXEMPT`, as the hygiene dashboard does.** A source file
with a whitelisted marker (codegen, lookup-table, enum-mega) on one of its first 5 lines may
hold up to 5,000 lines in CI too, where it used to be refused above 2,500. Both gates read one
file-size policy and state the same verdict for the same file.
- **A Claude 5 model's fixed sampling and adaptive thinking are named at check, not by an
HTTP 400.** The catalog records that Claude Fable 5.1, Fable 5, Opus 5.5, Opus 5, Sonnet
5.5, Sonnet 5 and Haiku 5.5 think by default and, on Anthropic's API, refuse a thinking
budget and any temperature but the default. `nika check` and the run's model admission
name a `temperature:` (on `infer:` or `agent:`) or a `thinking.budget_tokens` on such a
seat, a nickname included, with the repair; the Anthropic wire refuses the same request
before any byte instead of dropping the value. The Session's classification calls send
no temperature to these models.
- **A Claude Code session over ACP no longer reads as silent while it
thinks.** Every Claude Code session the engine opens (the authoring and
`infer:` one-shot profile, and the `agent:` seat) now asks the adapter
for adaptive thinking with its summary displayed
(`_meta.claudeCode.options.thinking`), unless a thinking option is
already named. Recent models otherwise default to signature-only
thinking whose text is empty, which the adapter never streams, so a long
thinking turn showed no frame at all until its answer, and could be cut
as silent. This changes what is displayed, not the model, effort, budget
or tools; thought frames stay counted, never stored as text, and the
silence allowance is unchanged. Codex and other adapters send nothing
new.
- **A `claude-code` call no longer runs with the person's own Claude Code setup.** `--tools ""`
empties only Claude Code's built-in tools: every MCP server of the person's configuration
(plugins and claude.ai connectors included), their hooks, plugins and personal CLAUDE.md still
loaded into each authoring, conversation and decision call through `claude -p`. Measured on
2.1.283: 180 MCP tools and about 115,000 input tokens for a one-word answer, and an authoring
call that ran a web-search MCP tool, then stopped at its one-turn limit, which the TUI showed as
« I couldn't use the authoring model for this part ». The call now also passes
`--strict-mcp-config` and an empty `--setting-sources`, the isolation the ACP completion profile
already has: a model or effort set in the person's Claude Code settings no longer applies to
these calls either, and the CLI's own default for the account answers, as on the ACP route.
- **Claude Sonnet 4.6 and Claude Haiku 4.5 are priced.** The catalogue still offers these two
legacy models, but they had no admission tariff: the author offered them as « prix non connu
ici », and no run ceiling could be checked against them. Two rows now read Anthropic's pricing
page of 2026-10-16 (Sonnet 4.6 at $3 in / $15 out per million, its whole 1M window at standard
rates; Haiku 4.5 at $1 / $5, a 200K window) and each model's own page for its limits. Claude
Sonnet 4.6's catalogue limits were also out of date (200K, 8K output): its page says 1M and
128K.
- **A refused cloud key reads « key refused », and a keyed route's own listing governs its
offers.** A Session's census used to call every keyed cloud route ready from the key's presence
alone: a key the provider had revoked was offered, and the offers came from the catalogue. The
census now sends each keyed OpenAI-compatible route one bounded, authenticated
`GET {base}/models` with its own key (about 3 s, all routes at once, never an inference): a 200
lets the listing govern the route's offers, a 401 or 403 reads « key refused » with the
provider's own words (the key withheld) and the route is not offered, and no answer leaves the
key configured, not validated.
- **Codex authors again on codex-cli 0.162.** Nika admits Codex as an author only on the codex-cli
minors where its pre-execution empty-tools profile was measured, and that was 0.160 alone: after
the CLI updated to 0.162.1, every authoring call refused with « codex-cli 0.162 has no measured
pre-execution empty-tools profile », so the `codex` account could not write anything. The
profile is now measured on 0.162.1 (every disabled feature reads `false`, a prompt asking to read
a file, run a command and search the web ran no tool and never saw the file, a JSON-schema answer
came back in one turn) and that minor is admitted. Any other unmeasured minor still refuses
before the prompt.
- **A Codex conversation's approval of Nika's own tool is now recognized.** codex-acp 1.13.1
announces each MCP call with the server and tool Codex routed it to, then asks Codex's
approval of it by the call's id alone (`{toolCallId, kind, status}`); Nika read only Claude
Code's tool name, so it rejected every such ask. Codex 0.156.1 asks one before each Nika tool
that is not read-only (`candidate_write` among them), and a rejection reaches the model as the
user cancelling the call, so no Codex-led Session could write a candidate. A turn now keeps
what each open call was announced as, and an ask by id alone is for that call; Codex's own
question (`Allow the <server> MCP server to run tool "<tool>"?`) is read when codex-acp cannot
tie the ask to one. Only an offered tool on Nika's server is allowed, once; an id never
announced or no longer open, another server, a tool Nika does not offer and a command stay
rejected. Claude Code's path is unchanged.
- **A Codex-led Session conversation can write its candidate.** Codex 0.156.1 asks before each
Nika tool that is not read-only, and the ask reaches the client only through its
`tool_call_mcp_elicitation` feature, which the Codex profile turned off with every other
feature. Codex then asked through `request_user_input` instead; codex-acp 1.13.1 answers that
itself with no answer, because Nika advertises no form elicitation, and Codex reads no answer
as the user cancelling the call. So `candidate_write` was cancelled without Nika ever being
asked. The conversation profile now keeps that feature on, and the spawn-time read-back
requires it on, so each ask reaches Nika as `session/request_permission` and Nika allows its
own tool once. The one-shot completion profile and `codex exec` keep it off.
- **An open name the request already settles is no longer asked.** When a
request leaves open where a file's name starts (`un payload
out/notification.json`), a folder another path of the request names
(`out/report.json`) settles it, and an answer the person gave counts on
every document door. A name nothing settles is still asked, with its
readings as the options and the path alone as the default.
- **A request opened or closed by « tout les matins » states its
schedule.** The compiler's reading of a request took « tous les » and
« toutes les » as the quantifier of a cadence at the start or at the
end of a sentence, but not « tout les » or « toute les » as people
often type them, so « tout les matins à 9h, … » kept no trigger. They
now read the same way; the singular « tout le matin » (all morning
long) still opens no schedule.
- **A child grant inside its parent's glob now composes.** A parent granting `fs.read: ./references/**` and a child declaring and reading exactly `./references/preview-shell.html` were refused with `NIKA-COMP-002`, because the parent ∩ child meet kept only grants spelled identically on both sides; repeating the parent's wider glob in the child was the only way through. The meet now keeps a pattern the other side contains, at its narrower spelling, for `fs.read`, `fs.write`, `net.http` and `tools` (`*.example.com` ∩ `api.example.com` is `api.example.com`, `nika:*` ∩ `nika:read` is `nika:read`), in the reading of the matcher each family is enforced with. `nika check` and the run share that one meet, so the child's read is admitted at run too, and a read outside the parent's glob is still refused (`NIKA-COMP-002` at check, `NIKA-SEC-004` at run). A child glob wider than its parent's is cut to the parent's tree, never widened past it. Path grants that leave the workspace (absolute, `~/`, climbing) or name `..` still meet only by their exact spelling, and two globs that overlap without either containing the other still meet to nothing.
- **A workflow that computes its read paths reads the project.** For a workflow reading
`./releves/*.csv` or `./notes/**` through computed paths, the Session said « fraîcheur des
sources non jugée : il ne lit aucun fichier du projet ». It now counts the declared read grant,
and says the freshness of those sources is not judged because they are a pattern.
- **« On continue » shows the waiting proposal whoever prepares.** After switching to an
intelligence that leads no conversation, « On continue là où on en était » reached the
compiler: a free, ready proposal was re-authored for 13 minutes into a new one with model
steps. A line that only asks to go on now shows the waiting proposal again as it stands, at the
consent door as in the conversation.
- **« On continue » shows the waiting proposal again.** After an intelligence switch, « On
continue là où on en était » sent a ready proposal back to the author, which wrote it anew for
minutes and lost part of it. With a proposal waiting, a line that only asks to go on shows the
same proposal again, as it stands. It is never a consent and never a change.
- **The authoring conventions show a total written alone as a workflow
writes it.** They said the value is « stated as `alone: true` on its
write ». `alone` is how the plan states that a file holds one computed
total by itself; `nika:write` takes no such argument, and no door that
reads the conventions (the native, sketch and document authors, their
verifier and the Session's author) states a plan. An author that copied
the line wrote an argument the check refuses (`NIKA-BUILTIN-001`). The
conventions now show the write's own form, the total's field as its
`content` (a json file holds `40`), and that several totals or a csv,
yaml or toml file are asked about rather than written. The plan's field,
the compiler's lowering and its refusals are unchanged.
- **The conversation knows about a run refused before it started.** Only a run that ended (exit 0
or 1, with a readable trace) reached the conversation's author. A run refused before it started
(exit 2, its cost floor above the ceiling) or around it reached nothing, so « pourquoi le run a
échoué ? » right after « NIKA-1709 · refusing to start » met « aucun run n'a échoué ». Every
observed run now rides the person's next line to the author, as a success already did; a failed
run of the author's own candidate is still repaired at once.
- **`nika:convert` reads semicolon and tab separated CSV, and writes a null as an empty
cell.** A European spreadsheet export (`nom;montant`, decimals with a comma) was read
with the comma: it became one column, or failed on its first decimal. The separator
is now counted on the first line outside quotes: a semicolon or a tab that outnumbers
the comma is the separator, otherwise the comma stays (RFC 4180). JSON to CSV wrote a
`null` as the text `null`; it is now an empty cell, as a missing key already was.
Cells are still strings: convert numbers with `tonumber` before arithmetic or
ordering.
- **`nika check` refuses a `nika:date` arg its op never reads.** `{ op: now, format: "%Y-%m-%d" }`
passed the check, because `format` is an arg of `nika:date`, and ran without it: the file got
`2026-10-15T21:39:15.511348+04:00`, matching neither declared format. Each op now reads only its
own args (`now`: tz · `add`/`subtract`: base, duration, tz · `format`: input, format, tz ·
`parse`: input, format · `diff`: start, end, unit), and an arg of another op is refused as « not
an arg of `nika:date` op `now` » with that op's own args. A templated op is left to the run.
- **`nika:date` documents every op that reads `tz:`.** The tool catalog
described `tz` as applying to `now` only, so `op: format` looked unable
to render local time, and workflows sliced ISO strings by hand instead.
`format` renders its fields in `tz:` and `add`/`subtract` apply calendar
units there. The description now says so, and adds that an input's own
offset is not kept when `tz:` is absent.
- **« désactive-le » removes only units the trigger host says it keeps.** When an earlier
conversation had installed a trigger, the Session built its launchd path in
`~/Library/LaunchAgents` itself. A host that keeps its units elsewhere was then handed a file it
never wrote. A host now says where its units are (`TriggerHost::unit_of`, given by the launchd
host). The declared state of a schedule also asks the host whether its unit is installed.
- **A System One decision seat no longer starts a batch with a request known to fail.** A
seat of `typesafe/jev-1.13.0` plans each batch inside the capacity the service documents (64k
tokens per request), splitting a request in halves before it leaves when its exact body would
exceed it, and never again sends a body as large as one the service refused for capacity. The
Foundry qualification of a large knowledge recall used to send the whole batch first and only
shrink after `max_tokens_exceeded` refusals. Other decision models keep their earlier
behaviour.
- **A `provider/name` decision seat is stated as sending each request once.** The compile
receipt and the `--decision-model` help said its client kept protocol retries; the provider
transport no longer re-sends anything, so the receipt now says « its own provider client, one
attempt per request ».
- **The decision seat's tally counts every call it sent.** A batched decision call (the whole
request's verification, its parts, Foundry qualification) is answered as a whole, but the
tally counted only single-question answers: `/status` said « 33 call(s) sent · 7 answered · 0
without a response ». A batched call that came back now counts as answered, and one the seat
answered with an HTTP error as without a response.
- **« Choisis toi-même » now settles a setting instead of asking it again.** When the person left a
value to Nika (« choisis toi-même » to the question for a time zone, « carte blanche pour le
reste »), the author chose one and the fidelity law refused it as an unauthorized selection,
so the same question came back (« le compilateur n'accepte pas que je le choisisse à votre
place »). A delegated selection may now be a value, but only one that no effect carries: a pure
step (`nika:date`, `nika:jq`, `nika:assert` and the rest of the pure-internal class, the call
proven pure) or the words of a task. A fetch, a write, a command, any other tool or agent
configuration, the document's model, permits, run access and secrets still refuse it, and the
person's own words must still be cited.
- **`/details` says what a conversation built, saved and ran.**
QA gauntlet q09: after two Saves and three runs it still said « no
workflow was read in this session yet », because only a compile round
set what it reads. With no such round, it now names the conversation
that wrote the workflow, its last revision, the workflow saved, the
preparation spend and the last run's trace.
- **`nika doctor` names a refused key.** A provider that answers 403 to the route's key now reads
« 403 · key refused for this route » with the fix to export a key the route accepts, where the
doctor printed a bare « 403 ».
- **`nika doctor` no longer calls a seat whose login check did not answer
"not signed in".** A harness seat whose own login command did not answer
within its deadline is now a warning that says the sign-in is unknown
and why ("the login check did not answer within 30 s"), and it teaches no
sign-in. A seat whose login command answered "signed out" still reads
"not signed in" with its sign-in gesture.
- **A dropped task now debits the tool spend it had already incurred.** A task cut by its
`timeout:` or by a `fail_fast` sibling debits, once, the cost reported by tool calls that had
already completed, including a ready MCP reply it never read. The task stays failed, with no
output, row or checkpoint credit.
- **`nika:edit` refuses an empty `find:` instead of rewriting the whole file.** An empty
`find:`, such as a templated input left empty, matched between every character: the
replacement landed between every byte (`price: 10` became `XpXrXiXcXeX…`) and the
task succeeded. It is now `NIKA-BUILTIN-EDIT-001`, refused before the file is read.
- **`Enter` pressed right after `Esc` in the intelligence list now acts.** Stepping back with
`Esc` (from the connections to the home list, for instance) armed the guard that ignores a
second press of the same `Enter`, so an `Enter` pressed within half a second on « Keep
current » or another row did nothing. Only an `Enter` that sent something starts that guard
now; after `Esc`, the next `Enter` is your next choice.
- **Every card can be left.** A saved workflow whose files changed since its
rehearsal showed a mandatory card with one act, « Adapter le workflow aux
fichiers modifiés »: `Esc`, words of one's own and `/new` all left it in
place, and only quitting the app did. `Esc` with nothing left to undo now
sets a decision's card aside, never answering it: the conversation says
what stays undecided (the workflow stays not run, a paused run stays
paused, a proposal still waits) and gives the line back. Words typed at
that recovery card are the person's next line, `/new` begins a new
conversation the card does not come back over, and a fresh spending
question keeps its card (its cancel or `Ctrl+C` leaves it). The refused
run no longer says « restore the original file »: it names what a retry
as rehearsed would put back, file by file, in place of what each holds
now (NIK-227).
- **A failed run no longer moves a person's own files into quarantine.** When a run
failed, `nika run` moved every file a successful `nika:write` or `nika:edit` returned to
`.nika/quarantine/<run>/`, including a `notes.md` that existed before the run and was
only edited. The next run then failed to read or edit it. Now only files the run created
move. A file that existed stays where it is, and its original bytes, copied before the
run first changed it, wait under `.nika/quarantine/<run>/originals/`. A writer reached
by `after: failure`, `terminal` or `unwind` keeps its file, since it records the failure.
The quarantine dir carries a `manifest.json`, and resuming the failed run with
`--resume` puts the moved files back before it starts. The run says on stderr which
files moved and where each kept file's original is.
- **A failed run's journal records its quarantine, signed or not.** What the teardown did
with a failed run's files rode only inside the signed `run_sealed` frame, so a run
without a signing key journaled nothing about it, and a crash between the moves and the
seal left only the manifest. A `quarantine_recorded` frame now lands right before the
seal: the quarantine directory, how many created files moved (and any move that
failed), how many of your files stayed in place, and the manifest with the sha256 of
its bytes. A signed run's seal covers it like every earlier line.
- **A feed item dated only by its revision keeps its date.** `mode: feed`
dropped an Atom entry's `<updated>` and a JSON Feed item's
`date_modified`, so an item carrying only that date looked undated and
a filter on today's items dropped it. Each item now carries `updated`
when the source dates a revision of its own, an RFC 3339 instant like
`published`; a date filter reads `published // updated`. An RSS item's
`pubDate` stays its `published` alone.
- **An author knows what a fetched feed returns.** The `nika:fetch`
contract an authoring model reads named the extraction modes only by
pointing to another page, so an author filtered a feed's items by the
feed's own date text and kept nothing. It now shows what each mode
returns, the feed's shape, and that feed dates are RFC 3339 instants
with an explicit offset; `nika:date` joins the everyday contracts to
compute a bound such as today.
- **`nika:fetch` honors its task's `timeout:`.** A fetch whose task declares `timeout: "3m"` gets
those three minutes for the whole request, as the stdlib states, instead of a fixed 30s that cut
long API calls (an image generation, a non-streamed inference) and a 30s idle guard that cut a
server computing before it answers. A fetch without a task `timeout:` keeps 30s, and a timed-out
fetch names the setting.
- **`nika:fetch` sends a JSON `body:` with `content-type: application/json`.** An object,
array or other non-string `body:` was serialized as JSON but sent with no content type,
so a receiver that parses by type read it as empty. It now carries
`application/json` unless the author's `headers:` already name a content type; a
string body is still sent as written.
- **`nika:fetch` returns a CSV, text or JSON body as served in its page modes.** The
default `markdown` mode, like `article` and `text`, read every body as HTML: a
`text/csv` export came back on one line with markdown escapes (`A\_1`), so a following
`nika:convert from: csv` produced no rows while the run succeeded, and a `text/plain`
body lost its line breaks. A body whose `Content-Type` is text other than HTML
(`text/*`, JSON, XML, CSV, YAML, NDJSON, TOML and their `+json`/`+xml`/`+yaml`
suffixes) now comes back unchanged, decoded by its BOM or declared charset, else as
UTF-8, else as windows-1252. HTML, XHTML and untyped bodies keep the extraction.
- **Files the person says they have in a folder are sources.** « J'ai deux fichiers CSV dans le
dossier releves (a.csv et b.csv) » made each file a write destination of the whole request,
because « dans » counted wherever it stood. Every revision reading `./releves/a.csv` was refused
as « UNREALIZED PATH », and one author wrote permits for files that do not exist to pass. A
path is now a destination only through a connector in its own sentence, in a clause that writes
there. A clause that says what the person has, or what there is, places existing material.
- **A second Ctrl-C no longer leaves exec children running.** The second
signal ended `nika run` with `exit(130)`, which runs no destructor, while
every `exec` child leads a process group of its own that neither the
signal to the CLI nor a terminal's Ctrl-C reaches: the child kept running
and writing after the CLI was gone. The abort now ends every process group
the run still owns first (SIGTERM, a one-second grace, SIGKILL, then a
bounded reap), never signals a group whose leader the process can no
longer wait for, and says on stderr how many groups ended and which did
not confirm their end. Nothing is rolled back: what already ran stays
done, and an effect in flight has an unknown outcome.
- **Gemini runs, priced at Google's own pages.** A budgeted run with a Gemini model was refused
(« this provider's wire has no bounded admission settlement »), any other run reported an
unknown cost, and Gemini 4 Argon, newer than the vendored price snapshot, read as unpriced
even to `nika check`. The Gemini wire now reserves, sends once and settles each text call at
its tariff: ten models (Gemini 4 Argon, 3.8 and 3.6 Flash, 3.5 and 3.1 Flash-Lite, 3.1 Pro
Preview, 3 Flash Preview, 2.5 Pro, 2.5 Flash and Flash-Lite) read the pricing page at peak
Standard rates, thinking billed with the answer. The picker and `nika check` read the same
prices, Gemini's snapshot rows included.
- **GPT-6 Sol and GPT-6.1 Sol take `max_completion_tokens`.** OpenAI refused them with « 'max_tokens'
is not supported with this model » (HTTP 400) on the first message: the catalog knew the
parameter only for GPT-6 Astra. Both observed models now send `max_completion_tokens`; the rule
stays exact, with no claim for the rest of the family.
- **Grok runs, priced at the charge xAI reports.** Every xAI model was unpriced at run time: a
workflow that fetched a page with Grok as its model was refused (« unknown-cost Run supports
only direct infer and local read/write/jq/assert »), and any other run reported an unknown
cost, while the model picker showed a price. grok-4.7 and grok-4.3 are now admitted from
their model pages and settle at the charge each reply reports (`cost_in_usd_ticks`). xAI's
reasoning is not bounded by `max_tokens` (measured: 1,973 reasoning tokens under a bound of
40), so a call reserves the model's whole window: a run ceiling needs about $3.40 per
grok-4.7 call in flight ($2.75 for grok-4.3), while the settled cost stays the real one.
- **`nika guard` no longer refuses plain shell it can read.** Loops,
`if`, `[ … ]`, `find` without `-exec`, `xargs` with any other command,
a heredoc script such as `python3 - <<'EOF'` and a command word in a
variable that cannot make a run (`"$EDITOR" notes.txt`) were refused
as `guard_unavailable`, blocking agent commands that run no workflow.
The guard now judges the commands inside them and keeps no opinion
when none is a `nika run`; `find -exec nika`, `xargs nika` and
`$N run x` stay visible.
- **A slow login check no longer reads a signed-in seat as signed out.** A
harness seat's own login command now has 30 s to answer: valid checks
were measured at 12 s and 25.8 s, and the former 10 s turned them into
a "not signed in" seat. The harness probe facts now also carry the
check's answer (`login`): signed in, signed out, or unknown when it did
not answer in time ("the login check did not answer within 30 s") or
could not run, so an unanswered check is no longer the same fact as a
signed-out seat. Admission still requires an answer before it admits a
seat, and no credential is read.
- **Hints name the program the person runs.** A proposal's hint line said « `nika check x.nika`
prints them » to a person running `nika-dev`, whose `nika` on the same machine was an older
release that refused the file. Hints in the Session, the check summary and the refused-run
story now name the running program (`nika-dev`, `nika`), and a paused run's meaning no longer
gives a command line: the Session asks its question itself.
- **The Session's hints name the program you run.** `/help` and `/proof` said `nika trace
verify` while the person ran `nika-dev`. On that Mac, `nika` was an older release that refused
the trace. Every command those hints name now uses the running program's own name.
- **`Home` takes the conversation back to its first message.** `PgUp` pages through the
conversation a screen at a time, so a long card such as a full proposal took several presses
to pass, and `Home` did nothing. With nothing typed in the box, `Home` now goes to the first
message, `Ctrl+Home` does it even with words typed, and `End` still brings the latest
messages back. The `?` key list names these keys.
- **An infer answer interrupted by its provider fails as a provider call.** When the provider
reports it interrupted the generation (`StopReason::Interrupted`), the answer is never the task
output: the task fails `NIKA-INFER-001`, transient, naming the provider's own word, and the
task's `retry:` decides. An answer cut at the output cap that parses, even one that satisfies
`schema:`, still fails `NIKA-INFER-005`.
- **« Inspect the exact changes » shows the lines a revision changes.** For a proposal that
replaces a saved workflow, it used to print the whole file (« replaces `clean-contacts.nika`
whole (109 lines …) » followed by all 109 lines), so you had to find the one line that changed.
It now opens on what changes against the file as it is now, with the size of the change
(`+1 −1 lines`) and each change shown removed then added among the lines around it. Every byte
still follows. A file that changed since the proposal was made is said so and not compared.
- **A verifier's unlocated doubt is asked where it is, then held as
unresolved.** The question whether a step does something the request
does not ask no longer offers a step that only reads paths the request
names and writes the output a part it carried asks: the checker's own
effect inference, with constants resolved, settles such steps, and
with none left the question is answered with no model call and
recorded on the attempt (`engine`). The previous check, which let a
named step pass without resolving its constants, is gone. When the
verifier rejects the whole request but carries every part, names no
step, and no trial run of those bytes can decide it, it is asked once
where its doubt is: a part or step it names is located and repaired
from as before, and naming nothing holds the workflow as unresolved,
never ready and never proposed, in words that say nothing was verified
(`unresolved` on the attempt).
- **Kept defaults, source freshness and the questions left read in French.** For a French speaker,
« kept the default », the source freshness lines and « 1 more after this one » are now said in
French; the receipt lines (« applied · wrote », « check · … · clean ✔ ») keep one wording.
- **A development build no longer asks for the keychain password on every run.** A macOS
keychain grant (« Always Allow ») is bound to the signature of the binary that asked. A
Developer ID release keeps the same signature from one version to the next, so its grant
sticks; an ad-hoc build (every `cargo build`, every dev build) is a new identity each time, so
the password prompt came back on every run. Now only a binary whose signature can keep a grant
opens the keychain for its run-key; any other takes the explicit `NIKA_RUN_KEY_FILE` pair or
the 0600 files under `~/.nika/keys`, silently, and runs unsigned when there is none, never
under a new key. `nika serve` never opens the keychain. A secret key file other users can read
is refused, as ssh refuses one. `nika doctor` says in one `run-key` line where the key comes
from, the fingerprint of the key this process would sign with, and why the keychain is or is
not part of it. A machine whose keychain holds the key a signed release signs with, beside a
0600 pair dev builds and `nika serve` sign with, has two identities: a release's doctor names
both keys, and `nika key align`, run from a signed release, gives the 0600 files the keychain
key and retires the one they held (the journals it sealed stay verifiable). At init, rotate and
align a signed release records the keychain key's fingerprint and the item's modification date
in `~/.nika/keys/keychain.json`; a dev build compares through that record only while the item
is unchanged, and otherwise says it does not know. `nika key init` in a process that does not
open the keychain refuses to mint a second key beside the keychain's, unless forced. Even a
signed release now opens the secret keychain item only to sign or to align: `nika key trust`,
`nika doctor`, the memory fold, a seal grade and a workflow signature read the public item
alone, and each item is read at most once per process. `nika key init` sees whether the keychain
holds a key from the items' attributes, never their data. `NIKA_KEYCHAIN=off` still turns the
keychain off everywhere.
- **Without a decision seat, an authoring call no longer receives every other knowledge entry.**
With a knowledge release lent and no decision seat selected, the author was shown, beside the
recalled pack, the descriptor of every other admitted entry and of every embedded template,
unqualified: 244 to 369 references (102 to 199 KB) per request on the release this build
embeds, measured over 16 requests. A descriptor is there for a decision seat to judge whether
its entry serves the request, and without a seat nobody asks it. Nothing is widened then: the
author reads the recalled pack, and the qualification record says why. With a seat, every
admitted entry is still asked.
- **A shared stopword no longer recalls knowledge for an authoring call.** The knowledge door's
text recall (BM25 over the release's rows) read every word of three letters or more, so a row
sharing only « the », « and » or « with » with the request was recalled and presented whole:
on the release this build embeds, a ten-word authoring request reached 22 of its 26 examples
and all 16 skills that way. The recall now reads informative words only, without the
compiler's shared stopwords. Every row it reaches is still presented whole, with no count or
byte quota, and the pack names its builder `knowledge-door-v7`.
- **A French article no longer recalls knowledge either.** The knowledge door's text recall also
reads past the compiler's retrieval lexicon (French articles, pronouns and politeness; English
request scaffolding such as « please » or « help me »), the words the compile's own retrieval
already reads past. On the release this build embeds, a French request for a news digest
recalled four French-titled patterns through « les » alone; it now recalls by its content
words. The lexicon moved beside the shared stopwords (`nika_compile_reader::text`) unchanged.
- **The Linux jail no longer drops a write grant without a word.** bwrap
can only bind a path that exists, and a jailed command cannot create its
own grant. The jail used `--bind-try`, which skipped such a grant
silently, so a command that had to create the granted file failed far
from the cause (on Linux only: macOS Seatbelt admits a file that does
not exist yet). Existing write grants now bind with `--bind` (a source
that vanishes before the mount fails loudly), and a spawn that leaves
grants unbound writes one line on stderr naming them with the remedy:
grant the directory, and make sure it exists before an exec writes into
it. An absent grant is not refused, since another task of the workflow
may be the one that creates it.
- **An MCP server's network allowlist now reaches its hosts.** On macOS a
server declared with `"network": {"allowlist": [...]}` reached no host at
all and could not even resolve a name: its jail admitted loopback only,
while `nika mcp approve` announced it as confined like `"allow"`. The
spawn now starts the loopback egress proxy that the exec allowlist uses,
points the server at it (`HTTPS_PROXY` and siblings) and fences the jail
to the proxy's port, so exactly the declared hosts answer. The approval
note says `through a loopback egress proxy`, and a refused host prints
one `nika:egress REFUSED` line naming the server's allowlist.
- **Nika's MCP server and client say only the protocol they speak.** The
server answered an `initialize` asking for MCP 2026-07-28 with that
revision, and an unknown revision with it too, yet the final 2026-07-28
text has no handshake and neither half implements its MUSTs; it now
answers 2025-11-25, the latest handshake revision, as does the
conversation server, and the 0.94.0 changelog line that said it spoke
2026-07-28 natively carries the correction. As a client, a `tools/call`
result whose `resultType` is `input_required` or `task` is a failed call
instead of an empty success; a reply that follows more than sixteen
interleaved notifications is read, the wait being bounded by its deadline
and by 8 MiB of skipped messages; and a call that times out is cancelled
with `notifications/cancelled` before the server is stopped.
- **`nika mcp --http` holds every connection to a deadline and serves them side by side.**
A request has 30 s in all to arrive, head and body, and its response 30 s to leave; each
connection runs on a thread of its own, at most eight at once, the next waiting in the
listener's backlog, so a slow client holds one connection for a bounded time. A failed accept
on the server's side pauses the loop briefly.
- **An MCP call the run stops waiting for is cancelled, not left running.**
When a task's `timeout:` cut an `invoke` of an `mcp:` tool, its call kept
running on its worker until the 120 s call timeout, holding the server's
pipe so every other call to that server waited behind it, and the server
was never told; the operator's Stop (Ctrl-C, SIGTERM, a Session's Stop)
waited for such a call to finish. The call now carries a stop: one not yet
written is never sent, and one in flight is cancelled with
`notifications/cancelled` while its session stays open for the next call.
Under the operator's Stop the task fails `NIKA-CANCEL-001`, never retried,
its `error.details.stopped_call` naming `{tool, call_id, outcome}` with
`outcome` `not_sent` or `unknown` (it was sent, so the server may have
acted), and the run's settlement lists every such call as `stopped_calls`
(`{task, tool, call_id, outcome}`), whether its task failed, was skipped
past or recovered. Other in-flight work still completes at the wave
boundary. A reply that already arrived now wins over a stop, a server
request that reuses the call's id is no longer taken for its reply, and a
stopped call's late reply no longer counts against the next call's byte
budget.
- **`/meaning` beside an authored proposal reads your request.** Beside a proposal an
intelligence wrote, `/meaning` printed the money card: no compiler reading stood behind it.
It now lists, in the language you write in, what Nika kept of your request: your words, each
value the workflow relies on with its part and where it comes from, what you left to Nika to
choose and the questions still open. The proposal keeps waiting for your answer.
- **The model question no longer demands `<provider>/<model>`.** On a subscription account (codex,
claude-code), a request with an AI step asked « Which explicit runtime provider/model should run
the language steps? » with nothing offered: « choisis toi-même » was taken as typed, and the
compiler answered « Name the runtime model as <provider>/<model> ». The question now offers the
configured model this conversation recommends (the saved default, else the newest configured
one), which Enter takes. A reply that leaves the choice to Nika takes it too, said as Nika's
choice beside the proposal, and a model named or described in words (« prends Claude Haiku ») is
read against the configured models whose name carries those words, never invented. A reply that
designates none of them binds nothing and says which model is offered.
- **A proposal's money line says its ceiling as it was set.**
« money: $0.1 USD » sat two lines from « ceiling $0.10 »: the Session's
money lines and project default printed a ceiling with the float's own
digits. They now read through the one ceiling formatter every other
surface uses (« $0.10 », « $0.001 », « $2 »).
- **More of the Session's own words in French.** A line the Session could not read now says, in
a French conversation, « je n'ai pas lu cette ligne comme un travail, et je n'ai pas su ce
qu'elle veut dire — rien n'a changé ». The same holds at a proposal, a question and a gate. A
revision's card says « Même workflow que la proposition vue » or « Changé depuis la
proposition vue », and a model failure says « la conversation n'a pas pu continuer ».
- **A Claude call is priced, so the Run ledger, `--max-cost-usd` and a Session allowance
count it.** The Anthropic wire never marked a reply's usage complete, so every Claude
call stayed cost-unknown ("provider did not report usage") and a Session allowance with
a Claude author was refused. A reply whose usage accounts for every billed meter (input,
output, cache read and 5-minute cache write, no server tool, standard tier, global
inference) is now complete: it is priced at the model's admission tariff, else the
pricing snapshot on Anthropic's own endpoint, and an allowance reserves, sends once and
settles a Claude text call through the Anthropic wire, the model named exactly, by
nickname (`anthropic/sonnet`) or through the `claude` alias. A reply missing a meter, or
a Claude Haiku 5.5 prompt above its admitted 100,000 tokens, stays unknown, never zero;
under an allowance, a Haiku 5.5 request whose bytes could exceed that tier is refused
before anything is sent.
- **The key a provider reads is named, and the Nika-scoped variable is the one advised.**
The probe lists the key variables set for each provider, names only, in the order the
run reads them; the census and `nika doctor` name the one that supplies the key
(`NIKA_ANTHROPIC_API_KEY` before `ANTHROPIC_API_KEY`), and two variables set with
different values earn a doctor warning naming both, compared in memory where the keys
are read and never printed. A missing key at run or in a conversation now advises
`NIKA_<ID>_API_KEY`, which no other tool reads (exporting `ANTHROPIC_API_KEY` can move
another application, such as Claude Code, onto that key), and a conversation shows it
without the command-line teaching. An API pick naming the `claude` alias
(`/intelligence 2 claude/<model>`) selects Anthropic.
- **`run.reasoning.effort` reaches a Claude 5 model.** The catalog records the levels this
engine names (`low`, `high`, `max`) for Claude Fable 5.1, Fable 5, Opus 5.5, Opus 5,
Sonnet 5.5, Sonnet 5 and Haiku 5.5, so a declared effort on such a model, a nickname
included, is admitted at check and rides the Anthropic request as
`output_config.effort` on Anthropic's own endpoint, read back from the bytes sent. An
earlier Claude model, a thinking budget beside the level or an operator-pointed endpoint
still refuses before any byte, and `nika check` names the file's refused access as the
blocker instead of an empty list beside ACCESS READY ✖.
- **`nika check` refuses a `nika:date` zone the run would refuse.**
`tz: "Paris"` checked clean, then the run failed at its first
`nika:date` task with `NIKA-BUILTIN-DATE-001 · unknown tz: Paris`. A
literal `tz:` is now judged against the embedded IANA time-zone
database the builtin itself reads, and an unknown zone is refused with
the run's code, naming the zone when one is obvious (`Paris` or
« L'heure de Paris » → `Europe/Paris`). A templated `tz:` is left to
the run. The ARGS rung now prints every refused literal value with its
own code and the checker's wording, where it said « has no `<arg>`
arg ».
- **`nika:notify` now reaches Slack and Discord webhooks.** A Slack incoming webhook
(`hooks.slack.com/services/…`) refused the stdlib body `{ message, severity, data? }`
with HTTP 400 `no_text`, so a project whose last step posts to Slack failed on every
run. Slack incoming webhooks now get `{ text, mrkdwn: false }` and Discord webhooks
(`discord.com/api/webhooks/{id}/{token}`) get `{ content, allowed_mentions }`: the
message, a non-`info` severity and the `data:` context as plain text. Slack's `&`, `<`
and `>` become entities. Discord's markdown and mention openers are escaped, no
mention is parsed, and the content is bounded to Discord's 2,000 characters. Every
other target, including a Slack Workflow Builder trigger, keeps the stdlib body. A
refused delivery (`NIKA-BUILTIN-NOTIFY-002`) now quotes the receiver's answer, as one
bounded line with the target's own parts withheld, and carries `details.status_code`.
- **A model offer shows the price its admission uses, or none.** A Session's
model offer showed the models.dev snapshot's list price, not the price its
admission uses: GLM 5.3 Flash on OpenRouter read $1.2 per million output
tokens while its admission reserves $1.6, the peak of its tariff, and a model
no tariff admits, such as Claude Sonnet 4.6 on an Anthropic key, showed a list
price all the same. An offer is now priced only from the qualified admission
tariff of its exact route and endpoint (the reply still settles at
OpenRouter's reported charge), and a route the admission cannot qualify shows
no price, never a bare snapshot price. Claude Sonnet 5.5, offered on an
Anthropic key without an admission tariff, shows no price, is never ranked by
the strongest-offer rule and is still chosen exactly when named.
- **One « newest » mark per model family, chosen by real recency.** Searching the intelligence
list for `deepseek` showed « newest DeepSeek » on three rows at once: DeepSeek V4 Pro on the
DeepSeek API, the dated V4 Flash 0731 on Scaleway and R1 0528 on Hugging Face, whose « 0528 »
was read as version 528. « newest Grok » stood on Grok 4.20 0309 rather than Grok 4.7. A date
written in digits (`0528`, `250731`) is now a snapshot date, never a version; a minor version
reads as a decimal, so 4.7 comes after 4.20; and a family is marked once in a list that spans
several connections, on its newest model.
- **GPT-6 and GPT-5.6 work with tools.** Any `agent:` task or tool-using author on GPT-6 Astra,
Sol, 6.1 Sol or Luna, or GPT-5.6 Sol, failed with HTTP 400 « Function tools with
reasoning_effort are not supported … use /v1/responses »: these models refuse function tools
on Chat Completions. A request with function tools to OpenAI's own endpoint now rides its
Responses API (stateless: `store: false`, reasoning items kept encrypted and sent back in
place), priced at the same verified tariff. A GPT-6 Astra agent that reads a file now answers
in three turns ($0.01).
- **OpenAI's zero usage meters no longer void a reply's usage.** OpenAI's usage object
carries audio and predicted-output meters on every text reply, even at zero, and Nika
read any such key as an unknown axis, so a direct OpenAI reply never had complete
usage. Zero meters now read as complete; a non-zero one, a cache write or an unknown
key still leaves the usage incomplete.
- **The free OpenRouter admission names a route OpenRouter still lists.**
`qwen/qwen3.8-27b:free` has left OpenRouter's catalogue; the declared-free
admission now names `nvidia/nemotron-3-super-120b-a12b:free`, read from its
endpoint listing of 2026-10-12: one endpoint at a zero prompt and completion
price, a 262,144-token window and 235,929 output tokens, for bounded text
requests only. A run naming the old id is no longer admitted as free.
- **A paid OpenRouter call settles at the charge OpenRouter reports.** A paid
OpenRouter reply always read as unknown cost, and an admitted paid route would have
turned its account Uncertain at the first settlement. A non-BYOK reply from
OpenRouter's own endpoint with complete usage now settles at its `usage.cost`, which
receipts name as a provider-reported charge; its catalogue tariff only reserves. A
BYOK reply, a gateway's `cost` or an unknown fee stays unknown. A charge above the
reservation still settles known, and that route then refuses its next call until its
catalogue tariff is refreshed.
- **An open-weight model named on another provider is no longer refused as a
wrong seat.** `nika check` refused `scaleway/llama-3.3-70b-versatile`, or a
local engine's `meta-llama/…` model, as served only by the one provider whose
catalogue row lists it. An `org/model` id, or a model the pricing snapshot
marks open-weight, now has no single owner, whoever lists it; a
provider-exclusive id pasted on another provider (`groq/grok-4.3`) is still
refused with its owner's id as the repair.
- **A long conversation no longer overflows an OpenAI-compatible model's window.** Nika asks a
route for its whole published output capacity, and an OpenAI-compatible route counts the prompt
and that output against one context window. On OpenRouter's DeepSeek V4 Flash (943,718 output
tokens in a 1,048,576-token window) every call failed with HTTP 400 once the conversation passed
about 105,000 tokens: « you requested about 1060562 tokens (… 943718 in the output) », after an
authoring loop of 30 calls and 13 minutes. The output budget sent is now what the prompt leaves
of the window, the prompt counted generously from the request's size, less a margin of 1/64 of
the window. A budget that already fits, and a route with no known window, are sent unchanged.
- **A task with a large output no longer stops the run.** The Session reads a run's frames line by
line and refused any frame over 1 MiB, ending the child mid-run: a workflow that fetched three
job boards (one of them 3.4 MB of JSON) failed with « Run cost review refused: Run frame exceeds
1 MiB; no automatic retry », its effects uncertain. A frame over the cap is now drained without
being kept and the run goes on to its own end; memory stays bounded. The Session says the step's
output was neither shown nor kept: the run's trace bounds its lines at the same 1 MiB, so its
journal stops at that step and the run settles with its evidence lost.
- **The intelligence picker states a model's own context window, and how far a tiered price
holds.** Claude Haiku 5.5 read "100k context" because its tariff, which prices prompts up
to 100,000 tokens, stood in for its million-token window. An option's `context_tokens` is
now the model's window (its provider row when a source is recorded for it, else the
tariff's, else the list row's), and `ModelFacts` gains `prices_up_to_tokens` when a tariff
prices only a cheaper part of that window: the TUI reads "from $0.10 in / $0.50 out per
MTok (≤100k tokens) · 1M context".
- **« active-le alors s'il vous plait » activates the schedule.** Only the bare activation words
(« active-le », « activate it ») opened the activation; with a courtesy around them the line went
on as a request and the workflow ran once more. A courtesy or a filler before or after them
(« alors », « s'il vous plaît », « stp », « please », « now », « ok ») now leaves the same
activation, and « active-la », « activez-le », « active la répétition » are read too. Anything
more, such as a cadence, keeps its route.
- **The project file's refusals teach a cadence the arming door reads.**
A beat with no or an empty `cadence:` was told to write
`cadence: "dimanche 18h07"`, which `n