# 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 `). 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 `". 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: , 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 `), 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 · ` note, and a fan-out's note is `for_each · 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: }` 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.` 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: ` is read by every `${{ group. }}` fold, but the `[dead-spend]` hint only counted `tasks..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 · `: 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..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 ` names the declared hosts (`# mock model · zero keys · may still reach nika.sh`). Before a mock rehearsal runs, `nika try` prints `may reach: — 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 --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 `) 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` 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 --beat