Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Changelog

Notable changes per release, written for people who run wisp. The release script publishes the section for the version being cut as the GitHub release notes and refuses to release without one. Keep an Unreleased section at the top while working; the version-bump commit renames it.

0.25.0

A command the sandbox refuses, such as brew upgrade, can now run outside it when you approve that one run, or when you type it yourself after !!; and wisp has a website, https://wisp.pidster.com, with guides and the docs.

Added:

  • The model can ask to run one command outside the sandbox, for the chores the sandbox refuses: brew upgrade (Homebrew runs a sandbox of its own, which cannot nest in wisp’s), cargo install, rustup update. run_command takes outsideSandbox; after the deny list, you are asked about that exact line every time, whatever its risk and your threshold, and approve it once or not at all: nothing remembers it, and --yes does not answer it. Every dialog, notification, and listing says OUTSIDE the sandbox, and the audit records it. Approved, the command runs with your privileges and no confinement, ~/.wisp included. Turn it off with wisp config set commandPolicy.sandbox.allowOutsideSandbox false; wisp doctor’s settings line shows it. When the sandbox refuses a write, the model is told it may ask (ADR 0064).
  • You can run a command outside the sandbox yourself in chat: !!brew upgrade (two marks together; ! !x runs !x in the sandbox). Typing it is the approval, as for !; the deny list and commandPolicy.sandbox.allowOutsideSandbox still apply, and chat says it runs OUTSIDE the sandbox before it starts. In wisp-tui, ! typed at the start of a command steps the input box from command mode’s amber to a muted red, and Backspace at the start steps back one mode at a time (ADR 0064, amended 2026-10-14).
  • A website, https://wisp.pidster.com: a homepage, and the documentation under /docs/, built from docs/ with search and an edit link on every page, in wisp-tui’s colours, dark or light as your system is. The docs open with new guides: installing and a first run, delegating from Claude Code and Codex, approvals and the sandbox, local models, configuration, and troubleshooting; then the reference, then the internals, every decision record among them, and this changelog (ADR 0065).

Fixed:

  • A Homebrew command the sandbox refused (brew install, brew upgrade) was not flagged as the sandbox’s refusal, because Homebrew reports its directories as not writable instead of failing with Operation not permitted; it now is, for you and the model, and the model is told it may ask to run it outside the sandbox (ADR 0054).

0.24.2

Commands the model runs may ask for a longer time limit, under a cap you set, and the default rises to 120 s.

Added:

  • run_command takes timeoutSeconds, so the model can give a long build or test the time it needs, and MCP respond takes commandTimeoutSeconds when a thread starts, the limit of that thread’s commands (refused on a thread that exists, as model is). Both are capped by a new setting, commandMaxTimeoutSeconds (1,800 s by default, 1 to 86,400, wisp config set); a request above it is cut to it. command.outcome records the limit a command ran under, and a timed-out result says how to ask for longer (ADR 0063).

Changed:

  • A model’s command that asks for no limit gets 120 s, not 60 (commandTimeoutSeconds). A git commit whose pre-commit hook ran the tests was killed at 60 s.

Fixed:

  • commandTimeoutSeconds: 0 means no limit, as documented; it set a limit of zero seconds, which killed every command at once.

0.24.1

A guide to installing local models; wisp doctor catches Apple’s model service refusing every request; and an MLX pull’s progress that no longer goes back.

  • A guide, How to install local models, signposted from the README: each runtime’s steps, which model to choose as measured, the window, and what to do when a model will not start.

Fixed:

  • wisp doctor asks the on-device model for a one-word reply, not only whether it is available, so Apple’s model service refusing every request fails the model finding. When the error is the service’s own, such as “Too many open files” from a modelmanagerd that has run for weeks, the finding says to restart it: sudo killall modelmanagerd.
  • An MLX model pull’s progress (wisp models pull, chat’s /models pull, the models tool) no longer goes back: a file that had just finished was briefly counted as not fetched until the next one started.

0.24.0

The model can ask you a question mid-turn; waiting MCP requests in plain chat; the approval banner only when the dialog goes unanswered; an interactive status bar in wisp-tui; and the risk test set grown from real use.

Added:

  • ask, a tool with which the model asks you one question mid-turn, free text or a choice of two to six options; off by default (tools.ask, or a tool list naming it). A question asking for a secret is refused, and a credential-shaped answer is masked (ADR 0062). In chat and wisp-tui the question waits until you answer (inquiry.timeoutSeconds, default 0; a deadline is optional), and Ctrl-C withdraws it. wisp chat --json sends it as a choice line of kind question, and a front end whose hello declares answer-questions is sent the questions waiting in wisp mcp servers too.
  • Under wisp mcp the question is filed in ~/.wisp/pending and announced once; wisp questions pending|answer|decline answers it from a terminal (answer prompts for the text), and so do wisp-tui and plain chat’s /questions answer|decline. respond holds the client’s dialog 60 s, then parks the turn and returns structuredContent.questions; the answer reaches the thread’s next turn. The client’s question form is built behind inquiry.elicitation, off until it has been tried by hand. Audited as inquiry.requested, inquiry.parked, inquiry.answered, and inquiry.settled; an answer becomes the thread’s answer fact as the person’s, and the verification pass flags “you chose” or “as you confirmed” when no question was answered.
  • Plain wisp chat shows commands, facts, and questions waiting in wisp mcp servers: a note before the next prompt, once per request, and another once it is answered elsewhere or withdrawn. It answers them with /approvals approve ID [once|session|project|always], /approvals deny ID, /facts keep ID, /facts drop ID, and /questions answer|decline ID, only when its input is a terminal; /approvals pending lists what waits. wisp-tui’s input takes the same commands, and Tab completes the waiting ids.
  • wisp-tui’s status bar is interactive: Down from the input’s last row focuses the model, Left and Right move between items, and Enter opens one: the model (/model), the context gauge (the context panel), questions: N (the waiting question’s picker), approvals: N (the waiting approval’s dialog, after its usual grace), and the last turn (the turn list). The line you were typing is kept. /model with no name, in wisp-tui, offers the models it can switch to.

Changed:

  • Under wisp mcp, the “approval needed” banner waits for the client’s dialog: it is posted only if the request is still unanswered after approval.bannerDelaySeconds (default 10, 0 to 3600), and not at all if the dialog, wisp approvals, wisp-tui, or plain chat answers first. A client without elicitation, or whose dialog cannot be sent, is still told at once; 0 restores the old behaviour. approval.pending records bannerDelaySeconds, and approval.settled records banner (posted or skipped).
  • wisp-tui’s status bar shows questions: N and approvals: N in place of ⚠ N waiting, and drops parts in turn when the row is narrow instead of cutting them off. Up and Down move the cursor a row in a multi-line message before they recall earlier lines.
  • The frozen risk test set grew from real use: 13 commands from the audit log, each confirmed by the operator, take it to 29 dangerous commands (from 25), with five false positives the gate makes today recorded for the classifier to learn (ADR 0038).

0.23.1

Fixed:

  • The secret scanner (scan_secrets, redact, wisp scan) no longer reads prose such as “per token: every …th layer” or “per drafted token: 748.125 MiB” as a credential assigned to a name. When a credential word follows another word and is followed by a colon, as in a sentence, the value now counts only if it mixes letters and digits and is not a number with a unit; key: value lines in config, env files, headers, and code are read as before.

0.23.0

A verification pass beside each reply, whole stored outputs in chat, larger eval sets with floors set from statistics, and a fix for edit_file on files with Windows line endings.

Added:

  • A verification pass beside each reply (ADR 0060): exact readers check the results a reply reports (a tool in call form with a result, a command said to have run, an exit status, a denial, a test or build outcome, a notification, a file removed, quoted output, a count) against the turn’s complete call list from the audit log, with each call’s whole output from the output store. An unsupported claim is shown under the reply as unsupported: "…" (why), carried as verified on wisp chat --json’s turn line and in wisp-tui, returned as verification by respond, and audited as response.verification. On the three fabricated runs of 2026-10-04 the readers flag 8 of 9 fabricated replies and no honest one.
  • verification.mode (off, readers by default, model): model adds one side call to the conversation’s model (thinking off, input bounded to 6,000 bytes) when the readers find nothing and the reply has result language. It stays off by default: measured on 2026-10-12, it flagged 6 of 14 honest replies on the on-device model.
  • /show and /last show the whole of an output the model was given only in part, from the output store, under a line saying so (and that the store cut it at 16 MiB, when it did); on a terminal it goes through $PAGER (less -R by default). When the store has nothing they say why (nothing held back, the audit off, or swept) and show what the tool returned. wisp-tui’s expanded view (Ctrl-O) shows the whole stored output, loaded 64 KiB at a time as you scroll, holding at most four pages.
  • wisp chat --json: a new inbound output request, answered with a view of kind output (one page of an output whole); command.typed records stored, the output store’s id for its whole output.

Changed:

  • A request to keep a fact as permanent (set_fact_scope permanent) no longer times out: it waits for you with no time limit, and nothing is kept until you answer with wisp facts keep|drop or in wisp-tui. It also outlives its thread and its server: a keep after the thread closed goes to the shared store, and after the server stopped, wisp facts keep or wisp-tui keeps it itself. The result no longer carries expiresAt.
  • scripts/wisp-mcp starts the server named wisp mcp (claude-code) (from WISP_MCP_CLIENT, set in .mcp.json), and wisp-tui starts its child as wisp chat (tui), so ps tells them apart; wisp finds its own executable by the kernel’s path, never by argv[0] (ADR 0061).
  • The answers wisp approvals approve|deny and wisp facts keep|drop stay terminal-only, after review (ADR 0046).
  • The model evals of edit_file, system_info, and draft_change are larger: 110 edit_file cases, 54 questions, and 30, 8, and 6 diffs. Each floor is set from the suite’s history pooled with the full run of 2026-10-12, so a release stops at most about 1% of the time by chance alone (before, edit_file’s half-floor did about 4% of the time). Commit subjects are judged by an explicit rubric of terms they must and must not contain. The release eval runs a fixed subset of each set; WISP_EVAL_FULL=1 scripts/check eval runs all of them (41 minutes on 2026-10-12).
  • The README overview shows the optional memory and models tools.

Fixed:

  • edit_file edits CRLF, classic Mac (CR-only), and mixed-ending files line by line, numbered exactly as read_file numbers them: each line keeps its own ending, a replacement’s line breaks take the file’s dominant ending, and a guard refuses any edit that would change lines it does not name. Before, a CRLF file was one line to edit_file, so an edit of its line 1 replaced the whole file. read_file now also ends a line at a lone carriage return.

0.22.0

The tool-output budget, models managed in chat by the person and the model, thinking chosen per call, Ctrl-C for a model’s turn, and shell loops judged by what they run.

Added:

  • One tool-output budget for every tool result, an eighth of the model’s window (4 KiB on the on-device model, 64 KiB at most), in place of the fixed 4 KiB. Output past it is kept whole in the conversation’s output store (~/.wisp/output/<session>/, 16 MiB per output, audited as output.kept by size and hash), and the model is shown the first and last lines with a marker and a note saying how to reach the rest, and for a command not to run it again (ADR 0050).
  • memory pages a stored output by the id its note names (recall output 3f9a2c1b from line 44) or by its entry, filters it (matching PATTERN, lines A-B, head N, tail N), summarises it with exact readers (a failed build by its root causes, a log by its severities, JSON by its shape), and condenses it with a question the conversation’s model answers over all of it, chunk by chunk.
  • inspect lists a turn’s tool calls one line each (what: calls) and filters audit by turn; a tool call repeated with the same arguments within a turn is answered with a line saying so.
  • /models pull REPOSITORY and /models trust PUBLISHER in chat and wisp-tui: the publisher question is a choice (refuse by default, pull once, trust from now on), the download’s progress shows in the status line, and Ctrl-C stops a pull, keeping what arrived for the next pull to resume. wisp-tui’s /models picker has a “pull a model…” row, and /models cancel [REPOSITORY] stops a pull the model asked for.
  • A models tool for the model, off by default (tools.models, or a tool list naming it): list and show freely; pull, enable, disable, and check only with the person’s approval. A pull is asked every time, never remembered, and not answered by --yes; trust and switch are refused. An approved pull runs in the background and its end arrives as a note in the conversation (ADR 0059).
  • Thinking per model: ollama.models.<name>.think, mlx.models.<name>.think, and llamacpp.models.<name>.think set one model’s thinking ahead of the runtime’s think; wisp models shows it as thinking (false).
  • respond takes think (true, false, or an Ollama level) when a thread starts; it is refused on an existing thread and shown in wisp://threads, and the prompt audit event records the thinking each turn sent.
  • wisp approvals, /approvals, and wisp://approvals mark project approvals whose folder is gone; wisp approvals clear --gone removes them.
  • wisp://threads/{thread_id}/output/{id} serves a whole stored output, paged; the output list marks stored outputs.
  • The output store is swept by age: a session’s stored tool outputs are removed context.outputDays days after its last use (default 30; 0 keeps them), at a later session’s start, at most 16 sessions and 100 ms a start, audited as outputSweep on session.start. wisp doctor reports the store’s size and what the next sweep would remove.

Changed:

  • wisp’s own calls no longer make a thinking model think: classifier verdicts, schema replies (including triage, summarise_diff, and thorough scan_secrets and redact), the fact distiller, the running summary, the assessment, and memory’s condense send think: false; change drafts and conversation turns keep the model’s default. Measured on 2026-10-10: granite4.2:8b not thinking matched granite4.1:8b at its speed and got every draft right.
  • Ctrl-C during a model’s turn stops the turn instead of quitting: the request is cancelled, a command the model started is stopped (SIGTERM, then SIGKILL after two seconds), and chat returns to the prompt with “stopped by you”; a second Ctrl-C quits. The same works through wisp-tui and the interrupt line, and the stop is audited as turn.stopped.
  • Loops and conditionals are judged and remembered by the commands inside them: for f in *.txt; do git add "$f"; done is git add *, so a new loop does not ask again for an approved command, and a keyword such as do is never remembered on its own. A body that uses a loop variable is judged by what it ranges over (rm "$f" over ~/* is dangerous).
  • find -exec commands are judged; python3 -c, perl -e, node -e, and similar inline programs are at least moderate; piping into an interpreter as its program (curl … | python3) is dangerous. A program named by a variable or substitution ($X args) is at least moderate and remembered by its exact text; a quote or escape inside a word (w''isp) or ${IFS} between words no longer hides a rule or a deny pattern.
  • A line whose risk only the whole line shows is asked once, as a whole, not part and then line.
  • run_command no longer discards all but the tail of each stream: its result is bounded as a whole, notes included, and the whole output is kept. commandMaxOutputBytes is unset by default and, when set, replaces the budget for run_command alone. read_file and memory page at the budget.
  • Each MCP respond thread gets an audit session of its own (<thread_id>-<suffix>, shown as auditSession), so a reused thread_id no longer mixes an earlier thread’s history into its resources; wisp://audit, wisp://audit/{session}, and wisp://threads/{thread_id}/audit are paged (?page=N&limit=M).
  • wisp models, /models, /model, and the models tool share one set of model actions; wisp models’ flags and output are unchanged. model.pull gains source, approval, and the outcome cancelled; model.publisher, model.verified, and config.change gain approval.

Fixed:

  • A spawned command inherits no descriptor of wisp’s or of another command’s (POSIX_SPAWN_CLOEXEC_DEFAULT; pipes, the output store, and read_file’s handles are close-on-exec).
  • read_file refuses FIFOs, devices, and sockets, and reads a file with no newlines a page at a time; edit_file’s write and append no longer load the existing file.
  • The risk rules no longer read a search pattern, an awk program, a pgrep pattern, or printed text as a pipe, a deletion, or a write (grep -vE "grep|bash -c", grep -E "a|python", pgrep -f 'rm -rf x', awk '… NF>6 …'), and the built-in deny patterns no longer refuse them; ls ~/.ssh/*.pub is safe and listing ~/.ssh/ is moderate; text written into a shell start-up file is judged as the commands it will be; python3 <(curl …), kubectl … delete, gh repo delete or edit --visibility, and wisp --yes are dangerous.
  • A ( inside double quotes is no longer read as a subshell, and a $(…) after a ' inside double quotes is no longer missed, so neither lets a command escape the risk check.
  • scripts/check eval shows each line as it runs again, instead of only when the run ends.

0.21.2

A patch for one fault found after 0.21.1.

Fixed:

  • A command’s timeout, the grace before a stopped command is killed, and every bounded wait (approvals, model checks) fire on time when other work fills Swift’s thread pool. Synchronous bridges for Core ML compiles and model resolution could take every pool thread while waiting on work that needed one, and timers waited behind them: a 200 ms command timeout went unenforced for a whole 30-second command. The bridges now run their work off the pool, and the timeouts are dispatch timers the pool cannot hold off. Under wisp mcp, enough concurrent model resolution or classifier loading could have let a model’s command outrun its timeout.

0.21.1

A patch release from a code and test review of the whole codebase on 2026-10-09. The approval gate, the quality gate, and the front ends each had a way to do less than they promised; every fix has a test.

Two small additions came with it, at the operator’s request: MLX pulls from any publisher, and no timeout for a command you type yourself.

Added:

  • wisp models pull fetches MLX models from any Hugging Face organisation, not only mlx-community. A publisher not in the new mlx.trustedPublishers setting (default ["mlx-community"], which is always trusted) is put to you first, with its licence and download size: pull it once, trust the publisher from now on, or refuse, the default. --trust-publisher skips that question for one pull; without a terminal and without it, the pull is refused. Every file check stays for every publisher, and each decision is audited as model.publisher.

Fixed, in the approval gate and the sandbox:

  • A ; or newline inside parentheses no longer hides a command from the risk check: in (curl … ; true) the curl was never judged or asked about. A part of a line wisp cannot read is now judged as written.
  • An approval covers only commands judged at its level or below: approving a moderate rm build/x.o for the session or the turn no longer covers rm -rf ~/Documents. A dangerous command is remembered by its exact text only, and standing approvals are bound to their level too.
  • Shell keywords and interpreters (do, then, sh, bash, eval, python3, xargs, …) are remembered by the exact command, never as do * or sh *, which covered anything; the scripts given to sh -c and eval are judged too.
  • The risk rules see through quotes (sh -c 'rm -rf ~/x', "sudo" ls), line continuations, and git’s options before its verb (git -C . push --force). Risks only the whole line shows (curl … | sh, env | grep TOKEN) are now asked about, and a bare env or printenv, which prints every secret, asks.
  • Commands and edit_file can no longer write wisp’s own home (~/.wisp), even when it lies inside the writable directories, so a command cannot answer its own approval or rewrite config.json.
  • wisp approvals revoke while wisp mcp runs is no longer undone by the server’s next approval; processes sharing approvals.json merge their changes under a lock.
  • A command’s background jobs, and children that ignore SIGTERM, no longer outlive it.
  • read_file of a link to a credential, or of a credential path in another case (~/.SSH/ID_RSA), asks as the credential itself would.
  • Approval dialogs and notifications, the MCP client’s included, show control characters escaped, so a command cannot redraw the dialog to hide itself.
  • A custom tool’s argument containing another placeholder ({b}) can no longer unquote that argument’s value.
  • $'…' quoting no longer confuses the command splitter. The deny list catches a nested wisp chat, respond, or mcp, and wisp approvals approve|deny or wisp facts keep|drop, behind nice, time, timeout, env -i, {, then, or do, or with the name quoted. Classifier versions containing / or .. are refused, and a pending approval’s binding covers the reasons shown and its expiry.

Fixed, in the front ends and the MCP server:

  • wisp-tui no longer answers an approval with a letter typed for the input: held keys go only into the input box, and a dialog takes keys only after half a second of quiet once it appears. An approval from wisp mcp that arrives while you are typing waits until the box is empty, with a note and a count in the status line.
  • Ctrl-C and Ctrl-D in wisp-tui no longer hang: it sends /quit so the transcript is saved, stops wisp after five seconds, shows wisp’s start-up errors, and exits with wisp’s status when wisp failed.
  • Two respond calls on one thread_id no longer interleave and swap receipts and refusals: a thread’s turns run one at a time, and close_thread waits for the turn under way.
  • An approval or choice that times out is withdrawn from wisp-tui and from the MCP client’s dialog, instead of staying up with an answer that is ignored.
  • wisp config set|unset and /config refuse to write when config.json exists but cannot be read, instead of replacing it and losing every other setting; config.json itself refuses an unparsable baseURL or an out-of-range timeoutSeconds or contextLength instead of silently using defaults.
  • The condensing tools’ path reads only regular files, and only up to their bound, so /dev/zero, a FIFO, or /dev/stdin can no longer hang wisp mcp or read its protocol channel; flaky_tests takes at most 10 paths.
  • wisp completions install writes through a symlink to wisp’s script instead of replacing the link. Failures at run time (wisp approvals approve|deny too late, wisp facts, a refused install) exit 1, not the usage code 64. An unknown wisp chat --json line is ignored instead of clearing the front end’s busy state; completion indices count Unicode scalars on both sides; wisp-tui no longer panics on a huge turnSeconds.

Fixed, in conversations, facts, and models:

  • Permanent facts are saved under a lock and merged with what other wisp processes wrote, so none is lost or undone; an unreadable ~/.wisp/facts.json is moved aside as facts.json.unreadable-<time>, not overwritten, and wisp and wisp doctor say so.
  • A command typed after ! is no longer counted as a turn, so condensing keeps the last real turn and a dropped command reaches the running summary; its note says not to run it again, as other commands’ references do.
  • A disabled model is refused however it is spelled (ollama:x or ollama:x:latest, a llama.cpp model’s name or .gguf path, an MLX name or its directory).
  • wisp models pull checks every file’s content, by its git blob id where the listing has no SHA-256, and restarts a file whose resumed download comes back from the wrong offset.
  • A llama.cpp model whose recorded tool check failed is text only, even when the server says it supports tools.
  • Condensing ahead, /tokens, and the status line use the conversation’s own token count, not an assessment’s or a distiller’s; a distiller’s thinking is no longer recorded as the turn’s; /model after /new keeps the conversation’s name and counters, and /new starts from the full instructions; facts and summary written in one call no longer re-distil turns already distilled.
  • Saved conversations that cannot be resumed say why, and a failed save no longer leaves a stale store.
  • llama.cpp and LM Studio: a reply cut off at the end of the window is condensed and retried; a cancelled request is no longer reported as “no server”; error bodies are read only to their bound; LM Studio’s reasoning: false means no thinking. MLX: a later <think> in a reply stays reply text, a schema reply’s budget allows for the closing tag, a request cancelled while waiting loads nothing, and a tool call is recovered only from a single malformed frame.

Fixed, in the quality gate and the release:

  • scripts/check eval fails when the evaluation fails. It took grep’s status, which matches a failed run’s lines, so no eval, and no release’s eval floors, could fail.
  • scripts/check eval record keeps every suite’s measurement when suites finish together, and refuses rather than overwrites a measurements.json it cannot read; the coverage commands refuse when the tests fail instead of comparing or writing an empty figure, and no longer retry failed tests silently.
  • scripts/check mcp-build builds the MCP server without stripping the MLX pins from harness/Package.resolved.

Changed:

  • A command you type after ! in chat or wisp-tui no longer has run_command’s 60-second timeout, so ! ollama pull … runs to the end. Ctrl-C stops it instead (SIGTERM to its process group, SIGKILL two seconds later), chat carries on with ↳ exit -15 (stopped by you), and a second Ctrl-C quits. The working line says “Ctrl-C stops it”; wisp chat --json gains an inbound interrupt line, and the audit records stopped: true.

  • Facts from a command typed after ! rank as observations, as a tool’s do, so a later run of the same command replaces them; what you state with /fact still wins.

  • Thread, session, and transcript names admit only ASCII [A-Za-z0-9._-], as the rule always said.

  • The gate compiles the tests and the model evaluations with warnings as errors, bounds every test’s wait, and no longer rebuilds everything between its build and test steps; the release preflight builds the MLX trait, checks the remote tag and that main is pushed, and prints the commands to finish a release that failed after tagging. The Homebrew formula drops its redundant version line.

0.21.0

Added:

  • llama.cpp and LM Studio as model backends, through one shared executor for servers that speak OpenAI’s chat-completions API (ADR 0058): llamacpp:<model> for llama-server (default http://127.0.0.1:8080) and lmstudio:<model> for LM Studio’s server (default http://127.0.0.1:1234), each with a config.json section (baseURL, timeoutSeconds, apiKey, and for llama.cpp think and models). Streaming, tool calls, schema replies, the model’s thinking, and usage work as for Ollama; wisp models lists each server’s models; the window is the one the server holds the model at (FROM server); a request the server refuses as too large is condensed and retried. A server’s API key comes from WISP_LLAMACPP_API_KEY or WISP_LMSTUDIO_API_KEY, or apiKey, and is never logged or shown. LM Studio reports which models call tools; a llama.cpp model is usable with tools once declared or checked with wisp models check llamacpp:<model>. Not yet tried against a real llama-server or LM Studio; checked live against Ollama’s own OpenAI-compatible endpoint.
  • A context window for one model: ollama.models.<name>.contextLength and mlx.models.<name>.contextLength hold that model to a size, ahead of ollama.contextLength or mlx.contextLength, while the rest are sized from memory (wisp config set ollama.models.qwen3.8:27b.contextLength 16384). wisp models shows model config as where the window came from, and wisp doctor says configured for this model.
  • wisp models pull resumes a file cut off part-way from where it stopped, with an HTTP range request, and checks the whole file against the listing’s SHA-256; a server that ignores the range sends it whole, and the pull says so. A real directory at ~/.wisp/models/mlx/<name> seeds the Hugging Face cache: its files that match the listing are copied (cloned on APFS) instead of fetched again.
  • respond’s receipt carries the turn’s token usage, usage with input, output, cached, and reasoning, for a runtime that reports it (Ollama, MLX, llama.cpp, LM Studio); the response audit event records it too. Apple’s models report none, and their receipts have no usage.
  • Shell completions for zsh, bash, and fish. Homebrew installs them with wisp; otherwise wisp completions install writes the script for your shell (from $SHELL, or named) to its per-user directory, replacing only a file wisp wrote, and says how to turn it on; wisp completions zsh|bash|fish prints it. Model names, tools, settings, approval ids, and the ids of requests waiting in ~/.wisp/pending are completed from wisp’s own files.

Changed:

  • docs/model-controls.md, the draft proposal for reasoning and speed controls, says which part ollama.think, mlx.think, and the thinking display already build, and what is still proposed.
  • The README’s overview picture names every runtime the model runs on: on device, Ollama, MLX, or Core AI.

Fixed:

  • Core AI lists a models directory that is a link, and a bundle linked into it, as MLX does.

0.20.0

Breaking:

  • New context defaults, from context checkpoint 2 (ADR 0057), which measured them over a 29-turn conversation on the on-device model, granite4.1:8b, and gemma4:12b, three runs each. The first takes a tool away from every conversation that was given all of them:

    • The model’s memory tool is off by default. With it the model scored fewer answers on all three models (4.3 against 5.3 of 10 on the on-device model, 7.3 against 8.0 on granite, 8.0 against 9.3 on gemma4). A conversation given every tool (wisp "…", chat, MCP respond without tools) no longer has it, its instructions no longer mention it, and references to earlier output no longer say memory "recall entry 7": for a tool that only reads (read_file, inspect, system_info, current_date) they say call it again to see it, and for any other tool (a command, an edit) that its output is not repeated and not to run it again to see it, so a command is never rerun to see what it printed; facts, the summary, and the references are otherwise unchanged, and /show still shows any output. wisp config set context.memory true (or /config set context.memory on) turns it on for every conversation; --tool memory or an MCP caller’s tools naming it gives it to one.
    • context.target is 0.6 instead of 0.5: condensing keeps more of the conversation and runs a little later. It scored 5.3 against 4.0 of 10 on the on-device model and 7.7 against 7.0 on granite.
    • assessment.taskChanges is restated instead of any, which matters only with assessment.enabled (still off): an inferred task changes only when a request states one. It kept the task on every run where any lost it on five of six.

    To restore the earlier defaults: wisp config set context.memory true, wisp config set context.target 0.5, and wisp config set assessment.taskChanges any.

Added:

  • scripts/check eval and eval compare measure mlx: models named in WISP_EVAL_MODELS: the evals are built with the MLX trait for them (the Metal toolchain is needed then), and runs naming none build as before.
  • An MLX reasoning model’s thinking is shown as an Ollama model’s is (ADR 0053): mlx:Qwen3-1.7B-4bit and other models whose chat template marks a thinking block (<think>…</think>) say thinking while it lasts, show ∴ thought for 2.0 s, 181 tokens with the thinking folded under it, count its tokens, and audit it as model.reasoning. It used to arrive in the reply, tags and all, when the model thought. The model never gets its thinking back.
  • mlx.think (true or false) asks MLX models whose chat template takes enable_thinking, such as Qwen3, to think or not; unset, a model declared reasoning thinks and any other is left to its template’s default (below).
  • assessment.taskChanges (any or restated): with the assessment on (assessment.enabled, still off by default), restated lets a request change the task the model inferred only when it states a task, such as Today's task: … or Let's switch to …, instead of on any request; other requests keep the task without a model call for it. restated is the default (Breaking, above); any behaves as before.
  • scripts/check eval checkpoint runs context checkpoint 2: the condensing target and headroom on a longer conversation, the half-window variants, memory on and off, the assessment’s task changes, and a model switch mid-conversation, ending with a table (docs/measurements.md).

Changed:

  • edit_file’s line edits forgive two slips small models make, without guessing. A rewritten line sent without its indentation (return 10 for return 10) keeps the old line’s, unless only whitespace changes, so a deliberate indent, dedent, or tab-to-space edit is written exactly. A stale line number with find edits the one line that holds find; none or several still change nothing. The result says when either applied and shows the edited line as it now reads, for example line 2 now: " return 10".

Fixed:

  • edit_file with line and an empty find refused the edit (no line contains an empty string); an empty find now checks nothing, as granite4.1:8b sends one for an argument it does not need.
  • ollama:gemma4:12b and gemma4:26b are sized from memory instead of falling back to 8,192 tokens. Ollama reports gemma4’s key-value heads and sliding-window layers per layer, which sizing did not read; it now counts such a model layer by layer, so only the layers that attend to the whole window (8 of 48 in 12b) grow with it, and counts the draft model Ollama runs beside gemma4. With 30 GB available, 12b gets its full 262,144 tokens. The reason in wisp models and the model.resolved event says how the layers were counted, for example 40 of 48 layers sliding-window (1,024 tokens), with a draft model. Other models’ windows are unchanged.
  • Hybrid models, which interleave attention with recurrent layers, are sized by their attention layers only. ollama:qwen3.8:27b was counted at 260 KiB a token for all 65 layers and stayed at 8,192 tokens; it is now 68 KiB a token plus 748 MiB of fixed recurrent state, as Ollama allocates it, and ollama:ornith:9b gets its full 262,144 tokens with 30 GB available instead of 65,536. MLX’s Falcon-H1 models count their Mamba-2 state. The reason names the class, for example 16 of 64 layers attention, with 1 draft layer of its own; 748 MiB recurrent state. Other models’ windows are unchanged.
  • MLX models whose chat template uses ChatML’s <|im_end|> stop there even when their generation config leaves it out. mlx:Falcon-H1-Tiny-Tool-Calling-90M-bf16 ran on to the token limit, writing tool results and further turns of its own, and a tool call in a turn it wrote for you was taken as its call.
  • MLX tool calls written as a JSON array in one <tool_call> frame, the form Falcon-H1-Tiny-Tool-Calling’s template asks for, are read as calls; they were left as text.
  • Ollama tool calls written as text in Mistral’s own format, read_file[ARGS]{"path": "…"}, are read as calls when the reply is exactly such calls. ollama:ministral-3:14b writes them and its template leaves them in the reply, so it made almost no calls (0 of 30 edit_file cases in the 2026-10-04 comparison). Text that only mentions the format, or names a tool the conversation does not offer, stays the reply.
  • MLX models are no longer told not to think when reasoning is not declared for them. mlx:Qwen3-1.7B-4bit called read_file, then replied with the file instead of editing it: 2 of 30 edit_file cases against 20 of 30 for the same model on Ollama, which leaves thinking to the model. Unset, mlx.think now leaves the model’s chat template to decide, as an unset ollama.think leaves Ollama; the same cases then passed 13 of 30, and drafted commit subjects 7 of 10 instead of 4. A model that thinks is slower: about 14 s an edit_file case instead of 1.3 s. Set mlx.think: false for the old behaviour.
  • An MLX schema reply, such as a draft_change subject, lets a model that thinks finish its thinking before the reply is held to the schema, as Ollama does; it was held to JSON from the first token.
  • An MLX conversation no longer breaks after a tool call whose arguments hold a null: every later request failed with “Cannot convert value of type NSNull to Jinja Value”.

0.19.0

Breaking:

  • The model can no longer start a wisp of its own. The default deny list refuses wisp respond, wisp chat, wisp mcp, and wisp "prompt" (by any path, after env or VAR=value), and so do commands you type after ! in chat; run a nested wisp from another terminal instead. wisp doctor, logs, tools, --version, and the other subcommands stay allowed (ADR 0054).
  • wisp models piped is a different shape: one tab-separated field per column of the new table, so the old detail; capabilities field is gone. Scripts should read wisp models --json (see Changed).

Added:

  • A reasoning model’s thinking is shown (ADR 0053). Ollama streams it for models such as qwen3.8:27b, gemma4 and ornith, and wisp used to drop it, so the turn looked idle. Now chat says thinking while it lasts (wisp-tui draws a thought bubble in its busy box), shows ∴ thought for 4.2 s, 42 tokens with the thinking folded under it (/show for the rest, /inspect thinking for every stretch), counts its tokens, and audits it as model.reasoning. Over MCP, wisp://threads/{thread_id}/reasoning lists it. It is kept for you: the model never gets its thinking back in a later request.
  • ollama.think (true, false, low, medium, high, max) asks a model that can think to think, or not, or how hard; unset leaves it to the model.
  • Under each reply, beside ran:, a muted line names the entries the reply cites (entry 19, entries 16-30) that the conversation does not hold: cited but not in this conversation: entries 19–30 (12). wisp chat --json carries it as cited, and respond returns unknownEntries and cited (ADR 0055).
  • wisp models pull mlx-community/<name> fetches an MLX model from Hugging Face into the Hugging Face cache (HF_HUB_CACHE, $HF_HOME/hub, or ~/.cache/huggingface/hub), in its own layout, so a model Hugging Face’s tools already fetched is reused file by file and nothing is stored twice, and links mlx:<name> to it. It says which files are already in the cache and which it would fetch, how many bytes, and where, and asks before downloading; a model the cache already holds is only linked. A directory already at the link’s path is kept unless you agree to move it to the Trash. Only from a terminal, only mlx-community, only the files a model directory needs, each checked against the listing (weights by SHA-256); an interrupted pull resumes with the files it finished. wisp models names cached models not yet linked. Audited as model.pull; the model is refused the command (ADR 0052).
  • mlx.contextLength and mlx.executor in config.json (and wisp config set).
  • Models can be turned off and on: wisp models disable|enable <name>, /models disable|enable <name> in chat, or, in wisp-tui, /models as a picker (↑↓, Space to turn one on or off, Enter to save, Esc to leave). A disabled model is not offered by /model or Tab and is refused by /model, --model, config.json’s model, and an MCP caller’s model, with how to enable it; the default model cannot be disabled. Stored as models.disabled in config.json, audited as config.change. Enabling a complete MLX model in the Hugging Face cache that is not linked links it, with no download (ADR 0056).
  • Enabling an MLX model leaves it usable: when config.json declares none of its capabilities, wisp loads it and asks three short questions (a reply, a tool call, a structured reply), each once and within a time limit, says what it is doing and each result, and records the capabilities that pass, with the day, in config.json. A model that cannot give a plain reply is refused and stays disabled; one that replies but does not call a tool is enabled for use with tools off only (--no-tools, tools: [] over MCP, the condensers), says so, and is listed text only; one that does both is enabled and usable. wisp models check <name> (or /models check in chat) checks again; a capability you declared by hand is kept, with a note, when its check fails. The listing marks verified capabilities (verified), and --json says how they are known (capabilitiesFrom). Audited as model.verified (ADR 0056, refined 2026-10-04).
  • wisp models --json, a field per column.
  • When chat’s configured model is unavailable as it starts (Ollama not running, say), chat starts on system and says so, with the /model to return to it, instead of refusing to start; audited as model.fallback. A model named with --model, wisp respond, and MCP respond still fail, so nothing changes model silently (ADR 0056).

Changed:

  • wisp models and chat’s /models are a table of everything wisp knows about each model: runtime, parameters, size, format, the context window wisp would use and how it was decided, where an MLX model lives, whether it is enabled, and its capabilities in plain words (tools, structured replies, thinking, vision). On a narrow terminal the least important columns are left out. Cached MLX models not yet linked are rows of the table rather than a note. Piped, every column is a tab-separated field, so the old detail; capabilities field is gone; use --json in scripts (ADR 0056).
  • When a command fails with Operation not permitted, wisp now checks the paths in the error against the sandbox’s writable roots instead of guessing, and tells the model in one line: the sandbox refused writing to a path and where commands may write, something else refused a path inside the roots, or, with no path to check, the sandbox may have refused it and no policy rule did. The note for a command you type after ! uses the same check, and command.outcome records it as sandboxRefusal (ADR 0054).
  • MLX models run through wisp’s own executor, on a par with Ollama’s: the context window is sized from the model’s config.json and the Mac’s memory (or mlx.contextLength) and shown by wisp doctor, token counts are exact with the model’s tokenizer, usage is reported, and each thread’s processed prompt is reused by its next request, so only what changed is processed. Images are not passed; mlx.executor: "bridge" runs a model through mlx-swift-lm’s bridge as before (ADR 0052).
  • Core AI models report the context window their bundle was exported for, from metadata.json, to the conversation, wisp doctor, and wisp models.

Fixed:

  • An Ollama reply cut short is no longer taken for a whole one. A stream that ended without the chunk that says done was kept as the complete reply; a connection lost mid-reply, or a server that went silent, failed with no Ollama server at …. Each now ends the turn with Ollama at … stopped before the reply was done or … sent nothing for N s (ollama.timeoutSeconds), nothing of the reply is kept, and the next turn goes on as usual.
  • The inspect tool’s config view (and /inspect config) cut the settings that sort last, version among them, once every backend’s settings made it pass 4 KiB; it is now sent as compact JSON when the pretty layout would not fit.

0.18.1

Fixed:

  • wisp chat from the Homebrew install opened the plain chat instead of wisp-tui (since 0.17.0): the formula puts wisp in libexec, beside the MLX library, and wisp-tui in bin, and wisp chat looked only beside itself. It now also looks in a bin beside its folder, and wisp doctor gains a front end finding that says where it found wisp-tui, or where it looked.

0.18.0

Breaking:

  • wisp://threads/{thread_id}/facts no longer carries summary (or summaries with ?all=true): the running summary moved to wisp://threads/{thread_id}/summary, below. /inspect facts no longer shows it either.

Added:

  • Commands you run yourself in chat: a line that starts with ! (! git status --short) runs the command in the conversation’s directory, under the same policy lists and sandbox as the model’s commands but without the risk classifier or an approval, since you typed it. Its output is shown and folded like a tool’s (/show for the rest), it is audited as command.typed, and the model is told on its next request, as your action, with a reference it can recall. wisp-tui has a command mode for it: ! at the start of the line turns the box a pale amber, and Backspace at the start of the line turns it back (ADR 0049).
  • wisp-tui’s input box is visibly inactive while a turn runs: dimmed, with what wisp is doing in place of the cursor, and keys typed meanwhile are held and applied when the turn ends instead of being dropped.
  • wisp chat --json: /inspect summary [all] answers with a view of kind summary, and a typed command’s command.typed event carries its output as a tool.result does.
  • Under each reply, a muted line of what the turn actually ran, counted from its audit events, never from the reply: ran: read_file ×2 · run_command ×3 (1 failed, 1 denied), or ran: no tools when a reply names a tool none of which ran. In chat, wisp-tui (ran on the turn’s end in wisp chat --json), and respond’s structuredContent.ran (ADR 0051).

Changed:

  • The running summary of earlier turns has a place of its own: /inspect summary [all] in chat (and wisp-tui’s panel) and wisp://threads/{thread_id}/summary over MCP. /inspect facts and wisp://threads/{thread_id}/facts show only facts.
  • wisp-tui’s dialogs (the output panel, approvals, choices) have a blank row below them, between the dialog and the status line, as they have the reply row above them.

Fixed:

  • Tab did not complete /inspect in chat or wisp-tui, though it is the only way to the context, facts, and summary views; the commands Tab offers now come from /help’s table, so none can be left out.
  • A fact with the same value from two sources (a chat’s workdir and the same one from a ! command) showed twice; a value is now held once, under the strongest source that asserted it.
  • Two approvals asked at the same moment over MCP, on a channel not used before (a fresh ~/.wisp/pending), could have one refused as “the pending directory is not safe to use”: the directory is now created with its private mode in one step, so no request sees it half-made.

0.17.0

Added:

  • MLX models (mlx:<name>) in the release: the Homebrew install is built with MLX and carries its Metal library, mlx.metallib, beside wisp in the Cellar’s libexec, so MLX models run without building wisp yourself (ADR 0047). The download grows by about 6 MB. wisp doctor has an MLX finding: the Metal library it found and that it loads, or what is missing.
  • Permanent facts over MCP (ADR 0048). A wisp mcp caller can ask for one of its thread’s facts to be kept as a permanent fact with set_fact_scope scope: permanent; the call returns at once with the request’s id, and a notification (“wisp: keep as a permanent fact?”, naming the fact and wisp facts keep <id>) tells you. Only you answer: wisp facts keep <id> or wisp facts drop <id> from a terminal, or in a running wisp-tui. The outcome shows as request on the fact’s resource and in wisp://facts/proposed. A dropped fact stays in its thread, which does not ask about it again; an unanswered request keeps nothing.
  • wisp facts pending lists the facts waiting to be kept.
  • respond’s result counts the permanent facts proposed in the server’s conversations that await you, as structuredContent.factsProposed.
  • wisp chat --json: a front end whose hello declares the keep-facts effect is sent the facts waiting to be kept as approval lines with kind: "fact", answered keep or drop; wisp-tui declares it and shows them as a dialog of their own (k keep, d drop).
  • The default policy refuses wisp facts keep|drop when the model runs it.
  • wisp doctor’s pending approvals finding also says how many facts wait to be kept.

Changed:

  • wisp --version and wisp-tui --version print the commit for a build that is not a release: 0.16.0-dev+4ab6eec, with (modified) when the working tree had changes, so a build from main is not mistaken for the release whose number it carries. A release prints the bare version. The audit log’s version stays bare.
  • set_fact_scope refuses to move a permanent fact (p…) whatever the target, saying it is yours; a caller that disagrees records a newer fact in its thread, which shows as a conflict.
  • memory’s argument guide no longer gives a task example, which cost 13 tokens in every conversation with memory; the task verb works as before.

0.16.0

Added:

  • wisp watch --settle <seconds>, and watch.settle in config.json (1 s by default, 0 for none): a run triggered by file changes starts only once no change has arrived for the settle period, so a checkout, a formatter, or save-all gives one run after the burst instead of a spurious failure part-way through it. The first run and --every runs are not delayed.
  • Approval over MCP for every client (ADR 0046). A command the model runs under wisp mcp that needs approval is also filed in ~/.wisp/pending and announced by a notification (“wisp: approval needed”, naming the command and wisp approvals approve <id>). Answer it from a terminal with wisp approvals approve <id> [--scope once|session|project|always] or wisp approvals deny <id>, or in a running wisp-tui, which shows it as its approval dialog. A client without elicitation, such as the Claude mobile app, can now be approved; with a client that has a dialog, both are asked and the first answer wins, which is also the way out when the dialog sticks. The calling agent cannot answer.
  • wisp approvals pending lists the commands waiting for approval in wisp mcp servers.
  • respond’s result lists the notifications its turn posted, as structuredContent.notifications.
  • approval.outOfBand (on by default): false asks through the MCP client’s dialog alone, as before.
  • wisp doctor checks ~/.wisp/pending: absent, or a directory only you can open, and how many requests wait.
  • The audit log records each step of an approval answered through another face: approval.pending, approval.answered, and approval.settled (logging.md).
  • wisp chat --json: a front end whose hello declares the approve-mcp effect is sent the commands waiting in wisp mcp servers as approval lines with source: "mcp", and a withdrawn line when one no longer waits; wisp-tui declares it.

Changed:

  • A client without elicitation is no longer refused at once: the command waits for an answer from wisp approvals or wisp-tui, up to approval.timeoutSeconds, then is refused as before.
  • The default run_command policy refuses wisp approvals approve and deny: the model never answers an approval.

Fixed:

  • A tool output’s reference no longer shows read_file’s paging hint as its last line or a fragment as its first: both lines are whole lines of content, a line next to a cut is marked with …, and the hint is kept as its own paging: more from offset N line.
  • read_file given a wildcard path that matches no file now says it takes one path and to list matches with run_command, where it said “file not found” and a small model retried the same path.

0.15.0

Breaking:

  • Transcripts saved before this version cannot be resumed. --resume needs the transcripts/<name>.store file that /save now writes beside a transcript; a transcript without one, or with one that cannot be read or does not match, is refused with transcript 'x' was saved by an older wisp and cannot be resumed; start a new conversation, where it used to resume without links to the audit log. Start a new conversation instead.
  • MCP resources about respond threads moved under wisp://threads/{thread_id}. A thread’s audit events are now wisp://threads/{thread_id}/audit; wisp://audit/{session} still serves every other session (the server’s own, triage-<id> and the other condensing tools’, CLI runs’) and refuses a thread’s id with a pointer to the new place. wisp://status no longer lists the threads: it gives threadCount and threadsURI, and wisp://threads lists them.
  • memory is now a built-in tool’s name, so a custom tool named memory in config.json’s tools.custom makes the file malformed (is a built-in tool's name), so wisp refuses it; rename the custom tool.

Changed:

  • wisp tools, wisp models, and wisp approvals lay out their listings like wisp --help on a terminal: aligned columns, with descriptions and long cells wrapped to the terminal’s width. Piped, they print the same tab-separated lines as before, so scripts keep working.
  • context.target is capped below the condensing trigger, at the context budget less 0.2 (0.65 of the window at the default 85%). A target at or near the budget made wisp condense, and distil facts, on almost every turn; the default of 0.5 is unaffected, and wisp doctor says when your setting is used as the cap.
  • Conversations condense less often and further. Instead of dropping to the last four turns whenever the window passed 85%, wisp now condenses when the context, your next message, and a turn of average size would pass 85%, and then brings the context down to half the window: earlier tool output as references, the turns that go distilled into facts and the summary, the oldest turns dropped, checking the size after each step and always keeping the last turn. context.target and context.headroomTurns in config.json (or wisp config set) change the half and the eight turns the average covers. When even the last turn and your message are too large for that, the turn goes on and you are told: chat prints a note, and MCP respond returns contextNote. A request that cannot fit the window at all now fails with the request does not fit the model's context window: …, saying how large it was and what to do, after one retry. context.condensation in the audit log gains the target, the size before and after, the headroom, the steps taken, and floor.
  • /help lists every chat command as it is now: /inspect facts with the summary of earlier turns and proposals from other conversations, the forms of /fact and /show IDs, /inspect as the general form of /status, /audit, and /approvals, and the /?, /q, help, and exit aliases. Long usages put their description on the next line, and under wisp-tui the list ends with its keys.
  • wisp-tui’s approval dialog has an empty line between what it asks about and the keys that answer it.
  • The notify tool now tells the model which route posted the notification (notification posted via terminal) where it said notification shown.
  • Unknown audit event kinds are shown rather than skipped: wisp logs, the inspect tool, and the audit resources now print a line written by another release whose kind this build does not know, as it is, where they used to leave it out. wisp logs --kind still accepts only the kinds listed in docs/logging.md.
  • The status line brightens what matters: the git branch, tokens written, and a context from half to 80% used are now in wisp’s brightest blue; under half used stays quiet, and past 80% is amber.
  • Ollama models get a context window sized for them when they are selected, instead of 8,192 tokens for all. wisp reads the model’s shape and maximum from Ollama and the memory available on the Mac, and takes the largest window whose cache fits half of what is free (never more than three quarters of the Mac’s memory). granite4.1:8b got 24,576 tokens on a 48 GB Mac, so conversations condense far less often. Setting ollama.contextLength still fixes one window for every model.
  • Chat, wisp-tui, and MCP show you each tool’s output as the tool returned it, so the model no longer needs to retype it: wisp’s system prompt now tells the model that you see the output and that it should comment on it rather than repeat it, unless asked to.

Added:

  • An assessment of each request, off by default (assessment.enabled in config.json, or wisp config set assessment.enabled true): before each turn, rules or one call to the model outside its context choose the tools the request registers (with a one-line-per-tool catalogue in the instructions), the facts repeated next to the request, and, in chat, the task and its objective, never replacing one you set with /task or an MCP caller’s task. assessment.tools picks request, task, or all. Each is audited as context.assessment. It is there to be measured; whether it is on by default waits for the eval.
  • memory "task …": the model can propose the conversation’s task and its objective (task fix the CI build; objective: swift test passes), as its own fact, never over yours. memory "task" still recalls the task.
  • Notifications come from your terminal in Ghostty, iTerm2, WezTerm, and kitty: the notify tool, wisp notify, and wisp watch write the terminal’s own notification sequence, so the banner carries the terminal’s name and icon and clicking it returns to it, instead of Script Editor. wisp-tui does the same in those terminals. A terminal may hold its banner back while its window has focus (Ghostty does). iTerm2 shows the sequence only with “Send escape sequence-generated alerts” on, so there wisp posts through the terminal app first. Elsewhere (Terminal.app, tmux, wisp mcp), display notification is sent to your terminal app by its bundle identifier, so the banner is still the terminal’s; macOS asks you to allow that once per app, and notifications.viaTerminalApp (on by default) turns it off. The notification audit event records the route taken (host, terminal, app, osascript) and why earlier ones were skipped, wisp doctor names the route it would take, and wisp notify --route host|terminal|app|osascript uses one route alone, to probe it.
  • wisp chat --json accepts a first hello line declaring what the front end does (approve, notify); with notify, wisp sends notify lines for the front end to post. A front end that sends none works as before.
  • wisp doctor checks the facts store (parses, mode 600, counts), the subject kinds, that every saved transcript can be resumed, the numeric settings’ ranges, and the configured model’s context window and how it is known.
  • Facts: wisp keeps short facts about the conversation and gives them to the model on every request, so a codename, the task, or whether the tests pass outlives the turns that said it. Facts come from tool output without a model (a test command’s exit status, a file read or written, the git branch, the working directory, a listening port), and, when older turns are dropped to fit the window, from one call to the model that distils what was said in them. Each is labelled with where it came from and is given as a record, never as instructions. Facts about the machine are shared by an MCP server’s threads for the life of the process; names, decisions, and preferences are kept across sessions in ~/.wisp/facts.json, but only once you state or approve them. The new facts setting turns them off, stops the model call, sets their share of the window, and adds or changes subject kinds.
  • Chat’s /inspect facts [all] lists the facts the model is given, with their sources and history; /fact SUBJECT [NAME] = VALUE states one as you, which outranks a tool and the model; /fact delete ID and /fact ID permanent|thread|session delete one or move it to another scope; /task [text] shows or sets the conversation’s task. wisp-tui shows the facts in its panel.
  • MCP respond takes an optional task, kept as the thread’s task and shown to the model next to each request, and the thread summary’s task is filled in. Facts resources, each under what owns it: wisp://threads/{thread_id}/facts lists the thread’s own facts (its task, the state of the work, its proposals) and …/facts/{fact_id} one fact’s history; the permanent facts are at wisp://facts (one, with its history, at wisp://facts/{fact_id}), the proposed ones at wisp://facts/proposed, and the session’s at wisp://session/facts. wisp://threads/{thread_id}/context/next shows every fact as the model is given it.
  • New audit events: context.distillation, fact.recorded, fact.superseded, fact.deleted, fact.scope.changed, fact.conflict.raised, and fact.conflict.resolved.
  • Facts are listed after each turn, and their scope is yours to change by command. Chat prints a quiet note under the reply when a turn recorded or changed facts (2 new facts: c7 release codename = BLUE HERON (model), … — /fact <id> permanent|thread|session; at most three named, then a count); wisp chat --json sends the same as a note and adds facts to the turn’s end, which wisp-tui shows as a note; and MCP respond returns them as structuredContent.facts, each with id, scope, subject, name, value, source, proposed, and uri. /fact ID permanent|thread|session moves a fact to the scope you name (permanent writes it to ~/.wisp/facts.json as yours, ranking with you; out of permanent takes it out). Over MCP the new set_fact_scope tool moves a thread’s fact or a session fact between thread and session; permanent is set from chat. wisp never asks over MCP whether to keep a proposed permanent fact. Audited as fact.scope.changed.
  • Chat’s /inspect facts also lists permanent facts proposed in the process’s other conversations (the one before a /new, say), and /fact CONVERSATION/ID permanent keeps one.
  • A running summary of the turns dropped to fit the window: when condensing has dropped three or more turns not yet summarised, the conversation’s model adds them to a short summary of what was asked, what was done (the tools it called, and on which files), and what was decided, oldest first, which the model is given after the facts, labelled as a record, never as instructions. So it can still say which file it read first once that turn is gone. Each version is saved with the conversation and restored on --resume. Chat’s /inspect facts shows it under “Summary of earlier turns” (all adds earlier versions), and MCP wisp://threads/{thread_id}/facts returns it as summary (summaries with ?all=true). The facts setting gains summary (off stops it) and summaryShare (its share of the window, 0.05 by default, on top of the facts’ share). Each call is audited as context.summary.
  • A memory tool for the model, the conversation’s memory (not the Mac’s RAM, which system_info reports). recall restores, for one turn, what the context holds only as a reference, a marker, a summary, or a fact: a stored tool output, reply, or prompt (recall entry 7), a whole turn (recall turn 3), the task and the prompt the conversation began with (recall task), the summary’s versions (recall summary), or a fact’s history with where each version came from (recall fact tests). Content is read from the audit log, and from the conversation’s store where the audit does not hold it; results are paged at 4 KiB and become a reference after their turn like any output. note SUBJECT NAME = VALUE records a fact as the model’s, below the person’s and a tool’s; a note of a permanent kind is a proposal until you keep it, and at most 12 are kept a turn. It is one of all the tools, so a conversation given a named list (--tool, MCP tools) has it only when the list names it; tools.disabled: ["memory"] leaves it out. References in a conversation with it say to see it: memory "recall entry 7" where they said call it again to see it, and the system prompt gains one line saying so. Each call is audited as context.memory, and each note kept as fact.recorded with method noted.
  • Each fact reaches the model as one line with its source after the value, - entity release codename: BLUE HERON — from the person, rather than in brackets, which the models copied into their replies. A fact a tool gave names the output it came from (from tool read_file, turn 2, entry 4), so the model can recall it once the turn is gone.
  • Chat prints each tool’s output under the call’s line, in the quiet tone, up to 20 lines (the new shownOutputLines setting; 0 for none), with a fold line naming /show <id> when there is more. /show prints an output whole, by that id or by its entry number.
  • wisp-tui shows each tool’s output folded in the scrollback; Ctrl-O opens the last one whole in a scrollable panel.
  • The model’s context is viewable at any time, at no cost to the model: /inspect context next in chat shows what the next request carries, /inspect context N what was composed at the start of turn N, and /inspect context turns what changed at each turn (a bare /inspect context still saves the files); wisp-tui shows the same in a panel (Ctrl-T, with Left and Right stepping through turns); and MCP callers read wisp://threads/{thread_id}/context, …/context/{turn}, and …/context/next.
  • MCP resources wisp://threads (the server’s threads, open or closed), wisp://threads/{thread_id} (one thread’s model, tools, turns, and links), and wisp://threads/{thread_id}/output (its tool calls with each output’s URI). Collections are paged with ?page=N.
  • In a long conversation, each tool’s output is sent to the model whole only in the turn that produced it; later requests carry a short reference instead (the tool, when it ran, whether it succeeded, its size, its first and last lines, and how to see it whole), so far more turns fit before older ones are dropped. Each switch is audited as context.reference.
  • wisp chat --json adds the output, its size, and the fold size to each tool.result event (output), and a view line that answers /inspect context next, N, or turns.
  • MCP respond results list the turn’s tool calls in structuredContent.calls: each call’s tool, arguments, command and exit status, and output size, with the output itself inline when it is at most 1 KiB, and otherwise a wisp://threads/{thread_id}/output/{id} reference. Read that resource template for the output verbatim, from the audit log. The output is what the tool returned, not the model’s account of it. The new inlineOutputBytes setting changes the threshold.
  • In a long conversation, a reply that retyped a tool’s output exactly, formatting aside (a file shown in full, a table of a command’s results), is sent to the model on later turns as a short note such as “(showed the person the read_file output, entry 7)”, saving the window for what matters. What you see is unchanged. Each cut is audited as context.cut; /inspect context shows the note. A copy with changes, such as a proposed edit shown as a changed file, is kept for the model.
  • /save writes a second file beside a saved transcript, transcripts/<name>.store, holding the links from each conversation entry (dropped ones too) to the audit events that recorded it. --resume reads it, so a resumed conversation stays connected to the log, and session.start gains carriedFrom, the sessions it came from.
  • /inspect context in chat saves the exact context the model will see next: instructions, prompts, tool calls, tool output, and replies, as Markdown and JSON in ~/.wisp/context/. Every condensation also saves the context before and after it, so you can read exactly which turns were dropped.
  • Every audit event has an id, so a record of the conversation can point to the exact event that holds a prompt, a reply, or a tool’s output (docs/logging.md).

Fixed:

  • wisp help mcp named only some of the MCP tools and resources. It now lists every tool the server declares (taken from the same list, so it cannot fall behind) and the thread, fact, and session-fact resources.
  • wisp config show, the wisp://config resource, and the inspect tool’s config view left out the facts, assessment, and context settings. They now show them with defaults applied, and a test fails when a section of config.json or a setting wisp config list names is not shown.
  • /inspect context and /tokens in chat now say “1 turn” and “1 time” for a count of one, not “1 turns”.
  • Long conversations on the on-device model are condensed again instead of failing with “Provided N tokens, but the maximum allowed is 8,192”. That model reports no token usage and reports an overflow differently from the framework’s documented error, so neither safeguard fired.
  • The respond tool told MCP callers the on-device model’s window is about 4k tokens; it is 8,192 on macOS 27, and the docs now say so throughout.
  • system_info’s folder sizes treat a blank path as the home folder, as an absent one already was; granite4.1:8b sent "" and was refused. When du cannot read some folders, the report now says the sizes may be low instead of presenting a partial total as the whole.
  • The tokens shown under a reply in chat (and in wisp-tui’s status line) no longer read low, or blank, on a turn in which wisp rebuilt the model’s session (to send an earlier tool output as a reference, to condense, or to retry after an overflow). The count now carries over from the session it replaced.

0.14.1

Changed:

  • The status line is shorter and says more. On the left, system:15% used · ~/src/wisp:main+12-3: the model with its context use, then the directory, branch, and lines added (green) and removed (red) since the last commit, instead of changes. On the right, the approval mode, and in wisp-tui the last turn and its tokens: last:3.1s · ↓4,009 ↑79, tokens read in pale yellow and written in pale blue. The footer under each reply uses the same arrows.

Fixed:

  • A tool call from an Ollama model that leaves out a required text argument no longer ends the turn with a ToolCallError. granite4.1:8b did this with system_info for “what are the busiest processes”. The argument is now filled in empty, as the tool’s description asks.

0.14.0

Changed:

  • wisp scan, wisp redact, scan_secrets, and redact find more credentials by rule, without the model:

    • Authorization headers and session cookies;
    • unquoted assignments to credential names (db_pass: …, X-Api-Key: …);
    • passwords given to mysql -p, curl -u, docker login -p, sshpass, and --password flags;
    • password hashes, PGP key blocks, and signed URLs;
    • more provider tokens (GitLab, PyPI, Docker, npm, Vault, Google OAuth, and others).

    A password in a URL is now reported as a secret, not as an email address. Email addresses on any domain reserved for examples (example.com, example.net, example.org, their subdomains, and .test, .example, .invalid) are no longer reported as personal data; before, only example.com and example.org were skipped. On the secrets test set the rules find 32% of secrets, up from 13%, with no more false alarms.

  • The thorough pass of wisp scan, wisp redact, and the MCP tools scan_secrets and redact runs on the on-device model unless you name another, whatever config.json’s model is. On the secrets test set it did better there than ollama:granite4.1:8b (macro-F1 0.67 against 0.56). wisp config set routing.tasks.secrets <model> sets a different default, and each routed pass is audited as model.routed.

Added:

  • wisp scan --personal and scan_secrets with personal find far more personal data, without the model. A small classifier shipped in the binary flags lines holding names, account holders, and addresses that the rules cannot recognise, shown as personal-data (classifier). On the secrets test set it finds 62% of personal lines, against 12% for the rules alone, at about 2 ms a line. With --thorough too, 80% are found. redact does not use it yet.
  • Watch what MCP clients have wisp do. In chat, /audit sessions lists the sessions in the audit log and how each began, and /audit <id> shows one session’s latest events, such as a respond thread; Tab completes the ids. In a terminal, wisp logs --follow (-f) prints events as they are written, and can be narrowed with --session, --kind, and --tool.
  • Chat shows what the approval gate decided for each command: its rating and why (· safe by rules: a known read-only command (0.2 ms)), and whether a standing approval let it through, you approved it, or it was denied or blocked by policy. Each reply ends with how long the turn took and the tokens it used (3.1 s · 4,009 tokens in, 79 out); wisp-tui shows the same in its status line.
  • While a turn runs, chat shows what it is doing and for how long, redrawn each second: … 12 s · running git status (8 s), or waiting for the model. wisp-tui shows the same in its status line.
  • MCP callers that ask for progress (a progressToken) are told what a respond call, or a tool that runs a command, is doing while it runs: each tool call, the gate’s rating, a wait for approval, and each outcome.
  • For front ends over wisp chat --json: an activity line says what the turn is doing, and the turn line’s end carries inputTokens and outputTokens.

Fixed:

  • Chat’s help said /audit shows this session’s events; it shows the latest of every session.
  • A thorough scan_secrets, redact, wisp scan, or wisp redact no longer fails as a whole when the model refuses or fails on one chunk, which the on-device model’s guardrails sometimes do on text full of credentials. The turn is asked once more; if it fails again, that chunk is checked by rule only, its number is reported in failedChunks, and the rest of the text and every rule finding are kept.

0.13.2

Changed:

  • The risk rules rate more dangerous commands dangerous:

    • credential reads: kubectl config view --raw, kubectl … get secret -o yaml with flags before get, az keyvault secret show, gcloud secrets versions access, vault kv get, gpg --export-secret-keys, security find-generic-password -g, and a credential echoed inside sh -c '…';
    • publishing to a registry (npm publish, cargo publish, mvn deploy, docker push, and others), except as a dry run;
    • deleting remote storage (aws s3 rb --force, aws s3 rm --recursive);
    • diskutil apfs deleteVolume;
    • git reflog expire, git gc --prune=now, and git checkout <ref> -- <paths>.

    Measured by cross-validation over 403 dangerous commands, the gate rates 1.5% of them safe, down from 4.2%, with no more prompts on safe commands.

Fixed:

  • git restore --staged, which only unstages, no longer counts as discarding local changes.

0.13.1

Changed:

  • The default risk classifier is the fast Core ML classifier this release ships (approval.classifier: coreml), not the on-device model: on 996 real commands it rates more commands exactly (817 against 664), prompts on half as many safe ones, and answers in under a millisecond however busy the Mac is. A configuration that sets approval.classifier keeps its choice, and approval.useModel: true still means the on-device model. wisp doctor checks the classifier and names the shipped version.

Fixed:

  • 0.13.0’s published binary already had this default, but its source, notes, and docs did not; this release is the build that matches them. scripts/release now refuses to publish when the working tree or HEAD changes while it runs.

0.13.0

Added:

  • Risk classifier versions: each release ships a default, risk@X.Y.Z-default, trained once, measured, and never changed, used by approval.classifier: coreml when no model is named, so a fast classifier needs no training. wisp classifier train adds a new version, risk@X.Y.Z-local.<n>, and never overwrites one; wisp classifier list, use, and remove manage them, and measure records each result in the version’s manifest. Versions live in ~/.wisp/classifiers/risk.
  • wisp classifier train leaves out every example that overlaps ~/.wisp/classifiers/risk/held-out.tsv or a file given with --exclude, so a test set of commands run on this Mac is never trained on.
  • /config in chat shows the configuration as YAML; wisp config stays JSON.
  • wisp-tui shows the lines you send in the scrollback like the input box, a shade darker, with its half-block strips above and below.
  • Chat’s commands follow the CLI’s nouns: /config get KEY (and wisp config get KEY) shows one setting and whether it is set or the default; /status, /approvals, and /audit replace /inspect status|approvals|audit, /approvals revoke [ID] removes a standing approval at once, and /inspect stays as an alias. Tab completes the new words and approval ids.

Changed:

  • The shipped risk classifier is trained from 2,061 examples, 1,075 of them real commands from development sessions, instead of 292 drafted ones. On a frozen set of 996 real commands, beside the rules, it rates 817 exactly, where the previous default rated 376.
  • The risk rules rate printing a credential dangerous: printenv or echo of a variable named for a password, token, secret, or key, env | grep for them, gh auth token, op read, aws configure get of a secret, and kubectl get secret -o yaml|json|jsonpath. Checking whether one is set (${TOKEN:+yes}) or its length is not.
  • The risk rules know a short list of read-only commands (ls, cat, grep, git status, git log, git diff, --version, and the like). For those the verdict is safe without asking a model classifier, so they cost no model call and never prompt. A command that writes, runs something else, or names a sensitive file is never on the list.

Fixed:

  • Redirecting to /dev/null no longer counts as writing a file in the risk rules.
  • The risk rules follow the training labels: building and testing the project (swift test, cargo build, make) no longer asks by itself, while xcodebuild, clean targets, and installs still do, and python3 -m http.server on every interface is dangerous, not moderate.
  • The risk rules no longer take git merge-base for git merge, git tag -l for making a tag, or a program’s open(…) or a word at inside a command for the open and at commands, so those read-only commands no longer ask for approval.
  • Training a risk classifier uses every example and gives the same classifier from the same examples: Create ML held back a random slice of them for its own validation, so two trainings disagreed on about 4% of commands.

0.12.0

Added:

  • /config set KEY VALUE and /config unset KEY in chat, and wisp config set and unset, change ~/.wisp/config.json without editing it: each value is checked against the setting, the file must still load, the rest of it is kept, and every change is audited. /config list and wisp config list show the settings that can be changed and their values. A change that weakens the approval gate or the audit is flagged.
  • /config set offers what you leave out: the settings, then the setting’s choices, including the models this Mac can run and the trained Core ML models on disk. wisp-tui shows a picker in place of the input (arrows, Enter, Esc, or type a value); the plain chat shows a numbered list. wisp chat --json gains choice and choose for it.
  • Tab completes slash commands in wisp-tui: the command, /config’s words, settings and their values, models after /model, and views after /inspect; several matches show above the input and Tab cycles through them. wisp chat --json gains complete and completions.

Fixed:

  • The chat status line shows the branch in a git worktree or a submodule, where .git is a file that points at the repository’s own git directory; it showed none.

0.11.0

Added:

  • wisp chat --json sends a turn line when a message goes to the model and when its reply is done, with the turn’s number, time, and outcome, and every event line carries text, the line the terminal chat shows for it. wisp-tui shows the running turn and the last one’s time in its status line, and words tool activity exactly as wisp chat does.
  • wisp-tui asks for approval in a bordered dialog in place of the input: the command, the line it is part of, the directory, every reason, the pattern, and the keys, with the answer kept in the scrollback as one line.
  • wisp-tui renders the Markdown in replies: headings, bullets, inline code, bold, and italics, with fenced code blocks kept as they are in the code colour.
  • wisp classifier train trains a risk classifier on this Mac in well under a second, from the bundled examples or your own, and wisp classifier measure reports any classifier’s accuracy and speed on labelled commands. A trained classifier answers in about 0.05 ms, against 1.3 to 2.3 s for the on-device model; use it with approval.classifier: coreml. The Core ML classifier now loads its model once instead of on every command, and reads contract 2 models.
  • wisp classifier train --from-audit also learns from this Mac’s audit log: the on-device model’s verdicts on the commands you have run, secrets redacted, a refused command at least moderate.
  • triage reads failures in the formats compilers and test runners print (Swift, clang, XCTest, rustc, swift-testing, cargo test, pytest, go test) exactly, and asks the model only about output those do not explain, so build and test output in a known format is triaged at once.
  • MCP tools dependency_audit (npm, cargo, or pip audit JSON reduced to one line per advisory with its fix, most severe first), flaky_tests (two or more test runs, or one command run several times, compared for tests that pass and fail), and hot_paths (folded stacks reduced to self time and the heaviest paths). None uses a model.

Removed:

  • scripts/train-risk-classifier and docs/examples/risk-labels.csv, which trained on the eval set; wisp classifier train replaces them.

Fixed:

  • A message that asks for nothing, such as test or hello, gets a short question back. The on-device model used to take it as output to quote, sometimes running a command to produce it (“Test output: "test".”), and kept the pattern for the rest of the conversation.
  • The rules rate deletion through find … -delete, find … -exec rm, and xargs rm as dangerous; they called it safe.
  • The rules rate as dangerous reading common token files (~/.config/gh/hosts.yml, .git-credentials, .npmrc, .pypirc, the Docker and kube configs), copying or printing every file a search of the home folder or the disk matches (find ~ … -exec cat), and printing stored passwords with security … -w. The default classifier let the first through: the on-device model rated reading gh’s token safe.
  • wisp-tui no longer redraws its band while idle, which could make the cursor flicker or restart its blink, and no longer resizes the band back and forth when deleting across a wrapped line.

0.10.3

Added:

  • wisp-tui’s input grows with the message: a row for each line or wrapped line, up to six, scrolling within them beyond that, and shrinking back when the message is sent. Long lines now wrap instead of scrolling sideways.

Fixed:

  • wisp-tui flickered when the input grew or shrank and as replies streamed in: each frame is now one synchronized update, and lines are added above the band by scrolling a region instead of redrawing it.

0.10.2

Added:

  • wisp-tui line editing: a cursor moved with Left and Right, word motions (Alt or Ctrl with an arrow, Alt-B and Alt-F), Home and End, Ctrl-A, E, U, K, and W, Delete, Alt-Backspace, bracketed paste, and Alt-Enter for a newline. A long line scrolls to keep the cursor in sight.

0.10.1

Fixed:

  • wisp config and the MCP resource wisp://config left out the notifications, tools, and routing settings; they now show every setting.

0.10.0

Added:

  • Routing by input size: with routing.ladder in the config (such as ["system", "ollama:qwen3.8:27b"]), draft_change and wisp draft pick the first model the measurements trust with a diff that large, and say which and why. Off unless configured; an explicit model wins.
  • Custom tools: declare your own tools for the model in ~/.wisp/config.json (tools.custom), each a command with {placeholders} and typed arguments, run through run_command’s policy, approval, and sandbox; tools.disabled leaves built-in tools out. See docs/tools/custom.md.
  • wisp draft [commit|pr|changelog] and the MCP tool draft_change: a commit message, a pull request description, or a changelog line drafted from a diff (the staged one by default), with the subject kept under 72 characters and a Why: line left for the reason.
  • condense_log reads this Mac’s unified log with last (such as 10m), optionally for one process or subsystem, in wisp’s own process: /usr/bin/log refuses to run in any sandbox.

Changed:

  • A session no longer re-judges a command line it has already classified: the model classifier’s verdicts are kept per line and working directory (about 1.4 s saved each time). Approval is decided as before; a failed classification is retried.

Fixed:

  • system_info asked about a named app (“Is Ollama running?”) often left the name out and fell back to ps, which cannot run in the sandbox; the name is now a required argument.
  • The condensing tools ignored a command’s exit status, so a failing command’s error message was condensed as though it were the output. They now return exitStatus and timedOut and start their text with a warning when the command failed.

0.9.0

Added:

  • wisp scan and the MCP tool scan_secrets: find credentials, and with --personal personal data, in files, standard input, or a command’s output, reported with masked previews. A diff is scanned by its added lines as path:line, so git diff --cached | wisp scan works as a pre-commit check; exits 1 on a finding.

  • wisp redact and the MCP tool redact: text with credentials and personal data replaced by numbered markers such as [REDACTED:email#1]. --thorough adds the on-device model for names, addresses, and identifiers, over text the rules have already redacted.

  • The MCP tool condense_log: a log (an app’s log, CI output, log show) as its distinct messages, grouped by template and ranked by severity and count, or a macOS crash report as the process, exception, and faulting thread’s frames. No model; a megabyte takes about a second.

  • The MCP tool json_shape: the structure of a JSON or JSON Lines file without its data, with redacted string examples. No model.

  • wisp watch <command>: reruns a command when files change (build output and .git ignored) or on an interval, triages a failing run with the model, and posts a notification when it starts or stops failing (--notify change|failure|always|never). The command is classified and approved once, when the watch starts, not on every run.

  • The model tool system_info: listening ports, free space, folder sizes, your busiest processes, memory, one process, battery, macOS and hardware, and network, from fixed read-only probes wisp parses, so wisp "what is using port 8080?" gets a short, right answer.

Fixed:

  • summarise_diff’s secret flag quoted the start of the added line, handing the credential to the caller; it now gives the kind and a masked preview.

0.8.3

Added:

  • /stats in chat: timings of the session’s recent model turns and classifier calls (count, failures, mean, P50, P95, maximum, prompt tokens where reported), then the latest calls. Kept in memory only, in a ring of the latest 256 calls.
  • /history in chat lists the lines typed this session, and in wisp-tui Up and Down recall them, with what was being typed kept as a draft.

Changed:

  • /models in chat is a table with a header, so the columns line up in the terminal and in wisp-tui; wisp models keeps its tab-separated lines for scripts.
  • A model classifier’s fallback verdict records classifier.failure in its classifier.verdict metadata.

0.8.2

Changed:

  • --model help mentions <backend>:<name> spellings and points to wisp models, not only system and private-cloud.
  • docs/backends.md records four Ollama models tried with wisp on 2026-09-23 (qwen3-coder, granite4.1:8b, ornith:9b, qwen3.8:27b): all drove the tools correctly, with timings.

0.8.1

Changed:

  • wisp models and /models list only the models that can serve the conversation: they must resolve and, when the conversation has tools, declare tool calling. --all adds the excluded ones with the reason; --no-tools judges for a conversation without tools. An Ollama model that cannot hold a conversation, such as an embedding model, is refused by --model and /model with that reason instead of failing at the first prompt.

0.8.0

Added:

  • Notifications: the model’s new notify tool and wisp notify <message> show a macOS notification. Text is bounded, at most five a minute (notifications.perMinute), notifications.enabled: false turns them off, and every request is audited as notification.

0.7.0

Added:

  • /models and /model <name> in chat: list the models this Mac can run and switch the conversation to one, keeping the transcript. Works in the plain chat and in wisp-tui.

Changed:

  • The model introduces itself as Wisp.

0.6.1

Fixed:

  • wisp chat launched through PATH did not find wisp-tui: it looked beside argv[0], a bare name, rather than beside the process’s real executable, so the plain chat started instead of the front end.

0.6.0

Added:

  • wisp chat --json, a headless chat speaking JSON Lines, and wisp-tui, a Rust front end over it that keeps the conversation in the terminal’s scrollback above a pinned input and status band. A merged from a one-day spike (ADR 0029); the protocol may still change. The formula installs both binaries, and wisp chat on a terminal hands the session to wisp-tui; --plain keeps the line-based chat.

0.5.0

Fixed:

  • An unanswered MCP approval now comes back at approval.timeoutSeconds as promised. The wait was decided at the deadline but not returned until the client eventually answered the dialog, which on 2026-09-21 took 11 to 56 minutes for three refusals.

Changed:

  • wisp chat shows its work: a status line above every prompt (model, directory, git branch and state, approval mode, context used), the model’s tool calls and results live as one dim line each, a compact approval dialog with a one-line key, colour on a terminal (off when piped or with NO_COLOR), and new commands /inspect, /status, /last, plus --yes. Replies alone go to stdout, as before.

0.4.0

Renamed: daimon is now wisp. The binary is wisp, the home directory ~/.wisp (WISP_HOME), the environment variables WISP_*, the unified-logging subsystem com.pidster.wisp, the MCP server wisp with wisp:// resources, and a new formula in the Homebrew tap (brew install pidster/tap/wisp). Nothing carries over automatically: move ~/.daimon to ~/.wisp yourself if you want your approvals and transcripts, and brew uninstall daimon. The daimon formula stays in the tap.

Added:

  • summarise_diff, a new MCP tool: run a command that prints a diff (or read a diff file) and get back a headline, one line per file with its change kind and line counts, and flags for secrets, deleted or disabled tests, and binary or generated content. Paths and counts come from the diff itself; the diff never leaves the Mac.
  • Measurements: scripts/check eval records what each delegated task achieved (triage, edit_file replace after read, schema-shaped replies, the risk classifier) into a resource embedded in the binary; wisp tools --markdown, wisp://tools, and the new wisp://measurements resource publish them so a caller knows what to trust.

Changed:

  • Approvals for programs whose first word is the verb (git, cargo, swift, npm, brew, docker and others listed in multiplexers.txt) are remembered by verb: git commit * and git push * are separate, so a session answer for one no longer covers the other. A standing approval stored under the old git * still counts until it expires.
  • edit_file replace takes line, the number read_file showed, with content as the whole new line and find as an optional check on that line; a drifted number changes nothing. The by-find form measured 3 of 5, because the model retyped the neighbouring line into content.
  • Local runtimes that truncate silently no longer lose the instructions: Agent condenses the transcript ahead of the window when the usage the last reply reported, plus the new prompt, would pass 85% of it, audited as context.condensation with reason budget. Ollama is asked for an explicit window on every request (ollama.contextLength, default 8192, sent as num_ctx), and /tokens shows the reported usage for models that cannot count.

0.3.0

Added:

  • edit_file, a new model tool: write a whole text file, append to it, or replace one exact occurrence of a piece of text. Writes are atomic and confined to the directories the sandbox lets commands write under, every edit passes the risk classifier and approval as edit_file <mode> <path>, and each edit is audited as file.write and listed in the receipt’s files.
  • triage, a new MCP tool: run a build or test command on this Mac (or read an output file) and get back only the failures as kind, location, message, judged chunk by chunk by the on-device model. The raw output never leaves the Mac; the command runs under the same policy, sandbox, and approval as the model’s own run_command.
  • Structured output: respond takes a schema (a JSON Schema object in an accepted subset) and returns JSON of that shape, parsed into structuredContent.output; the CLI takes --schema <path>. A model that does not declare guided generation is refused before generation.
  • respond results carry a receipt: the turn’s tool calls with arguments and result sizes, commands with exit status, policy denials, approval decisions, and errors, folded from the thread’s audit events so a calling harness can verify delegated work without reading the log.

Fixed:

  • --model private-cloud failed after the request with an opaque ModelManagerError 1046. Private Cloud Compute needs the managed com.apple.developer.private-cloud-compute entitlement, which an ad-hoc signed command-line tool cannot carry; wisp now checks its own signature and refuses the model with a sentence before anything is sent. wisp models shows the same reason.

0.2.0

Added:

  • Model backends are a registry: --model <backend>:<name> names a local runtime by scheme, wisp models lists every backend’s models with their declared capabilities, and a backend this build lacks is a clear error. Ollama now reports each model’s real capabilities from its /api/show, so an embedding model is refused for tool use before generation rather than failing during it.

  • --no-tools on the CLI and tools: [] over MCP open a text-only conversation, which any model can run; a request that needs tool calling on a model that does not declare it is refused with a hint.

  • The audit log records model.resolved when a conversation opens: backend, model, asset, declared capabilities and who declared them, and the tools in use.

  • Core AI: --model coreai:<bundle> runs a model exported to Apple’s Core AI format, in wisp’s own process, through the bridge from apple/coreai-models. Bundles live under <home>/models/coreai (config.json coreai.modelsDirectory) or are named by path; wisp models lists them with kind, compression, source, and size; a missing bundle is refused with where wisp looked and the export command. Capabilities come from the bundle. See docs/backends.md.

  • inspect, a read-only tool the model can call to see wisp’s effective config, this conversation’s status, the standing approvals, or recent audit events, bounded to 4 KiB.

  • MCP resources wisp://config, wisp://status, wisp://approvals, wisp://audit, and the template wisp://audit/{session} for one thread’s events, so a calling harness can read wisp’s state without a model turn.

  • wisp config prints the effective configuration as JSON.

  • MLX Swift: --model mlx:<directory> runs a model in MLX or Hugging Face safetensors layout in wisp’s process through mlx-swift-lm’s bridge, in builds made with --traits MLX (the release is); a build without the trait refuses mlx: models with the reason. Capabilities are declared by the operator per model in config.json’s mlx.models; an undeclared model is text only. Verified with mlx-community/Qwen3-1.7B-4bit: text replies in 2.5 s including load, and the tool loop 3 of 3 with toolCalling declared. The Homebrew release does not include MLX, because it needs a Metal library bundle beside the binary; build with --traits MLX yourself. See docs/backends.md.

  • approval.classifier chooses what judges commands beside the rules: rules, system-model (the default, unchanged), or coreml, a Core ML text classifier you train from a text,label CSV with scripts/train-risk-classifier. The model must follow a versioned contract or it is rejected; every failure or low-confidence verdict is moderate with the reason; the audit records the model’s identity, version, label, and confidence. Measured on 2026-09-20: a model trained on the 45-command eval set scores 45/45 on it (its own training data, so no evidence of judgement); trained on the 35 non-held-out commands it got 5 of the 10 held-out ones right, and two dangerous commands it called safe were kept off safe only by the confidence guard. Not fit to judge alone; measure your own with WISP_COREML_MODEL=<path> scripts/check eval before relying on it.

Changed:

  • The run_command sandbox also allows writes under the per-user cache directory (getconf DARWIN_USER_CACHE_DIR), where Clang keeps its module cache; a build that compiles a C module inside the sandbox (SwiftPM compiling a dependency’s manifest, say) used to fail with “could not build Objective-C module ‘Darwin’”.
  • wisp with no prompt on a terminal prints its help instead of waiting silently for stdin; a piped stdin is still read.

0.1.5

Fixed:

  • Codex could not connect: its initialize carries capabilities.experimental with object values, which the MCP specification allows, and the Swift SDK rejected the whole request with -32603. wisp now normalises such messages before the SDK decodes them. wisp never reads the field; no client feature is enabled by it. The captured request is a regression test.

0.1.4

Added:

  • Three-layer instructions: wisp’s own system prompt (a text file in the source tree, embedded at build time), the operator’s systemPromptExtension in config.json, and the caller’s --instructions or MCP instructions, rendered in that order. A caller can no longer replace wisp’s framing. The old config key instructions is still read as the extension.
  • The audit session.start event records the extension and the caller’s instructions separately.
  • wisp mcp --tool selects the tools threads get unless a respond call names its own.

Changed:

  • Releases run the full test suite and a coverage gate; line coverage must not fall below the recorded baseline.
  • --resume of a missing or invalid transcript name is a usage error (exit 64), like other bad inputs.

0.1.3

Added:

  • Run sessions on a local Ollama model with --model ollama:<name>, in config.json, or per MCP thread. wisp models lists Apple’s two models with availability and every model Ollama serves. New ollama config section for the server address and per-request timeout; wisp doctor checks the configured model resolves.
  • Approvals have four scopes: this turn, this session, this project (30 days, this directory), always (30 days). Project and always approvals persist under ~/.wisp/approvals.json; wisp approvals lists, revokes, and clears them. Dangerous commands are never persisted.
  • Each simple command in a line (a && b | c) is checked and approved on its own and remembered by its program, so head -n 5 x is remembered as head *.
  • A once-approval covers the rest of the turn, however many commands the model runs.
  • The MCP respond result reports refusals: commands the gate refused this turn, with reasons.
  • A bare exit, quit, or q ends wisp chat.

Changed:

  • The approval threshold is safe, moderate, dangerous, or never; the on-device classifier runs only when the rules did not decide.
  • Every face of wisp (respond, chat, mcp) shares one set-up path and one approval store, so a “this project” answer given over MCP is written once and honoured everywhere.

Fixed:

  • “This project” approvals given over MCP were not being persisted.
  • Partial commandPolicy objects in config.json keep every other default.

0.1.2 and earlier

See the GitHub releases for 0.1.0 to 0.1.2: the first Homebrew releases, with the sandboxed run_command, read_file, current_date, the audit log, the risk classifier and approval gate, the MCP server with per-thread conversations, and model selection between system and private-cloud.