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_commandtakesoutsideSandbox; 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--yesdoes not answer it. Every dialog, notification, and listing saysOUTSIDE the sandbox, and the audit records it. Approved, the command runs with your privileges and no confinement,~/.wispincluded. Turn it off withwisp config set commandPolicy.sandbox.allowOutsideSandbox false;wisp doctor’ssettingsline 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;! !xruns!xin the sandbox). Typing it is the approval, as for!; the deny list andcommandPolicy.sandbox.allowOutsideSandboxstill apply, and chat says it runs OUTSIDE the sandbox before it starts. Inwisp-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 fromdocs/with search and an edit link on every page, inwisp-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 withOperation 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_commandtakestimeoutSeconds, so the model can give a long build or test the time it needs, and MCPrespondtakescommandTimeoutSecondswhen a thread starts, the limit of that thread’s commands (refused on a thread that exists, asmodelis). 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.outcomerecords 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). Agit commitwhose pre-commit hook ran the tests was killed at 60 s.
Fixed:
commandTimeoutSeconds: 0means 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 doctorasks the on-device model for a one-word reply, not only whether it is available, so Apple’s model service refusing every request fails themodelfinding. When the error is the service’s own, such as “Too many open files” from amodelmanagerdthat 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, themodelstool) 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 andwisp-tuithe question waits until you answer (inquiry.timeoutSeconds, default 0; a deadline is optional), and Ctrl-C withdraws it.wisp chat --jsonsends it as achoiceline of kindquestion, and a front end whosehellodeclaresanswer-questionsis sent the questions waiting inwisp mcpservers too.- Under
wisp mcpthe question is filed in~/.wisp/pendingand announced once;wisp questions pending|answer|declineanswers it from a terminal (answerprompts for the text), and so dowisp-tuiand plain chat’s/questions answer|decline.respondholds the client’s dialog 60 s, then parks the turn and returnsstructuredContent.questions; the answer reaches the thread’s next turn. The client’s question form is built behindinquiry.elicitation, off until it has been tried by hand. Audited asinquiry.requested,inquiry.parked,inquiry.answered, andinquiry.settled; an answer becomes the thread’sanswerfact as the person’s, and the verification pass flags “you chose” or “as you confirmed” when no question was answered. - Plain
wisp chatshows commands, facts, and questions waiting inwisp mcpservers: 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 pendinglists 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./modelwith no name, inwisp-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 afterapproval.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;0restores the old behaviour.approval.pendingrecordsbannerDelaySeconds, andapproval.settledrecordsbanner(postedorskipped). wisp-tui’s status bar showsquestions: Nandapprovals: Nin 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: valuelines 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 asverifiedonwisp chat --json’s turn line and inwisp-tui, returned asverificationbyrespond, and audited asresponse.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,readersby default,model):modeladds 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./showand/lastshow 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 -Rby 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 inboundoutputrequest, answered with aviewof kindoutput(one page of an output whole);command.typedrecordsstored, the output store’s id for its whole output.
Changed:
- A request to keep a fact as permanent (
set_fact_scopepermanent) no longer times out: it waits for you with no time limit, and nothing is kept until you answer withwisp facts keep|dropor inwisp-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 keeporwisp-tuikeeps it itself. The result no longer carriesexpiresAt. scripts/wisp-mcpstarts the server namedwisp mcp (claude-code)(fromWISP_MCP_CLIENT, set in.mcp.json), andwisp-tuistarts its child aswisp chat (tui), sopstells them apart; wisp finds its own executable by the kernel’s path, never byargv[0](ADR 0061).- The answers
wisp approvals approve|denyandwisp facts keep|dropstay terminal-only, after review (ADR 0046). - The model evals of
edit_file,system_info, anddraft_changeare larger: 110edit_filecases, 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 evalruns all of them (41 minutes on 2026-10-12). - The README overview shows the optional
memoryandmodelstools.
Fixed:
edit_fileedits CRLF, classic Mac (CR-only), and mixed-ending files line by line, numbered exactly asread_filenumbers 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 toedit_file, so an edit of its line 1 replaced the whole file.read_filenow 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 asoutput.keptby 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). memorypages 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), andcondenses it with a question the conversation’s model answers over all of it, chunk by chunk.inspectlists a turn’s tool calls one line each (what: calls) and filtersauditbyturn; a tool call repeated with the same arguments within a turn is answered with a line saying so./models pull REPOSITORYand/models trust PUBLISHERin chat andwisp-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/modelspicker has a “pull a model…” row, and/models cancel [REPOSITORY]stops a pull the model asked for.- A
modelstool for the model, off by default (tools.models, or a tool list naming it):listandshowfreely;pull,enable,disable, andcheckonly with the person’s approval. A pull is asked every time, never remembered, and not answered by--yes;trustandswitchare 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, andllamacpp.models.<name>.thinkset one model’s thinking ahead of the runtime’sthink;wisp modelsshows it asthinking (false). respondtakesthink(true, false, or an Ollama level) when a thread starts; it is refused on an existing thread and shown inwisp://threads, and thepromptaudit event records the thinking each turn sent.wisp approvals,/approvals, andwisp://approvalsmark project approvals whose folder is gone;wisp approvals clear --goneremoves 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.outputDaysdays 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 asoutputSweeponsession.start.wisp doctorreports 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 thoroughscan_secretsandredact), the fact distiller, the running summary, the assessment, andmemory’scondensesendthink: false; change drafts and conversation turns keep the model’s default. Measured on 2026-10-10:granite4.2:8bnot thinking matchedgranite4.1:8bat 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-tuiand theinterruptline, and the stop is audited asturn.stopped. - Loops and conditionals are judged and remembered by the commands inside them:
for f in *.txt; do git add "$f"; doneisgit add *, so a new loop does not ask again for an approved command, and a keyword such asdois 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 -execcommands 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_commandno longer discards all but the tail of each stream: its result is bounded as a whole, notes included, and the whole output is kept.commandMaxOutputBytesis unset by default and, when set, replaces the budget forrun_commandalone.read_fileandmemorypage at the budget.- Each MCP
respondthread gets an audit session of its own (<thread_id>-<suffix>, shown asauditSession), so a reusedthread_idno longer mixes an earlier thread’s history into its resources;wisp://audit,wisp://audit/{session}, andwisp://threads/{thread_id}/auditare paged (?page=N&limit=M). wisp models,/models,/model, and themodelstool share one set of model actions;wisp models’ flags and output are unchanged.model.pullgainssource,approval, and the outcomecancelled;model.publisher,model.verified, andconfig.changegainapproval.
Fixed:
- A spawned command inherits no descriptor of wisp’s or of another command’s (
POSIX_SPAWN_CLOEXEC_DEFAULT; pipes, the output store, andread_file’s handles are close-on-exec). read_filerefuses FIFOs, devices, and sockets, and reads a file with no newlines a page at a time;edit_file’swriteandappendno longer load the existing file.- The risk rules no longer read a search pattern, an awk program, a
pgreppattern, 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/*.pubis 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 deleteoredit --visibility, andwisp --yesare 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 evalshows 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 pullfetches MLX models from any Hugging Face organisation, not onlymlx-community. A publisher not in the newmlx.trustedPublisherssetting (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-publisherskips 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 asmodel.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)thecurlwas 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.ofor the session or the turn no longer coversrm -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 asdo *orsh *, which covered anything; the scripts given tosh -candevalare 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 bareenvorprintenv, which prints every secret, asks. - Commands and
edit_filecan 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 rewriteconfig.json. wisp approvals revokewhilewisp mcpruns is no longer undone by the server’s next approval; processes sharingapprovals.jsonmerge their changes under a lock.- A command’s background jobs, and children that ignore SIGTERM, no longer outlive it.
read_fileof 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 nestedwisp chat,respond, ormcp, andwisp approvals approve|denyorwisp facts keep|drop, behindnice,time,timeout,env -i,{,then, ordo, 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-tuino 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 fromwisp mcpthat 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-tuino longer hang: it sends/quitso 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
respondcalls on onethread_idno longer interleave and swap receipts and refusals: a thread’s turns run one at a time, andclose_threadwaits for the turn under way. - An approval or choice that times out is withdrawn from
wisp-tuiand from the MCP client’s dialog, instead of staying up with an answer that is ignored. wisp config set|unsetand/configrefuse to write whenconfig.jsonexists but cannot be read, instead of replacing it and losing every other setting;config.jsonitself refuses an unparsablebaseURLor an out-of-rangetimeoutSecondsorcontextLengthinstead of silently using defaults.- The condensing tools’
pathreads only regular files, and only up to their bound, so/dev/zero, a FIFO, or/dev/stdincan no longer hangwisp mcpor read its protocol channel;flaky_teststakes at most 10 paths. wisp completions installwrites through a symlink to wisp’s script instead of replacing the link. Failures at run time (wisp approvals approve|denytoo late,wisp facts, a refused install) exit 1, not the usage code 64. An unknownwisp chat --jsonline is ignored instead of clearing the front end’s busy state; completion indices count Unicode scalars on both sides;wisp-tuino longer panics on a hugeturnSeconds.
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.jsonis moved aside asfacts.json.unreadable-<time>, not overwritten, and wisp andwisp doctorsay 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:xorollama:x:latest, a llama.cpp model’s name or.ggufpath, an MLX name or its directory). wisp models pullchecks 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;/modelafter/newkeeps the conversation’s name and counters, and/newstarts 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: falsemeans 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 evalfails when the evaluation fails. It tookgrep’s status, which matches a failed run’s lines, so no eval, and no release’s eval floors, could fail.scripts/check eval recordkeeps every suite’s measurement when suites finish together, and refuses rather than overwrites ameasurements.jsonit 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-buildbuilds the MCP server without stripping the MLX pins fromharness/Package.resolved.
Changed:
-
A command you type after
!in chat orwisp-tuino longer hasrun_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 --jsongains an inboundinterruptline, and the audit recordsstopped: 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/factstill 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
mainis pushed, and prints the commands to finish a release that failed after tagging. The Homebrew formula drops its redundantversionline.
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>forllama-server(defaulthttp://127.0.0.1:8080) andlmstudio:<model>for LM Studio’s server (defaulthttp://127.0.0.1:1234), each with aconfig.jsonsection (baseURL,timeoutSeconds,apiKey, and for llama.cppthinkandmodels). Streaming, tool calls, schema replies, the model’s thinking, and usage work as for Ollama;wisp modelslists each server’s models; the window is the one the server holds the model at (FROMserver); a request the server refuses as too large is condensed and retried. A server’s API key comes fromWISP_LLAMACPP_API_KEYorWISP_LMSTUDIO_API_KEY, orapiKey, 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 withwisp models check llamacpp:<model>. Not yet tried against a realllama-serveror LM Studio; checked live against Ollama’s own OpenAI-compatible endpoint. - A context window for one model:
ollama.models.<name>.contextLengthandmlx.models.<name>.contextLengthhold that model to a size, ahead ofollama.contextLengthormlx.contextLength, while the rest are sized from memory (wisp config set ollama.models.qwen3.8:27b.contextLength 16384).wisp modelsshowsmodel configas where the window came from, andwisp doctorsaysconfigured for this model. wisp models pullresumes 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,usagewithinput,output,cached, andreasoning, for a runtime that reports it (Ollama, MLX, llama.cpp, LM Studio); theresponseaudit event records it too. Apple’s models report none, and their receipts have nousage.- Shell completions for zsh, bash, and fish. Homebrew installs them with wisp; otherwise
wisp completions installwrites 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|fishprints it. Model names, tools, settings, approval ids, and the ids of requests waiting in~/.wisp/pendingare completed from wisp’s own files.
Changed:
docs/model-controls.md, the draft proposal for reasoning and speed controls, says which partollama.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, andgemma4:12b, three runs each. The first takes a tool away from every conversation that was given all of them:- The model’s
memorytool 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, MCPrespondwithouttools) no longer has it, its instructions no longer mention it, and references to earlier output no longer saymemory "recall entry 7": for a tool that only reads (read_file,inspect,system_info,current_date) they saycall 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/showstill shows any output.wisp config set context.memory true(or/config set context.memory on) turns it on for every conversation;--tool memoryor an MCP caller’stoolsnaming it gives it to one. context.targetis 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.taskChangesisrestatedinstead ofany, which matters only withassessment.enabled(still off): an inferred task changes only when a request states one. It kept the task on every run whereanylost it on five of six.
To restore the earlier defaults:
wisp config set context.memory true,wisp config set context.target 0.5, andwisp config set assessment.taskChanges any. - The model’s
Added:
scripts/check evalandeval comparemeasuremlx:models named inWISP_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-4bitand other models whose chat template marks a thinking block (<think>…</think>) saythinkingwhile it lasts, show∴ thought for 2.0 s, 181 tokenswith the thinking folded under it, count its tokens, and audit it asmodel.reasoning. It used to arrive in the reply, tags and all, when the model thought. The model never gets its thinking back. mlx.think(trueorfalse) asks MLX models whose chat template takesenable_thinking, such as Qwen3, to think or not; unset, a model declaredreasoningthinks and any other is left to its template’s default (below).assessment.taskChanges(anyorrestated): with the assessment on (assessment.enabled, still off by default),restatedlets a request change the task the model inferred only when it states a task, such asToday's task: …orLet's switch to …, instead of on any request; other requests keep the task without a model call for it.restatedis the default (Breaking, above);anybehaves as before.scripts/check eval checkpointruns context checkpoint 2: the condensing target and headroom on a longer conversation, the half-window variants,memoryon 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 10forreturn 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 withfindedits the one line that holdsfind; none or several still change nothing. The result says when either applied and shows the edited line as it now reads, for exampleline 2 now: " return 10".
Fixed:
edit_filewithlineand an emptyfindrefused the edit (no line contains an empty string); an emptyfindnow checks nothing, as granite4.1:8b sends one for an argument it does not need.ollama:gemma4:12bandgemma4:26bare 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 inwisp modelsand themodel.resolvedevent says how the layers were counted, for example40 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:27bwas 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, andollama:ornith:9bgets 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 example16 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-bf16ran 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:14bwrites them and its template leaves them in the reply, so it made almost no calls (0 of 30edit_filecases 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
reasoningis not declared for them.mlx:Qwen3-1.7B-4bitcalledread_file, then replied with the file instead of editing it: 2 of 30edit_filecases against 20 of 30 for the same model on Ollama, which leaves thinking to the model. Unset,mlx.thinknow leaves the model’s chat template to decide, as an unsetollama.thinkleaves 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 anedit_filecase instead of 1.3 s. Setmlx.think: falsefor the old behaviour. - An MLX schema reply, such as a
draft_changesubject, 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, andwisp "prompt"(by any path, afterenvorVAR=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 modelspiped is a different shape: one tab-separated field per column of the new table, so the olddetail; capabilitiesfield is gone. Scripts should readwisp models --json(see Changed).
Added:
- A reasoning model’s thinking is shown (ADR 0053). Ollama streams it for models such as
qwen3.8:27b,gemma4andornith, and wisp used to drop it, so the turn looked idle. Now chat saysthinkingwhile it lasts (wisp-tuidraws a thought bubble in its busy box), shows∴ thought for 4.2 s, 42 tokenswith the thinking folded under it (/showfor the rest,/inspect thinkingfor every stretch), counts its tokens, and audits it asmodel.reasoning. Over MCP,wisp://threads/{thread_id}/reasoninglists 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 --jsoncarries it ascited, andrespondreturnsunknownEntriesandcited(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 linksmlx:<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, onlymlx-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 modelsnames cached models not yet linked. Audited asmodel.pull; the model is refused the command (ADR 0052).mlx.contextLengthandmlx.executorinconfig.json(andwisp config set).- Models can be turned off and on:
wisp models disable|enable <name>,/models disable|enable <name>in chat, or, inwisp-tui,/modelsas a picker (↑↓, Space to turn one on or off, Enter to save, Esc to leave). A disabled model is not offered by/modelor Tab and is refused by/model,--model,config.json’smodel, and an MCP caller’smodel, with how to enable it; the default model cannot be disabled. Stored asmodels.disabledinconfig.json, audited asconfig.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.jsondeclares 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, inconfig.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 listedtext only; one that does both is enabled and usable.wisp models check <name>(or/models checkin 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--jsonsays how they are known (capabilitiesFrom). Audited asmodel.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
systemand says so, with the/modelto return to it, instead of refusing to start; audited asmodel.fallback. A model named with--model,wisp respond, and MCPrespondstill fail, so nothing changes model silently (ADR 0056).
Changed:
wisp modelsand chat’s/modelsare 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 olddetail; capabilitiesfield is gone; use--jsonin 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, andcommand.outcomerecords it assandboxRefusal(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.jsonand the Mac’s memory (ormlx.contextLength) and shown bywisp 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, andwisp models.
Fixed:
- An Ollama reply cut short is no longer taken for a whole one. A stream that ended without the chunk that says
donewas kept as the complete reply; a connection lost mid-reply, or a server that went silent, failed withno Ollama server at …. Each now ends the turn withOllama at … stopped before the reply was doneor… sent nothing for N s (ollama.timeoutSeconds), nothing of the reply is kept, and the next turn goes on as usual. - The
inspecttool’sconfigview (and/inspect config) cut the settings that sort last,versionamong 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 chatfrom the Homebrew install opened the plain chat instead ofwisp-tui(since 0.17.0): the formula putswispinlibexec, beside the MLX library, andwisp-tuiinbin, andwisp chatlooked only beside itself. It now also looks in abinbeside its folder, andwisp doctorgains afront endfinding that says where it foundwisp-tui, or where it looked.
0.18.0
Breaking:
wisp://threads/{thread_id}/factsno longer carriessummary(orsummarieswith?all=true): the running summary moved towisp://threads/{thread_id}/summary, below./inspect factsno 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 (/showfor the rest), it is audited ascommand.typed, and the model is told on its next request, as your action, with a reference it can recall.wisp-tuihas 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 aviewof kindsummary, and a typed command’scommand.typedevent carries itsoutputas atool.resultdoes.- 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), orran: no toolswhen a reply names a tool none of which ran. In chat,wisp-tui(ranon the turn’s end inwisp chat --json), andrespond’sstructuredContent.ran(ADR 0051).
Changed:
- The running summary of earlier turns has a place of its own:
/inspect summary [all]in chat (andwisp-tui’s panel) andwisp://threads/{thread_id}/summaryover MCP./inspect factsandwisp://threads/{thread_id}/factsshow 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
/inspectin chat orwisp-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, besidewispin the Cellar’slibexec, so MLX models run without building wisp yourself (ADR 0047). The download grows by about 6 MB.wisp doctorhas anMLXfinding: the Metal library it found and that it loads, or what is missing. - Permanent facts over MCP (ADR 0048). A
wisp mcpcaller can ask for one of its thread’s facts to be kept as a permanent fact withset_fact_scopescope: permanent; the call returns at once with the request’s id, and a notification (“wisp: keep as a permanent fact?”, naming the fact andwisp facts keep <id>) tells you. Only you answer:wisp facts keep <id>orwisp facts drop <id>from a terminal, or in a runningwisp-tui. The outcome shows asrequeston the fact’s resource and inwisp://facts/proposed. A dropped fact stays in its thread, which does not ask about it again; an unanswered request keeps nothing. wisp facts pendinglists the facts waiting to be kept.respond’s result counts the permanent facts proposed in the server’s conversations that await you, asstructuredContent.factsProposed.wisp chat --json: a front end whosehellodeclares thekeep-factseffect is sent the facts waiting to be kept asapprovallines withkind: "fact", answeredkeepordrop;wisp-tuideclares it and shows them as a dialog of their own (kkeep,ddrop).- The default policy refuses
wisp facts keep|dropwhen the model runs it. wisp doctor’spending approvalsfinding also says how many facts wait to be kept.
Changed:
wisp --versionandwisp-tui --versionprint 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 frommainis not mistaken for the release whose number it carries. A release prints the bare version. The audit log’sversionstays bare.set_fact_scoperefuses 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 ataskexample, which cost 13 tokens in every conversation withmemory; thetaskverb works as before.
0.16.0
Added:
wisp watch --settle <seconds>, andwatch.settleinconfig.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--everyruns are not delayed.- Approval over MCP for every client (ADR 0046).
A command the model runs under
wisp mcpthat needs approval is also filed in~/.wisp/pendingand announced by a notification (“wisp: approval needed”, naming the command andwisp approvals approve <id>). Answer it from a terminal withwisp approvals approve <id> [--scope once|session|project|always]orwisp approvals deny <id>, or in a runningwisp-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 pendinglists the commands waiting for approval inwisp mcpservers.respond’s result lists the notifications its turn posted, asstructuredContent.notifications.approval.outOfBand(on by default):falseasks through the MCP client’s dialog alone, as before.wisp doctorchecks~/.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, andapproval.settled(logging.md). wisp chat --json: a front end whosehellodeclares theapprove-mcpeffect is sent the commands waiting inwisp mcpservers asapprovallines withsource: "mcp", and awithdrawnline when one no longer waits;wisp-tuideclares it.
Changed:
- A client without elicitation is no longer refused at once: the command waits for an answer from
wisp approvalsorwisp-tui, up toapproval.timeoutSeconds, then is refused as before. - The default
run_commandpolicy refuseswisp approvals approveanddeny: 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 ownpaging: more from offset Nline. read_filegiven a wildcard path that matches no file now says it takes one path and to list matches withrun_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.
--resumeneeds thetranscripts/<name>.storefile that/savenow writes beside a transcript; a transcript without one, or with one that cannot be read or does not match, is refused withtranscript '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
respondthreads moved underwisp://threads/{thread_id}. A thread’s audit events are nowwisp://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://statusno longer lists the threads: it givesthreadCountandthreadsURI, andwisp://threadslists them. memoryis now a built-in tool’s name, so a custom tool namedmemoryinconfig.json’stools.custommakes the file malformed (is a built-in tool's name), so wisp refuses it; rename the custom tool.
Changed:
wisp tools,wisp models, andwisp approvalslay out their listings likewisp --helpon 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.targetis 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, andwisp doctorsays 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.targetandcontext.headroomTurnsinconfig.json(orwisp 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 MCPrespondreturnscontextNote. A request that cannot fit the window at all now fails withthe request does not fit the model's context window: …, saying how large it was and what to do, after one retry.context.condensationin the audit log gains the target, the size before and after, the headroom, the steps taken, andfloor. /helplists every chat command as it is now:/inspect factswith the summary of earlier turns and proposals from other conversations, the forms of/factand/showIDs,/inspectas the general form of/status,/audit, and/approvals, and the/?,/q,help, andexitaliases. Long usages put their description on the next line, and underwisp-tuithe 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
notifytool now tells the model which route posted the notification (notification posted via terminal) where it saidnotification shown. - Unknown audit event kinds are shown rather than skipped:
wisp logs, theinspecttool, and the audit resources now print a line written by another release whosekindthis build does not know, as it is, where they used to leave it out.wisp logs --kindstill accepts only the kinds listed indocs/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:8bgot 24,576 tokens on a 48 GB Mac, so conversations condense far less often. Settingollama.contextLengthstill 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.enabledinconfig.json, orwisp 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/taskor an MCP caller’stask.assessment.toolspicksrequest,task, orall. Each is audited ascontext.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
notifytool,wisp notify, andwisp watchwrite 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-tuidoes 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 notificationis 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, andnotifications.viaTerminalApp(on by default) turns it off. Thenotificationaudit event records the route taken (host,terminal,app,osascript) and why earlier ones were skipped,wisp doctornames the route it would take, andwisp notify --route host|terminal|app|osascriptuses one route alone, to probe it. wisp chat --jsonaccepts a firsthelloline declaring what the front end does (approve,notify); withnotify, wisp sendsnotifylines for the front end to post. A front end that sends none works as before.wisp doctorchecks 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 newfactssetting 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] = VALUEstates one as you, which outranks a tool and the model;/fact delete IDand/fact ID permanent|thread|sessiondelete one or move it to another scope;/task [text]shows or sets the conversation’s task.wisp-tuishows the facts in its panel. - MCP
respondtakes an optionaltask, kept as the thread’s task and shown to the model next to each request, and the thread summary’staskis filled in. Facts resources, each under what owns it:wisp://threads/{thread_id}/factslists 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 atwisp://facts(one, with its history, atwisp://facts/{fact_id}), the proposed ones atwisp://facts/proposed, and the session’s atwisp://session/facts.wisp://threads/{thread_id}/context/nextshows 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, andfact.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 --jsonsends the same as anoteand addsfactsto the turn’s end, whichwisp-tuishows as a note; and MCPrespondreturns them asstructuredContent.facts, each withid,scope,subject,name,value,source,proposed, anduri./fact ID permanent|thread|sessionmoves a fact to the scope you name (permanentwrites it to~/.wisp/facts.jsonas yours, ranking with you; out ofpermanenttakes it out). Over MCP the newset_fact_scopetool moves a thread’s fact or a session fact betweenthreadandsession;permanentis set from chat. wisp never asks over MCP whether to keep a proposed permanent fact. Audited asfact.scope.changed. - Chat’s
/inspect factsalso lists permanent facts proposed in the process’s other conversations (the one before a/new, say), and/fact CONVERSATION/ID permanentkeeps 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 factsshows it under “Summary of earlier turns” (alladds earlier versions), and MCPwisp://threads/{thread_id}/factsreturns it assummary(summarieswith?all=true). Thefactssetting gainssummary(off stops it) andsummaryShare(its share of the window, 0.05 by default, on top of the facts’share). Each call is audited ascontext.summary. - A
memorytool for the model, the conversation’s memory (not the Mac’s RAM, whichsystem_inforeports).recallrestores, 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 = VALUErecords 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, MCPtools) has it only when the list names it;tools.disabled: ["memory"]leaves it out. References in a conversation with it sayto see it: memory "recall entry 7"where they saidcall it again to see it, and the system prompt gains one line saying so. Each call is audited ascontext.memory, and each note kept asfact.recordedwith methodnoted. - 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
shownOutputLinessetting;0for none), with a fold line naming/show <id>when there is more./showprints an output whole, by that id or by its entry number. wisp-tuishows 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 nextin chat shows what the next request carries,/inspect context Nwhat was composed at the start of turn N, and/inspect context turnswhat changed at each turn (a bare/inspect contextstill saves the files);wisp-tuishows the same in a panel (Ctrl-T, with Left and Right stepping through turns); and MCP callers readwisp://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), andwisp://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 --jsonadds the output, its size, and the fold size to eachtool.resultevent (output), and aviewline that answers/inspect context next,N, orturns.- MCP
respondresults list the turn’s tool calls instructuredContent.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 awisp://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 newinlineOutputBytessetting 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 contextshows the note. A copy with changes, such as a proposed edit shown as a changed file, is kept for the model. /savewrites 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.--resumereads it, so a resumed conversation stays connected to the log, andsession.startgainscarriedFrom, the sessions it came from./inspect contextin 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 mcpnamed 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, thewisp://configresource, and theinspecttool’sconfigview left out thefacts,assessment, andcontextsettings. They now show them with defaults applied, and a test fails when a section ofconfig.jsonor a settingwisp config listnames is not shown./inspect contextand/tokensin 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
respondtool 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 blankpathas the home folder, as an absent one already was;granite4.1:8bsent""and was refused. Whenducannot 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 ofchanges. On the right, the approval mode, and inwisp-tuithe 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:8bdid this withsystem_infofor “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, andredactfind more credentials by rule, without the model:Authorizationheaders 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--passwordflags; - 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, onlyexample.comandexample.orgwere 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 toolsscan_secretsandredactruns on the on-device model unless you name another, whateverconfig.json’smodelis. On the secrets test set it did better there thanollama: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 asmodel.routed.
Added:
wisp scan --personalandscan_secretswithpersonalfind 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 aspersonal-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--thoroughtoo, 80% are found.redactdoes not use it yet.- Watch what MCP clients have wisp do. In chat,
/audit sessionslists the sessions in the audit log and how each began, and/audit <id>shows one session’s latest events, such as arespondthread; 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-tuishows 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), orwaiting for the model.wisp-tuishows the same in its status line. - MCP callers that ask for progress (a
progressToken) are told what arespondcall, 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: anactivityline says what the turn is doing, and theturnline’s end carriesinputTokensandoutputTokens.
Fixed:
- Chat’s help said
/auditshows this session’s events; it shows the latest of every session. - A thorough
scan_secrets,redact,wisp scan, orwisp redactno 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 infailedChunks, 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 yamlwith flags beforeget,az keyvault secret show,gcloud secrets versions access,vault kv get,gpg --export-secret-keys,security find-generic-password -g, and a credential echoed insidesh -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, andgit 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.
- credential reads:
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 setsapproval.classifierkeeps its choice, andapproval.useModel: truestill means the on-device model.wisp doctorchecks 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/releasenow refuses to publish when the working tree orHEADchanges 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 byapproval.classifier: coremlwhen no model is named, so a fast classifier needs no training.wisp classifier trainadds a new version,risk@X.Y.Z-local.<n>, and never overwrites one;wisp classifier list,use, andremovemanage them, andmeasurerecords each result in the version’s manifest. Versions live in~/.wisp/classifiers/risk. wisp classifier trainleaves out every example that overlaps~/.wisp/classifiers/risk/held-out.tsvor a file given with--exclude, so a test set of commands run on this Mac is never trained on./configin chat shows the configuration as YAML;wisp configstays JSON.wisp-tuishows 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(andwisp config get KEY) shows one setting and whether it is set or the default;/status,/approvals, and/auditreplace/inspect status|approvals|audit,/approvals revoke [ID]removes a standing approval at once, and/inspectstays 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:
printenvorechoof a variable named for a password, token, secret, or key,env | grepfor them,gh auth token,op read,aws configure getof a secret, andkubectl 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 issafewithout 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/nullno 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, whilexcodebuild, clean targets, and installs still do, andpython3 -m http.serveron every interface is dangerous, not moderate. - The risk rules no longer take
git merge-baseforgit merge,git tag -lfor making a tag, or a program’sopen(…)or a wordatinside a command for theopenandatcommands, 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 VALUEand/config unset KEYin chat, andwisp config setandunset, change~/.wisp/config.jsonwithout 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 listandwisp config listshow the settings that can be changed and their values. A change that weakens the approval gate or the audit is flagged./config setoffers 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-tuishows a picker in place of the input (arrows, Enter, Esc, or type a value); the plain chat shows a numbered list.wisp chat --jsongainschoiceandchoosefor 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 --jsongainscompleteandcompletions.
Fixed:
- The chat status line shows the branch in a git worktree or a submodule, where
.gitis a file that points at the repository’s own git directory; it showed none.
0.11.0
Added:
wisp chat --jsonsends aturnline when a message goes to the model and when its reply is done, with the turn’s number, time, and outcome, and everyeventline carriestext, the line the terminal chat shows for it.wisp-tuishows the running turn and the last one’s time in its status line, and words tool activity exactly aswisp chatdoes.wisp-tuiasks 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-tuirenders 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 traintrains a risk classifier on this Mac in well under a second, from the bundled examples or your own, andwisp classifier measurereports 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 withapproval.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-auditalso 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 leastmoderate.triagereads 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), andhot_paths(folded stacks reduced to self time and the heaviest paths). None uses a model.
Removed:
scripts/train-risk-classifieranddocs/examples/risk-labels.csv, which trained on the eval set;wisp classifier trainreplaces them.
Fixed:
- A message that asks for nothing, such as
testorhello, 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, andxargs rmas 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 withsecurity … -w. The default classifier let the first through: the on-device model rated readinggh’s token safe. wisp-tuino 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-tuiflickered 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-tuiline 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 configand the MCP resourcewisp://configleft out thenotifications,tools, androutingsettings; they now show every setting.
0.10.0
Added:
- Routing by input size: with
routing.ladderin the config (such as["system", "ollama:qwen3.8:27b"]),draft_changeandwisp draftpick 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 throughrun_command’s policy, approval, and sandbox;tools.disabledleaves built-in tools out. Seedocs/tools/custom.md. wisp draft [commit|pr|changelog]and the MCP tooldraft_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 aWhy:line left for the reason.condense_logreads this Mac’s unified log withlast(such as10m), optionally for oneprocessorsubsystem, in wisp’s own process:/usr/bin/logrefuses 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_infoasked about a named app (“Is Ollama running?”) often left the name out and fell back tops, 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
exitStatusandtimedOutand start their text with a warning when the command failed.
0.9.0
Added:
-
wisp scanand the MCP toolscan_secrets: find credentials, and with--personalpersonal data, in files, standard input, or a command’s output, reported with masked previews. A diff is scanned by its added lines aspath:line, sogit diff --cached | wisp scanworks as a pre-commit check; exits 1 on a finding. -
wisp redactand the MCP toolredact: text with credentials and personal data replaced by numbered markers such as[REDACTED:email#1].--thoroughadds 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.gitignored) 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, sowisp "what is using port 8080?"gets a short, right answer.
Fixed:
summarise_diff’ssecretflag 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:
/statsin 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./historyin chat lists the lines typed this session, and inwisp-tuiUp and Down recall them, with what was being typed kept as a draft.
Changed:
/modelsin chat is a table with a header, so the columns line up in the terminal and inwisp-tui;wisp modelskeeps its tab-separated lines for scripts.- A model classifier’s fallback verdict records
classifier.failurein itsclassifier.verdictmetadata.
0.8.2
Changed:
--modelhelp mentions<backend>:<name>spellings and points towisp models, not onlysystemandprivate-cloud.docs/backends.mdrecords 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 modelsand/modelslist only the models that can serve the conversation: they must resolve and, when the conversation has tools, declare tool calling.--alladds the excluded ones with the reason;--no-toolsjudges for a conversation without tools. An Ollama model that cannot hold a conversation, such as an embedding model, is refused by--modeland/modelwith that reason instead of failing at the first prompt.
0.8.0
Added:
- Notifications: the model’s new
notifytool andwisp notify <message>show a macOS notification. Text is bounded, at most five a minute (notifications.perMinute),notifications.enabled: falseturns them off, and every request is audited asnotification.
0.7.0
Added:
/modelsand/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 inwisp-tui.
Changed:
- The model introduces itself as Wisp.
0.6.1
Fixed:
wisp chatlaunched throughPATHdid not findwisp-tui: it looked besideargv[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, andwisp-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, andwisp chaton a terminal hands the session towisp-tui;--plainkeeps the line-based chat.
0.5.0
Fixed:
- An unanswered MCP approval now comes back at
approval.timeoutSecondsas 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 chatshows 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 withNO_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 evalrecords what each delegated task achieved (triage,edit_filereplace after read, schema-shaped replies, the risk classifier) into a resource embedded in the binary;wisp tools --markdown,wisp://tools, and the newwisp://measurementsresource publish them so a caller knows what to trust.
Changed:
- Approvals for programs whose first word is the verb (
git,cargo,swift,npm,brew,dockerand others listed inmultiplexers.txt) are remembered by verb:git commit *andgit push *are separate, so a session answer for one no longer covers the other. A standing approval stored under the oldgit *still counts until it expires. edit_filereplace takesline, the numberread_fileshowed, withcontentas the whole new line andfindas an optional check on that line; a drifted number changes nothing. The by-findform measured 3 of 5, because the model retyped the neighbouring line intocontent.- Local runtimes that truncate silently no longer lose the instructions:
Agentcondenses the transcript ahead of the window when the usage the last reply reported, plus the new prompt, would pass 85% of it, audited ascontext.condensationwith reasonbudget. Ollama is asked for an explicit window on every request (ollama.contextLength, default 8192, sent asnum_ctx), and/tokensshows 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 asedit_file <mode> <path>, and each edit is audited asfile.writeand listed in the receipt’sfiles.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 askind,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 ownrun_command.- Structured output:
respondtakes aschema(a JSON Schema object in an accepted subset) and returns JSON of that shape, parsed intostructuredContent.output; the CLI takes--schema <path>. A model that does not declare guided generation is refused before generation. respondresults carry areceipt: 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-cloudfailed after the request with an opaqueModelManagerError1046. Private Cloud Compute needs the managedcom.apple.developer.private-cloud-computeentitlement, 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 modelsshows the same reason.
0.2.0
Added:
-
Model backends are a registry:
--model <backend>:<name>names a local runtime by scheme,wisp modelslists 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-toolson the CLI andtools: []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.resolvedwhen 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 fromapple/coreai-models. Bundles live under<home>/models/coreai(config.jsoncoreai.modelsDirectory) or are named by path;wisp modelslists 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. Seedocs/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 templatewisp://audit/{session}for one thread’s events, so a calling harness can read wisp’s state without a model turn. -
wisp configprints the effective configuration as JSON. -
MLX Swift:
--model mlx:<directory>runs a model in MLX or Hugging Face safetensors layout in wisp’s process throughmlx-swift-lm’s bridge, in builds made with--traits MLX(the release is); a build without the trait refusesmlx:models with the reason. Capabilities are declared by the operator per model inconfig.json’smlx.models; an undeclared model is text only. Verified withmlx-community/Qwen3-1.7B-4bit: text replies in 2.5 s including load, and the tool loop 3 of 3 withtoolCallingdeclared. The Homebrew release does not include MLX, because it needs a Metal library bundle beside the binary; build with--traits MLXyourself. Seedocs/backends.md. -
approval.classifierchooses what judges commands beside the rules:rules,system-model(the default, unchanged), orcoreml, a Core ML text classifier you train from atext,labelCSV withscripts/train-risk-classifier. The model must follow a versioned contract or it is rejected; every failure or low-confidence verdict ismoderatewith 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 calledsafewere kept offsafeonly by the confidence guard. Not fit to judge alone; measure your own withWISP_COREML_MODEL=<path> scripts/check evalbefore relying on it.
Changed:
- The
run_commandsandbox 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’”. wispwith 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
initializecarriescapabilities.experimentalwith 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
systemPromptExtensioninconfig.json, and the caller’s--instructionsor MCPinstructions, rendered in that order. A caller can no longer replace wisp’s framing. The old config keyinstructionsis still read as the extension. - The audit
session.startevent records the extension and the caller’s instructions separately. wisp mcp --toolselects the tools threads get unless arespondcall names its own.
Changed:
- Releases run the full test suite and a coverage gate; line coverage must not fall below the recorded baseline.
--resumeof 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>, inconfig.json, or per MCP thread.wisp modelslists Apple’s two models with availability and every model Ollama serves. Newollamaconfig section for the server address and per-request timeout;wisp doctorchecks 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 approvalslists, 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, sohead -n 5 xis remembered ashead *. - A once-approval covers the rest of the turn, however many commands the model runs.
- The MCP
respondresult reportsrefusals: commands the gate refused this turn, with reasons. - A bare
exit,quit, orqendswisp chat.
Changed:
- The approval threshold is
safe,moderate,dangerous, ornever; 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
commandPolicyobjects inconfig.jsonkeep 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.