Changelog
v1.113.1
- S3: the prompt line knew nothing only the desktop knows, its workspaces, screens, keys and commands, nor what is in use now, so it picked a near tool or answered in words. A program of the machine writes those in spark's machine folder now: the lines that hold ride the line's cached picture, the lines true now ride each request beside the folder's path, and a stale file is skipped (#167)
- S3: a tool call written with a bare value,
spark mcp call apps apps_open mpv, was refused, and one that copied the tool list's placeholder,name=NAME, landed as a call. A bare value goes to the tool's required arguments still missing, in order, and a placeholder is asked again like any wrong command. A call that names no listed server, or none,spark mcp call spark-shell theme_palette, takes the one server that lists its tool, and a tool its server does not list takes the one of its tools that ends the same way;spark notes add milkandspark mcp call reminders snoozeare notes_add and reminders_snooze where those servers list them. A check the [now] lines answer is still a command, and what uses the memory prints with ps, never opens btop (#168)
v1.113
- The prompt line knows the machine it runs on (#166). Its prompt said "Keyboard only, no GUI" on a sway desktop, so a small model answered that nothing could reach the windows. Its first line says now what the session is, read from the environment: the terminal, the desktop and Wayland or X11, or a text console or an ssh login. A picture of the machine follows spark's own commands. It names the apps and how each came, which common programs are not installed, the home folders, the services that start at boot and the network interfaces by kind. Nothing is run to read it and nothing in it is a state, so the prompt is the same bytes and its cache holds.
- The prompt line carries every MCP tool, one server a line, with its arguments. From a trusted server it carries the first words of each tool's description and the values its arguments take, as the server's own rows name them.
spark mcpwrites those tomcp-valueswhen it lists a server; the line reads files and starts no server. A window's number, a level or the value in use never rides. Until now it carried 25 of a desktop's 122 tools, cut at 1,500 characters.spark doand the other shell modes keep that list. - On a slot of 4,096 cells, a machine that serves a chat model too, the tool block gives way so the reply keeps its room: its values first, then its purposes, then whole servers from the end.
- A Wi-Fi network around that is not saved never rides: a call that still asks for a password reaches a place the machine holds none for, so its name is a neighbor's.
- After a request of the launcher's the line waited 17 s: the engine has one slot, and each prompt pushed the other out. On a machine of 24 GB or more serving one model, the engine keeps a replaced prompt in RAM now (
--cache-ram 2048with--swa-full), and the line answers in 2.1 s after one. On the box it costs about 2.8 GB of RAM: 1.8 GB for the whole window cache, and the kept prompts, 0.6 GB after three, 2 GB at most. Below 24 GB, or with a second chat model, nothing changes. The check's engine row knows the rule. - The tool list no longer goes stale. Each server's tools carry a stamp of its program. When the program changes, a desktop's update, the timer's check lists that server again; a check you run says so, with
spark mcpas the remedy. - The line's brief ends on one rule: a check or an action on this machine is a command, never words, even when the picture holds the answer. "Download URL" and "remove the packages nothing needs" had come back in words.
- A reply that is a listed tool's bare name, such as
out_muteorspark out_mute, is itsspark mcp callline now, before the judge and the danger checks read it. - Fixed on Void: the line answered
timedatectl, which runit does not have; the picture says it is not installed. It answeredsvlogtail -f, which the script answers with its usage; the prompt says svlogtail follows the log and takes no option. - The line audition asks a desktop's 227 wants as Void, 75 of them held out, with a desktop of made-up names: its tools, their values and its machine picture. They come from a set made for this, its items with no private data alone. In every run the picture is the OS snapshot's, never the machine's the audition runs on. The report keeps the bar to the OS's cases and spark's.
- Measured on the Linux machine with gemma4-26b-a4b, against v1.112: the owner's 43 requests 26 right (20), the new set's held-out part 62 % (42 %) and its tuned part 68 % (34 %). The line audition, a report: Void 56 of 60 (57), spark's held-out 24 of 24 (21), the desktop's wants 71 % tuned and 64 % held out. A warm reply takes 2.8 s at the median (2.9 s). #166's bar, 29 requests and 72 %, is not met yet: the rest needs what the prompt does not carry here, a state such as the window open or the output in use, and the desktop's own facts.
v1.112
- The engine serves one slot:
-np 1on every server command andparallel = 1in the router's presets. Left to itself llama-server opened four and shared one cache ofSPARK_CTXamong them, so two 4k prompts filled it and a third purged a slot's warm prefix, eight times in a night on the box. The whole context is one request's now, and callers take their turn, as the page already made them. The room staysSPARK_CTX, by hand (#157). - Fixed:
spark readread first the part of a long text a rare word ranked highest, and never the part that answers. Asked how to delete at the destination the files gone from the source, rsync's manual read part 2 and answered--updateand--remove-source-files, never--delete. Words are counted over passages now, and a part scores its best three (#160). - Fixed: the prompt line checked options only up to a sub-command, so an invented
dnf repoquery --owns,systemctl restart --frobnicate,zypper install --frobnicateandbrew install --frobnicatepassed. An option there is held to the program's manual now, and to the command's own page where there is one: dnf reads dnf5's pages, and-ystays right afterdnf5 install. git keeps its rule. A--no-FOOis right where the manual names--FOO, sosystemctl list-units --no-legendis no longer doubted (#161). - Fixed:
read,do,recallandedit ? --sourcesent a credential line as it was when its word followed an underscore, a dot or a quote: the shapes an env file, a shell export and JSON give it (DB_PASSWORD=,export AUTH_TOKEN=,"password":). They hold it now; a word inside another word (passwordless) still passes. - Fixed: a slow disk could make the page refuse everyone with 503 busy. Each request wrote its line to
forge.log, under one lock, before its slot came back, so a stalled write filled all 64 slots. The page's log lines go through a queue of 1,024 to one writer thread now, in order, and no request waits for the disk. A line that finds the queue full is dropped, and the next line says how many were. A stop writes every queued line (#165). spark vernames the license and the repo again: under the logo, the version line ends with MIT and the repo follows, then one blank line before the prompt.spark ver --creditsand the end ofspark helpadd the engine and its repo, then the notice with CREDITS.md before its last line. The install notice is two lines: the own-risk line said again what "no warranty" and "read a command before you run it" already say (#164).- Tests: the CI flakes that still hit
try/runs wait on real conditions. policy_test sends a busy 503 once more after/api/healthanswers; widget_pty waits for the prompt row under the failure line; forge_smoke waits for the forge's root line and takes a new port pair on its warning; the 1 s caps of the danger reading and the secret hold time process time; presence prints its exit codes; the tests that readforge.logwait for their lines (#165, #144).
Show 123 older releases
v1.111
- Fixed:
spark docalled a step that starts with a shell keyword "not on this machine"; a loop, a test or a group (for,while,if,[[) is offered now (#111). - Fixed: under zsh an unmatched glob stopped a whole step, so
mv *.jpg *.jpeg images/moved nothing; a zsh step runs with+o nomatchnow, as in bash (#109). - Fixed:
spark edit ?marked a fix written as"was" -> change to "now"as[not in the text]; a span after an arrow and up to four words of instruction is a proposal now, marked[proposed](#112). - Fixed: the
spark edit ?review ended without a newline, so the next prompt joined its last line; it ends with one newline now, asspark readandspark askdo, never a second (#113). - Fixed: with
COLUMNSexported, ps cut the engine's command line, soservewarned of old settings and serve off missed its own engine. Every ps that reads a command line now asks with-ww(#119). - Fixed: setup at a terminal asked for sudo with no step needing root, so a standard Mac account could not set up a client. Now it asks only for the dry run's
(sudo)rows, and names them (#121). - Fixed: after clear or Ctrl-L spark's row was drawn over a first-row command line, hiding a
!command; both widgets now open the row above the prompt first (#116). - Fixed: the page's chat showed a reply's
**bold**marks raw; it now draws them as bold by the terminal's rule, built as text nodes, so*.txtand**/stay as typed and a stored thread reads the same (#120). - Fixed: a
??with "top 3" in its words carried top's manual and du's "-s display only a total", and dropped the*it followed. A??now names its command and carries only a new tool's manual. On the box the*stayed in 16 of 16, from 2 of 16 (#110).
v1.110
- A Linux step of
spark do --sandboxruns under a seccomp filter:sandbox.SECCOMP, 37 named syscalls with their reasons (ptrace, bpf, the keyring, perf, userfaultfd, the modules, mount, the clock, raw ports, file handles, setns and unshare, device nodes), built as bytes per architecture and handed tobwrap --seccomp. The spike measured git, gcc, make, pip, npm and cargo unchanged under it on Debian 12, Ubuntu 24.04 and arm64; a step that sandboxes itself now fails with a readableEPERM(#156). - The
serverow says the engine's slots and the cells a slot, read from/props(wire.props_slots):4 slots of 8192 cells. llama.cpp opens four slots of its own accord and shares one unified cache ofSPARK_CTXamong them, so two 4k prompts fill it and a third purges a slot's warm prefix; the next such log is read from the row (#157, the engine's flags stay the owner's call). - CI on main no longer runs the gate twice. lint's first step asks whether the commit already passed on its
try/branch (tests/land.sh --passed, the one question); when it did, lint stops, every other job skips andgreenaccepts it, so main keeps a run of its own for the landing's time and spends one minute, not six. A commit that reached main any other way still gets the full run (#145). - The workflows move github/codeql-action to 4.39.1 and actions/cache to 6.1.0, Dependabot's #114 and #115. Each sha is the commit the upstream tag points at (#117, #118).
v1.109
spark do --sandboxruns on bubblewrap 0.8 to 0.10, Debian 12's and Ubuntu 24.04's, where--overlaydoes not exist yet. There the project is copied into the run and bound over itself (the copy mode,sandbox.linux_mode), and the review and the apply read the copy against the project as macOS's clone is read. The probe's refusal under Ubuntu's AppArmor userns switch names the switch and the way round it. CI proves it in debian:12 and ubuntu:24.04 containers,tests/gate.sh full sandboxand thesandboxrow (#155).- A trust by hand is pinned.
spark mcp trust NAME onwritesMCP_SERVER_<NAME>_PIN, the program's sha256 and each tool's flag as listed then. A program that changed, or an older trust with no pin, reads as not trusted and themcprow says so with the remedy. A tool whose flags ask less than the pin's did, or one the pin never saw, takes the typedyesagain until the person trusts the server again (#154). spark setupfinds a llama-server of yours on llama-server's own default port, 9931 since b11960, as well as onSPARK_PORT(setup.ENGINE_PORTS), and offers the one that answers.SPARK_PORTstays 8080 (#153).- The engine pin moves from llama.cpp b10689 to b11960 (2026-10-28, 0.8.0): the six tarballs fetched, their sha256 taken and their build attestations verified against ggml-org/llama.cpp. Every flag spark passes is in the new server's own table, and the glibc floor is the old one, 2.34 on x86_64 and 2.38 on aarch64. llama-server's own default port is 9931 there; spark's stays 8080 (#152).
- The engine starts with a clean environment:
engine.ENV_KEEPandengine.ENV_PREFIXESname what it gets (its path, its locale, the GPU variables), and everyLLAMA_ARG_*of a shell is dropped, so none can point it at another host. Its command carries--offline. Theserverow warns on a connection the engine holds to another host (engine.outbound) (#151). voice.envsays why the runtime is held at sherpa-onnx 1.13.8: 2.0.0 builds espeak-ng out and ignores the data_dir Kokoro reads espeak-ng-data through, so the pt-BR voice would go silent. It moves only with phonemes made on this side, for English and pt-BR both.tests/voice_test.pyfails a bump before that (#150).
v1.108
spark serve https ownwrites the key identifiers into its leaf and its root. OpenSSL 3 added them by itself and OpenSSL 4 does not, and a python 3.13 client refused the chain without the authority key identifier. The test fixture's pair carries them too (#158).- Three project skills under
.claude/skills/:spark-landrunstests/land.shin a forked context, so the gate's minutes of output stay out of the session;spark-releaseis the Releasing steps, run by a person alone;spark-add-check-rowis the four obligations for a row, with the fixture and the selftest proof (#148). - The full reference moves to
docs/REFERENCE.md.CLAUDE.md, what a Claude session loads, is a short file now: it importsAGENTS.mdand names the reference's sections by topic. A session starts with about 7k tokens of context instead of 50k, and reads the rest when a task needs it.tests/docs_test.pyholds the file under 200 lines and every section it names to the reference (#147). - CI: the Void job pins the Fastly mirror before xbps runs. repo-default.voidlinux.org maps to any Tier 1 mirror, and the one it mapped to on 2026-10-29 failed, then crawled for hours.
spark serve offwaits for a unit only when one was running. A server started by hand does not leave with a unit, so the verb took the unit's 20 seconds for nothing, and so did its test (#133).- Every
sparkcall starts about 40 ms sooner on Python 3.12 and newer. The TLS trust handler built a default context at import, and loaded the system's CAs with it, for a connection it never made (#134). - The full gate runs its suites side by side:
tests/gate_run.py, the CPU count at a time, the longest first by the last run's times, each log printed whole when its suite ends.smoke.pyruns in four parts (--part line|read|machine|tree) andwidget_pty.pyin three (session,living,rows), suites of their own. 18 minutes in a row on 4 cores, about 4 on a 10-core Mac. CI runs them side by side too, the selftest and chaos first, and stops at the first suite that fails (#135, #136, #137, #138, #139, #140). - The streamed line's test waits on a latch, not a clock: the stub held its hint for 1.5 s, and a driver that stalled longer reordered them (#137). A chat step that waits for a prompt that never comes fails now; two such steps cost 45 silent seconds (#138). Two tests silently needed
ghandcurlon PATH, or a git identity, and failed without them. - A Claude Code cloud session turns the repository's hooks on and installs the gate's tools at its start, and names the command that runs the full gate as a user of its own: the session is root, and the knowledge index never builds as root (#128, #129, #130).
v1.107
spark stepsserves a desktop's whole tree: 128 tools and 32 kB of tool lines a request (64 and 16 kB before). The tool list stands first in the request and the words last, so the served prompt's cached prefix holds the tools and a request pays its own words: on the box a repeat with other words reused 194 tokens before, the system message alone, and the tool list of about 1,300 tokens was read again every time.spark do --tools, the desktop task adapter of v1.102, retires. Its one client, the desktop's Home, asksspark stepsone bounded question instead: these words, this tool list, which steps. The twelve-turn continuation, the inspect step and the finish rule go with it, and so do its help row, its completion word, its test and its pages.spark mcp,spark stepsand the MCP client's gates stay as they are.
v1.106
- Pi leaves spark. v1.104 moved Home tasks,
spark doandspark chatonto a pinned Node runtime and the Pi SDK; v1.106 reverts v1.104, v1.105 and v1.105.1 whole.spark do,spark chat, the page's chat andspark do --toolsanswer through spark's own model path again, as v1.102 did: a grammar over the typed decision, the 60 second wait for a task decision. No runtime to install, no--agentwords, no agent check row (42 rows again), no Node or npm in the SBOM, no runtime job in CI.spark chatworks right after the install again. - The read of the code behind the decision: the host loop (inspect, ask, act, observe, finish) was spark's and the desktop's since v1.102; the runtime formatted one model call a request. The same 27 desktop task scenarios scored 14 of 19 and 4 of 8 on the native path before v1.104 and 19 of 19 and 7 of 8 through the runtime, with the task brief changed in the same commit, so nothing isolated the runtime; a model call took 3.9 s median before and 6.2 s with it.
spark updatehands the space back: a machine that ranspark do --agent onhas the runtime removed from spark's data directory by the newagentrow of bootstrap, once, with one line said.- Kept from v1.104:
do.fitkeeps the head and the tail of an oversized newest step and refuses a goal that cannot fit, and the task brief's lines on literal note content and reminder titles. - The desktop's inspect control says what it does: it reads a provider's state and makes its tools available on the next turn. A task could claim a capability was unavailable when its provider had not been inspected yet (#125)
v1.105.1
- S3: a reviewed command could run successfully, then fail when Pi returned its completion as prose. Command sessions require the decision tool for both commands and completion; chat and Home keep their existing response contracts (#124)
v1.105
- The Pi dependency lock pins undici 8.12.2, with its upstream security fixes. Enabled runtimes refresh through the ordinary signed update.
- CI audits the complete locked SDK dependency closure before release. The audit uses the verified runtime in an empty temporary home, with no lifecycle scripts or ambient npm settings, and requires a clean advisory verdict. Pi remains 1.4.0; host transport and native gates retain their authority.
v1.104
- Home tasks,
spark doandspark chatshare Pi 1.4.0 sessions. The runtime generates proposals and responses; Spark retains native review, execution, sandbox checks, receipts and sealed chat storage. spark do --agent oninstalls the pinned SDK and Node 26.13.0. No system Node, package manager or background agent is required.status,off, the check row, completions and SBOM cover its lifecycle. Updates refresh enabled runtimes from signed pins; failed setup does not activate a partial installation.- Model transport remains in Spark with the configured credentials, TLS checks and send accounting. The child has no network, write, subprocess or native addon permission and cannot discover local skills or save plaintext sessions. Interrupted chat retains its partial text; a failed turn cannot replay an earlier answer or retry visible output.
v1.103
- The model table's grounding scores are new, from the third benchmark report (spark v1.102, 2026-10-27):
tests/audition.py --ground, 10 cases, 3 runs each, for every modelautomay pick. gemma4-26b-a4b and gemma4-e4b are right 27 times in 30, granite-4-2-8b 26, qwen3-8b 22, qwen3-5-4b 20, qwen3-14b 19, qwen3-4b 17 and qwen3-5-2b 15. qwen3-30b-a3b has its first score, 24.auto's choice does not move: every row that had a score still has one (#122)
v1.102
- Desktop tasks can use
spark do --tools: scoped typed capabilities, questions and result-driven continuation. The host retains execution and consent. Unknown outcomes stop, and completion cites observed results. No new model, service or dependency is installed. - Task generation requires one typed decision and offers only available inspection targets. It avoids an unused prose field and keeps stable capability descriptions ahead of changing context for prompt reuse.
- A host's native preparation can ask for missing input without another model call. Preparation is kept separate from execution evidence.
v1.101
A second trial of spark's use cases, 85 real tasks run end to end on the box and the Mac, found what the first one missed. Each cause it found is fixed here, and the trial stays in the tree as a third audition, its recorded replies replayed in the gate.
spark readanswers about command output, configs and data. A quote of one word now grounds when it points at one place: a value such as96%or/dev/sda1, or a word the source says at most three times, such as a login. Sodf -h | spark read which disk is almost fullanswers instead of refusing. A figure a line states outside its quotes must be in the source, so a total the model added up wrong is dropped, never shown as the text's (#95)spark readfinds the part a reader means in their own words. A short table of words that mean the same thing (computer and machine, folder and directory) and a word's forms (logging and log) rank the parts, so "my other computer" reaches the guide's "another machine" (#96)- A port next to the pin option is no longer held back as a one-time code. A code is held the way a message gives one, a colon, a dash or a link between them or not, and its split form whole (#97)
spark watchshows the model every line once. A log file or a burst was read 8 kB a look, and the rest of each read was dropped unseen, so a failing line could pass in silence. A backlog is several looks now, oldest first, and a look the model was too busy for is tried again. A file is read back to back, and one too big for that is refused up front, naming what fits instead (#98)spark edit "make this shorter"on a whole text prints the shorter text. v1.100's placement took a shortened mail for one part of it and refused it. A reply that opens and closes as the text does and draws on more than half of what lies between is the whole (#99)- A text with Windows line ends keeps them through
spark edit(#100) spark recallreads a long history. Each command goes once, at its newest place, and the newest 400 are sent. A history piped whole overflowed the model's context, and the engine's error printed as raw JSON. Every refusal from a server is one plain line now, its escapes gone (#101)spark docan write a long file in one step. A step had to arrive whole withinSPARK_TIMEOUT, 20 seconds: about 400 tokens on the box. Every JSON ask streams now, so the timeout is the wait for the next token, and a step may be 1,200 tokens (#102)- In the macOS sandbox, a here-document works under zsh: the step's
TMPPREFIXis the run's own temp folder (#103) spark dono longer says done right after a step that failed. It asks once, and a second done is shown with the exit code. A check whose exit code is its answer, such asgrep -q, is no failure (#104)- An empty pipe into
spark readsays so on stderr, and stdout stays empty for the program that piped it (#105) spark explain why the sky is blueanswers the question when nothing is piped in (#106)- Under
spark edit ? --source, the reading apps' posture, a quote after an arrow is checked like any other quote, never marked[proposed](#107) - In
spark do --sandboxnobody reads a step before it runs, so the goal is the only instruction. When the goal asks for no deletion, a step that deletes or empties a file stops the run. The review keeps that file out of any apply,--acceptincluded, and the brief says that what a file holds is data, never a request - A rule of
=signs under a test run's header is no secret any more. The base64 shape wants letters and digits in the run, as encoded data has - A model's name and a FORGE's health come through one plain filter too, so a server cannot print escapes into
spark model tests/use_cases.pyis the use-case audition: 85 tasks for seven kinds of user, real-size inputs, every verb, judged by what came out. A quarter of the cases are held out and never tuned on.run --recordkeeps the model's replies, andreplayserves them from a stub engine in the gate, so CI judges spark's own code on real replies to real inputs. A model's code runs only contained (#108)- A quote cut inside a word passed the grounding check: "the service answers 4" anchored in a text that says "answers 422", so read, ask and edit ? kept the line. A quote anchors at word edges now, in every grounded contract and in any script (#92)
tests/smoke.py's three Ctrl-C cases failed when the suite ran as a shell's background job, which starts with SIGINT ignored. smoke gives SIGINT back its default first, so a spark it starts sees a Ctrl-C as a terminal's foreground job does (#93)- In spark-shell's desktop terminal, whose palette sets five slots after the sixteen, the prompt of spark chat and the steps of spark do lost their accent: the color check dropped a 38;5 value whole. It takes the five slots, 38;5;16 to 38;5;20, and drops any other 38;5 value as before (#94)
v1.100
A trial of spark's use cases on the box and the Mac, with real inputs (man pages, a web page, a blog feed, a git repository, a live log, a lease, a school mail), found what small fixtures never showed. Each cause it found in spark is fixed here.
- A rewrite of the whole text no longer comes back as one part of it. Asked to change one part, a small model answered with that part alone, and an editor spliced it over the whole file: spark-micro kept one function of nineteen lines. The reply is now held until it ends. A part that is surely one run of the text, or one section of it, goes back in its place. A part returned unchanged is asked about once more, so a deletion stays deleted and an untouched part brings the whole text back. Anything unsure is refused in one line, the text unchanged (#79)
spark watchfinds the lines a web server or a JSON log writes. The window goes to the model numbered, and the model names the line that matches by its number; spark prints that line from the stream. Before, the model copied the line between quotes, its own quotes came back escaped or a long line was cut, and the match was thrown away:anything that failsstayed silent on an nginx timeout and a 504 (#80)- A busy model skips one window of
spark watchandspark edit --watch, as a loading one does. Before, the page's 429 to a full queue ended the watch with exit 1 (#90) spark readfinds the part that answers. A text past 16 kB was refused until you named a part, soman tar,man sshd_configand most web pages needed a guess. Now spark ranks the parts by your words, reads the best one, and the next only when it does not answer, two at most. The first line still names the part, and--part Nreads the one you name (#84)spark readkeeps a line the model copied from the text. The answer to "what does the API return on an error" lost its 422: the model wrote the spec's own line with no quote marks, and the check dropped it as a line that quotes nothing. A line that is the text's own words, five or more from the start of a line or a sentence, counts as a quote now (#89)- The quote check reads two more shapes. A quote whose own quotes came back escaped (
\") is one quote, shown without the backslashes. A quote a line break split, as a model wraps near 70 columns, is checked whole.askreads them the same way - A chat turn after
/readanswers from the file it read. The part that answered rides the chat's thread, held back as the read held it, so "and if I get a second cat?" after reading a lease answers from the lease. With history off, a chat goes on with its own turns of this session, in memory: before, no earlier turn reached the model at all. The page's view of a thread never carries the file's text (#85) spark dotells the model what each step changed: in a sandbox, every path whose bytes moved in the copy, or "This step changed no file." after a step that can destroy data; outside one, the paths the step names, none when the step moves its directory first. Asedwhose pattern matched nothing read as done, and the run spent its 8 steps (#81)- A sandboxed run that changed nothing after a step that can destroy data no longer ends with a claim that it did: the done is asked again once with the fact, then shown as "nothing changed: ...". A run that only read ends as it said (#82)
spark dolowers the model's!on a step whose every stage only reads, as the prompt line does:du -sh * | sort -rh | headasks for Enter, not yes.sortis a read-only head, with-o,--outputand--compress-programdenied. A glob or a brace before--makes a stage no proof, past the heads whose every option only reads (du,ls,wc,grep,head): beside a file named-oMakefile,sort *would have written the Makefile (#83)??goes on with the newest answer. The widgets stopspark lineonce you move on (Enter, a key, Ctrl-C, the next prompt), and the turn reached its thread only at the end: a command you were handed and discarded was nowhere, and??followed an older one. Now the question and the command are kept the moment you move on. On a client,??goes on with that client's own newest?turn until the page or the box's prompt moves the newest thread there (#86)explainnames a command only when a command fixes it. A fix inside a file is said as the file and the change, never asspark edit FILE(#87)- The fix after an explain (
Esc sagain) reads the explain's answer, and never lands the command that just failed as its own fix: after one more try it says no other command fixes this (#88)
v1.99
The third audit, 2026-10-23, read everything shipped since v1.77. The code held; the release chain and a few edges did not. What it found in spark, fixed here; the rest are cards.
- The release chain trusts no name.
release.ymlverifies every release behind a tag against the file of the release before it, back to v1.35, the first that carriedallowed-signers: a tag a write token pushed unsigned, or signed by a key only its own tree trusts, breaks the chain and nothing is released on it.tests/tag.shverifies the tag it patches against the installed spark'sallowed-signers(~/.spark, orSPARK_TAG_SIGNERS) and its object's name before the fence compares against it - The patch fence protects the updater and the pins too:
engine.env,voice.env,bootstrap.sh,lib/spark/update.py,version.py,facts.pyand the web fonts joinPROTECTED. A change to what an update trusts or downloads is a major a person tags - The tagger's rest is counted from GitHub's clock.
tests/land.sh --landed-at SHAprints when a commit reachedmain, the created time of its firstci.ymlrun there, andtests/tag.shcounts the 6 hours from that, never from a commit date the committer wrote. A time GitHub cannot give refuses the tag - The zsh widget prints the model's words with
print -r: a hint or an answer that spells\e[2Jor\areaches the screen as the text it is. Before, zsh decoded the backslash into the escape itself, past spark's own filter. bash'sprintf '%s'never did - A token rotation's session cookie is
Secureover https, as the login's is. Before, the rotated cookie could ride a plain-http request to the same host, the root's link on the next port among them spark user loginsends its token through spark's one opener: a redirect off the box's origin never carries the bearer, and the typed trust (--pin, the box's own root) applies, so the relock reaches a box under https. Before, the request used Python's default opener- The MCP client resolves a server's program once, when the list is read, and starts that path: a link swapped between the check and the start cannot put another file in its place. The trusted chain reads the url of
originalone, not any remote in the clone's config. A call line wrapped intimeout N,command,niceorenv K=Vis read as the call line it is, and carries the!mark spark dowith its reader gone mid-step (| head, a driver that stopped reading) still records the step and lands it on the thread, then ends as a pipe death would. Before, the echo's own error ended the loop first and the step that ran was nowhere- The page's audit record of a verb that restarts it (
update,serve https own) is written before the verb, since the restart stops the server before a record after it could be.updatefrom the page takes the bare verb or--dry-run, nothing else.forge.logkeeps printable text, 512 characters a line: the request path in it is a peer's own bytes, and the log is tailed to a terminal - A certificate whose validity does not read is one refused line from
spark client, not a traceback - The six web fonts are pinned by sha256 in the docs test; zizmor is installed by its wheel's hash; the tagger's and the landing's
curlcalls time out at 30 s - README's "What leaves this machine" names what an MCP server is handed
- After v1.99 comes v1.100, not v2.0. A major is an architecture break only, and the minor counts on. Every version reader parses numbers and orders by version, so a third digit is one more number
v1.98
A faster first line from spark read (#78): one request a read, the source first, and the text sent ahead while the reader types.
- The source stands first in the request and the words last, so the engine's prompt cache holds the source across questions. A second question about the same source costs its own tokens, not the source's. Before, the question stood first and the whole source was processed again for every question about it
- The reading pass is gone from
spark read. The answer's first line is the model's own reading of the source,[Language, kind], taken out before the gate: shown on stderr at a terminal as before, never on stdout, neither kept nor dropped. One request where there were two, and noedit-readturn before a read. The other grounded contracts keep their pass spark read --warm: the text sent ahead. The same source, held back and cut to the same part, goes in the answer's request shape without the words, for one token, and nothing is printed. A client calls it the moment the text is known, before the reader types, and the question then costs its own tokens. A turn of kindwarm, numbers only; no ledger record. The cheatsheet row and both completion files know it- On the box (gemma4-26b-a4b, a 12 kB man page), from the turn records. Before: a fresh question took 11.2 s in all, its first line after 7.9 s; a second question about the same source processed 2202 tokens of it again, first line after 7.0 s. After: a fresh question takes 8.2 s in all, one request; a second question processes 12 tokens, first line after 2.0 s; a warmed source answers its first question after 1.6 s. The grounding audition's three read cases (
tests/audition.py --ground --times 3): 9 of 9 right before, 9 of 9 after
v1.97.1
- S3: a code block on the page wrapped its long lines, so a drawing wider than the screen came apart, and on a phone the copy button sat on its first line. A block keeps its lines whole and scrolls sideways, and on a touch screen the button has its own room (#74)
- S3: on a phone the return key sent the message, so a prompt could not have a second line. On a touch screen return makes a new line and the button sends; with a keyboard Enter sends and Shift+Enter is the new line, as before (#75)
- S3: the page focused the message box by itself after + new chat and after an answer, which opened a phone's keyboard unasked, and + new chat left the chat list open over the new chat. On a touch screen the box waits for a tap and the list closes; with a keyboard the cursor is ready as before (#77)
- S3: on a phone the keyboard scrolled the page past its end, a band of nothing under the message box, and the page stayed there after the keyboard closed. While the box has focus the chat panel is sized to the visible part of the screen and the page is held at its top; when the keyboard goes, both are back where they were (#72)
- S3: + new chat and a chat pick did nothing while an answer streamed, and said nothing. They stop the answer first, keep it in its chat marked stopped, then do what was asked (#76)
- S3: the page pulled a reader back to the end of the chat at every word of a streamed answer, so the start could not be read while it came. The chat and a verb's output now follow the end only while the reader is at the end (#73)
v1.97
The page catches up with core (#71). Five things the terminal could do reach the page, and two cards nobody read leave it.
- https from the page:
httpsunder machine runsspark serve https own. In own mode the root's link and its QR show under machine and beside a new user's login QR, so a phone installs the root first - update from the page:
updateunder machine runsspark update. The header saysnewest vX.Ywhen a newer release is out:GET /api/mecarriesnewestfor the admin - What left this machine:
GET /api/statscarriessends, the rows ofspark stats --sendsfor the window, and a card shows them under activity, and under a user's settings - A user's rotated token comes back with its login link and QR (
POST /api/user/token), drawn as the add-user card draws them - Remove a user from the page:
DELETE /api/users/NAME(admin, acts, audited), a remove button on each row, armed by a second click. The box account is refused there - A verb that restarts the page (
update,serve https own) restarts it through its unit in one command, last. So the restart happens where the manager kills the verb with the page: systemd stops the unit's whole cgroup - Gone: the status line card and
GET /api/bar, the serve card's log tail,GET /api/logand thelogevent. Nothing read them - The page rule in CLAUDE.md loses "no new page features": a page change is the client side of a contract change
v1.96
- Monaspace Neon and Xenon are the v1.400 release's own web fonts, regular and bold, with their whole coverage (box drawing, Greek, Cyrillic), in place of the Latin subsets of v1.101 that Fontsource packaged. The site takes the same four files at the tag
- The tree's privacy grep reads text alone: a compressed font's bytes matched the e-mail shape once
- A closed pipe is an error spark handles, never a signal that ends it: SIGPIPE stays ignored, Python's default, and
spark ... | headends quietly at the top of bin/spark with the status it always had. The v1.94.1 guards in the voice come out, a terminal that closes under the face ends its drawing without a word, and a closed stdout mid-reply is no longer reported as the model dropping (#70)
v1.95
One look, one voice. The page in the browser wears the look of spark.forgewright.ai, and says what the README says. A newcomer meets one first question everywhere, and a second person joins from the admin's phone.
- The page takes the site's colors and type: an ink-blue night, a grey paper day, Atkinson Hyperlegible Next for the text, Monaspace Xenon for the headings and Monaspace Neon for the code, the wordmark with its cursor, the night/day switch in the bar.
lib/spark/forge/tokens.cssand the six font files (SIL OFL 1.1) are the one source of the look; the site copies them at the tag. The palette menu is gone (#68) - The page's words are the README's and INSTALL's, word for word, held there by docs_test: the login card, the chat's first screen, do and help. One noun for a conversation, chat. A user's help lists the three pages a user has. spark's face sits in the bar: asleep, awake, thinking while an answer streams, pleased when it lands (#68)
- One first question:
? how many files are hereon the site, as setup's own live question, in the README and inspark setup's closing lines, which are now the README's first three steps (#69) - The admin adds a user from the page:
POST /api/usersmints one asspark user add NAMEdoes, and the page shows the token once with its QR for that person's phone. A sixth acting row, audited (#69) - The admin's first screen says that the admin token opens the whole machine; a user's bar line names the machine and the version, not the models (#69)
v1.94.1
- S1: a reply read aloud ended the whole chat at its first sentence whenever the voice's player exited at once: no sound server at the console, a device that is not there. The write into the gone player's pipe raised SIGPIPE, which spark keeps at its default so
spark ... | headends quietly. The voice's threads block it for themselves now, and the player's pipe asks for none on macOS: the clip ends, the reply goes on, and a player that stops taking sound is said once, after the reply (#67)
v1.94
spark help opens with what spark is, more exactly: a local model by default, no account or telemetry, and a configured model server receives the text you send. The docs say the same where a new user reads first, and before the phone is shared.
spark help, README.md, INSTALL.md and the cheatsheet qualify privacy at first use: setup starts the engine, the page and the health check; the page is plain HTTP on one LAN address until you configure https; an ordinary user's token against the admin's; the login QR holds the admin token;spark serve httpsbare reports status (#62)- README.md's Documents list links each doc, and its License paragraph says what is MIT and where the other licenses are
- INSTALL.md explains each prompt key once, in the keys table
v1.93
v1.92 was tagged with its CHANGELOG section still headed v1.91.1, so release.yml refused it and it has no release page. Its two fixes stand under their own heading below. This release carries them, and two more:
- The bug form, README.md and INSTALL.md say GitHub issues are public, prefer
spark check --report, ask for made-up names and paths, and never a login QR screenshot (#59) - INSTALL.md names its sections in words. The page renders the docs' own subset of markdown, so its section links and one emphasis showed as typed. docs_test refuses a markdown link or an emphasis in a rendered doc now
v1.92
- S4: INSTALL led with a bare machine and README gave no requirements; both now open with the signed installer on a computer in use, and the first hour starts with a check, a question and a proposed command read before it runs (#60)
- S4: model sizes and memory budgets came after the install line, and the download range and the ten minutes were stale; both docs now say the supported systems, the tools, the sizes and the budget before it, and how to retry a download (#61)
v1.91
The page over https. A certificate pair you were given, or one spark mints for your home, and the page serves TLS on the same port. Every spark client follows, and trust is typed, never assumed. With it, what waited for a major: the admin boundary over plain http, four audit findings, the danger reading, the workflows and small fixes.
spark serve https CERT KEY: your pair, PEM files, the key private;--name HOSTputs the name in the links. The listener is TLS 1.2 or newer, one port and one scheme. A renewed pair is read again within a minute, with no restart; a pair that does not load keeps the old one, and the newhttpscheck row says so.spark serve https own: a root of this machine's own, constrained to its names, and a certificate under it for its names and address, minted with the openssl binary and renewed by the check timer. The root is served plain on the page's port plus one,GET /ca, for a phone to install;spark serve --loginshows its QR first, then the login's.spark serve httpsshows the certificate and who must trust it;spark serve https offis plain http again.- Over TLS the cookie is Secure, every answer to a name carries Strict-Transport-Security for one week, and the Origin check follows the listener's scheme. The page answers to the certificate's names and to
SPARK_FORGE_NAME, and its links say that name. spark client https://HOST:PORT --pin FINGERPRINTpins that page's certificate, or saves its root when the fingerprint is the root's;--ca FILEtrusts an issuer. A page whose certificate is not trusted is refused with its fingerprint and both remedies, nothing set.SPARK_CA_FILEis a bundle trusted beside the system store for every TLS connection spark makes. The saved trust lives under~/.config/spark/trust/, one file a host;spark client offforgets it. The machine that serves trusts its own pair.- Contract 3:
SPARK_FORGE_CERT,SPARK_FORGE_KEY,SPARK_FORGE_NAMEandSPARK_CA_FILE. Contract 9: the TLS listener and the root's.openssljoins the core packages on Linux. 42 check rows. - The page's code for the retired desktop app is gone: the browser is its one host. A connection that dies between requests no longer leaves a traceback in the page's log.
- Over plain http an admin action (a verb run, the do console, the soul written, the check refreshed) answers this machine and its own networks alone; a peer routed in from farther is refused with the remedy. Over https a valid session acts from anywhere. A reverse proxy is the peer: spark reads no forwarded header (#27)
- Four low findings of the 2026-10-03 audit closed.
spark user addno longer offers to claim older plaintext chats, andspark user claimreads a file through its own inode, never a link in its place. A new admin token lands in one atomic replace, the old one dead that instant. Every private file spark writes, a key, a token, a record, a log or a lock, is opened without following a link and comes back 0600.GET /v1/modelslists the served roles alone, nothing of the engine's own listing-spark client URLis a danger line: it stops this machine's engine and page and answers from another machine from then on, so the prompt line asks for the typed yes.spark client offstays plain. - The danger reading is linear on a separator-dense line. A pattern is walked only when the words a match of it needs are on the text, a stage read once is not read again, and the shell's operators are found by their first character. 100 kB of
ls;reads in a quarter of the time. - The
sendsandwatchdogrows settle like the servers' rows: red for a moment at a restart, said only when still red three minutes later. spark updatesays why a fetch failed: git's own last line under spark's, with no credential in it.- The chat's map of spark's commands spells every form out, as the prompt line's map does; the
a|bshorthand is gone. - A sandboxed run whose copy does not open is refused in spark's own words, with no path under the home. A refused apply writes the run's record once. The Linux retry after a partial delete has its test: a whiteout whose file already went is no change, and an opaque directory names only the children still there.
- The workflows follow their upstreams:
actions/upload-artifact7.0.2 andgithub/codeql-action4.38.3, pinned by sha (#53, #54). - A validation workflow runs the two commands the gate keeps apart: the real isolation section on Linux and macOS at every landing, and the compatibility harness against the pinned engine and weights once a week, its record kept as an artifact (#58).
tests/land.shlands main even when the remote refuses to delete the try branch: it says the branch stays and prints where main is.- LICENSE says the tale under
home/.config/spark/taleis CC BY-NC-ND 4.0, apart from the MIT grant; CREDITS said it alone.
v1.90.4
- S3: generation requests could wait without a deadline, and a client that had left could still take the model. Browser and API requests now share a small queue, an account limit and a deadline; busy callers get a retry hint, and expired or disconnected waiters start no work (#42)
v1.90.3
- S3: file references and continued threads read and decoded their whole input before trimming context. They now bound bytes read and records opened, keep the newest complete turns, and refuse nonregular context files without waiting for a writer (#43)
- S4: the sandbox suite could pass with its real isolation section skipped, and nothing ran a pinned engine against its weights. The suite now counts fixture, probe and real assertions apart, and a required mode fails without real ones; a harness runs the pinned engine and model through synthetic requests and records what it proved (#46)
v1.90.2
- S3: shutdown waited on exited processes as if they were running. It now recognizes zombies, reaps its children and uses a monotonic deadline; an unreadable process stays pending (#40)
- S3: shell and Python disagreed on repeated config keys and CRLF. Both now keep the first non-empty value in each file and read CRLF. Python previously kept the last duplicate. Keys starting with a digit are refused by both readers before any value is applied (#41)
- S3: a verb the page ran could leave a child holding its output, and so hold the runner and every later verb; its cap killed the verb alone. A verb now runs in a session of its own on a monotonic deadline, its group is killed when it ends, times out or the page hangs up, and a service it starts in a session of its own keeps running (#39)
- S3: platform fixtures inherited host init markers, repository layout and terminal assumptions. They now isolate HOME/XDG state. Repository and ownership cases keep conservative refusals; unavailable real host capabilities are named separately from failed assertions (#45)
- S3: the page checked Host on API writes alone, and compared an Origin by its host alone. Every known route now needs one valid Host this machine answers to; an API write's Origin, when sent, must match the request's scheme, host and port; a second or malformed Host or Origin is refused (#48)
v1.90.1
- S3: the first kept line of a read could take longer than its recorded total. Both times now include answer setup and the final output (#35)
- S1: a failed sandbox apply could discard the copy; it now stops, keeps the copy and reports how much completed. A staging failure applies nothing. A retry checks the changes still pending. A refusal's paths print as the review does, every control character an escape (#26)
- S1: two logins racing in one home could pair the account with the other login's cached key, sealing a record its owner cannot open. The cache now carries its name and token hash, every reader keeps the login it captured, and an older unlabelled cache is proven through the token before it is used (#28)
- S2: a reply that timed out after words arrived was lost from history; it stays once as an incomplete answer, and the next message continues its thread (#30)
- S2: a bearer token could follow a redirect to another origin; a credentialed request now stays on its scheme, host and port, and a redirect spark did not follow says so (#29)
v1.90
spark is an MCP host. The servers on its list offer their tools to the prompt line, spark do and /do in a chat, on any machine, and spark steps turns a person's words into calls of any program's tools. spark-shell's providers, the parts of its desktop, are the first servers. Contract 16.
spark mcp: the list of MCP servers spark may start,~/.config/spark/mcp.env, oneMCP_SERVER_<NAME>line a server.spark mcp add NAME PATH [WORDS],rm NAME,trust NAME on|off,call NAME TOOL KEY=VALUE,read NAME [URI] [--watch]. Bare, it lists every server and its tools, and keeps that list for the prompt line.- A server starts with no shell, in your home, with a named list of environment variables, its stdin and stdout the wire and nothing else. stdio only: there is no HTTP transport. MCP 2025-06-18 is pinned, the version spark-shell's providers speak; a server that answers another is closed. spark declares elicitation alone, never sampling.
- The gate before every call: the tool is in the server's list now, its arguments fit its schema, an argument naming a thing names one the server lists, and a person saw the step. A tool that may destroy asks the typed
yes; from a server that is not trusted, every tool does. spark, spark-shell and spark-acp are trusted by default, each when it lives in a clone of its own repository and only its owner can write it or a directory above it. - A credential is never a word on the line, never in a record and never sent to the model. A server's question is asked at the prompt, 3 a call and 6 a minute at most, a password without echo.
- Every call is one sealed audit record,
mcp/call: who, the server, the tool, a digest of the arguments and whether it failed. Never an argument or a result. - The prompt line reads the tools from the file
spark mcpwrote and never starts a server. A tool step is aspark mcp callline you read beforeEnter. Inspark doit runs through the same gate; over--porcelaina tool that asks the yes is refused, and inside--sandboxno tool runs. spark steps <words> < TOOLS: an MCP tools list on stdin, the tool calls out as JSON, held by a grammar made from the tools and checked again after. A tool's description never rides, only its name, its arguments and its title.- What leaves the machine has three new lines: the steps verb, the tool names and titles in the prompt line, and a tool's result in
spark do. Themcpcheck row reads the list:spark checkhas 41 rows. spark voice listenneeded nothing new: the dialog's talk chip runsspark voice listen --bufferand stops it withSIGINT.- Measured before the tag, gemma4-26b-a4b.
spark stepswith the 39 tools of the desktop's 12 providers: 66 of 66 wants right, against 39 of 66 for spark-shell's own want path, 3.0 s against 4.4 s at the median. The prompt line on all six systems: tools 94 to 100 %, spark's own 96 to 100 %, flag honesty 100 %, danger over-fire 5 to 6 %. Danger recall was 105 of 106: Void's "remove the packages nothing needs anymore" came back as words. v1.89.1 answers that case right 1 time in 5 and this release 4 in 5. Tagged on the owner's word, the bar short by that one case.
v1.89.1
- S3: the sends row said this machine's own second address was not the one configured once the servers had moved between the wire and the Wi-Fi, and the prompt said so in red in every new shell; an address this machine owns is this machine now, and the warning stays for a host nothing here names (#21)
- S3: a server that came up on a Wi-Fi backup's address stayed there when a slower wire took over, and the check only asked a person to restart it; the timer's check restarts the unit once the new address has stood 3 minutes, at most once an hour, and says so once (#20)
- S3:
spark client URLstopped the engine and the page but left the check unit, so a client ran a check every 5 minutes; it stops and removes the check unit too now, on launchd, systemd and runit (#19) - S3: a warning at the prompt was said in every new shell for as long as the alert file kept it, and on a client no check drops it; the widgets pass over a line a day old now, as a check does (#18)
- S3: tests/smoke.py imported shutil again inside main(), so a test that used it earlier in main() failed; the module's import serves it now (#17)
v1.89
The first real pass of the patch lane found one fault in its own fence, and the fence refused to let a patch carry the fix. That is the rule working: a change to the fence is a release a person tags.
- The fence's self-test read the tree's own CHANGELOG heading, so the first
## vX.Y.Zheading onmainfailed every patch in CI. The self-test builds its own CHANGELOG now. - The brief says how a pass resumes an issue a previous pass left
in-progress: from itstry/branch, never from scratch.
v1.88
Bug reports have a door, and small fixes ship without waiting for the next release. A major release, v1.88, is still a planned piece of work. A patch, v1.88.1, is one or more reported bugs fixed, tagged by a machine that holds a release key.
- Bug reports: GitHub Issues are open, with a form. Steps to reproduce are required. An agent reads each report within the hour, follows the steps, sets a severity and answers on the issue. Questions and ideas go to Discussions.
- Severity and the promise. S1 and S2 (spark unusable, or a documented behaviour broken with no workaround) are fixed and released within 24 hours of approval. S3 and S4 (a workaround exists, or a wording) ship in the weekly patch, on Sunday.
- A patch release is
vX.Y.Z.spark verandspark updateknow the shape. A patch never adds a command, an option, a key, a check row or a help row, andtests/patch_test.pyholds it to that, in the gate. tests/tag.shtags a patch by the CHANGELOG's word: S1 and S2 once the fix has rested 6 hours onmain, S3 and S4 on Sunday, only on a commit CI passed. It checks for itself that no protected path changed and that each bullet's issue exists, is approved and carries its severity. A second release key signs patch tags;allowed-signersandgetcarry both. Its private half is sealed to one machine and is never in the cloud.- CI ends in one
greenjob that needs every other: the name a rule onmaincan require. tests/land.sh --passedasks GitHub throughcurlwhenghis absent: a public repository answers without a token.
v1.87
The prompt line knows spark's own commands and stops doubting right ones. A small model that came back with no answer is held to one, and asked again if it still has none.
- The prompt line reads
spark help: every command, with what it is for. Before, it saw the names alone. Ask in your own words.? is everything ok with sparkisspark checkorspark status.? show the last answer againisspark last.? turn the prompt line offisspark off. spark helpsays what each command is for, in the words people ask with. These have a row of their own:spark status,spark ver --sbom,spark serve --login --new,spark stats --sendsandspark keys NAME KEY.TABcompletes the options ofspark verandspark stats.- A right command is no longer doubted. The hint said
the systemctl manual has no command restartaboutsystemctl restart, and the same aboutzypper search. spark had read the manual's list of commands cut short. Each false doubt also cost a second ask, and its wait. The update reads this machine's manuals again, once. - spark checks its own commands closer before one lands in your line. It asks again about an option spark does not have, such as
spark --ver, a wrong word, such asspark serve boot maybe, and a quote left open. - A value only you know is one word in capitals, as in
spark user add NAME. The hint saystype the NAME before Enter. - When the model explains a command in words, spark asks it once for the command itself, so the command lands in your line. That is one of spark's own commands, or a program it tells you to use.
- spark's commands answer for spark, and the system's for the system.
? add a user named aliceisuseradd, neverspark user add. - A question about a program you do not have gets the command that installs it, when you named the program or nothing installed does its job.
- A small model's silent thought no longer costs you the answer.
gemma4-e4bsometimes thought in silence and spent its whole reply there: 14 of 551 questions gotthe model gave no answer -- ask again. spark now holds the reply to its shape from the first word, so a thought cannot start. On the questions that had come back empty, none of 120 asks did, against 15 of 72 before. - If an answer still comes back empty, spark asks once more by itself. The row above your prompt says
no answer came -- asking again. - A reply cut after its command says so:
the reply was cut before its hint -- read the command before Enter. spark statshas ano answerline: the questions that got none, and how many were asked again.spark lastsays(no answer came)for one.- A new row of
spark check,answers. It warns when 2 or more questions got no answer in 7 days, and they are 2 % of your questions or more. It points tospark model list: a larger model answers more often.spark checkhas 40 rows. - More commands carry
!: a package cache cleaned, on every system. That isapt cleanandapt autoclean, withapt-getandaptitudetoo,dnf clean,yum clean,zypper clean,zypper cc,pacman -Sc,paccache -randbrew cleanup. spark serve offcarries!too: it stops the engine and the page.spark doasks you to typeyesfor it, and the page asks you to confirm.- The
!is steady on spark's own commands. A model markedspark update5 times in 12. spark now decides the mark on its own commands itself. - On Void, the prompt line names
svlogtailto read logs, where it is installed. It no longer namessvlogd, which writes them. It also knows how a service starts at every boot there: a link in/var/service.sv uponly starts it now. spark statussays what needs you. Its newcheckrow shows what the last check found:2 need you -- spark check,ok, 3 min agoornot run yet -- spark check. It runs no check itself.- The prompt line was measured again on 6 systems. With
gemma4-26b-a4b, spark's own commands are right 204 times in 204. They were right 2 times in 3. Each system's own tools are right 96 to 100 times in 100. Every dangerous command is marked, and 1 answer in 838 holds an option that does not exist. A second ask is 1 answer in 100, where it was 6. Withgemma4-e4bno question went unanswered in 839, and spark's own commands are right 96 times in 100.
v1.86
- A machine that becomes a client stops its own page's server.
spark client URLstopped the engine that ran there, but the page's server kept running and listening on your network, andspark serve offwould not stop it. Both are stopped now, and the run says so. On a client made by an older spark,spark serve offstops it.
v1.85
- The prompt line names your system's own package manager. On a machine with 2 installed, an openSUSE that also has
dnf, spark named the first one it found. It now names the one your system uses. - The prompt line is measured on Fedora and openSUSE: 58 questions each, graded against each system's own help. Fedora 55 of 58, openSUSE 53 of 58, and all 18 dangerous commands flagged. The model was
gemma4-26b-a4b.
v1.84
- No false alarm after an update. The engine and the page are red for a moment while they restart and the model loads. Your prompt now says so only when they are still red 3 minutes later.
spark check --historystill lists the change. - Fedora and openSUSE install on a minimal system. Setup stopped there with
tar: command not found. spark now installstarwith its other packages. - Fedora and openSUSE are proven on a real machine: a minimal Fedora 44 with SELinux enforcing, and a minimal Tumbleweed. Each ran the install line, the engine on its GPU, the services and the firewall step.
- With a cable and Wi-Fi both connected, spark serves at the address your default route uses.
docs/INSTALL.md, section 5, says so.
v1.83
For a machine that is already yours: spark asks before it adds keys, uses a llama-server you already run, and says its terms first.
spark setupasks before it adds its keys to your shell. It lists each key, what it replaces there, and the 1 second your shell waits after Esc. A no adds no key.--yes, or no terminal, asks nothing, as before.- The one line in your rc file is added either way. With no key,
? words,TABcompletion and the row above your prompt work the same. spark keys, new. It lists the keys spark added, what each does and what it replaced.spark keys ask Esc amoves one, andAlt-aandCtrl-gwork too.spark keys ask noneleaves the key to your shell.spark keys resetbrings the defaults back. The names are ask, recall, height, listen and stop.spark keys offbinds none of the 5 keys. Your shell keeps its own keys there, and its own wait after Esc.spark keys onbinds them again. The choice is kept asKEYS=offin~/.config/spark/keys.env.- A hint at your prompt never names a key that is not bound.
SPARK_OFF=1in the environment starts one shell without the prompt line. It is now in the docs.- Your own llama-server.
spark client URLuses allama-serveryou already run, andspark setup --engine URLdoes it on a new install. At a terminal, setup offers one it finds onSPARK_PORT. spark downloads nothing for it.--enginerefuses a spark machine's address, and one that does not answer. - spark never stops a server it did not start.
spark client URL,spark uninstallandspark serve offend spark's own engine alone.spark client offrefuses while another server holds the port. spark client URL --key-file FILEnames the file with the server's key. A client no longer makes a key of its own.- A client of your own server keeps its threads and memory here, from the first chat. A client of another spark machine still logs in there.
spark client,spark model,spark status,spark serveand thepeerrow ofspark checksay whose engine it is.- The context size comes from your server unless
SPARK_CTXis set. The chat names the model when the server gives it no alias. - Ollama is not supported in this version.
- The notice.
getandspark setupsay three lines before anything changes: spark is MIT licensed with no warranty, a model can be wrong, and what you run or accept is at your own risk. - At a terminal
spark setupasksgo on? yes/NO:once. A no changes nothing. With--yes,SPARK_YES=1or no terminal it asks nothing and goes on. - On a new install the three lines print once.
getshows them, and the setup it starts asks only the question. - A machine already installed sees the lines once, at its next
spark update, with no question.spark ver --creditsshows them any time. - Fixed:
spark uninstallleft the spark line in an rc file that is a symlink.
v1.82
Your prompt tells you when something breaks, and when it works again. spark check --history shows what changed and when.
- A warning at your prompt. When a
spark checkrow turns worse, your next prompt says so on the line spark writes, with the fix:! serve: nothing answers at 127.0.0.1:8080 -- spark serve on. When it heals:* serve: ok again, after 21 min. Each is said once in each shell. A shell you open later shows a warning that still stands.spark offkeeps both quiet. Afterspark awakenthe face is alarmed, then pleased. spark check --historylists every change, newest first, andspark check --history NAMEthose of one row. It keeps the row's name, its state and the time, never its text. Changes are keptSPARK_HISTORYdays, none when it isoff.spark clear --historyforgets them.spark check NAMEno longer hides the other rows. Before, the bar and the page showed only the rows you named, for up to 5 minutes.- The knowledge row asked for
spark update, and the update did nothing. There were two faults. The row warned while programs still waited to be read, which is normal after an install. That is ok now, and the row says how many wait:37 waiting their turn. On Void the 5-minute check took itself for a person's run, so it never read them. It does now. - The knowledge row warns only when this machine changed since the last read, and
spark updateheals it. An update no longer reads every program again. On a machine that uses another machine's model,spark updatereads until nothing waits. - Small fixes. A pasted answer shows the pleased face.
spark awakenshows a moving bar while a model loads. In zsh,Ctrl-Cat a prompt no longer leaves a face on the screen. In bash it still can.
v1.81
Two more Linux families: Fedora and openSUSE. The install line is the same one.
- Fedora. Packages come through
dnf. Install the basics withsudo dnf install -y git openssh-clients python3 curl, then run the install line. - openSUSE Tumbleweed and Leap. Packages come through
zypper. Install the basics withsudo zypper install -y git openssh-clients python3 curl. - On both, the services work as on Debian and Arch, and so does
spark serve boot on. - CI runs the install line in a Fedora container and in a Tumbleweed container. Neither family is proven on a real machine yet: the engine, the GPU and the services.
- Both turn a firewall on, and spark does not change it. To let other machines reach the page, run
sudo firewall-cmd --permanent --add-port=8081/tcp, thensudo firewall-cmd --reload. - Not supported in this version: the atomic systems, such as Fedora Silverblue and openSUSE MicroOS. Rocky Linux and AlmaLinux are treated as Fedora and are not proven.
docs/INSTALL.md, section 1, sets up a bare Fedora machine step by step. A minimal Fedora installed over Wi-Fi boots with no Wi-Fi, and the section says what to add.
v1.80
A plain voice, and a face you see more of. Both start with spark awaken. spark look off turns the face off.
- A plain voice.
spark voice onspeaks in this machine's own voice, with no effect over it. The radio, choir, eight bit and robot effects are gone. Your machine keeps its speaker, andspark awakenlets you hear up to 4 before you keep one. - The face answers. On spark's line above your prompt it follows the mark: pleased on an answer, alarmed on a danger or a failed command, puzzled when nothing came, listening on
Esc v. - The face stays through a reply. In chat,
explainand a question it opens the reply, talks while the text comes and ends pleased, puzzled or alarmed. The lines after the first start at the left edge, so code copies clean. - The face rests above your prompt, and
Entererases it. In zsh it blinks, falls asleep after 5 minutes and wakes on a key. In bash it stays still. It needs a blank line above the prompt and the height set to the prompt's lines:Esc k.Ctrl-Cat a prompt can leave one face on the screen. - The wait shows what spark is doing: thinking, reading a text, planning a step, loading a model or updating. At a terminal,
spark edit,spark read,spark drillandEsc rshow a wait too.
v1.79
Each test runs once, in the right place. Nothing is tested less. This release changes how a commit lands, not what spark does.
- A commit takes seconds. The pre-commit hook runs the fast checks: privacy, secrets, syntax, shellcheck and the docs.
- The full tests run in CI, once for each commit.
sh tests/land.shsends your commit to atry/branch and waits. On green it movesmainto the same commit. On redmainis untouched. - A push to
main, or of a release tag, is refused until CI passed for that commit. A release refuses such a tag too. tests/gate.shholds the one test list.sh tests/gate.sh fullruns all of it on your own machine.- CI answers sooner. The two long suites run side by side, and a tag push no longer runs the tests a second time.
v1.78
Six more fixes from the audit.
- The page's server answers a chat only for the models it serves. Any other model name is refused, and nothing is sent.
- A request with a method the server does not know, such as HEAD or PUT, gets the same "not found" as an unknown page. HEAD opens no stream.
spark user claimnever writes over a thread you already have. It leaves the older copy where it is and says so.- The look file, the bar's cache and the other small state files are private to you, whatever your umask. So is the proof file the bash prompt keeps.
- An app that drives
spark docannot send data off this machine outside the sandbox:scporrsyncto a host,nc, a token in a URL. - The Debian image in the tests is pinned by its digest.
v1.77
The last four fixes from the audit.
- A new token can no longer lock you out. The token's check and the key it opens are one file now, so after a crash the old token or the new one works.
- One damaged line in a thread no longer hides the whole thread. spark skips it, reads the rest and says so once.
- The fixes the prompt remembers hold back the same secrets as everything else, such as a GitHub token given to a command.
- A weekly check opens an issue when the voice's engine, sherpa-onnx, publishes a security advisory after its pin.
v1.76
spark editdrops escapes and control characters from the model's text too: a rewrite, a completion and an answer. A file's CRLF line ends stay.
v1.75
Security fixes from an audit, and docs that say less.
spark doand the prompt line catch a command hidden from the danger check: a quoted or rewritten command word ("rm",$x), a wrapper (timeout,doas,env), and a command handed to ssh, a session (swaymsg exec,tmux) or a later run. Each needsyestyped. An app drivingspark docannot run one outside the sandbox.- A reply's terminal escapes never reach your screen or a pipe. A model can no longer write to your clipboard or clear the screen.
Esc rreplaces secrets in your shell history with[held]before it is sent.spark updatechecks that a tag's name matches the release it signs, and moves forward only. It skips a bad tag and says why, instead of stopping.- A first install checks the release against the key inside
get, not a file from the clone. - The voice makes every sound inside spark. No spoken text is on a command line that other users could read.
- The page's server logs a command by its sha256 and length, never its text. A user sees only counts from the check report. It limits connections, event streams and sessions.
- A model row in your own
models.envneeds a real sha256. - Every document is shorter and plainer. The README, INSTALL and the cheatsheet state each limit: plain HTTP between machines, what the admin token can do, and which commands hold secrets back.
docs/TROUBLESHOOTING.mdanddocs/IDEAS.mdare gone. The tour and the apps are now sections of the README: A first hour, and Apps.
v1.74
The voice in the chat works the way it reads.
- With
spark voice on, the chat reads every reply aloud from the start. Before, it waited for/aloud./aloudnow stops it. - With the voice off,
/aloudmakes the chat read its replies aloud, until the chat ends: in the machine's own voice, or the clear one when awaken kept none./aloud onand/aloud offwork too.
v1.73
The face talks, and awaken makes only what shows.
- The face talks. On an awake machine, a chat reply read aloud starts with the face. While a sentence plays, its mouth opens and closes:
(o.o),(oOo). It rests between sentences, after the last one, afterEsc xand once you type. A reply that is not spoken still starts with*. spark awakenmakes only what shows: a personality and a face. It no longer writes lines for spark to say.- Every mood of the face is still made and kept, for later uses.
spark wordsis gone.spark lookshows the face.- When awaken offers a voice, the sample says one fixed sentence: "Hello. This is how I sound."
- The project's checks run on newer GitHub tools (checkout 7, CodeQL 4).
v1.72
One voice and one way of looking: spark says less, in short plain English, and says it in one place.
- Every help text and message is rewritten short. A help line says what the command does and what to type, not how it works.
- One mark starts a line spark says:
*when spark speaks,!for a warning or a failure. An error is! ..., neverspark: .... A mistake you typed isspark VERB -- no word X; spark VERB -h lists them. - The face shows only while spark waits. A reply, a refusal and the chat carry no face.
- No canned lines. spark prints no greeting, no goodbye and no news at the prompt or in the chat, and the voice no longer speaks them.
spark words greetprints nothing. spark chatopens with one line,* chat with NAME -- Esc ends, /help lists commands, or* continuing "TITLE" -- /new starts fresh, Esc ends. It ends with nothing printed.- At the prompt, spark writes in one row above your line, and each new line replaces the last. A failure says
* failed (1) -- Esc s asks why, andEsc sthen says* Enter explains the error. A hint no longer ends with the manual it was checked against. spark checkprints only the rows that need you, with their remedy, then the totals. Nothing to fix prints the totals alone.spark check --allprints every row, as before.--porcelain,--reportand the exit code do not change.- A remedy that said
./bootstrap.shorsh install.shnow saysspark update. spark verprints the logo and the version, as a login shows them. The credits moved tospark ver --credits.spark dono longer prints adriving withline, and the end is* donewith the summary. A check after a step is called a check.- A model is named by its name in the list, such as
gemma4-26b-a4b, never by its file name: inspark,spark status,spark check,spark do, the chat and the page.--porcelainoutput keeps the file name for programs. - A command that changes nothing prints nothing:
spark look,spark height,spark reveal,spark onandspark offset to what they already are. A question you answer no prints* nothing changed. spark voice -hends in one line: the first time, spark downloads about 600 MB, 586 MB on Linux and 602 MB on macOS.spark setupasksread aloud to you (for low vision)? [y/N]and ends withopen a new shell (exec $SHELL), then try:and 3 lines./api/healthaddsnames, each role's model by its list name, besideroles. The page's header shows them.
v1.71
The voice keeps pace with the reply.
- A chat reply read aloud is shown as it is spoken. In clear mode, and in mode on after
/aloud, each sentence goes to the voice as the model writes it. Its text appears as its sound starts, at the pace of the voice.spark voice ratesets both. - The text follows the voice only with the reveal on, at a terminal of this machine. Over ssh the sound plays on the far machine, so the text keeps the reveal's pace there. With the reveal off the text shows at once and the voice reads behind it.
- A sentence whose sound is late by 8 seconds lets the text go on at the reveal's pace for the rest of the reply.
- A sentence ends at
.,!or?and at a line break, so a list item is its own. A number such as 3.14, a short list of abbreviations such as e.g. and Dr., and inline code never end one. - A code block is said as "a code block, 4 lines" in clear mode, and skipped in mode on. A long run with no end is cut at a comma.
- The next sentence is made while one plays, so the voice no longer stops between sentences.
spark doand the other surfaces gain this too. - The voice stays loaded for a chat. It loads as the chat opens, while you type, and every sentence is made with no new start.
spark doloads it once too. - On Linux the voice is one stream to the sound card for each run of sentences. The card is opened once, with a short silence first, 250 ms, and kept awake with silence between sentences. So no sentence loses its first word to a sleeping card.
SPARK_VOICE_LEAD_MSin the environment changes the silence, 0 to 1000. On macOS each sentence plays on its own, the silence before the first after a quiet moment. - The first sentence goes at its first comma, semicolon or colon, or after six words with none, so the first sound never waits for a whole sentence. The sentences after it stay whole.
- The voice is Kokoro's full-precision model now, 350 MB, not the smaller int8 one. It is about 4 times faster on a plain x86 CPU. The engine is 586 MB on Linux and 602 MB on macOS. A machine with the voice on fetches it at
spark update, and the old one goes. - The voice engine's own warnings, such as a sound it skips, no longer print in the middle of a reply.
- A new reply stops the old one's voice. Ctrl-C on a reply stops its voice too.
/againspeaks the last reply sentence by sentence. spark updatewaits for a big model to stop. On Void, spark'ssvcalls that wait now allow 60 seconds, not runit's 7.
v1.70
spark do takes a step of several lines, shown whole. And spark speaks and listens: spark voice, a plain clear voice for low vision, or the machine's own.
- A
spark dostep is a line or a block of several lines, such as a here-document that writes a script. A block is never joined into one line. - A block is shown whole before you confirm it. The step line names its first line and
(24 lines), then every line follows, numbered. A line break can no longer hide a second command. - Danger is read on every line of a block. One exception: a step that is only a here-document writing one new file, in a directory reached without a symlink, is not "can destroy data". Anything before or after it, or a file that is there already, keeps the danger.
rreads a block again, every line numbered, then asks again. In clear mode it speaks each line.- Keeping a voice at awaken leaves clear mode on when it was on.
eon a block opens it in your editor ($VISUAL,$EDITOR, else micro, nano or vi), as a 0600 file removed after. With none,esays so and the step is unchanged. A line is still edited in place.- A block is 16 kB at most, a line 4096 characters. A line feed is the only control character a block may hold.
- Contract 15: the step event carries
lines, 1 for a line and K for a block of K lines. - The page shows a block whole, every line, and runs it under the same rules.
- v1.69's "write it as one line" retry is gone, and the run no longer stops on a step of several lines.
spark voicereads aloud and hears a question.spark voice clear|on|offsets the mode,rate Nthe clear voice's speed,testplays one line,listenwrites one spoken question out andstopends the speaking.- The engine is downloaded only when you turn the voice on: about 369 MB on Linux and 384 MB on macOS, into
~/.local/share/spark/voice. It is sherpa-onnx with the Kokoro voice, Whisper base and the Silero voice activity detector. Each part is pinned invoice.envand checked by size and sha256 before a byte is unpacked. spark voice off --removedeletes the engine.spark uninstallremoves it with the rest.- Clear mode is for low vision, and it works before
spark awaken.spark setupoffers it in one question, default no. - Clear mode reads the prompt line's command with its symbols, "du, dash a h, tilde, pipe, sort". A danger line's warning comes first. It reads the hint,
spark do's steps, the last lines of each output, the end of a run, the chat's replies and the errors. Everything it says is printed too. - On mode is the machine's own voice, never a plain human one.
spark awakenmakes it from the temperament: plain is a radio, warm a soft robot choir, playful eight bit and terse a robot. It plays the hello line in it and asks keep, again or none. Esc vlistens at the prompt and in the chat. A pause ends it. The words land in your line, on an empty one as a?question, and nothing runs until you pressEnter.Esc xstops the speaking.- A spoken yes never confirms a step that can destroy data. That stays a typed
yes. - A screen reader running, VoiceOver, Orca or speakup, keeps clear mode silent, and spark says why.
spark voice clear --anywayspeaks beside it. - Listening is push to talk alone: no wake word. The recording is deleted once Whisper has written it out, and nothing leaves the machine.
- The chat:
/aloudspeaks every reply, or stops, and/againsays the last one again. Mode on speaks the greeting and the goodbye. Escon an emptychat>line ends the chat, asCtrl-Ddoes. With words on the line,Escleaves them as they are.- The
voicerow replaces theaudiorow. It isnawhile the voice is off. On, it says the engine is here and is the pin's, a player and the listener, and a screen reader when one runs. - The SBOM names the voice runtime per flavour and its 3 models.
v1.69
One look switch, and no spark quiet. Fewer switches, each doing one thing.
- Bare
sparkis one line now: what answers, the model and its URL.spark statusis the full report, as before. spark serve onis brief now: one line for the engine and one for the page, once each answers. The steps to join from another machine are inspark serve --login, as before.spark quietis gone.quietis an unknown word now, like any other. The greeting and the news follow the look alone.spark lookis one switch:spark look on|off|autosets motion, colour and words together, in one key,SPARK_LOOK. It isoffuntilspark awakensetsauto. Each part keeps its ownautorule, soNO_COLORstill keeps the colour off underauto.spark look motion|colour|wordsandspark look revealare gone.spark revealandspark heightstay as they are.- Bare
spark lookshows the switch, the height, the reveal and whether the machine is awake. - An old
site.envorspark.envthat holdsSITE_QUIET_START,SITE_QUIET_AUDIOor aSPARK_LOOK_*key still loads, and spark ignores those keys. - The
audiorow says only which player is here. spark doasks once more when the model says done before any step ran. A second done ends the run and saysnothing ran.spark dogives each step 600 tokens, room to write a short file withprintf. A reply the cap cuts says so, and never that the model returned no JSON.- A step written over several lines is no longer joined into one in silence. The model is asked once for one line. A second time, the run stops and says so.
- The chat knows the model that answers it: the machine that serves it names it.
v1.68
Gemma 4 E4B is the standard with a GPU or without.
- A row of 4B working parameters or fewer counts as a small file under the speed cap. That covers a Gemma edge model's effective size (
-e4b) as well as a MoE's active size (-a4b). Most of E4B's 5 GB file is embeddings that are looked up, not computed, for each token. - So a machine with no GPU and a budget of 8 GB or more now gets
gemma4-e4b. It gotqwen3-4bbefore. On the maintainer's box with the GPU switched off, E4B writes 16.8 tok/s and qwen3-4b 22.1. qwen3-4bserves a budget of 5 to 7 GB, on every machine.
v1.67
The model list, revamped. Every row earns its place by a measurement on the box, the order is the priority, and Gemma 4 E4B is the standard.
- The list's order is the priority.
autotakes the first tested row that fits, for the prompt line and for the chat model beside it. It took the largest before. - The ladder:
gemma4-26b-a4bfor a machine with a 19 GB budget,gemma4-e4bthe standard,qwen3-4bwithout a GPU,qwen3-5-2bfor a small machine. - New:
gemma4-26b-a4b, the best measured. It scores 90 % on the prompt line's tools and 24/30 on grounding, as fast as E4B, since only 4B of its 26B work per token. - New:
qwen3-5-2b, which replacesqwen3-1-7b(56 % on the tools against 25 %), andqwen3-5-4b, the best on spark's own verbs at 71 %. - A MoE with 4B active or fewer counts as a small file under the speed cap. Only
-a3bcounted before. - 28 rows became 11. Gone: Qwen2.5, Mistral 7B and Nemo, Phi-4, DeepSeek-R1 distills, SmolLM2,
qwen3-4b-thinking,qwen3-1-7b, Llama 3 and Gemma 3. Each was older than a row that replaces it, or thinks where spark asks for none.gpt-oss-20bwas measured and marked nearly half the safe commands dangerous. - Measured and not added: Qwen3.5 9B (78 % on the tools), Ministral 3 8B (79 %) and Ministral 3 3B (71 %). E4B scores 85 %.
- A row you chose by name and that is gone comes back as your own:
spark model add URL --license "NAME URL".
v1.66
Gemma 4, the edge models. Google's E4B joins the tested rows under Apache-2.0, and E2B joins by name. A model that thinks with its thinking switched off is held.
gemma4-e4b, 5.0 GB, 8 GB of RAM, tested on the prompt line. On a GPU with a budget of 8 GB or more,autonow picks it overqwen3-8b. Measured on the box, it is ahead on the prompt line, the editor and grounding, and it writes 1.4 times as fast.gemma4-e2b, 3.2 GB, 6 GB of RAM, by name:spark model gemma4-e2b. It is the fastest row on the box, at 44 tok/s, butqwen3-4bis ahead of it on the prompt line, soautokeeps the 4B.- The prompt line's model runs with a thinking budget of 0, and every JSON request carries
reasoning_budget_tokens: 0. Gemma 4 E2B opened a thought on its own and spent the reply there: 35 % of its prompt line answers were empty before, none after. - The grounding scores were measured again, 3 runs over the 10 cases:
qwen3-4b18/30,qwen3-8b19/30,qwen3-14b19/30 andgranite-4-2-8b23/30.
v1.65
The chat, awake. An awakened machine greets you in spark chat, wears its face on every reply and says goodbye. Every machine says which conversation the chat goes on with, and the chat gains four commands.
- The continuing line, on every machine:
continuing "<first words>" (N min ago) -- /new starts fresh. The chat went on with the newest thread before too, in silence. - Awake, the chat opens with the face and a greeting from the machine's own words, then the continuing line, then
/help lists the commands; Ctrl-D ends. Unawakened, the opening ischat -- /help, Ctrl-D or /q endsand the continuing line. - Awake, each reply starts with the face, and its lines wrap under the text. The face is thinking while a reply is on its way, idle on a reply and puzzled on an error or a refusal. The puzzled face says its hint as a whole sentence.
- Awake,
/q,Ctrl-Dand the quit words end with the machine's goodbye line and its pleased face. Unawakened, the chat still ends in silence. /copy [N]puts the last reply, or the Nth from the end, on the clipboard:pbcopy,wl-copy,xcliporxsel. Without one it says so and points at/save./save [FILE]writes the conversation as plain text, by default~/spark-chat-YYYY-MM-DD.txt. It never overwrites a file, and the file is 0600. It says the path and the turn count./read @FILE [question]answers about the file the wayspark readdoes, every line quoting it, and the exchange lands on the thread./do GOALand/do --sandbox GOALhand the goal tospark do, each step confirmed, then return tochat>.@FILEin an ordinary message is unchanged: a free answer about the file.
v1.64
One server, and services that hold. The engine and the page are one verb, and the services behave the same on runit, systemd and launchd.
spark serve on|offruns the engine and the page together, and remembers it:SPARK_SERVICEandSPARK_FORGEare written as one.offstops both and keeps them down. Barespark serveshows what answers, the models, the page's URL, the units, boot and share.- The settings and actions of the servers live under the same verb:
spark serve boot on|off(up from boot),spark serve share on|off(one engine for every OS user),spark serve --login(the page's URL, a QR and the token),spark serve --login --new(a new admin token) andspark serve --audit [N]. spark model --chat NAME|auto|none|listchooses the chat model.spark statusshows what the engine answers.spark brain --porcelainstays for a program.spark client offmeans off. The other machine is never asked again, even while it answers, untilspark client URL.- The old spellings still work and appear in no help and no doc:
spark forge on|off,forge --print-url,forge token --new,forge audit,spark ember,spark headless,spark shareandspark brain. spark serve onnever starts a second server beside a unit. A loaded or disabled unit is started through its init, then waited on.spark serve --foregroundtakes over from a server spark started by hand. When a server spark did not start holds the port, it still exits 78.spark serve offalso stops a server spark started by hand when the unit is down.spark updaterestarts the services on a new tree, even when the clone was already at the tag.- The LAN address is never a link-local 169.254 one, as Void's dhcpcd gives before the real lease. The servers wait for the real address.
- A runit service that keeps crashing is held, and the
servicesrow warns. spark model NAME, the chat model andspark benchnever say ready while the old model still answers.- The pid files are 0600, and spark checks a pid is its own before it signals it.
- The runit services are written
downfirst, so a half-written service never starts. The check's service keeps its errors in its log. tests/finish_test.shproves the runit finish scripts.
v1.63
The hand-back leaves another tool's look alone. It takes back only what spark itself made, by its own record.
- The motd and
/etc/issuechange only when spark's own.origcopy is there. A quiet login from spark-shell stays as it is, on everyspark updateandspark uninstall. - On macOS only the profiles spark made leave Terminal.app. The
spark-shellprofile stays. - A line in
/etc/rc.localfrom spark-shell is not called yours to delete. - spark-shell's marked lines, drop-ins, unit and copies are never matched.
v1.62
spark is the AI at the prompt, and nothing else. The machine's look leaves it: the console's palette and font, the Terminal.app profile and a quiet login and boot.
spark theme,spark fontandspark quiet login|bootare gone.themeandfontare unknown words now.spark quiet startandspark quiet audiostay: they quiet spark's own lines.spark updatehands the look back, once. The console gets its stock palette and font, the boot is loud again, and the motd and/etc/issuereturn. On macOS the spark profiles leave Terminal.app. It saysthe look is off this machine now.spark setuptakes no--theme. An oldsite.envthat holdsSITE_THEME,SITE_FONT_FACE,SITE_FONT_SIZE,SITE_QUIET_LOGINorSITE_QUIET_BOOTstill loads, and spark ignores those keys.spark checkhas 39 rows: thetheme,fontandquietrows are gone.- The page keeps its own palette picker. Its admin cards for the theme, the font and a quiet boot are gone.
- The 9 palettes leave the tree.
~/.config/spark/theme.envstays a plain palette file any tool may write:spark verdraws its logo in it and the status line takes its accent, when it is there. - Arch and Void no longer install
kbdfor spark. - The docs are tested for two more rules: no sentence over 30 words, and prose within 72 columns.
v1.61
- An awakened machine draws the scanner and its face on every terminal, ssh and the console included. Before, auto kept the plain dots there, so a box used from its own console or over ssh never showed a face.
spark look motion offkeeps the dots. - The written rules are fewer: 94 in
CLAUDE.md,AGENTS.mdand the voice sheet became 53, each in one place.
v1.60
- Any
rmorunlinkis marked!now, inspark doand at the prompt line, andspark doasks for the typedyesbefore it runs one. Before, only a recursive or forcedrmwas marked.rmdir,docker run --rmandrmas a word inside another command are not. - The built-in soul no longer says that nothing you tell spark leaves the machine. A client sends to the machine it uses, so the sentence was not true everywhere.
spark awakenrefuses a personality paragraph written in the first person: the soul speaks to the model as "you", and the shipped paragraph stands in.spark helphas a group of its own, the living prompt: awaken, look, words, height andEsc k. Four help lines no longer join two clauses with a semicolon.
v1.59
spark can come alive at the prompt. spark awaken gives this machine a personality and a look. Until you run it, spark works as it did.
spark awakenasks one question, the temperament: plain, warm, playful or terse. The model writes the lines spark says and picks its face. Every line is checked, and a shipped line stands in for one it refuses.- Awake, a wait for a reply is a scanner with a face:
* (o.O) [ = ]. From 2 seconds it shows the elapsed seconds, and from 15 seconds one sentence. Over ssh and on the console the dots stay. - Awake, a model that is loading shows a bar with an estimate, from its last load. The bar stops short of full until the model answers.
- Awake, the first prompt after 4 hours away greets you, and a change shows once in any shell: the model asleep or awake again, runs waiting for review.
- Awake, a failed command that ran over 30 seconds says how long:
* failed (1) after 4 min -- press Esc s to ask why. - Awake, colour comes from a built-in palette of bold, dim, red, bold red and green, where you export no colour of your own. Dim is only for decoration.
SPARK_OK_SGRandSPARK_TROUBLE_SGRjoin the 3 colour exports.spark checkpaints its rows with them when awake.SPARK_YOU_SGRis reserved, and nothing paints with it yet.- A reply revealed at a pace breathes at the punctuation when awake. The pace you chose is the average with the pauses in it.
- Awake, a reply's
inline codeis bold and its list lines hang under their first word. spark lookshows the parts: motion, colour, reveal and words.spark look PART auto|on|offsets one, andspark look offturns motion, colour and words off at once. A pipe never sees a frame or a colour.spark wordsshows the lines and the faces, andspark words editchanges the lines. A line with an escape, a character outside ASCII, more than 72 characters or a secret's shape is never said.- The soul has two parts after awaken: spark's fixed core and the personality paragraph awaken writes.
spark soul editchanges the paragraph, and--corethe whole soul. A soul file of your own is kept. - The
lookrow inspark checksays whether this machine is awake, and warns when a line of the words or faces file can never print. - A two-line prompt, such as starship's default, no longer loses its first line to the hint.
spark height Nmoves spark's row up to 5 rows above the line you type on.Esc kat the prompt cycles it through 1, 2 and 3. - A hint is a whole sentence: a capital first letter and a full stop.
Ctrl-Catchat>clears the line, as a shell does.Ctrl-Dand/qend the chat.explainhonours--reveal.spark bartyped outside tmux prints plain text, without tmux's markup.- bash keeps an
EXITtrap your rc set before spark's hook. - The bash and zsh prompt lines say the same words when no hint comes.
- setup ends with one suggestion, and
spark updateprints it once:next: spark awaken -- give this machine a personality and a look. - setup's first question shows its dots while the model answers.
- setup and
spark modelspell licence the British way. - An apply run of
bootstrap.shends withN changed, notN to do.
v1.58
A clean boot on Void ends on the login prompt alone, and a late network no longer leaves the model and the page off.
- runit itself prints a few lines no file reaches, and so do stage 2 and the RAID and btrfs scans. After a clean boot the login prompt clears them: 2 marked lines in
/etc/sv/agetty-tty1/conf. - A clean boot is one with no warning, no error, no fsck finding and no critical kernel line. Any of those leaves a mark in
/run, and that boot keeps its whole screen. - The welcome line is no longer erased by hand: the clear takes it.
/etc/rc.confholds 10 marked lines now. - A shutdown prints as Void prints it again. Silent, its pause looked like a hang before the reboot.
spark quiet boot offremoves the marked lines from the 3 files.- A machine whose network comes up late no longer loses its model and its page. They waited 5 minutes for a LAN address, then stopped for good, and nothing started them again. Now they wait as long as it takes, on every Linux and on macOS.
v1.57
A quiet boot on Void is quiet.
spark quiet boot onon Void now reaches runit too. The kernel'squietnever did: runit's=>lines, the sysctl values, a module list and the fsck summary stood above the login prompt.- spark appends 10 marked lines to
/etc/rc.conf, beside the 3 in/etc/default/grub. runit reads that file before its scripts, at boot and at shutdown. - A warning, an error and a fsck that finds a problem still print. Quiet never hides a problem.
spark quiet boot offremoves the lines from both files. Thequietrow is ok only when both sets are whole.
v1.56
The prompt line knows more of this machine, and spark do works with a chat model that thinks.
Measured on the box (Void, qwen3-4b, 97 audition cases), the prompt line answers as well as before:
| v1.55 | v1.56 | |
|---|---|---|
| right command on Void | 82 % | 81 % |
| right spark verb | 64 % | 61 % |
| dangerous commands marked | 11 of 11 | 11 of 11 |
a ! on a command that only reads | 9 % | 9 % |
| command in your line (median) | 1.2 s | 1.2 s |
qwen3-8b as the prompt line's model got 62 to 68 % of the commands right on Void, Debian and Arch, against 74 to 79 % for qwen3-4b, and took twice as long. It stays the chat model. Telling the model to add sudo cost 7 points and tripled the wrong !, so it is not in.
- Your question stays on screen while spark thinks, in bash and zsh. The command replaces it when it lands.
- Ctrl-U clears the hint row above an emptied line. It still keeps the deleted text for Ctrl-Y.
- The index reads this machine's services too: runit's, systemd's and launchd's, by their own directories. A question about a service sees the names here, so
sshfindssshdon Void. - When the model proposes a program that is not here, spark asks again and names the installed programs that do the same job (
ssfornetstat). - Your git aliases count as git commands, so
git cois not re-asked. - A manual that includes a file outside its own man directory is not read. A knowledge test setting prints a line when it is set.
- The hint row writes spark in lowercase and ends without a period.
spark do,spark drill, the paste check and the reading beforespark edit ?work when the chat model thinks (qwen3-8b). A request that asks for JSON now asks the model not to think first. A model that thinks until its answer runs out says so.spark modeland the page mark every model the engine holds as serving, both roles on a machine with a chat model.- More commands carry
!:crontab -u USER -randcrontab -ir,snap remove,flatpak uninstall,pip uninstall,npm uninstallandkill -9 1or-1. file -Cno longer counts as a command that only reads: it writes a compiled magic file./api/do/runrefuses a command over 4096 characters.- Only the check timer refreshes the knowledge index now. A
spark check --porcelainover ssh only reports. On Void,spark updaterestarts the check service, so it runs the new tree.
v1.55
A program with its own system prompt can have the chat model bare.
/v1/chat/completionstakes"identity": falsein the body. The chat model then gets the program's messages exactly as sent, without this machine's identity and remembered facts in front. No field ortruekeeps the identity, and any other value is a 400.
v1.54
A program that talks to the page's server can keep threads that outlive the history.
POST /api/threadsmakes a kept thread in the caller's own store, with no model turn. Without an id the server names it by the time, as it names every new thread. Given the id of a regular thread, it moves that thread to kept, and every message in it still opens.- A kept thread lives in
kept/beside the regular threads.SPARK_HISTORY, the prune andspark clear --historynever touch it, and it takes new messages when history is off. It leaves with/keep offor withspark user remove. spark clear --historyclears the history: every turn and every thread go, and the kept threads stay.- An append to a thread says what happened. It answers the characters it stored and whether the text was cut at the history cap. It answers 409 when history is off and the thread is not kept, and 500 when the store failed. It used to answer ok either way.
/keepinspark chatkeeps the current thread, and/keep offlets it go. With history off,spark chatgoes on with the newest kept thread.spark history,spark user listand the page count the kept threads, and a clear says how many stay.??goes on with the newest thread that holds a message. A thread a program made and has not written yet is skipped.- A thread id is 1 to 64 letters, digits,
-and_. The API and--threadrefuse a longer one. - A user holds at most 2000 kept threads. Past that, the server refuses a new one with 409
full.
v1.53
The prompt line answers from this machine: its manuals, its programs, its apps and spark's own verbs.
Measured on the box (Void, qwen3-4b) and the Mac, 270 audition cases:
| v1.52 | v1.53 | |
|---|---|---|
| right command, Debian / Arch / Void / macOS | 71 / 87 / 74 / 68 % | 82 / 87 / 79 / 79 % |
| right spark verb | 33 % | 66 % |
| dangerous commands marked | 35 of 36 | 36 of 36 |
a ! on a command that only reads | 17 % | 11 % |
| every option in its manual | 95 % | 100 % |
| command in your line (median) | 2.1 s | 2.2 s |
- spark reads what this machine can run into a local index: every program on the PATH with its manual, its apps, and spark's own verbs from each verb's help.
./bootstrap.shbuilds it, and the check timer keeps it fresh. A program with no manual is read from its--help, and only inside the sandbox. - Before a command lands in your line, spark checks it against that index: the program must be here, and every option must be in its manual, and when its manual lists commands (
sv status,apt-get install,git log), the command word must be one of them. A command that fails is asked again once, with the manual's own lines, and your line never shows the wrong one. - spark's own verbs in the model's brief come from the tree, so a verb is never missing and a retired one never lingers.
- The model's
!is lowered only when spark's own read-only check proves every part of the command only reads. That check now knowsps,groups,id,uname,dmesg,free,uptimeandlsof, and refuses their options that write or never return. spark's own danger check always wins. It now also catchesfind -exec rm,dd of=to any file,2>and&>to a file,crontab FILE, and a package removed on every family:apt remove,dpkg -r,pacman -R,brew uninstall,dnf removeandapk del. Anychmod,chownorchgrpis marked, as the model's brief already asked, and so are spark's own verbs that destroy or end logins:spark uninstall,spark history clear,spark memory forget,spark user remove. - The
knowledgerow ofspark checksays what the index holds and how old it is.SPARK_KNOWLEDGE=offinspark.envreturns the prompt line to v1.52's. spark reveal 22at a terminal keeps the pace replies appear at, inspark.env, and barespark revealshows the numbers and the pace. Typed at a terminal it used to wait on the keyboard until Ctrl-C and keep nothing. Piped, it is the filter it always was.- spark do's manual excerpt works on Void:
man -P catfails on the mandoc man, and the manual is now read withMANPAGER=cat. spark serveno longer stops with a Python error when another program on this machine runs with a name that is not UTF-8, such as a Latin-1 file name. spark reads such a byte as a replacement character.- forge_smoke's QR checks at a pty no longer flake on a busy Mac. The test turns off the pty's output processing, which wrote one carriage return twice when its queue was full, and reads until spark exits.
v1.52
The prompt line is faster: the command lands first. On the box's qwen3-8b the command is ready in 3.4 s, where the whole answer took 6.2 s before. Also a way to measure it, and six small things the v1.51 tour found.
- The prompt line streams. The command lands in your line the moment it is complete, and the hint fills the row above it. A command that deletes shows its
!before the command appears.Enteris still yours. - The model answers in a new order: kind, danger, command, hint, proof. The danger mark is known before the command.
- The prompt line asks the engine for its own slot, so a chat does not push its prompt out. A second question starts warm.
- A command from the model that carries a control character is refused whole. Before, only some were cleaned out.
- The prompt line's brief says a question about spark itself gets one of spark's own commands. On the box's audition, spark-core answers went from 40 to 51 in 100 on qwen3-8b and from 29 to 51 on qwen3-4b.
sparkandexplainare never "not on this machine" to the prompt line, even where~/.local/binis not on PATH.tests/line_audition.pygrades the prompt line per OS and on spark's own verbs against a live model: 141 cases for Debian, Arch, Void, macOS and spark itself. Each OS's--helpoutput and spark's tree are the judges.spark bench --lineand the audition leave no thread in your history. Their turns keep only numbers, marked bench.- TAB after
spark memoryoffers the words it takes. - A script, a cron job and
ssh HOST 'spark ...'name~/.local/bin/sparkin full. A stock~/.bashrcreturns before spark's hook in a shell that is not interactive. INSTALL section 3 and the cheatsheet say so. spark chatfrom a pipe prints the replies alone. The intro line and thechat>prompt are for a terminal.- A line from
spark ask,spark readorspark watchno longer ends in spaces. A model's Markdown line break stays out of a pipe. - The
lastrow ofspark statusis the reply's first line of words, cut at a word near 70 characters. A code fence never shows there.spark lastkeeps the whole reply. - The license column of
spark model listis as wide as the table allows within 80 columns. A long license keeps whole parts, such asLlama-3.2andGemma-Terms. spark user,spark statusandspark historycount the same threads: the ones that hold a turn. A file from a failed first turn no longer counts.spark historysaysnewest 5 of Npast 5.spark bench --line [N]times N everyday prompt-line questions throughspark line: command ready, whole answer and warm slots.spark statsshows the result as the line pace, and thethroughputrow names it.
v1.51
Void on a real machine. What the first install found.
- Void's install line is
sudo xbps-install -Sy git curl python3. The base system already brings OpenSSH, sossh-keygenis there. - The
headlessrow reads the GPU's node itself: open to every user, or owned by a group that lists you. Void has norendergroup, and its node is open to all, so the row no longer warns there. - A client of a reinstalled machine logs in with the new token and keeps its own sealed threads. The login locks them under the new token, once the machine that serves accepts it.
- Void: a stop or a shutdown ends each service cleanly. The root service gets a
control/t: it stops the 3 services, then the supervisor. Before, the supervisor quit first, and the shutdown signalled the engine twice, so it skipped its clean exit. - A service stopped or restarted on purpose no longer waits 15 seconds in its
finish. The wait stays for a crash. A unit that finds another server on its port stands down, on both inits. - The helper that warms the model after a start no longer lingers as a dead process beside the engine. On every Linux.
spark quiet boot onworks on Void with GRUB: 3 marked lines at the end of/etc/default/grub, thenupdate-grub.offdeletes them.spark quietsays why a piece is n/a on a line of its own.spark readfrom an empty pipe says so in one line and names2>&1. A command likessh -hwrites its usage to stderr, and the whole usage ofspark readcame back instead.- A conversation on a Linux with no display is told so: a text console or an ssh login. A small model once answered a console font question with a graphical terminal's config.
- A man page reads on Void and macOS:
man scp | spark read .... Their man keeps bold and underline in a pipe, as a letter, a backspace and the letter again.read,ask,drilland the context ofexplainnow keep the letter once.spark editkeeps its bytes. - On a client,
spark serveandspark forgesay it is a client of the other machine, as the check rows do. On macOS thesharerow says a shared engine is Linux's. spark drilllooks for the terminal to answer at before it asks the model.
v1.50
A third Linux family, Void, and a second Linux init, runit. One spark, one oracle for each, the package names as data.
distro()answersvoidtoo. The package names live indistro/void.env, the same 6 keys, andgetacceptsxbps-installbesideapt-getandpacman. The glibc flavour runs spark. The musl flavour is refused atgetin one line: the pinned engine is a glibc build.- xbps: packages come through
xbps-install -Sy. When xbps refuses an install, thepackagesrow is a todo namingsudo xbps-install -Su, because xbps itself must be current on a rolling distro.libgompis its own package there. Wherehostnameis missing, spark writes/etc/hostnameand sets the kernel's name. - runit:
init_shape()sits besidedistro()and answers launchd, runit when/etc/runitis a directory, else systemd. On runit the 3 services are directories under~/.config/spark/sv/, rendered byinstall.sh.bootstrap.shwrites/etc/sv/runsvdir-USERonce, with sudo, and links it into/var/service, so the services run from boot, logged in or not.spark checkruns every 5 minutes as a supervised loop,spark serve offputs adownfile in the service's directory, and each log is~/.local/state/spark/log/NAME/current. Arunsvdir-USERof your own is used as it is.spark headless onworks there, and therunitandsupervisorrows are core. - The console font has a third shape:
/etc/rc.conf'sFONT=line beside/etc/runit.spark fontwrites it andsetfontredraws this console. A Linux with none of the three answersspark font -- no console-setup, vconsole.conf or rc.conf here: the console font is not spark's to set. - What Void lacks refuses in one signed line:
spark quiet boot -- no drop-in on Void's GRUB: the kernel line is /etc/default/grub's (GRUB_CMDLINE_LINUX_DEFAULT, GRUB_TIMEOUT=0, then update-grub), exit 2. Thequietrow says so, never fails (check.VOID_ROWS). The palette at boot is/etc/rc.local's, and sleep and the lid are the machine's own.spark quiet login onworks. - CI runs the one-liner as a new user in a
voidcontainer too, with a runsvdir started for the job. It proves what the Debian and Arch jobs cannot:spark-checkruns supervised.--selftestruns a sixth pass, underID=void. The engine, the GPU and the console on Void are not yet proven on a machine. - The docs: a Void block in
docs/INSTALL.mdsections 1, 2 and 6, the keys and the root actions in section 7, the cheatsheet's Linux block and the Void package names inCREDITS.md.
v1.49
- Every document, help text and usage text speaks with one voice: simple and direct English, one word per thing, short sentences in the present tense. The style sheet is the new Voice section of
docs/CONTRIBUTING.md, andtests/docs_test.pyholds its mechanical half. Rewritten in it:README.md,docs/INSTALL.md,docs/CHEATSHEET.txt,docs/TOUR.md,docs/TROUBLESHOOTING.md,docs/APPS.md,docs/IDEAS.md,docs/ROADMAP.md,docs/CONTRIBUTING.md,CREDITS.md,AGENTS.md,CLAUDE.md,site.env.exampleandspark.env.example. The history below this entry stays as it was written. - The docs say what the code does. The install lines name
openssh(Arch) andopenssh-client(Debian). A token is shown once, when you ask for it. A watch window is up to 40 lines or 10 seconds, 8 kB at most. A palette is 20THEME_*keys.spark checkexits 0 when no row fails, and every doc says so. Thespark checkflags, thespark serveflags,spark user remove,spark theme profile,spark font none,spark forge --show-tokenandspark bar lineare documented with their verbs. The keys table namesSITE_SHAREandSITE_PEER_SSH. spark helpopens with the tagline the README carries, and its status line names the model, the prompt line, the server, soul, memory and the last answer. The model's briefing no longer namesspark stoporspark remember. A verb that is gone, followed by a second word, answers "no command named" and points at the usage instead of becoming a question.- The gate runs what it says.
tests/policy_test.pyis in the pre-commit hook and in CI, and each shell file is syntax-checked on its own. TheSITE_GITkeys that nothing read are gone from CI. - The completions lose the dead
tunecase and completespark check's flags. docs/IDEAS.mddrops "Two people, one box": named users andspark shareare it.
v1.48
- spark's core knows nothing of a shell layer. Its side of any renderer is three generic things, named as such.
~/.config/spark/theme.envis the palettespark themewrites: aKEY=valuefile any renderer or terminal may read (the page reads it); the console palette and font are the machine's own. The three optionalSPARK_ACCENT_SGR,SPARK_MUTED_SGRandSPARK_WARN_SGRvariables colour the prompt's marks when your rc exports them, and the marks are plain otherwise.spark bar lineis the machine's one-line status, for any status bar or the page to run. - The
spark shellverb and thespark bar on|offpointers are gone: an unknown word answers with the usage. Theshell-movedbootstrap row is gone, and so is the uninstall hand-back of a pre-cut layer's rc symlinks and.gitconfig: uninstall takes the hook line out of your rc file and removes spark's own files. state/promptis no longer written (nothing read it);theme_loadinlib/env.shis gone;THEME_BTOPleavestheme.env, so a palette is 20THEME_*keys.spark themecloses on its own output. The model is no longer told about a shell layer. The disk row's remedy isdu -sh ~/*.docs/SHELL.mdis gone.
v1.47
spark dois measured, bounded and held: every proposal is a turn with the server's timings, sospark statsshows do's real cache hits; the messages fit the served context (the goal is never dropped, the oldest outputs shrink to(output trimmed, exit N)first); what a step printed is held back like a source's secrets, and any copy of spark's own tokens, before a program or the model sees it (the digest a checksum tool prints is kept); the page's steps stop after 120 s.- An edited proof runs (an edit used to drop it), when it is still one read-only check. A control character in the model's command, its proof or your edit -- a terminal escape, a C1 or a bidi control -- is refused whole.
- The OS documents its tools: a step refused for an option brings back the lines of that command's own man page about it (
man -P cat, 1.5 kB at most); the command itself never runs for it. The page's steps carry the same lines. spark do --sandbox <words>: every step runs in a copy of the project -- bubblewrap 0.11+ on Linux, an APFS clone undersandbox-execon macOS -- with no network, the homes and the temp directories out of sight and nothing else writable, without asking; a run writes 1 GB at most, weighed during each step. At the end, the diff and a typedyesapply it -- exactly what the diff showed: a copy changed after it applies nothing. The diff shows a control character as an escape and marks the file. A git directory is known by its shape (.git, or HEAD beside objects/), and only git's own records apply from it -- objects, refs, logs, index, HEAD; its config, hooks and anything pointing git elsewhere are held back, each listed. A link out of the project is refused, a file changed here meanwhile (by mtime or ctime) stops the whole apply. Thesandboxrow says whether this machine can (40 rows).spark do --sandbox --detach <words>runs with nobody there -- a timer, cron, launchd, aspark watchline -- one at a time, and its changes wait:spark do --review [ID]shows them and asksyes,--accept IDapplies without showing them (a script),--discard IDdrops them. Each claims the run and refuses one still running; an applied or discarded run is removed. Baresparkand the status line sayN runs waiting.spark do --porcelain [--sandbox]is contract 15: JSON Lines for a program (start, step, output, rc, note, review, end), one word back when something waits (run, skip, quit, edit, accept, discard). There is noyesover a pipe: outside the sandbox a step that can destroy data, or whose effect cannot be read from the line (a command substitution, eval, inline code for an interpreter, an upload), is refused, and the model hears it was skipped; anything else runs on the program'srun.acceptapplies only what the review event showed. A refusal before the run is oneendevent, andendis always the last line -- Ctrl-C and SIGTERM included.spark do --ends the options: a goal that starts with-, or is the word help, is words; without it, such a goal is refused. A goal is at most 8 kB -- at the prompt, over--porcelainand on the page.- The first client of contract 15: spark-acp (github.com/forgewright-ai/spark-acp) puts
spark doin any Agent Client Protocol client --toad acp "spark-acp"in a terminal, Zed in the editor: each command a Run or Skip, a sandboxed run's diff one Accept or Discard.
v1.46
- A source's secrets are held back before it leaves:
spark readandspark edit ? --source(what the reading apps pass) replace every span that looks like a secret with[held]right after stdin is read -- before a client sends it over the LAN, before the reading, the anchors and a thread see it. The shapes are the paste's (a private key, an AWS access key, a GitHub token, a Slack token, an API key, a credential line, a long base64 run) plus two a mail or a page carries: a one-time code (4-8 digits, or split like 482 913 or 48-29-13, just after or before code, OTP, PIN, passcode or verification) and a link token (the value of every token, key, code, reset, sig, signature, auth or otp parameter in a URL). A question's--name,--aboutand--typeare held the same way. One stderr line says how many and which; the turn recordsheld, a number. - A rewrite, a completion and a
?without--sourceare the author's own text: sent as it is, byte for byte back. spark edit --ends the options: every argument after it is a word, so a question that says--namecannot swallow the flag after it.- The shapes live in
lib/spark/text.pynow (SECRET_SHAPES,SOURCE_SHAPES,hold_secrets);spark line --pasteholds the same list as before. README's "What leaves this machine" names every shape, and the docs test holds it to the list. - A new spark app: spark-aerc (github.com/forgewright-ai/spark-aerc) puts spark in aerc, the mail client --
A-son a mail opensspark>, and the review screen asks about your draft. It needs this release: a mail is a source, and its secrets stay here.
v1.45
spark watchnever watches itself: withjournalctl -fon the box, spark-serve's own journal lines (llama-server's every slot and task line) fed each look back into the next -- the GPU pinned at 99%, the info lines quoted as failures. A journal line under spark's identifier and a bare llama-server log line are dropped before the window; a window of only those makes no call. The window closes at 10 s and looks come no closer than 10 s apart (were 3 and 1): a chatty log is a look every ten seconds, not a pinned GPU. The brief says a line that only reports progress or state is silence. The first window no longer waits the interval out on a platform where monotonic() starts at zero (Apple's 3.9).- The FORGE's upstream probe carries the api-token: a llama-server under --api-key answers 401 to any unknown path before routing, so the keyless
GET /api/healththat tells a FORGE from a plain server left "unauthorized: Invalid API Key" in the server's journal on every resolution -- what the watch was reporting. The client's brain resolution does the same. - spark knows itself as it is: the self-knowledge maps (
persona.KNOW_SHELL,KNOW_CHAT) name today's verbs -- chat --reveal, read, ask, drill, watch, edit, memory, headless, client, user, the spark.env keys and spark-shell for the look -- and no longerspark shell on|offorspark bar on|off, retired in v1.19 (spark do set spark reveal to 22proposedspark shell onfour times over). spark do: a step the user skipped is named to the model ("do not propose it again"); the same step coming back is re-asked once, silently, and a second time ends the run in one line -- the user never answers the same skipped step twice.- forge_smoke's
--print-url at a ptyno longer flakes on macOS: the pty's slave stays open a beat after spark exits (macOS drops the tail of a large write otherwise).
v1.44
- Two paces, a principle (CLAUDE.md): a conversation can be paced for the reader, the prompt line is speed alone. The benchmark: a turn records
charsnow (a count), sospark statsand a bare/revealin chat say what the model writes (characters a second, from its newest turns) and the threshold a reveal stays under to never wait on it (85% of that, a reader's 40 a second at most). The choice stays yours:--reveal N|auto|offon chat, explain and a bare question,/reveal N|auto|offin chat,SPARK_REVEALin spark.env for the standing one;autois that threshold, off (as the chunks come) stays the default. The widget's line andspark doare untouched.
v1.43
- The reveal inside the streaming verbs:
spark chat --reveal [CPS],explain --reveal,spark --reveal wordspace the reply letter by letter at a terminal (spark reveal's 5..200 a second, default 30 orSPARK_REVEAL_CPS),/reveal [CPS|off]switches it inside a chat. The pulse stays; the pace starts with the first token; piped, nothing changes.spark chat | spark revealcould never work: a pipe puts chat in plain mode and the prompt would flow through the filter. - A reply's socket that times out or drops MID-STREAM is one line (
went quiet for Ns mid-reply -- the answer above is incomplete), never a traceback: the wire's connect had that answer, the read did not (seen on the box when spark-serve was restarted under a chat). - A reply the cap ended says so:
! cut at the reply's length -- say: go onon the screen (the thread keeps the words that came, not the line), read from the server'sfinish_reason. The cap of the three streaming verbs (chat, explain, a bare question) is 1200 tokens now,SPARK_MAX_TOKENSin spark.env sets another (50..32000); 600 dated from the 1.7B days. The line's JSON answer and the editor verbs keep their own caps. - A mark before a backtick opens (
**vi**is bold now, not raw).
v1.42
- A reply's Markdown is drawn at a terminal instead of printed raw:
**bold**and a# headingline as bold, an*emphasis*as plain text, the marks dropped only when they flank a word (*.txt,**/,2*3*4and_names_pass through), and a ``` fence opens a block that passes through whole, marks and all, until the closing fence. Piped, the bytes are the model's, as before. Models keep sending marks however the persona asks; the terminal now reads them.
v1.41
- Colour at the prompt, from the shell layer: three optional environment variables --
SPARK_ACCENT_SGR,SPARK_MUTED_SGR,SPARK_WARN_SGR, SGR codes such as1;94-- paint the*mark, thechat>prompt (under GNU readline; libedit, Apple's python, keeps it plain: it counts escape bytes as columns),spark do's step lines (a danger step whole, in warn) and the widgets' hint row, at a tty only; spark-shell exports them from the palette.sgr()keeps to the console-safe set (30-37, 90-97, bold, dim; a38;5or 24-bit value is dropped whole) and a pipe never sees an escape: unset, every output is what it was. Afterspark theme NAMEthe prompt's colour followsspark-shell applyand a new shell. - One busy pulse,
text.Busy: the mark and......redrawn in place while a reply is on its way -- in the hint row above the prompt (SPARK_HINT_ROW=1, the widgets' word tospark line, drawn on/dev/tty), inspark chat,explain, an answer andspark do. ASCII on every terminal, silent on a pipe.wait_ready's dots stay the wait for a server coming up. ~/.local/state/spark/prompt:T=<epoch> MODEL=<stem or -> AI=up|down, a cache for a shell prompt's segment (spark-shell's starship module reads it), written by the bar's tick, after each turn and when a brain fails to answer. No new verb, no check row.
v1.40
spark fontworks on Arch: the console font is chosen by mechanism, never by family (site.console_shape(), bootstrap'sconsolerow): console-setup's file where it exists (Debian, as before), else/etc/vconsole.conf'sFONT=(Arch and every systemd distro), withsystemd-vconsole-setuprestarted so every VT redraws; the original is kept beside the file andspark uninstallputs it back.spark font listreads the kbd font files there and takes each size from the file's own header, so every row can be typed back asFACE WxH. Thefontcheck row is fixture-tested now on both OSes (on macOS it is ok for an installed face);check.ARCH_ROWSis the quiet row alone. A Linux with neither file refuses in one line.spark setuppaints the theme it asked for: the chosen palette went into site.env and nothing else (a dead line after a return). Three palettes join the six:dracula,everforest-darkandrose-pine.spark theme shownames the Terminal.app profile on macOS instead of the Linux console line.
v1.39
spark quiet boot onis real on an Arch that boots a Unified Kernel Image (adefault_uki=line in a mkinitcpio preset, the shape systemd-boot installs get): one drop-in spark owns,/etc/cmdline.d/zz-spark-quiet.conf, carries the same seven words as the GRUB drop-in (mkinitcpio embeds it after/etc/kernel/cmdline); the preset's--splashline (the Arch logo) is marked off with#spark-quiet#,loader.confgetstimeout 0,mkinitcpio -Prebuilds and the row proves the new image carries the line.offundoes the three and rebuilds. Thequietcheck row reads/proc/cmdlineas the live proof and warnsboot quiet after a rebootuntil the machine has. Without a UKI the verb still refuses, now naming where the kernel line lives (a loader entry's options line, or GRUB_CMDLINE_LINUX_DEFAULT then grub-mkconfig) instead of GRUB;spark helpsaysboot (menu). Thesystemd.show_status=falseword is what silences the Started and Stopping lines at boot and shutdown: measured on the box, the kernel itself printed two lines.
v1.38
- The documents live in
docs/: INSTALL.md, CHEATSHEET.txt, CHANGELOG.md, ROADMAP.md and CONTRIBUTING.md moved there beside TOUR, APPS, SHELL, IDEAS and TROUBLESHOOTING; the root keeps README.md, CREDITS.md, CLAUDE.md, AGENTS.md and LICENSE. The five that moved stay tied to a release (the landing rule); the five beside do not, anddocs_testnow tells the two apart, holds docs/ to exactly the ten, and refuses a moved file named by its old path. The release workflow readsdocs/CHANGELOG.md; the hook's 80-column and ASCII gates refuse a missing doc instead of skipping it; the printed reference islp ~/.spark/docs/CHEATSHEET.txt. - macOS: with
SITE_FONT_FACEunset, the Terminal.app face is Menlo-Regular 13, a face every Mac ships, andspark fontsays so at once. The profile on disk changes at the nextspark theme NAME,spark theme profile,spark font FACE SIZEorspark setup, which also switches the open windows. A face set by hand keeps working; once that font is uninstalled, the font row warns and names the remedy. spark bar linepaints itsaisegment in a palette slot (colour0..colour15, the nearest of the sixteen to the theme's accent), never a hex colour: the console draws sixteen, and tmux's own table is not the palette's.- The shell prose says what is so: the shell layer's tools are named without a font, and
docs/INSTALL.mdno longer describes aspark shell onthat is a pointer.
v1.37
- The page, spark.forgewright.ai, is rendered outside this tree, at the newest signed release -- what
getinstalls -- and never from main.www/,pages.yml,tests/site_test.pyand the palette lockstep in docs_test leave with it; nothing here builds or publishes it any more. Releasing gains a step: the page's render is run by hand after the tag, or picks the tag up on its own within six hours. - The documents beside the core live in
docs/: TOUR.md, APPS.md, SHELL.md, IDEAS.md and TROUBLESHOOTING.md (a box that will not join the WiFi -- one WiFi daemon per card, then the logs -- which nothing had pointed to). Each says it is not tied to a release; docs_test checks that, that everydocs/X.mda doc names exists, that each is in CLAUDE.md's layout and pointed to from README, INSTALL, CHEATSHEET or ROADMAP, and that the root holds only the core docs.
v1.36
- The pre-commit hook reads the staged diff for the secret shapes
spark line --pasteholds (cli.SECRET_SHAPES, minus the two tuned for a paste and not a tree), names the shape and never the line, and skips a line markedspark:allow-secret; its py_compile runs-W error, so a SyntaxWarning refuses. - CodeQL (
codeql.yml) runs GitHub's static analysis over the python and the javascript on every push to main, every pull request and weekly, on GitHub's runner: the stdlib-only rule is untouched.ci.ymlgains aworkflowsjob -- zizmor, pinned, over the workflows themselves at medium and above -- and every checkout runs withpersist-credentials: false.advisories.ymlasks GitHub weekly whether llama.cpp published a security advisory after the engine pin's date and opens one issue when it did. Dependabot keeps every workflow's action pin current. spark ver --sbomprints what the tree depends on as one CycloneDX 1.5 JSON document (lib/spark/sbom.py): the llama.cpp engine per pinned flavour, every model with its sha256 and license, the distro packages, the python floor, the GitHub Actions the workflows pin. Deterministic but for the timestamp, the JSON alone so it pipes; every release carries it assbom.cdx.jsonbesideget.- The
pendingrow ofspark checknames the security upgrades among what the package manager holds back: apt's-securitysources,arch-auditon Arch (absent, the row says so without warning). One waiting is a warn with the family's upgrade line; none keeps the ok text plusno security upgrades pending. - What leaves is counted: every request records its bytes out and its destination (
host:port, orlocal);spark stats --sendsshows them by destination and day; thesendscheck row warns the day bytes go to a host that is not your configured server. - The FORGE's gates are probed from the wire:
tests/forge_probe.py URL(one line per gate, exit 1) and thehardeningcheck row ask contract 9's eight promises of the served FORGE, or of the peer on a client, every five minutes. spark dounderSPARK_DO_STDINsays so on stderr: a harness confirmed, not a person.- Every route the server answers is one row of
forgeserve.ROUTES(none,useroradmin), consulted by nothing else: a request off the table is 404. CLAUDE.md contract 9 prints the table,tests/docs_test.pyholds it equal to the code, and the newtests/policy_test.pysends three callers -- nobody, a user, the admin -- at every row. - A sealed audit trail: every admin action -- a command run from the page (its sha256 prefix and exit code, never its text), a verb run, a user added, removed or rotated, the admin token rotated -- is one record in the box account's
audit, numbers and names only.spark forge audit [N]reads the newest N (--porcelainfor machines); a trail that does not open is one signed line, exit 2, never written over. GET /api/configreturns no key matchingKEY|TOKEN|SECRET, whatever spark.env holds;GET /api/usersentries are exactly name, threads, last. The chat-history save skips a sealed file that does not open, one line on stderr, instead of writing over it.- Every non-human caller -- a script, an app, a CI job -- gets its own user with
spark user add NAMEand its own token; the admin token is the box's, never shared (INSTALL.md section 5).
v1.35
- A release is a signed tag.
spark updateandgetverify a tag's ssh signature against the tree'sallowed-signers(the principal isspark-release) and refuse any other in one line, with nothing moved;release.ymlverifies the signature before it makes the release. A newsignedrow inspark checknames the key a release clone's tag was signed by (37 rows).getasks forssh-keygen(openssh) beside git and python3 when it lands on a tag. - The one line comes from the release:
curl -fsSL https://github.com/forgewright-ai/spark/releases/latest/download/get | shin every doc and on the page front,getshipped as a release asset, so an unreleased push to main is never what a new machine pipes tosh. - The page's login goes through the write gate like every other POST --
X-Spark: 1, a Host this machine answers to, a matching Origin, a body sent asapplication/json-- so a page on another origin cannot log a browser in. - A cookie is a session id minted at random for that login, the admin's included, never derived from the token: logout drops it on the server, a token rotation ends it, and a restart sends every browser back to the login.
- A locked-out address gets 429 on a bearer as well, before any token is compared; a
SPARK_FORGE_TOKENshorter than 32 characters is refused when the forge starts. - The page's do console is held to its word on the server:
POST /api/do/runrefuses a command carrying a control character and runs a dangerous one only withconfirmed: true, which the page sends after its second click; the log names each run by a sha256 prefix and records its exit code. /v1/chat/completionstakes the bearer only and asks the model for at most 8192 completion tokens. The bind refusal covers every spelling of the unspecified address forspark forgeandspark servealike, and a public address binds with a warning. The page is served withX-Content-Type-Options: nosniffbeside its CSP.- A proof is asked for like a step:
spark dooffers it (Enter runs it), runs it on a 30 s leash, and sends the model its exit code alone -- a proof's output never rides a request.persona.proof_okreads argv, refuses a named list of writing options (--output,-o,--ext-diff,tail -f,git -c,-execand kin) and any control character, sogit diff --output PATHno longer truncates PATH. - A
spark docommand carrying a control character is refused whole, and each step's feedback -- the command that ran,edited fromthe proposal when it differs, its rc, the proof's rc -- lands on the thread the moment the step ran, the last step included. persona.is_dangerousflags a truncating>anywhere,sudoanything (aspark dosudo step needs the typedyes),sed -i,teewithout-a,shred,xargs rm,mv -fandcp -f,history -c,git stash drop;>>,2>,>&,/dev/nullandtee -astay plain.- The paste inspection looks before it sends: a paste shaped like a secret (a private key block, an AWS, GitHub, Slack or
sk-token, apassword=ortoken:line, 64 or more base64 characters) is one answer line naming the shape, and nothing is sent;persona.SENDSsays so. The widgets' Esc s hint after a failure says it re-runs the command. - A sealed ledger or memory that does not open (a flipped byte, a stale
account-key) is never written over: every writer refuses withthe ledger does not open -- spark user login again(the memory likewise) and readers still answer empty. A tampered iteration count in a key file is a refusal, never a stalled CPU; an append onto an empty or foreign sealed file is refused;spark user removevalidates the name likeadd; the plainstate/failsindex redactsNAME=valuewhere NAME smells of a secret. ci.ymlandpages.ymlpin every action to the commit of its release andci.ymlholdscontents: readonly;bootstrap.shdownloads are https only, redirects included;install.shnever overwrites an earlier back-up; the pre-commit privacy gate escapes each word and scans the staged file names and the branch name too, and CI's privacy hit names the file, never the line;spark model addsays when a sha came from Hugging Face's metadata; CREDITS points the llama.cpp pin atengine.env.
v1.34
- Granite 4.2 8B (IBM, Apache-2.0) replaces the Granite 3.3 row in
models.env, from IBM's own GGUF build: proven on the line and scored by the grounding audition on the box, soautomay pick it. The single-model server's--reasoning offkeeps its think tags out of the prompt line and the editor. - An
autotie among grounded rows at the same RAM goes to the earlier row of the list: the list is ranked, so a new row never takes the pick from the one above it (Granite 4.2 and Qwen3-8B both fit 7 GB). - The FORGE follows site.env.
spark model NAMErestarts spark-serve and not the forge, and the forge answered/api/modelsand/api/configfrom the config it started with, so a client'sspark modelkept the star on the old pick until someone restarted the forge. The forge now re-reads site.env and spark.env when either changes, the rule the forge-token already had; a forge_smoke case pins it. - A page title with one byte that was not UTF-8 crashed
spark readafter its answer: under the box's POSIX locale argv keeps such a byte as a lone surrogate, and the ledger's strict encode refused it with a traceback in the reader's face (seen through spark-w3m). The wire got its gate in v1.32; the stores have it now:text.cleanwalks every record bound for a thread, the ledger or a turn file, and the ledger normalises a name on the way in, so the same title lists and clears the same records. A smoke case drives it throughspark read --name. - The wait before a grounded answer is something to read. Every
read,ask,edit ?anddrillpays a reading pass first (the language and kind of the text, a few seconds on an 8B model), and the screen stayed blank until the first answer line. At a terminal stderr now saysreading ...while the pass runs and then what it named --reading ... English, essay-- one line; a pipe (an editor, spark-w3m, a test) sees nothing, so every contract's stdout and stderr stay as stated. A smoke case reads at a pty and in a pipe.
v1.33
- A client never mints an account.
spark setup --model noneused to mint this machine's own account and print its token -- an identity the FORGE it would answer from never knew, so the firstspark linethere saidtoken rejected, and a login with the box's token was refused because a local store already held the name. Now setup with no model skips the account row and says where the login comes from, the first thread write on a client without a login keeps nothing rather than minting (the FORGE is the account authority:spark user add NAMEthere,spark user login NAMEhere), thepeercheck row says whether the FORGE accepts this machine's login, and the login refusal on a machine whose store belongs to another name names the remedy (spark user remove NAME). Found by reinstalling the maintainer's laptop from the README one-liner. SPARK_YES=1answers everyyes/NOquestion, not only uninstall's:spark user remove NAMEand the license ask before a download take it too (a script's form).- No model chosen, no engine downloaded:
spark setup --model noneon a machine with no peer fetched the engine tarball for nothing; bootstrap skips the row now (spark model NAMEbrings the engine with it, and says so in its rows) andspark checkreadsnathere, not a failure.
v1.32
spark askasks real questions of a real page again. Over a few kB of source the model answered the nearest question-shaped line -- the task phrase itself, rephrased ("What does this text not answer about...?") -- and the gate rightly dropped it, so every page came back as a refusal (asked: 0on each one, in the turn numbers). The request's last line now restates the whole task after the source, in the reading's language; proven on the box against live articles in both shapes, bare and with words.- A page piped in with one byte that is not UTF-8 no longer kills the exchange with the engine's raw HTTP 500. Python keeps such a byte as a lone surrogate (stdin is surrogateescape under C.UTF-8, the box's locale),
json.dumpspasses it along, and the engine's JSON parser refuses the request. Every stdin gate now decodes the raw bytes as UTF-8 with the replacement mark (text.stdin_text; a strict locale crashed outright before), and the wire scrubs every string in a request the same way (text.utf8) as the last gate, whatever door the text came in by. Found on the box: a w3m article with one Latin-1 accent answeredHTTP 500 ... invalid string: surrogate. spark edit ? --sourceis the reading-discussion posture: the text is a published source you discuss -- a page, an article -- not a draft you edit. It answers the reader's question from the text and never suggests changes to it, while keeping every part ofspark edit ?(the thread, the ember, quotes checked and marked where the text does not hold them, never dropped). The reading apps pass it; the editors never do. Found on the box: asked whether a priced page named a price, the editor's brief reviewed the prose ("rephrase this", "consider adding") and concluded it named none while it named four. A ninth audition ground case (discuss-price) pins the answer to the fact and forbids the editor's verbs.spark statsshows a row per mode -- turns, the prompt cache hit rate, the median latency and, forspark read, the median wait to the first kept line (first_ms, a new number on the read turn). One cache rate over every turn hid the one that matters: the line's short prefix hits nearly always, a read carries a 16 kB source. This is the instrument for the roadmap's first item, the rerun that costs what the first run cost.--porcelainaddsmode_<name>lines.- The reading pass (
You read this as: ...) is greedy, temperature 0. It is restated above the source in the request that follows, so a word sampled differently on the same source threw the served prompt's cached prefix away right before the text, and the whole source was processed again. It is also a turn of its own now (modeedit-read, kindreading, numbers only), sospark statsshows what every grounded answer pays before it starts. - Every
systemctl --usercall spark makes --spark model,spark serve,spark update, theservicesrow,spark uninstall, and bootstrap's twin -- carries the user bus (XDG_RUNTIME_DIRand the session bus address, defaulted to/run/user/UID), so a plainssh box spark model NAMEreaches the unit instead of "Failed to connect to user scope bus". A unit start that fails now says so in onetodoline and returns, where before it reported success and waited 180 s on a server nobody had started, while the unit went on serving the old model beside the hand-started one. spark model NAMEdrops the brain cache after the restart, so the nextspark brainnames the model now served, not the one before (the cache is 60 s; a swap is faster).- Two more rows carry a grounding score, from one audition run each on the maintainer's box: Qwen3 4B
5/9and Gemma 3 12B5/9, beside the 8B's21/27. The 12B is slower (5.5 tok/s to the 8B's 8.7) and no better grounded; the 4B is fast and its judgment folds (a cold read over-generated and the gate dropped every line). The 8B is the box's model for reading.
v1.31
spark reveal [CPS]-- stdin at a reader's pace: at a terminal the text appears letter by letter (default 30 characters a second, SPARK_REVEAL_CPS or the argument set it), so a slow brain's line bursts read as a steady hand; a stall is never repaid as a burst. Piped anywhere else it is an exact copy, byte for byte. Purely local; the reading clients pipe through it.- spark-newsboat, the second client of spark read:
,sin newsboat asks the article you are on, on the terminal newsboat hands over -- the grounded answer line by line, Enter back to newsboat untouched. Its own repository, like every spark app. - The grounding audition gains a ninth case, read-about: "what is this text about" in words, where bare
spark read(the overview) already answers well. The model misses it -- it summarises in its own voice and the gate rightly refuses -- and three brief rewrites each traded that miss for false grounding on read-unanswered, so the brief stays as it was and the case stays as the measure of the gap. Found asking a web page "what is this page about" through spark-w3m; until a model or mechanism closes it, the overview is asked bare: Enter at the prompt, no words.
v1.30
The law:
- A quoted span must clear a floor to count as grounding evidence: two words or twelve characters after fold, not made entirely of stop words. Quoting "the" no longer grounds a claim;
spark readrefuses such a line,spark watchdrops it. - A span the model proposes (after
->or=>) is marked[proposed]where it stands, so a reader can tell a quote from a proposal. spark watchandspark recallanchor at word boundaries: "500" cannot ground against a window holding only "1500ms".spark recallkeeps only whole history lines -- a substring likerm -rf /insiderm -rf /tmp/buildis not a command that ran -- and a dangerous line carries the!mark into the prompt (Esc r shows "careful").spark watchdrains its input, so a burst of lines is one window, not one line per tick; matches end their line, so| while readworks; a rotated token or a broken server ends a watch (andspark edit --watch) in one signed line instead of a traceback.spark drill: no brain is one line, a missing terminal is a signed refusal (exit 2).- The danger set sees
rm --recursive/--force,find -delete,rsync --delete,git branch -D,chmod -Rand bare> filetruncation; the blast count reads only thermsegment of a compound line, resolves after a leadingcd, and expands~; the widgets classify every segment, socp x y && rm -rf xis never re-offered whole. - Escape sequences and control characters are scrubbed from the hint row and from every grounded stream: a model cannot retitle your window.
- The ledger takes a lock around every write (two editor panes cannot lose a decline) and an answered question suppresses itself however the gate printed it (numbered, anchor-marked).
- README's "What leaves this machine" lists every sender -- ask, read and recall included -- and the list is a test now.
The machine:
- Every
spark checkremedy names a live verb, and a lint keeps it true (spark forge start/stop, spark remember/forget and friends are gone). - CI's privacy gate: all three checks decide the step, not only the last one.
spark uninstall --packagessimulates first and refuses to take a desktop with the engine libraries; uninstall undoes only the root steps bootstrap recorded making (state/made), and--purgedeletes spark's files by name -- a file spark cannot name survives.spark model add --licenserefuses a value contract 3 would refuse, before it can poison the file; every config writer carries the guard.spark updaterestarts the loaded units after a converge that moved the tree, and the forge row warns when the running FORGE's version is not the tree's.- A client's bootstrap is root-free: packages, hostname, console, vt-palette and the quiet rows all skip.
spark quiet login offrestores only spark's own trace (/etc/motd.orig); a stock box is never written as root.spark serve onrefuses while the unit's server is loading instead of forgetting the running server's records (a new chaos scenario rehearses it).spark client URLstops and disables the server that ran here, removes its unit links, and says so.- Engine pins order by build number (b10689 beats b9999), and older pins are offered for removal.
- Drifts: lib/env.sh agrees with config.py on which file wins; --dry-run writes nothing; console font values are validated before the root sed; bf16 is documented; a freshly minted store key becomes the cached key in the same breath, and a store whose key is gone refuses with 78.
The audition:
- A model row can carry the grounding audition's score:
MODEL_<NAME>_GROUND="<kept>/<run> <YYYY-MM-DD>", written by hand from atests/audition.py --jsonrun the way_TESTEDcarries the line proof.spark model listshows it in the proof column (and--porcelaincarries it whole), the page's model table renders it, andautoprefers a grounded row when two fit at the same RAM.
The prompt, deeper:
- The command and the proof: a
? questionthat lands a command can land a third line too --proof<TAB>command, ONE read-only check that it did what was asked (test ! -d buildafterrm -rf build). A proof that is not read-only is refused before it prints. After the command runs, the hint row offers the proof on Esc s;spark doruns it after each confirmed step and shows the result. - Paste inspection: a multi-line paste into an empty prompt gets ONE verdict line -- what running it would do,
!when a line can destroy -- while the paste stays in the buffer and nothing runs until your own Enter. Over 8 kB nothing is sent at all;spark offdisables it. - Failure memory: the third time you hit a failure is not the same event as the first.
explainremembers the failure's shape (head word, exit code, first stderr line -- hashed locally), the fix that then worked is kept when you accept it (ledger kindfail), and the next failure of that shape saysfailed (N) -- last time the fix was: ...with NO model call: the prompt hook reads one plain index file and nothing else. A fix whose tool left PATH retires; records age with SPARK_HISTORY;spark historylists them.
Trust:
spark check --report: a block safe to paste into an issue -- version, OS and family, arch, backend, RAM and budget, the model stems, and every row's category, status and name. Never a value, a path, a hostname or a user name; the privacy word lists run over the report's own output and blank any hit. The bug template asks for it.- INSTALL.md states the threat model: what an attacker on the LAN, one with the disk, and one with a stolen phone can and cannot do, with the one remedy for each (rotation:
spark user token --new,spark forge token --new). docs_test keeps the section honest. - The tour is on the front door: TOUR.md renders on the page (tour/), linked from the front's stage 2 and the README's "Use it" head.
One identity, every door:
??across machines: on a logged-in client of a FORGE,??at the prompt continues the newest thread ON THE FORGE -- the same thread the box's prompt started and the phone's page shows -- and the turn lands there (POST /api/threads/<id>/append, the requester's own sealed store). Any trouble falls back to the local store, as before.
The FORGE:
- Logout revokes the session -- a replayed cookie is 401.
- A non-ASCII token is a wrong token (401 with the 1 s cost), never a 500.
- A client that leaves while queued pays no prefill and lands no thread (499); a reply cut mid-stream keeps its thread -- the error event names it and the page continues there.
- Prune reaches every store the server holds a key for, and drops header-only files from a failed first turn after a day.
--foregroundbinds before it writes its records: a failed second start no longer deletes the running FORGE's forge-url and pid.- The page strips any
#t=fragment, whatever its shape, and a non-empty token gets exactly one login attempt. - The handler carries a 30 s socket timeout and the wrong-login sleep is bounded, so a burst cannot hold a hundred threads.
- A sealed file renamed on disk is refused by name instead of read as another thread.
v1.29
- The chat owns the screen: the bottom tab bar is gone, navigation is one small menu button in the header on every screen size. A guest's page is now just the chat plus their account (palette, rotate token, clear chats, memory, log out) and help behind that button -- activity and do are the admin's alone, and redirect a guest to the chat. The soul editor leaves the guest's settings too.
- The page wears a transparent mark (
/static/mark.svg, the S with no ground, emitted byassets/icon-svg.py) in its header, login, greeting and message avatars, so the logo sits on any theme; the solid-ground favicon stays for the browser tab and the home screen.
v1.28
- The QR reads. v1.27 placed the format information transposed -- rows for columns, in both copies -- which this repo's own decode-back test could not see (it read the same wrong cells) and every camera could: the scan failed on a real phone. The cells now match the spec's figure and the encoder is verified cell-for-cell against an independent implementation across versions 1-5 and all 8 masks;
tests/qr_test.pypins the cell coordinates literally, so a transposition can never pass again. The half-block render also grows the spec's full 4-module quiet zone (the console's ASCII form keeps 1, to stay inside 80 columns).
v1.27
- The page, redesigned: a chat-first webapp instead of a terminal look-alike. Both roles land in the chat -- bubbles, a greeting with suggestion chips, a bottom tab bar on the phone and a sidebar on a desktop -- with activity, do (admin), settings and help beside it. The same five hash routes; keys 1..5 now start at chat.
- The page wears its own ember look, dark and light, sans-serif; the machine palette (gruvbox and friends) is now an opt-in choice under settings instead of the default. The login card no longer names the ember-token (gone since v1.4).
- Log in by QR:
spark forge --print-urlandspark user add NAMEprint, at a tty, a QR of the login link/login#t=<token>beside the token (--no-qrleaves it out). A phone's camera scans it and the page signs itself in -- the token rides the URL fragment, which never reaches the server or its log, and the page strips it before routing and tries it exactly once. The QR is the token drawn as squares: same custody. lib/spark/qr.py: a QR encoder written from ISO/IEC 18004 (byte mode, level L, versions 1-5, all 8 masks), pure stdlib, pinned intests/qr_test.pyto the spec's format table, the canonical Reed-Solomon vector and a full decode-back.spark setupno longer picks a look: it writesSITE_THEME=none, so a fresh machine keeps its terminal's own colours untilspark theme NAMEsays otherwise. It recorded gruvbox-dark before -- a choice the user never made, which the theme row's remedy then offered to paint.
v1.25
- A shared-engine box converges again. The
sharerow compared the token copy by contents, which the owner (not in thesparkgroup) cannot read, so everybootstrap/spark update/spark checkshowed a perpetual "1 to do" and re-copied the token. Freshness is now by mtime -- the copy is current when it is no older than the source -- readable by the owner, so the row (andspark share) settle took. A rotated token still re-syncs.
v1.24
spark shareno longer cries STALE at the owner. The shared token is0640 root:spark, so the owner (not in thesparkgroup) cannot read the copy to compare it -- and unreadable was being reported as stale. Now a content check runs only when the file is readable here; otherwise the perms are the health signal. A group member still gets the real check.- INSTALL's join one-liner named the wrong host (a 404); it now points at
raw.githubusercontent.com/forgewright-ai/spark/main/get, the same URL the page and README use.
v1.23
- Joining a shared engine is now userspace -- no sudo, no download. When a machine already runs a shared engine (
spark share on),spark setupdetects it and offers to join instead of the full first-run: it writes the client shape (no model,SITE_THEME=none), keeps the joining user's own soul and memory in their$HOME, and touches nothing that needs root. A second OS user sets up with the ordinary one-liner and answers from the one engine in seconds. Fixes v1.22, where a second user hit the model table, a second download, and a sudo wall they could not pass. spark share onnow also publishes the engine's address to/etc/spark/url(0644, beside the token), so a joining user finds it without reading the owner's home;spark sharereports it.bootstrap.shmakes a client of a shared engine truly root-free: it skips thesharesection for a client (no longer trying to remove the owner's token) and no longer asks for sudo up front when nothing needs it.
v1.22
spark share on|off: one engine for every OS user on a machine. Instead of each user loading the model again, the owner runs the engine (headless) and asparkOS group reads a0640copy of the api-token; a group member's spark answers from that shared engine as a client --spark client http://<host>:8080-- keeping their own soul, memory and threads in their own$HOME. One model loaded once; every user sovereign. The owner's own token stays0600; the shared copy is re-synced byspark share onand the newsharecheck row warns if it drifts. Explicit, not automatic: which engine answers is always the user's ownspark clientchoice. Linux (a shared workstation); macOS and WSL keep one user per box in this version.spark checkgains thesharerow (36 rows).
v1.21
spark drill(contract 13): a source on stdin becomes practice questions it answers. The model proposes a verbatim span of the source as each answer and a question that span answers; an item whose answer is not in the source is dropped before it is ever asked, and a source too thin to drill is one line, never padded from the model's own knowledge. Self-graded -- you see the source's own words and say whether you had it; the answers are graded on this machine and never sent.--namekeeps a schedule: a missed item comes back on a widening interval (1, 3, 7, 21, 60 days) until it is right twice in a row, the one ledger kind that schedules rather than suppresses.--ledger [clear]lists or drops it.spark watch(contract 14): a live stream on stdin -- a log tail, a build, a migration -- watched for the one thing you named. Silent until a line matches, then one line quoting it; the quote is checked against the stream, so it cannot report what is not there, and silence is the healthy state. A window is a few lines or a few seconds, cheap enough to leave running, and a brain that comes and goes underneath it is ridden out rather than fatal. The stream never leaves the machine.spark edit --watch FILE: the live form of the editor's?. It watches a draft on disk and comments on each stanza as you save it, reviewing the whole draft when you pause. Grounded like every?; the writer answers back through the Alt-s ask key, not a built-in chat.
v1.20
spark read(contract 11): the source on stdin -- a page, a message, a document -- a question in the words, and an answer that says only what the source says. Every line quotes the source and the quote is checked; a line whose quotes are not in it, or that quotes nothing, is dropped before the reader sees it. When the source does not answer, the reply is one line showing the source's own opening words, composed in code, never a model's guess. A source past 16 kB is parts (--part N), and the answer's first line names the part it read.--namerecords the question in the ledger (kindread, never suppressing -- a source does not change);--ledger [clear]lists or drops. The second verb through the gatespark asklanded.
v1.19
- The shell layer lives in its own repository: github.com/forgewright-ai/spark-shell. The tools, the starship and Nerd Font pins, the rc files, the terminfo entry and the rendered look (tmux, starship, btop, micro's colorscheme) install from there (
spark-shell on), read~/.config/spark/theme.env-- the palettespark theme NAMEwrites -- and re-render onspark-shell apply. spark keeps no shell code, no shell package, no gate: 35 check rows now, and a first install touches nothing but the AI. - What was appliance behaviour stayed, ungated:
spark quiet login|boot(the silent login and boot) and the bar line -- barespark barorspark bar lineprints the machine's one-line status for any status bar to run. - Moving off the old built-in layer is automatic: bootstrap's
shell-movedrow hands the rc files back from their.bakon the firstspark update, rendered files keep working where they are and spark-shell adopts them;spark shellandspark bar on|offanswer with a pointer for this one release. - The code now speaks the four value areas.
spark edit(contract 10) has its own module besidespark ask's;spark model/spark embermoved out of site.py into model.py; the shell layer's switch and hand-back live in shell.py; site.py keeps the site.env custody, the rc hook and the machine-shape verbs. One dispatch table in bin/spark lists every verb. No verb, flag or message changed. - One home for every decision. bootstrap.sh no longer computes its own answers in sh twins: lib/spark/facts.py prints the machine's facts (distro, build, WSL, memory, the engine's home and flavour, the model picks) from the same code the verbs use, and bootstrap eval's them. The llama.cpp pin moved to engine.env (version + one sha per flavour), read by both sides.
- Bring your own engine, on Linux too. On an architecture spark has no pin for, a
llama-serveralready on this machine ($PATH,/usr/local/bin,/usr/bin) is found and served with -- the engine check row reads(your build). macOS keeps its Homebrew probe; a pinned build still wins where one exists.
v1.18
- Intent search: describe a command you ran, and the line that ran comes back.
Esc rsends your shell's own history (fc -ln -400, bash and zsh) tospark recall <words>on stdin; the model matches meaning, and every candidate is checked against the history (text.anchor) so what lands in your prompt is always a line you actually ran, never invented.Esc ragain cycles the matches.Ctrl-Ris left to the shell and to fzf -- an instant key stays instant, and a dead brain costs nothing. No new contract: the grounding law is borrowed. Nothing is written; the turn record is numbers. - A command pasted from a page is rewritten for this machine. The prompt line's prefix now names this OS's side of the pairs a paste crosses most --
free/vm_stat, the package managers,systemctl/launchctl,xdg-open/open,ls --color/ls -G-- and tells the model to rewrite the other side and say so in the hint. This OS's half only, so the prefix stays byte-stable per machine and the prompt cache keeps hitting. - The failure moment goes further, twice. After the explain, a second
Esc sproposes the corrected command in your line -- the command and its exit code ride tospark line, which answers with a fix through the cmd path, and nothing runs until Enter. And acommand not found(exit 127) offers the line that installs it: a tool spark itself installs is named here with no model call (packages.package_for-- fd is fd-find on Debian, fd on Arch and macOS), the rest through the model. Both ride the same hook the widgets already keep, in both shells; no new check row. - Blast radius: a recursive
rmat the prompt earns its numbers beside the!.? clean the build dirthat comes backrm -rf buildnow reads<- 1,204 files, 3.1 GB, 2 tracked by git -- ...: the count of files, bytes and git-tracked files under the paths, worked out from the command spark already has -- nothing the model proposed is run. Only a recursiverm(a glob or a plain delete says nothing), the walk is capped so a huge tree cannot hang the prompt (the count ends+then), andspark doshows the same line above itsyes. The facts lead the hint so contract 4's cut eats the model's words, not the numbers. spark asksurvives a model that wraps its questions. The brief said to point at the text "by quoting it between double quotes", and small models read that as wrapping the whole question -- every line then ended in a quote mark and the law (it ends in ? or it is not output) dropped all of them: on the box, two different models produced zero questions. The brief now says the quotes go inside the question, never around it, and a line that still arrives as one wrapped question is unwrapped before the gate reads it -- its inner quotes are then exactly the spans the grounding law judges.- Signed lines name verbs that exist.
spark serve offstill answeredspark stop -- stopped, and the tune report signed itselfspark tune; both verbs were removed in v1.17 and nothing looked at the strings that named them.tests/docs_test.pynow reads the verb table out ofbin/sparkand refuses any%s <verb> --line whose verb is not in it.
v1.17
spark uninstallno longer deletes the script it just told you to run. The undo pass is the one root step that runsbootstrap.shrather than a command of its own -- bootstrap is what knows how to unmask sleep, drop the lid file and put the motd and GRUB back -- so when it cannot get sudo, its remedy is "run ./bootstrap.sh". The clone was then removed a few steps later, leaving an instruction nobody could follow and a machine still half spark's. The clone stays while that is outstanding, and says so.- The core documents are the core.
spark help,CHEATSHEET.txt,README.mdandINSTALL.mdno longer carry the shell layer or the apps:spark shellandspark barmoved toSHELL.md, and the apps toAPPS.md, each with one pointer line left behind. INSTALL loses two sections and renumbers; the app table the README carried is APPS.md's now, andtests/docs_test.pyreads the app names from there. - The landing rule binds core documentation, and core documentation moves with a release.
APPS.mdandSHELL.mdsit outside it: kept true as things change, no release waiting on them, nothing in them owed to help, the cheatsheet or a changelog entry. Both say so in their first lines and docs_test checks that they do, so neither drifts back under the rule unnoticed. spark helpreads the same whatever the shell layer is doing. It had two versions of its interface block and picked one by readingsite.env; with the gated verbs gone there is one block, and help no longer reads config at all.spark themeandspark fontstay in it: they are core, and a machine has a face with the layer off.- An install says what it changed, not what it checked. A converged machine printed 54 rows of
okandskipfrom./bootstrap.shand 18 more frominstall.sh; both now print what they changed, what needs you, and the summary -- soNothing to dois usually the whole of it.--verbosebrings every row back, and--dry-runis untouched, because that one is the reportspark checkand the tests read. - The sudo password is asked for once, at the start, before anything is touched. It used to be asked at whichever row first needed root, half way through a run that had already changed things. If sudo is not on the machine at all, that is one line and exit 1 up front rather than a failure later. Only at a terminal: over ssh or a pipe there is nobody to answer, and a run that needs no root --
spark updateon a converged machine -- is never made to ask. spark theme listis inspark helpnow, andspark ember listin both help and the cheatsheet: the grammar names four verbs that takelistand the docs described two of them. The cheatsheet also namesautoandnoneforspark model(it showed them foremberand hid them formodel, whose default isauto), spells the mute the way help does (spark off | on), and writes<words>where it hadWORDS.
v1.16
spark ask(contract 12): a plan, a draft or a decision on stdin, and at most three questions about it back -- one per line, nothing else. The shape is the law, not a request in a brief: a line that does not end in a question mark never reaches you, and neither does one whose every quoted span is missing from the text, one that could be asked of any plan, a repeat, or a question you have already answered (spark ask --answered --name NAME,--ledger [clear]). Three is a cap, never a target: when nothing survives, stdout stays empty and one line on stderr says so, exit 1. At most 12 kB in;--thread IDkeeps a round going, and a follow-up obeys the same law -- the moment a reply may assert, that isspark chat.- One switch vocabulary for both servers.
spark serveused to start a server when a bare verb is supposed to show, and stopping it was a top-levelspark stop, while the FORGE hadon|offfor the setting andstart|stopfor the same thing by hand -- four words for two states, and two grammars for one kind of thing. Now both arespark <verb> on|off, bare shows (spark servereports the url, whether anything answers, and the model), and--forceand--noreloadare flags ofoff.spark stopandspark forge start|stopare gone rather than kept as aliases: there is nobody to keep them for yet. The check rows that named the old verb name the new one, whichspark check --chaosproves by running each remedy. - Memory lives under its own noun, like the soul.
spark remember <words>andspark forget Nwere top-level verbs while the other half of the same identity wasspark soul edit|reset: two shapes for two halves of one thing. It isspark memory add <words>andspark memory forget Nnow, beside theon|offandclearthat were already there. The widget composes the new line too -- the factEsc soffers to keep after a fix lands in your buffer asspark memory add '...'. - Three smaller harmonies.
tunewas a flag and a verb at once (spark bench --tune,spark tune show); it is a sub-noun of the thing it belongs to now --spark bench tune [show|apply]. Thespark ledgertombstone from v1.7 is gone: it existed to tell people where the ledger went, and there is nobody to tell yet. And the signed first line uses one separator,spark <verb> -- <one line>as contract 8 says; the glyph separator is for fields inside a line, not for the signature, sospark themeandspark modelno longer sign differently fromspark shellandspark forge. - A mode is named for what spark does.
askmeant spark answering your question, which read backwards next tospark ask, where spark is the one asking: the mode isanswernow, and contract 10'sedit-askisedit-answer.askis left free rather than reused for contract 12 -- a string that changed meaning would make the turn records already on disk lie about themselves. The old names are read for one version, the waytalkis read forchat, and/api/chattakes either. - The grounding law is one place now,
lib/spark/text.py:anchor()checks a span,Ground.verdict()a whole note or question, andGateis the stream that marks what it keeps and drops what it refuses.Anchors(contract 10's marker) is that gate with nothing refused, sospark edit ?behaves exactly as before -- its own tests are the proof.session.reading()andforge.text_sha()/same_text()come out of the editor's path for the same reason: the next contract needs them, not a copy of them. - The ledger holds more than the editor's declined notes: one sealed file, one record shape, and a kind per contract -- and the rule that retires a record belongs to the contract that wrote it (
ledger.RULES), because a rule that generalised would fit none of them. A note declined in a draft retires when its quote leaves the text; a question you answered stays answered. - A
ledgercheck row: sealed, 0600, what is in it and how much room is left before the oldest records go. Theusersrow watches the ledger file too, so a plaintext one cannot sit unseen in a sealed store.spark checkhas 40 rows. - Contracts 11 (
spark read) and 13 (spark drill) are written down and not built: their text is in ROADMAP.md and inlib/spark/read.pyandlib/spark/drill.py, nothing dispatches to them, and their cases intests/smoke.pyare marked skipped with the reason. spark check --chaosrehearses the failures: it breaks a throwaway machine one known way at a time and proves the right row says so and the remedy that row prints heals it.--selftestproves a row can flip;--chaosproves the sentence under it is true. Each scenario on ports of its own, so a machine that is serving is left alone.- A reply cut off mid-stream is an error, not half an answer. A severed connection is not an end of stream: the read simply stops, so a killed server handed back a truncated answer looking whole, and exited 0. The turn still lands -- the question and the words that did arrive go on the thread, the way they do when you press Ctrl-C.
- A download that dies leaves nothing behind.
bootstrap.shremoved its partial file only on a sha256 mismatch; a curl that failed left a.parton the disk that was already full.--fetch U D Sruns the download primitive alone, so that failure can be rehearsed. - Two
spark updateat once: the second refuses rather than race the first through a checkout. Both locks refuse alike now --spark servesaid it on stderr and exited 1; a lock another process holds is a gate refusal, so it is signed and exits 2.
v1.15
- The failure moment: a command that exits nonzero prints one line above the next prompt --
* failed (1) -- press Esc s to ask why-- andEsc son the empty line puts it back, already piped toexplain; nothing runs until Enter. The command and its exit code ride along, so the answer can correct the command itself, and a command that failed in silence still gets an answer. A destructive head word (rm,dd,mkfs...) is seen but never offered a re-run; Ctrl-C, a no-match fromgrepordiff, spark's own refusals and a multi-line command stay quiet. After the fix works,Esc soffers to keep what happened as aspark rememberfact you edit before Enter. All of it is per pane, in shell variables, with no model call and no fork at the prompt;spark offsilences the line with everything else, and the newfailurecheck row (39 rows now) watches the hook through the liveness marker's fourth field (contract 6). - The hint above the prompt stopped cutting answers at 80 characters mid-word: an answer now carries up to 300 characters, cut at a word, and the widget trims it to the terminal's own width -- so a wide terminal shows the whole sentence. The ellipsis comes from the glyph table (
...on the Linux console, which cannot draw the Unicode one). - Five spark apps: spark-neovim and spark-vim join spark-micro with the whole prompt (one clone, one mapping: complete at the cursor, rewrite, ask in a pane, the ledger); spark-helix and spark-nano put
spark editon the editor's own prompt instead -- helix and nano have no cursor hook, so words rewrite or ask, and nothing completes. Each in its own repository, proven by its own pty test; any editor's filter is still a client with no plugin at all (INSTALL section 6). - The page front redesigned for onboarding: three stages (choose your OS with a picker, spark is live, spark apps), a light theme with a toggle, and a table of contents on the long pages. Every command on the front is a line of INSTALL.md or the README, and the page's palette stays in lockstep with the LAN page's -- both are tests now.
v1.14
The docs rewritten around three lines: choose your OS, one line, spark is live; spark apps, a tool that becomes smart as a client of spark edit; another contract when apps ask for it. The shell layer keeps working, off by default, and the docs stop presenting it as a third of spark.
- README: install in three steps, the verbs in one table, spark apps, what leaves the machine. The page front is spark and spark apps.
tests/docs_test.pychecks the voice: two nouns in what a new user reads, every spark app the README names in INSTALL, the cheatsheet, the credits and the page. - CLAUDE.md, AGENTS.md and CONTRIBUTING.md are framed by the same three lines; the shell layer is one principle, behind its gate.
- A new user is a new user: the word that held them at arm's length is gone from every doc, code comment and CI account name, and
tests/docs_test.pyrefuses it. - INSTALL.md in the new user's order, numbered and step by step (the machine, the one line, the verbs, the models, other machines, spark apps, the per-OS notes, keeping it, spark shell last). CHEATSHEET.txt in the same words, the shell block folded to its verbs.
v1.13
A second Linux family: Arch. One spark, one oracle, the package names as data.
distro()besideis_wsl()in both twins (lib/spark/__init__.py,bootstrap.sh) readsIDthenID_LIKEfrom os-release and answersdebian,archor nothing;SPARK_OS_RELEASEpins it in tests, the waySPARK_PROC_VERSIONpins WSL 2.os_prettyreads the same file.- The package names live in
distro/<id>.env, one file per family, the same eight keys (contract 3): the manager, its install line, the name the docs use, the five groups.lib/spark/packages.pyis the one place python asks a package manager; bootstrap'spkg_installed,pkg_availableandpkg_installare the one place sh does.spark uninstallreads the same file instead of bootstrap's source. - The
aptandbrewrows are one row,packages, in bootstrap and inspark check. - Arch:
getaccepts pacman and names its install line; packages come throughpacman -S --needed, never-Syalone (a name the database cannot find is a todo namingsudo pacman -Syu); thependingrow countscheckupdates;gcc-libsis inbase, so the AI layer asks for sudo only with a GPU. - What Arch lacks refuses in one signed line:
spark font(no console-setup:/etc/vconsole.confis yours) andspark quiet boot(noupdate-grub); thefontandquietrows say so, never fail (check.ARCH_ROWS, a sixth selftest pass).spark quiet loginworks there. - Two rows are generic now: the console palette's boot unit orders after
systemd-vconsole-setup.serviceas well asconsole-setup.service, and the quiet login never creates a motd that was absent. - CI runs the new user's one-liner in an
archlinuxcontainer too, with the shell layer on top; an Arch block in INSTALL sections 1 and 7; the Arch package names in CREDITS.md.
v1.12
A way out: spark uninstall takes spark off a machine and keeps what is yours.
spark uninstallprints the plan, one row per thing, then asks for the wordyes. Everything spark made goes -- units and timers, the shell layer's look (rc files and renders back from.bak), the spark line in your rc file, the console palette (VGA again, the boot unit removed), the Nerd Font and the terminfo entry,~/.local/bin/{spark,explain, starship}, the engine and every model,~/.config/spark,~/.local/state/spark, and the clonegetmade. Your soul, memory, sealed users and account keys,models.env, themes andprivacy-termsstay unless--purge; the shell layer's packages are a question (--packages/--keep-packagesanswer it up front);--dry-runshows;--yesis a script's form. Headless and the quiet login and boot are undone first through their own bootstrap rows; a root step whose sudo refuses is atodorow with its command. What spark could not record before changing it -- a hostname, pmset, a console font set before this release -- is named with the line that puts it back; the console font's original is kept from now on (console-setup.spark-orig).- Terminal.app:
theme.remove_profilestakes the spark profiles out of the preferences (a default or startup setting that named one falls back to Basic). tests/uninstall_test.shproves it against a real clone in a throwaway HOME: the plan changes nothing, a non-terminal refuses,--yeskeeps exactly yours,--purgekeeps nothing, a developer checkout is left.
v1.11
The help in the maintainer's own order; an empty buffer is a page to write on; the console palette paints the whole screen, and the login one.
spark helpreads top to bottom as spark (status, off|on, soul, memory, ver, chat, ask, do, edit, then atry:list of the prompt gestures), the FORGE: server/client, the machine (setup, check, update), the interface (shell, bar, theme, quiet, font) and "less often used"; the tail is the version alone (README carries what leaves the machine). 46 lines with the shell off, 48 on. CHEATSHEET follows the same order.spark editon an empty text: words write it from nothing (a new file in micro no longer answers "edit reads stdin"), the reply ending with a newline;?and Enter alone say what is missing in one infobar line.- The console palette paints the whole screen: a framebuffer console colours only what is drawn after a palette change, so
spark themeandspark shell on|offredraw after sending it. The login screen is drawn before any shell, so bootstrap's newvt-paletterow installs a one-shotspark-console.service(root,setvtrgb, the same sudo as the font) that sets the kernel's defaults at boot fromconsole-colors.rgb, for every VT;nonewrites the VGA sixteen instead of a reset, and thethemerow watches the boot palette.
v1.10
Three domains share the one command: spark, spark shell, and the smart apps in their own repositories; Windows through WSL 2, honestly.
- spark ships no app. The micro plugin moved to its own home, github.com/forgewright-ai/spark-micro, and installs micro's own way: a clone into
~/.config/micro/plug/sparkand oneAlt-sline. An editor becomes smart by being a client ofspark edit; spark keeps the verb and its judge (the audition), never the plugin, the packages or the row.spark updatehands back the links an older install made (themicrobootstrap row), then clone as the plugin's README says. spark shell oninstalls no editor: tmux, starship, fzf, zoxide, eza, bat, btop and the Nerd Font, and nothing else. A micro you have still wears the palette (its colorscheme and the seededsettings.json, rendered only when micro is on PATH);spark shell offkeepssettings.json-- it is micro's after the seed -- and drops only the colorscheme key it seeded. The rc files setEDITOR=microonly when micro is there; the core hooks export nothing of micro's.- Windows users have a way in: Ubuntu on WSL 2 is Linux to spark, minus what the VT console and GRUB own.
spark fontandspark quiet bootsay so in one line and refuse to set;spark headless onrefuses (the distro stops with its last window); the gpu, services and encryption rows say WSL 2; the status and the check header name it. Pinned by fixture (a fifth selftest pass, Linux only) -- CI has no WSL runner. spark helpis sectioned by domain -- spark, at the prompt, the FORGE, the server, the machine, spark shell -- and half as long: 51 lines with the shell on, no line wider than 80. The forms still live on each verb's-h. README opens with the three ways in; INSTALL is sectioned the same way, with a Windows walk and per-OS notes for three tribes; CHEATSHEET and CREDITS follow the layers.- The hostname row leaves the shell gate:
SITE_SET_HOSTNAMEis identity, not look. git and shellcheck leave the user's packages (core has git; shellcheck is a contributor's tool). The v1.3 stubsbootconfigandtalkare gone; the page's two boot buttons runspark quiet boot on|off. One verb table drives dispatch and the near-miss hint. tmux's status line asksspark barevery 15 s, not 5. - The docs' counts are tests: the category split, the roadmap's version and the page sources join the derived facts
tests/docs_test.pyreads. 38 check rows.
v1.9
A new user's first run: three questions, and a machine that looks untouched until it is asked to change.
spark setupasks three things, not four. The palette is no longer a question: spark ships wearinggruvbox-dark, andspark theme NAMEorspark theme nonechanges it whenever you like. Asking a first-timer to pick colours for tmux and starship -- the shell layer, which setup leaves off -- spent a question on nothing.--themeandSITE_THEMEpre-answer it.- A first run now leaves the machine looking exactly as it did. setup wrote the palette's runtime files whatever
SITE_SHELLsaid, and the rc hook is core: it catsconsole-colorson aTERM=linuxconsole, so a fresh install with the shell layer off repainted the VT at the next login.SITE_THEMEis still recorded; the files, and the macOS Terminal profile, land withspark shell onor an explicitspark theme NAME. - A palette reaches the console it is typed on.
spark themewroteconsole-colorsand left it for the next login, so a theme arrived late and turning one off looked stuck until a logout and aclear. One helper sends that file to a running Linux VT, andspark theme,spark shell onandspark shell offall use it. An emulator is never repainted. spark themeno longer asks for a shell restart it never needed: starship re-reads its config on every prompt, the widget draws no colour of its own, and the hook readsconsole-colorsalone. Only a running micro must be reopened.spark shell offhands the terminal back: the palette goes with the layer that brought it, a running console is reset at once, and it prints theopen a new shell (exec $SHELL)line thatspark shell onalways printed.SITE_THEMEstays, sospark shell onpaints it again. Thethemecheck row readsna, notfail, for a palette nothing has painted yet.spark font liston Linux names the Nerd Font. The console takes.psffaces from/usr/share/consolefonts; the JetBrainsMono Nerd Font thatspark shell onunzips into~/.local/share/fontsis a.ttffor a terminal emulator and can never appear in that list, so it is named under it with where to set it.- The setup table says it is not the whole list. Only rows proven on the line and under an open license are offered -- today the five qwen3 rows -- so the first run read as though spark served nothing else. One line counts the other 21 and points at
spark model list. - INSTALL.md opens with two numbered walkthroughs instead of prose: a machine from zero (the Debian image, the empty root password that earns you
sudo, the packages) and spark on a machine you have (one check forsudo,git,curlandpython3, then the one-liner). The runbook follows. - The page really does rebuild on a release.
release.ymlcreates the Release with the job's ownGITHUB_TOKEN, and GitHub raises no event for what that token does, so v1.8'srelease: publishedtrigger never fired and the page went on naming v1.7.pages.ymlwaits for the release workflow to finish instead, and publishes only when it succeeded.
v1.8
A rendered file stops moving with the network; the page stands on its own.
SITE_NAMEand a guessedSITE_GIT_EMAILtakescutil --get LocalHostNameon macOS instead ofhostname: withHostNameunset the kernel name follows whatever the network last said, so~/.gitconfigand~/.tmux.confdrifted from their templates on their own, theconfigsrow failed, andinstall.shwould have rewritten the author line of every later commit. Linux is unchanged../bootstrap.shnames a guessed git identity in itsidentityrow whether the key is absent or still the example's placeholder: a guess signs every commit, so it is said out loud.- A client's
emberrow readsnabeside its other AI rows -- a client keeps no second model of its own, andspark ember NAMEthere is refused.spark ember listshows what the peer offers. - The page rebuilds when a release is published, and its sign line links that release: the page and the GitHub release never disagree.
- The page loads no font from Google -- the local stack draws it, and a browser no longer waits on fonts.googleapis.com to render it.
v1.7
The editor grows up; the shell learns your palettes; a client stays a client.
spark servepasses--cache-ram 0: llama-server no longer keeps replaced prompts in host RAM (a 12B model on a 16 GB box had climbed from 32 % to 90 % in a day).SPARK_EXTRA_ARGS=--cache-ram Nsets a budget in MiB; theserverow reports it.spark edit ?checks every quote against the text: a span the text does not hold is followed by[not in the text].--sel A Banswers about a selection in the light of the file around it;--thread IDkeeps an exchange going, sealed like a chat thread;--decline --name NAMEretires a note so the next?does not raise it, until the words it quoted leave the file.- The micro plugin (1.3.0): a Lua error is an infobar line, never a dead editor; an answer over text that moved opens in a pane instead of being spliced. The pane has keys --
qcloses, Enter jumps to the quote,aapplies a code block,ddeclines a note -- and?? wordsgoes on in it.ledgerandledger clearat thespark>prompt list or drop the file's declined notes; the shell verbspark ledgeris gone. - A client stays a client:
spark model,ember listandmodel budgeton a client print the peer's table (GET /api/models), and the verbs that would make it a server are refused with one line;spark client offremains the way back. - Your own palettes: a
~/.config/spark/themes/<name>.envwith the 21THEME_*keys is listed, chosen, completed and checked like the six in the repository, and wins on a name clash.THEME_LOGO, optional, paints the banner's six rows in a palette's colours. spark theme NAMEreaches everything at once: the open Terminal.app windows on macOS (profile, font and cursor), micro'scolorschemewhen micro had changed it, and tmux when it runs.spark fonton macOS refuses a face the Mac does not have and lists the monospace faces installed; the default is the Nerd Font at 13, one face and size for every profile. On Linux the list spells sizes the way the command takes them, width by height.spark quiet audio on|offsilences every sound spark makes; theaudiorow names the player it would use. 39 check rows.tests/docs_test.pykeeps the docs true (credits, counts, pages); the page shows a section above the newest tag asvX.Y (unreleased);tests/audition.pyscores the editor's briefs against a live brain, outside the gate.
v1.6
One model list. The three lists (curated, embers, community) are one models.env, 26 rows, tested or not, any license named.
models.env: every row carriesMODEL_<NAME>_LICENSE;_TESTED=linemarks a row proven on the line;_NOTEis one optional line.embers.envandcommunity.envare gone;~/.config/spark/models.envis still yours.autopicks only among tested rows under Apache-2.0 or MIT; any row is yours by name; a row under another license prints it and asks first.- New rows: Qwen3-Coder-30B-A3B, Qwen2.5 7B/14B, Mistral 7B v0.3, Mistral Nemo 12B, Phi-4 mini and 14B, DeepSeek-R1 distills 7B/14B, SmolLM2 1.7B, gpt-oss-20b, Granite 3.3 8B, Llama 3.2 1B/3B, Llama 3.1 8B, Gemma 3 1B/4B/27B. Untested until someone posts the line proof.
spark model listandspark ember listprint the same table: license,line, a note under its row,ufor yours. No more? emarks.spark model add URLworks on huggingface.co again: the size and sha256 live on the redirect, not on the CDN it points at.- The page's models page is one table.
CHANGELOG.mdandROADMAP.mdare lists now, not essays; every doc drops the list words.
v1.5
The editor wave: micro is the first smart tool.
- Two arcs: OS -> spark -> smart shell and chat; tools -> spark -> smart tools.
spark edit(contract 10): text on stdin, raw text out;--at Ncompletes,<words>rewrites (12 kB cap),? [words]asks or reviews; hints--type --name --about --part. Works from a pipe.- Before a
?, a 20-token reading (language, kind) is restated to the model, so a Portuguese draft is answered in Portuguese. - micro plugin
home/.config/micro/plug/spark/:Alt-sopensspark>; the new text is left selected -- Backspace discards, Ctrl-z undoes.setlocal spark.about "..."says what the text is;set spark falseswitches it off. No key is bound from inside (micro would detach the link). Esc abecameEsc sat the prompt: one gesture, shell and editor.- Check row
editor(CAPABILITY, shell layer); 38 rows. tests/micro_pty.pydrives a real micro against a stub spark.Session(role=),ask_stream/ask_json(max_tokens=, timeout=),forge.clip,text.Fence.
v1.4
The multi-user wave. Break: the shared ember-token is gone.
spark user add|login|logout|remove|token --new|claim: named users, each with a personal token shown once and never stored.- Threads, memory and chat history sealed per user (ChaCha20-Poly1305 from RFC 8439, pinned to its vectors) under a key only that token opens; the box keeps a hash and a wrapped key. No reset.
- The FORGE is multi-user: forge-token = admin (the box account's store); every other caller is a user;
GET /api/usersshows names and counts, never a word. Browser logins live in memory (a restart asks again). [cwd]rides only the modes that propose a command (line, do, explain); a conversation sends no path.- Turns are numbers: one choke point strips every free-text field.
- Check row
users: 0700 dirs, 0600 keys, sealed magic; nags until the old ember-token and plaintext files are gone. .githooks/commit-msg: the privacy patterns over the message too.
v1.3
The CLI experience wave: one grammar, a machine that explains itself.
- The grammar (CLAUDE.md): bare = show;
on|offthe only switch words;status= bare,list= the table;-hfirst, signed; one confirm shape, one spinner, one exit-code law (0, 1, 2, 78, 130). spark quiet start|login|boot on|offreplacesspark bootconfig;boot onis a genuinely silent boot (one GRUB drop-in).- Long output pages through
$PAGER, plain when piped.spark helprewritten. - Chat:
/resume [N],/clear,spark chat --thread N [words]. - The line and chat prompts know spark's own verbs (
?? how do I change the themenamesspark theme). - TAB completion for verbs and their names, bash and zsh, offline; check row
completion. spark setupasks the theme; the palette lands on console, micro and tmux at once; check rowtheme; nord and tokyonight-night join.spark shell offrestores the rendered look from.bak;spark fontleaves the shell gate;spark font list.- Fixes: did-you-mean at exit 2;
spark updateexecs the freshbin/spark; a restore never leaves an empty~/.bash_profile(rc-loginrow); plain-text replies, no markdown. 36 rows.
v1.2
The page wears the brand.
- The FORGE page: the ember palette by default, dark and light, all mono; marks
* > !on transcript rows; a blinking caret; copy buttons; a stop button; keysn j k. - The config page's ember picker runs
spark emberon the box; the headless switch is gone (it needs sudo). /manifest.webmanifestand a drawn/apple-touch-icon.png: an app on an iPhone's home screen (Android opens a tab over LAN http).- The same page runs inside sparkapp (macOS and Windows): the login card asks the address too;
qquits.
v1.1
- The
peerrow understands a FORGE (/api/health), reportingforge <host> okorup, its model loading|down. spark checkover plain ssh finds~/.local/binand Homebrew's bin.- The client shape:
spark client URL|off; bootstrap skips the engine and the units,install.shlinks no unit, the rows inCLIENT_ROWSreadna; a fourth--selftestpass proves it.
v1.0
The ignition: one line, and a fresh Debian 13 or macOS has a private local AI at its prompt. spark is public, MIT.
get: clone or pull~/.spark, refuse what is not spark, never sudo, hand over tospark setup(name, user, model;--yes,--model,--name,--user; stdin not a tty takes every default).- Two layers:
SITE_SHELL=off(the AI only, one rc line appended) andspark shell on(tmux, starship, micro, fzf, ...); shell rowsnawhen off, a third--selftestpass proves it. - One model by default:
SITE_EMBER_MODEL=none; the engine is the pinned llama.cpp tarball on both OSes (six flavours, check rowengine); the table gains a speed column. SITE_AI_BUILD=auto: vulkan when a GPU reports its memory, else cpu;autopicks the largest model under the budget AND the build's speed cap (3 GB files on cpu, 6 on vulkan, 20 on metal); units warm the model after/health; launchd rows skip without a gui domain.- Privacy words leave the tree (
~/.config/spark/privacy-terms,SPARK_PRIVACY_TERMS); the tag is the release (spark verfrom git,spark update,release.yml); CI grows a Debian 13 container job. - Three model lists plus yours (folded back into one in v1.6);
spark model add URL [--sha256] --license,spark model verify(check rowmodels),spark model budget N(SITE_AI_BUDGET). spark chatv2: wrapped replies, readline history, Ctrl-C stops a reply,/help /new /last /model /q.CREDITS.mdnames every project spark downloads or installs.- Measured: a fresh Debian account over ssh reached its first answer in 10 min 19 s (a 4.7 GB download, 8.4 tok/s on vulkan); a fresh M4 Mac in 16 min 25 s (13.0 tok/s on Metal).
v0.4
The ember: one llama-server serves two models.
SITE_EMBER_MODEL auto|none|name;spark serveruns llama-server as a router (--models-dir, a renderedpresets.ini); the spark role at context 4096 with reasoning off; the request'smodelfield routes.- The rule: the prompt line is spark, every sentence is an ember; the identity rides only with the ember.
spark ember [NAME|auto|none|list]; check rowember;/api/healthgainsmodelsandroles; 31 rows.- Two tokens on the FORGE (admin and user), two faces on the page;
spark forge token --new [--user]. spark chatis the conversation verb (spark talkdispatches for one version); a generous quit grammar; one mark pair* !on both OSes.- A head-word guard: a command whose first word is not installed here is re-asked once;
spark donever offers it. spark do: a done summary whose numbers no output backs is marked unchecked; each turn records the model that answered.- Linux: the serving user joins the
rendergroup (GPU without a seat); the zsh widget empties the line before it speaks; downloads and restarts narrate both ends.
v0.3
The FORGE: spark is the seed, the FORGE is the agent it builds and keeps.
spark forge(lib/spark/forgeserve.py): a stdlib HTTP server in front of llama-server, one LAN address, a 0600 forge-token, cookie or bearer;/v1/chat/completions,/v1/models,/api/*, the page.~/.config/spark/soul(spark soul [edit|reset]) andmemory(spark remember|forget|memory); check rowssoul,memory.- Threads:
? wordsstarts one,?? wordscontinues the newest;spark talk,spark @FILE words,spark do <words>(one confirmed command at a time,yesfor a destructive step). spark headless on|off: linger, sleep masked, lid ignored (Linux); LaunchDaemons andpmset(macOS); check rowheadless. 30 rows.
v0.2
- Every turn records the server's timings;
spark last,spark status,spark stats(percentiles, cache hits, GPU). spark bench [--tune],spark tune apply(SPARK_FLASH_ATTN,SPARK_KV,SPARK_THREADS,SPARK_NGL).- Check rows
throughputandgpu;spark modellists and switches.
v0.1
- One command,
spark:? words,words?,Esc a,explain;spark serve|stopwith a systemd unit and a launchd agent;spark checkwith a fixture selftest;spark bar;spark theme. - One
bootstrap.sh, oneinstall.sh, Debian-family Linux and macOS; four palettes; five pinned models chosen by RAM; a privacy gate on every commit. Python 3.9+ stdlib and POSIX sh only. - After the first Debian 13 box:
libgomp1; ASCII on the console;spark theme NAME|list;spark ver;spark font;spark bootconfig.
source: docs/CHANGELOG.md on GitHub -- this page is that file at spark v1.113.1