spark
curl -fsSL https://github.com/forgewright-ai/spark/releases/latest/download/get | sh

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 milk and spark mcp call reminders snooze are 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 mcp writes those to mcp-values when 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 do and 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 2048 with --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 mcp as 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_mute or spark out_mute, is its spark mcp call line 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 answered svlogtail -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 1 on every server command and parallel = 1 in the router's presets. Left to itself llama-server opened four and shared one cache of SPARK_CTX among 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 stays SPARK_CTX, by hand (#157).
  • Fixed: spark read read 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 --update and --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 --frobnicate and brew install --frobnicate passed. 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 -y stays right after dnf5 install. git keeps its rule. A --no-FOO is right where the manual names --FOO, so systemctl list-units --no-legend is no longer doubted (#161).
  • Fixed: read, do, recall and edit ? --source sent 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 ver names 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 --credits and the end of spark help add 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/health answers; 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 read forge.log wait for their lines (#165, #144).
Show 123 older releases

v1.111

  • Fixed: spark do called 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 nomatch now, 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, as spark read and spark ask do, never a second (#113).
  • Fixed: with COLUMNS exported, ps cut the engine's command line, so serve warned 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 *.txt and **/ 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 --sandbox runs 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 to bwrap --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 readable EPERM (#156).
  • The serve row 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 of SPARK_CTX among 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 and green accepts 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 --sandbox runs on bubblewrap 0.8 to 0.10, Debian 12's and Ubuntu 24.04's, where --overlay does 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 sandbox and the sandbox row (#155).
  • A trust by hand is pinned. spark mcp trust NAME on writes MCP_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 the mcp row says so with the remedy. A tool whose flags ask less than the pin's did, or one the pin never saw, takes the typed yes again until the person trusts the server again (#154).
  • spark setup finds a llama-server of yours on llama-server's own default port, 9931 since b11960, as well as on SPARK_PORT (setup.ENGINE_PORTS), and offers the one that answers. SPARK_PORT stays 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_KEEP and engine.ENV_PREFIXES name what it gets (its path, its locale, the GPU variables), and every LLAMA_ARG_* of a shell is dropped, so none can point it at another host. Its command carries --offline. The serve row warns on a connection the engine holds to another host (engine.outbound) (#151).
  • voice.env says 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.py fails a bump before that (#150).

v1.108

  • spark serve https own writes 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-land runs tests/land.sh in a forked context, so the gate's minutes of output stay out of the session; spark-release is the Releasing steps, run by a person alone; spark-add-check-row is 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 imports AGENTS.md and 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.py holds 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 off waits 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 spark call 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.py runs in four parts (--part line|read|machine|tree) and widget_pty.py in 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 gh and curl on 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 steps serves 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, asks spark steps one 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 steps and the MCP client's gates stay as they are.

v1.106

  • Pi leaves spark. v1.104 moved Home tasks, spark do and spark chat onto 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 and spark do --tools answer 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 --agent words, no agent check row (42 rows again), no Node or npm in the SBOM, no runtime job in CI. spark chat works 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 update hands the space back: a machine that ran spark do --agent on has the runtime removed from spark's data directory by the new agent row of bootstrap, once, with one line said.
  • Kept from v1.104: do.fit keeps 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 do and spark chat share 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 on installs 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 model auto may 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 read answers about command output, configs and data. A quote of one word now grounds when it points at one place: a value such as 96% or /dev/sda1, or a word the source says at most three times, such as a login. So df -h | spark read which disk is almost full answers 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 read finds 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 watch shows 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 recall reads 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 do can write a long file in one step. A step had to arrive whole within SPARK_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 TMPPREFIX is the run's own temp folder (#103)
  • spark do no 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 as grep -q, is no failure (#104)
  • An empty pipe into spark read says so on stderr, and stdout stays empty for the program that piped it (#105)
  • spark explain why the sky is blue answers 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 --sandbox nobody 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, --accept included, 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.py is 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 --record keeps the model's replies, and replay serves 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 watch finds 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 fails stayed silent on an nginx timeout and a 504 (#80)
  • A busy model skips one window of spark watch and spark edit --watch, as a loading one does. Before, the page's 429 to a full queue ended the watch with exit 1 (#90)
  • spark read finds the part that answers. A text past 16 kB was refused until you named a part, so man tar, man sshd_config and 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 N reads the one you name (#84)
  • spark read keeps 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. ask reads them the same way
  • A chat turn after /read answers 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 do tells 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. A sed whose 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 do lowers the model's ! on a step whose every stage only reads, as the prompt line does: du -sh * | sort -rh | head asks for Enter, not yes. sort is a read-only head, with -o, --output and --compress-program denied. 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 stop spark line once 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)
  • explain names a command only when a command fixes it. A fix inside a file is said as the file and the change, never as spark edit FILE (#87)
  • The fix after an explain (Esc s again) 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.yml verifies every release behind a tag against the file of the release before it, back to v1.35, the first that carried allowed-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.sh verifies the tag it patches against the installed spark's allowed-signers (~/.spark, or SPARK_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.py and the web fonts join PROTECTED. 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 SHA prints when a commit reached main, the created time of its first ci.yml run there, and tests/tag.sh counts 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[2J or \a reaches the screen as the text it is. Before, zsh decoded the backslash into the escape itself, past spark's own filter. bash's printf '%s' never did
  • A token rotation's session cookie is Secure over 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 login sends 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 origin alone, not any remote in the clone's config. A call line wrapped in timeout N, command, nice or env K=V is read as the call line it is, and carries the ! mark
  • spark do with 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. update from the page takes the bare verb or --dry-run, nothing else. forge.log keeps 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 curl calls 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 no edit-read turn 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 kind warm, 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: https under machine runs spark 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: update under machine runs spark update. The header says newest vX.Y when a newer release is out: GET /api/me carries newest for the admin
  • What left this machine: GET /api/stats carries sends, the rows of spark stats --sends for 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/log and the log event. 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 ... | head ends 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.css and 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 here on the site, as setup's own live question, in the README and in spark setup's closing lines, which are now the README's first three steps (#69)
  • The admin adds a user from the page: POST /api/users mints one as spark user add NAME does, 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 ... | head ends 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 https bare 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 HOST puts 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 new https check 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 --login shows its QR first, then the login's. spark serve https shows the certificate and who must trust it; spark serve https off is 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 FINGERPRINT pins that page's certificate, or saves its root when the fingerprint is the root's; --ca FILE trusts an issuer. A page whose certificate is not trusted is refused with its fingerprint and both remedies, nothing set. SPARK_CA_FILE is 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 off forgets it. The machine that serves trusts its own pair.
  • Contract 3: SPARK_FORGE_CERT, SPARK_FORGE_KEY, SPARK_FORGE_NAME and SPARK_CA_FILE. Contract 9: the TLS listener and the root's. openssl joins 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 add no longer offers to claim older plaintext chats, and spark user claim reads 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/models lists the served roles alone, nothing of the engine's own listing- spark client URL is 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 off stays 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 sends and watchdog rows settle like the servers' rows: red for a moment at a restart, said only when still red three minutes later.
  • spark update says 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|b shorthand 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-artifact 7.0.2 and github/codeql-action 4.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.sh lands 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/tale is 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, one MCP_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 mcp wrote and never starts a server. A tool step is a spark mcp call line you read before Enter. In spark do it runs through the same gate; over --porcelain a tool that asks the yes is refused, and inside --sandbox no 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. The mcp check row reads the list: spark check has 41 rows.
  • spark voice listen needed nothing new: the dialog's talk chip runs spark voice listen --buffer and stops it with SIGINT.
  • Measured before the tag, gemma4-26b-a4b. spark steps with 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 URL stopped 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.Z heading on main failed 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 its try/ 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 ver and spark update know the shape. A patch never adds a command, an option, a key, a check row or a help row, and tests/patch_test.py holds it to that, in the gate.
  • tests/tag.sh tags a patch by the CHANGELOG's word: S1 and S2 once the fix has rested 6 hours on main, 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-signers and get carry both. Its private half is sealed to one machine and is never in the cloud.
  • CI ends in one green job that needs every other: the name a rule on main can require.
  • tests/land.sh --passed asks GitHub through curl when gh is 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 spark is spark check or spark status. ? show the last answer again is spark last. ? turn the prompt line off is spark off.
  • spark help says 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 --sends and spark keys NAME KEY. TAB completes the options of spark ver and spark stats.
  • A right command is no longer doubted. The hint said the systemctl manual has no command restart about systemctl restart, and the same about zypper 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 as spark 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 says type 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 alice is useradd, never spark 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-e4b sometimes thought in silence and spent its whole reply there: 14 of 551 questions got the 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 stats has a no answer line: the questions that got none, and how many were asked again. spark last says (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 to spark model list: a larger model answers more often. spark check has 40 rows.
  • More commands carry !: a package cache cleaned, on every system. That is apt clean and apt autoclean, with apt-get and aptitude too, dnf clean, yum clean, zypper clean, zypper cc, pacman -Sc, paccache -r and brew cleanup.
  • spark serve off carries ! too: it stops the engine and the page. spark do asks you to type yes for it, and the page asks you to confirm.
  • The ! is steady on spark's own commands. A model marked spark update 5 times in 12. spark now decides the mark on its own commands itself.
  • On Void, the prompt line names svlogtail to read logs, where it is installed. It no longer names svlogd, which writes them. It also knows how a service starts at every boot there: a link in /var/service. sv up only starts it now.
  • spark status says what needs you. Its new check row shows what the last check found: 2 need you -- spark check, ok, 3 min ago or not 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. With gemma4-e4b no 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 URL stopped the engine that ran there, but the page's server kept running and listening on your network, and spark serve off would not stop it. Both are stopped now, and the run says so. On a client made by an older spark, spark serve off stops 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 --history still lists the change.
  • Fedora and openSUSE install on a minimal system. Setup stopped there with tar: command not found. spark now installs tar with 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 setup asks 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, TAB completion 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 a moves one, and Alt-a and Ctrl-g work too. spark keys ask none leaves the key to your shell. spark keys reset brings the defaults back. The names are ask, recall, height, listen and stop.
  • spark keys off binds none of the 5 keys. Your shell keeps its own keys there, and its own wait after Esc. spark keys on binds them again. The choice is kept as KEYS=off in ~/.config/spark/keys.env.
  • A hint at your prompt never names a key that is not bound.
  • SPARK_OFF=1 in the environment starts one shell without the prompt line. It is now in the docs.
  • Your own llama-server. spark client URL uses a llama-server you already run, and spark setup --engine URL does it on a new install. At a terminal, setup offers one it finds on SPARK_PORT. spark downloads nothing for it. --engine refuses a spark machine's address, and one that does not answer.
  • spark never stops a server it did not start. spark client URL, spark uninstall and spark serve off end spark's own engine alone. spark client off refuses while another server holds the port.
  • spark client URL --key-file FILE names 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 serve and the peer row of spark check say whose engine it is.
  • The context size comes from your server unless SPARK_CTX is set. The chat names the model when the server gives it no alias.
  • Ollama is not supported in this version.
  • The notice. get and spark setup say 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 setup asks go on? yes/NO: once. A no changes nothing. With --yes, SPARK_YES=1 or no terminal it asks nothing and goes on.
  • On a new install the three lines print once. get shows 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 --credits shows them any time.
  • Fixed: spark uninstall left 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 check row 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 off keeps both quiet. After spark awaken the face is alarmed, then pleased.
  • spark check --history lists every change, newest first, and spark check --history NAME those of one row. It keeps the row's name, its state and the time, never its text. Changes are kept SPARK_HISTORY days, none when it is off. spark clear --history forgets them.
  • spark check NAME no 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 update heals it. An update no longer reads every program again. On a machine that uses another machine's model, spark update reads until nothing waits.
  • Small fixes. A pasted answer shows the pleased face. spark awaken shows a moving bar while a model loads. In zsh, Ctrl-C at 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 with sudo dnf install -y git openssh-clients python3 curl, then run the install line.
  • openSUSE Tumbleweed and Leap. Packages come through zypper. Install the basics with sudo 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, then sudo 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 on speaks 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, and spark awaken lets 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, explain and 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 Enter erases 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-C at 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 drill and Esc r show 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.sh sends your commit to a try/ branch and waits. On green it moves main to the same commit. On red main is 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.sh holds the one test list. sh tests/gate.sh full runs 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 claim never 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 do cannot send data off this machine outside the sandbox: scp or rsync to 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 edit drops 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 do and 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 needs yes typed. An app driving spark do cannot 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 r replaces secrets in your shell history with [held] before it is sent.
  • spark update checks 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.env needs 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.md and docs/IDEAS.md are 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. /aloud now stops it.
  • With the voice off, /aloud makes 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 on and /aloud off work 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, after Esc x and once you type. A reply that is not spoken still starts with *.
  • spark awaken makes 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 words is gone. spark look shows 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 ! ..., never spark: .... A mistake you typed is spark 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 greet prints nothing.
  • spark chat opens 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, and Esc s then says * Enter explains the error. A hint no longer ends with the manual it was checked against.
  • spark check prints only the rows that need you, with their remedy, then the totals. Nothing to fix prints the totals alone. spark check --all prints every row, as before. --porcelain, --report and the exit code do not change.
  • A remedy that said ./bootstrap.sh or sh install.sh now says spark update.
  • spark ver prints the logo and the version, as a login shows them. The credits moved to spark ver --credits.
  • spark do no longer prints a driving with line, and the end is * done with 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: in spark, spark status, spark check, spark do, the chat and the page. --porcelain output keeps the file name for programs.
  • A command that changes nothing prints nothing: spark look, spark height, spark reveal, spark on and spark off set to what they already are. A question you answer no prints * nothing changed.
  • spark voice -h ends in one line: the first time, spark downloads about 600 MB, 586 MB on Linux and 602 MB on macOS.
  • spark setup asks read aloud to you (for low vision)? [y/N] and ends with open a new shell (exec $SHELL), then try: and 3 lines.
  • /api/health adds names, each role's model by its list name, beside roles. 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 rate sets 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 do and 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 do loads 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_MS in 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. /again speaks the last reply sentence by sentence.
  • spark update waits for a big model to stop. On Void, spark's sv calls 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 do step 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.
  • r reads 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.
  • e on a block opens it in your editor ($VISUAL, $EDITOR, else micro, nano or vi), as a 0600 file removed after. With none, e says 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 voice reads aloud and hears a question. spark voice clear|on|off sets the mode, rate N the clear voice's speed, test plays one line, listen writes one spoken question out and stop ends 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 in voice.env and checked by size and sha256 before a byte is unpacked.
  • spark voice off --remove deletes the engine. spark uninstall removes it with the rest.
  • Clear mode is for low vision, and it works before spark awaken. spark setup offers 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 awaken makes 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 v listens 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 press Enter.
  • Esc x stops 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 --anyway speaks 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: /aloud speaks every reply, or stops, and /again says the last one again. Mode on speaks the greeting and the goodbye.
  • Esc on an empty chat> line ends the chat, as Ctrl-D does. With words on the line, Esc leaves them as they are.
  • The voice row replaces the audio row. It is na while 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 spark is one line now: what answers, the model and its URL. spark status is the full report, as before.
  • spark serve on is brief now: one line for the engine and one for the page, once each answers. The steps to join from another machine are in spark serve --login, as before.
  • spark quiet is gone. quiet is an unknown word now, like any other. The greeting and the news follow the look alone.
  • spark look is one switch: spark look on|off|auto sets motion, colour and words together, in one key, SPARK_LOOK. It is off until spark awaken sets auto. Each part keeps its own auto rule, so NO_COLOR still keeps the colour off under auto.
  • spark look motion|colour|words and spark look reveal are gone. spark reveal and spark height stay as they are.
  • Bare spark look shows the switch, the height, the reveal and whether the machine is awake.
  • An old site.env or spark.env that holds SITE_QUIET_START, SITE_QUIET_AUDIO or a SPARK_LOOK_* key still loads, and spark ignores those keys.
  • The audio row says only which player is here.
  • spark do asks once more when the model says done before any step ran. A second done ends the run and says nothing ran.
  • spark do gives each step 600 tokens, room to write a short file with printf. 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 got qwen3-4b before. On the maintainer's box with the GPU switched off, E4B writes 16.8 tok/s and qwen3-4b 22.1.
  • qwen3-4b serves 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. auto takes 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-a4b for a machine with a 19 GB budget, gemma4-e4b the standard, qwen3-4b without a GPU, qwen3-5-2b for 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 replaces qwen3-1-7b (56 % on the tools against 25 %), and qwen3-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 -a3b counted 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-20b was 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, auto now picks it over qwen3-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, but qwen3-4b is ahead of it on the prompt line, so auto keeps 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-4b 18/30, qwen3-8b 19/30, qwen3-14b 19/30 and granite-4-2-8b 23/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 is chat -- /help, Ctrl-D or /q ends and 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-D and 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, xclip or xsel. 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 way spark read does, every line quoting it, and the exchange lands on the thread.
  • /do GOAL and /do --sandbox GOAL hand the goal to spark do, each step confirmed, then return to chat> .
  • @FILE in 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|off runs the engine and the page together, and remembers it: SPARK_SERVICE and SPARK_FORGE are written as one. off stops both and keeps them down. Bare spark serve shows 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) and spark serve --audit [N].
  • spark model --chat NAME|auto|none|list chooses the chat model.
  • spark status shows what the engine answers. spark brain --porcelain stays for a program.
  • spark client off means off. The other machine is never asked again, even while it answers, until spark 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 share and spark brain.
  • spark serve on never starts a second server beside a unit. A loaded or disabled unit is started through its init, then waited on.
  • spark serve --foreground takes over from a server spark started by hand. When a server spark did not start holds the port, it still exits 78.
  • spark serve off also stops a server spark started by hand when the unit is down.
  • spark update restarts 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 services row warns.
  • spark model NAME, the chat model and spark bench never 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 down first, so a half-written service never starts. The check's service keeps its errors in its log.
  • tests/finish_test.sh proves 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/issue change only when spark's own .orig copy is there. A quiet login from spark-shell stays as it is, on every spark update and spark uninstall.
  • On macOS only the profiles spark made leave Terminal.app. The spark-shell profile stays.
  • A line in /etc/rc.local from 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 font and spark quiet login|boot are gone. theme and font are unknown words now. spark quiet start and spark quiet audio stay: they quiet spark's own lines.
  • spark update hands the look back, once. The console gets its stock palette and font, the boot is loud again, and the motd and /etc/issue return. On macOS the spark profiles leave Terminal.app. It says the look is off this machine now.
  • spark setup takes no --theme. An old site.env that holds SITE_THEME, SITE_FONT_FACE, SITE_FONT_SIZE, SITE_QUIET_LOGIN or SITE_QUIET_BOOT still loads, and spark ignores those keys.
  • spark check has 39 rows: the theme, font and quiet rows 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.env stays a plain palette file any tool may write: spark ver draws its logo in it and the status line takes its accent, when it is there.
  • Arch and Void no longer install kbd for 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 off keeps the dots.
  • The written rules are fewer: 94 in CLAUDE.md, AGENTS.md and the voice sheet became 53, each in one place.

v1.60

  • Any rm or unlink is marked ! now, in spark do and at the prompt line, and spark do asks for the typed yes before it runs one. Before, only a recursive or forced rm was marked. rmdir, docker run --rm and rm as 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 awaken refuses a personality paragraph written in the first person: the soul speaks to the model as "you", and the shipped paragraph stands in.
  • spark help has a group of its own, the living prompt: awaken, look, words, height and Esc 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 awaken asks 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_SGR and SPARK_TROUBLE_SGR join the 3 colour exports. spark check paints its rows with them when awake. SPARK_YOU_SGR is 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 code is bold and its list lines hang under their first word.
  • spark look shows the parts: motion, colour, reveal and words. spark look PART auto|on|off sets one, and spark look off turns motion, colour and words off at once. A pipe never sees a frame or a colour.
  • spark words shows the lines and the faces, and spark words edit changes 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 edit changes the paragraph, and --core the whole soul. A soul file of your own is kept.
  • The look row in spark check says 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 N moves spark's row up to 5 rows above the line you type on. Esc k at the prompt cycles it through 1, 2 and 3.
  • A hint is a whole sentence: a capital first letter and a full stop.
  • Ctrl-C at chat> clears the line, as a shell does. Ctrl-D and /q end the chat.
  • explain honours --reveal.
  • spark bar typed outside tmux prints plain text, without tmux's markup.
  • bash keeps an EXIT trap 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 update prints 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 model spell licence the British way.
  • An apply run of bootstrap.sh ends with N changed, not N 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.conf holds 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 off removes 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 on on Void now reaches runit too. The kernel's quiet never 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 off removes the lines from both files. The quiet row 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.55v1.56
right command on Void82 %81 %
right spark verb64 %61 %
dangerous commands marked11 of 1111 of 11
a ! on a command that only reads9 %9 %
command in your line (median)1.2 s1.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 ssh finds sshd on Void.
  • When the model proposes a program that is not here, spark asks again and names the installed programs that do the same job (ss for netstat).
  • Your git aliases count as git commands, so git co is 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 before spark 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 model and 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 -r and crontab -ir, snap remove, flatpak uninstall, pip uninstall, npm uninstall and kill -9 1 or -1.
  • file -C no longer counts as a command that only reads: it writes a compiled magic file.
  • /api/do/run refuses a command over 4096 characters.
  • Only the check timer refreshes the knowledge index now. A spark check --porcelain over ssh only reports. On Void, spark update restarts 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/completions takes "identity": false in 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 or true keeps 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/threads makes 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 and spark clear --history never touch it, and it takes new messages when history is off. It leaves with /keep off or with spark user remove.
  • spark clear --history clears 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.
  • /keep in spark chat keeps the current thread, and /keep off lets it go. With history off, spark chat goes on with the newest kept thread. spark history, spark user list and 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 --thread refuse 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.52v1.53
right command, Debian / Arch / Void / macOS71 / 87 / 74 / 68 %82 / 87 / 79 / 79 %
right spark verb33 %66 %
dangerous commands marked35 of 3636 of 36
a ! on a command that only reads17 %11 %
every option in its manual95 %100 %
command in your line (median)2.1 s2.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.sh builds 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 knows ps, groups, id, uname, dmesg, free, uptime and lsof, and refuses their options that write or never return. spark's own danger check always wins. It now also catches find -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 remove and apk del. Any chmod, chown or chgrp is 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 knowledge row of spark check says what the index holds and how old it is. SPARK_KNOWLEDGE=off in spark.env returns the prompt line to v1.52's.
  • spark reveal 22 at a terminal keeps the pace replies appear at, in spark.env, and bare spark reveal shows 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 cat fails on the mandoc man, and the manual is now read with MANPAGER=cat.
  • spark serve no 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. Enter is 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.
  • spark and explain are never "not on this machine" to the prompt line, even where ~/.local/bin is not on PATH.
  • tests/line_audition.py grades 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 --help output and spark's tree are the judges.
  • spark bench --line and the audition leave no thread in your history. Their turns keep only numbers, marked bench.
  • TAB after spark memory offers the words it takes.
  • A script, a cron job and ssh HOST 'spark ...' name ~/.local/bin/spark in full. A stock ~/.bashrc returns before spark's hook in a shell that is not interactive. INSTALL section 3 and the cheatsheet say so.
  • spark chat from a pipe prints the replies alone. The intro line and the chat> prompt are for a terminal.
  • A line from spark ask, spark read or spark watch no longer ends in spaces. A model's Markdown line break stays out of a pipe.
  • The last row of spark status is the reply's first line of words, cut at a word near 70 characters. A code fence never shows there. spark last keeps the whole reply.
  • The license column of spark model list is as wide as the table allows within 80 columns. A long license keeps whole parts, such as Llama-3.2 and Gemma-Terms.
  • spark user, spark status and spark history count the same threads: the ones that hold a turn. A file from a failed first turn no longer counts. spark history says newest 5 of N past 5.
  • spark bench --line [N] times N everyday prompt-line questions through spark line: command ready, whole answer and warm slots. spark stats shows the result as the line pace, and the throughput row 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, so ssh-keygen is there.
  • The headless row reads the GPU's node itself: open to every user, or owned by a group that lists you. Void has no render group, 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 on works on Void with GRUB: 3 marked lines at the end of /etc/default/grub, then update-grub. off deletes them.
  • spark quiet says why a piece is n/a on a line of its own.
  • spark read from an empty pipe says so in one line and names 2>&1. A command like ssh -h writes its usage to stderr, and the whole usage of spark read came 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, drill and the context of explain now keep the letter once. spark edit keeps its bytes.
  • On a client, spark serve and spark forge say it is a client of the other machine, as the check rows do. On macOS the share row says a shared engine is Linux's.
  • spark drill looks 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() answers void too. The package names live in distro/void.env, the same 6 keys, and get accepts xbps-install beside apt-get and pacman. The glibc flavour runs spark. The musl flavour is refused at get in one line: the pinned engine is a glibc build.
  • xbps: packages come through xbps-install -Sy. When xbps refuses an install, the packages row is a todo naming sudo xbps-install -Su, because xbps itself must be current on a rolling distro. libgomp is its own package there. Where hostname is missing, spark writes /etc/hostname and sets the kernel's name.
  • runit: init_shape() sits beside distro() and answers launchd, runit when /etc/runit is a directory, else systemd. On runit the 3 services are directories under ~/.config/spark/sv/, rendered by install.sh. bootstrap.sh writes /etc/sv/runsvdir-USER once, with sudo, and links it into /var/service, so the services run from boot, logged in or not. spark check runs every 5 minutes as a supervised loop, spark serve off puts a down file in the service's directory, and each log is ~/.local/state/spark/log/NAME/current. A runsvdir-USER of your own is used as it is. spark headless on works there, and the runit and supervisor rows are core.
  • The console font has a third shape: /etc/rc.conf's FONT= line beside /etc/runit. spark font writes it and setfont redraws this console. A Linux with none of the three answers spark 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. The quiet row 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 on works.
  • CI runs the one-liner as a new user in a void container too, with a runsvdir started for the job. It proves what the Debian and Arch jobs cannot: spark-check runs supervised. --selftest runs a sixth pass, under ID=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.md sections 1, 2 and 6, the keys and the root actions in section 7, the cheatsheet's Linux block and the Void package names in CREDITS.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, and tests/docs_test.py holds 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.example and spark.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) and openssh-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 20 THEME_* keys. spark check exits 0 when no row fails, and every doc says so. The spark check flags, the spark serve flags, spark user remove, spark theme profile, spark font none, spark forge --show-token and spark bar line are documented with their verbs. The keys table names SITE_SHARE and SITE_PEER_SSH.
  • spark help opens 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 names spark stop or spark 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.py is in the pre-commit hook and in CI, and each shell file is syntax-checked on its own. The SITE_GIT keys that nothing read are gone from CI.
  • The completions lose the dead tune case and complete spark check's flags.
  • docs/IDEAS.md drops "Two people, one box": named users and spark share are 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.env is the palette spark theme writes: a KEY=value file any renderer or terminal may read (the page reads it); the console palette and font are the machine's own. The three optional SPARK_ACCENT_SGR, SPARK_MUTED_SGR and SPARK_WARN_SGR variables colour the prompt's marks when your rc exports them, and the marks are plain otherwise. spark bar line is the machine's one-line status, for any status bar or the page to run.
  • The spark shell verb and the spark bar on|off pointers are gone: an unknown word answers with the usage. The shell-moved bootstrap 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/prompt is no longer written (nothing read it); theme_load in lib/env.sh is gone; THEME_BTOP leaves theme.env, so a palette is 20 THEME_* keys. spark theme closes on its own output. The model is no longer told about a shell layer. The disk row's remedy is du -sh ~/*. docs/SHELL.md is gone.

v1.47

  • spark do is measured, bounded and held: every proposal is a turn with the server's timings, so spark stats shows 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 under sandbox-exec on 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 typed yes apply 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. The sandbox row says whether this machine can (40 rows).
  • spark do --sandbox --detach <words> runs with nobody there -- a timer, cron, launchd, a spark watch line -- one at a time, and its changes wait: spark do --review [ID] shows them and asks yes, --accept ID applies without showing them (a script), --discard ID drops them. Each claims the run and refuses one still running; an applied or discarded run is removed. Bare spark and the status line say N 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 no yes over 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's run. accept applies only what the review event showed. A refusal before the run is one end event, and end is 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 --porcelain and on the page.
  • The first client of contract 15: spark-acp (github.com/forgewright-ai/spark-acp) puts spark do in 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 read and spark 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, --about and --type are held the same way. One stderr line says how many and which; the turn records held, a number.
  • A rewrite, a completion and a ? without --source are 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 --name cannot swallow the flag after it.
  • The shapes live in lib/spark/text.py now (SECRET_SHAPES, SOURCE_SHAPES, hold_secrets); spark line --paste holds 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-s on a mail opens spark> , 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 watch never watches itself: with journalctl -f on 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/health that 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 longer spark shell on|off or spark bar on|off, retired in v1.19 (spark do set spark reveal to 22 proposed spark shell on four 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 pty no 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 chars now (a count), so spark stats and a bare /reveal in 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|off on chat, explain and a bare question, /reveal N|auto|off in chat, SPARK_REVEAL in spark.env for the standing one; auto is that threshold, off (as the chunks come) stays the default. The widget's line and spark do are untouched.

v1.43

  • The reveal inside the streaming verbs: spark chat --reveal [CPS], explain --reveal, spark --reveal words pace the reply letter by letter at a terminal (spark reveal's 5..200 a second, default 30 or SPARK_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 reveal could 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 on on the screen (the thread keeps the words that came, not the line), read from the server's finish_reason. The cap of the three streaming verbs (chat, explain, a bare question) is 1200 tokens now, SPARK_MAX_TOKENS in 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 # heading line as bold, an *emphasis* as plain text, the marks dropped only when they flank a word (*.txt, **/, 2*3*4 and _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 as 1;94 -- paint the * mark, the chat> 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; a 38;5 or 24-bit value is dropped whole) and a pipe never sees an escape: unset, every output is what it was. After spark theme NAME the prompt's colour follows spark-shell apply and 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 to spark line, drawn on /dev/tty), in spark chat, explain, an answer and spark 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 font works on Arch: the console font is chosen by mechanism, never by family (site.console_shape(), bootstrap's console row): console-setup's file where it exists (Debian, as before), else /etc/vconsole.conf's FONT= (Arch and every systemd distro), with systemd-vconsole-setup restarted so every VT redraws; the original is kept beside the file and spark uninstall puts it back. spark font list reads the kbd font files there and takes each size from the file's own header, so every row can be typed back as FACE WxH. The font check row is fixture-tested now on both OSes (on macOS it is ok for an installed face); check.ARCH_ROWS is the quiet row alone. A Linux with neither file refuses in one line.
  • spark setup paints 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-dark and rose-pine. spark theme show names the Terminal.app profile on macOS instead of the Linux console line.

v1.39

  • spark quiet boot on is real on an Arch that boots a Unified Kernel Image (a default_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 --splash line (the Arch logo) is marked off with #spark-quiet# , loader.conf gets timeout 0, mkinitcpio -P rebuilds and the row proves the new image carries the line. off undoes the three and rebuilds. The quiet check row reads /proc/cmdline as the live proof and warns boot quiet after a reboot until 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 help says boot (menu). The systemd.show_status=false word 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, and docs_test now tells the two apart, holds docs/ to exactly the ten, and refuses a moved file named by its old path. The release workflow reads docs/CHANGELOG.md; the hook's 80-column and ASCII gates refuse a missing doc instead of skipping it; the printed reference is lp ~/.spark/docs/CHEATSHEET.txt.
  • macOS: with SITE_FONT_FACE unset, the Terminal.app face is Menlo-Regular 13, a face every Mac ships, and spark font says so at once. The profile on disk changes at the next spark theme NAME, spark theme profile, spark font FACE SIZE or spark 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 line paints its ai segment 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.md no longer describes a spark shell on that is a pointer.

v1.37

  • The page, spark.forgewright.ai, is rendered outside this tree, at the newest signed release -- what get installs -- and never from main. www/, pages.yml, tests/site_test.py and 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 every docs/X.md a 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 --paste holds (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 marked spark: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.yml gains a workflows job -- zizmor, pinned, over the workflows themselves at medium and above -- and every checkout runs with persist-credentials: false. advisories.yml asks 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 --sbom prints 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 as sbom.cdx.json beside get.
  • The pending row of spark check names the security upgrades among what the package manager holds back: apt's -security sources, arch-audit on Arch (absent, the row says so without warning). One waiting is a warn with the family's upgrade line; none keeps the ok text plus no security upgrades pending.
  • What leaves is counted: every request records its bytes out and its destination (host:port, or local); spark stats --sends shows them by destination and day; the sends check 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 the hardening check row ask contract 9's eight promises of the served FORGE, or of the peer on a client, every five minutes.
  • spark do under SPARK_DO_STDIN says so on stderr: a harness confirmed, not a person.
  • Every route the server answers is one row of forgeserve.ROUTES (none, user or admin), consulted by nothing else: a request off the table is 404. CLAUDE.md contract 9 prints the table, tests/docs_test.py holds it equal to the code, and the new tests/policy_test.py sends 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 (--porcelain for machines); a trail that does not open is one signed line, exit 2, never written over.
  • GET /api/config returns no key matching KEY|TOKEN|SECRET, whatever spark.env holds; GET /api/users entries 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 NAME and 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 update and get verify a tag's ssh signature against the tree's allowed-signers (the principal is spark-release) and refuse any other in one line, with nothing moved; release.yml verifies the signature before it makes the release. A new signed row in spark check names the key a release clone's tag was signed by (37 rows). get asks for ssh-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 | sh in every doc and on the page front, get shipped as a release asset, so an unreleased push to main is never what a new machine pipes to sh.
  • 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 as application/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_TOKEN shorter 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/run refuses a command carrying a control character and runs a dangerous one only with confirmed: 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/completions takes the bearer only and asks the model for at most 8192 completion tokens. The bind refusal covers every spelling of the unspecified address for spark forge and spark serve alike, and a public address binds with a warning. The page is served with X-Content-Type-Options: nosniff beside its CSP.
  • A proof is asked for like a step: spark do offers 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_ok reads argv, refuses a named list of writing options (--output, -o, --ext-diff, tail -f, git -c, -exec and kin) and any control character, so git diff --output PATH no longer truncates PATH.
  • A spark do command carrying a control character is refused whole, and each step's feedback -- the command that ran, edited from the 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_dangerous flags a truncating > anywhere, sudo anything (a spark do sudo step needs the typed yes), sed -i, tee without -a, shred, xargs rm, mv -f and cp -f, history -c, git stash drop; >>, 2>, >&, /dev/null and tee -a stay 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, a password= or token: line, 64 or more base64 characters) is one answer line naming the shape, and nothing is sent; persona.SENDS says 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 with the 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 remove validates the name like add; the plain state/fails index redacts NAME=value where NAME smells of a secret.
  • ci.yml and pages.yml pin every action to the commit of its release and ci.yml holds contents: read only; bootstrap.sh downloads are https only, redirects included; install.sh never 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 add says when a sha came from Hugging Face's metadata; CREDITS points the llama.cpp pin at engine.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, so auto may pick it. The single-model server's --reasoning off keeps its think tags out of the prompt line and the editor.
  • An auto tie 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 NAME restarts spark-serve and not the forge, and the forge answered /api/models and /api/config from the config it started with, so a client's spark model kept 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 read after 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.clean walks 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 through spark read --name.
  • The wait before a grounded answer is something to read. Every read, ask, edit ? and drill pays 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 says reading ... 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 none used to mint this machine's own account and print its token -- an identity the FORGE it would answer from never knew, so the first spark line there said token 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 NAME there, spark user login NAME here), the peer check 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=1 answers every yes/NO question, not only uninstall's: spark user remove NAME and the license ask before a download take it too (a script's form).
  • No model chosen, no engine downloaded: spark setup --model none on a machine with no peer fetched the engine tarball for nothing; bootstrap skips the row now (spark model NAME brings the engine with it, and says so in its rows) and spark check reads na there, not a failure.

v1.32

  • spark ask asks 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: 0 on 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.dumps passes 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 answered HTTP 500 ... invalid string: surrogate.
  • spark edit ? --source is 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 of spark 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 stats shows a row per mode -- turns, the prompt cache hit rate, the median latency and, for spark 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. --porcelain adds mode_<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 (mode edit-read, kind reading, numbers only), so spark stats shows what every grounded answer pays before it starts.
  • Every systemctl --user call spark makes -- spark model, spark serve, spark update, the services row, spark uninstall, and bootstrap's twin -- carries the user bus (XDG_RUNTIME_DIR and the session bus address, defaulted to /run/user/UID), so a plain ssh box spark model NAME reaches the unit instead of "Failed to connect to user scope bus". A unit start that fails now says so in one todo line 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 NAME drops the brain cache after the restart, so the next spark brain names 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/9 and Gemma 3 12B 5/9, beside the 8B's 21/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: ,s in 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 read refuses such a line, spark watch drops 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 watch and spark recall anchor at word boundaries: "500" cannot ground against a window holding only "1500ms".
  • spark recall keeps only whole history lines -- a substring like rm -rf / inside rm -rf /tmp/build is not a command that ran -- and a dangerous line carries the ! mark into the prompt (Esc r shows "careful").
  • spark watch drains its input, so a burst of lines is one window, not one line per tick; matches end their line, so | while read works; a rotated token or a broken server ends a watch (and spark 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 -R and bare > file truncation; the blast count reads only the rm segment of a compound line, resolves after a leading cd, and expands ~; the widgets classify every segment, so cp x y && rm -rf x is 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 check remedy 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 --packages simulates first and refuses to take a desktop with the engine libraries; uninstall undoes only the root steps bootstrap recorded making (state/made), and --purge deletes spark's files by name -- a file spark cannot name survives.
  • spark model add --license refuses a value contract 3 would refuse, before it can poison the file; every config writer carries the guard.
  • spark update restarts 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 off restores only spark's own trace (/etc/motd.orig); a stock box is never written as root.
  • spark serve on refuses while the unit's server is loading instead of forgetting the running server's records (a new chaos scenario rehearses it).
  • spark client URL stops 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 a tests/audition.py --json run the way _TESTED carries the line proof. spark model list shows it in the proof column (and --porcelain carries it whole), the page's model table renders it, and auto prefers a grounded row when two fit at the same RAM.

The prompt, deeper:

  • The command and the proof: a ? question that lands a command can land a third line too -- proof<TAB>command, ONE read-only check that it did what was asked (test ! -d build after rm -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 do runs 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 off disables it.
  • Failure memory: the third time you hit a failure is not the same event as the first. explain remembers 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 kind fail), and the next failure of that shape says failed (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 history lists 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.
  • --foreground binds 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 by assets/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.py pins 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-url and spark user add NAME print, at a tty, a QR of the login link /login#t=<token> beside the token (--no-qr leaves 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 in tests/qr_test.py to the spec's format table, the canonical Reed-Solomon vector and a full decode-back.
  • spark setup no longer picks a look: it writes SITE_THEME=none, so a fresh machine keeps its terminal's own colours until spark theme NAME says 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 share row compared the token copy by contents, which the owner (not in the spark group) cannot read, so every bootstrap/spark update/spark check showed 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 (and spark share) settle to ok. A rotated token still re-syncs.

v1.24

  • spark share no longer cries STALE at the owner. The shared token is 0640 root:spark, so the owner (not in the spark group) 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 setup detects 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 on now 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 share reports it.
  • bootstrap.sh makes a client of a shared engine truly root-free: it skips the share section 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 a spark OS group reads a 0640 copy 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 stays 0600; the shared copy is re-synced by spark share on and the new share check row warns if it drifts. Explicit, not automatic: which engine answers is always the user's own spark client choice. Linux (a shared workstation); macOS and WSL keep one user per box in this version. spark check gains the share row (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. --name keeps 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. --name records the question in the ledger (kind read, never suppressing -- a source does not change); --ledger [clear] lists or drops. The second verb through the gate spark ask landed.

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 palette spark theme NAME writes -- and re-render on spark-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 -- bare spark bar or spark bar line prints the machine's one-line status for any status bar to run.
  • Moving off the old built-in layer is automatic: bootstrap's shell-moved row hands the rc files back from their .bak on the first spark update, rendered files keep working where they are and spark-shell adopts them; spark shell and spark bar on|off answer with a pointer for this one release.
  • The code now speaks the four value areas. spark edit (contract 10) has its own module beside spark ask's; spark model / spark ember moved 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-server already 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 r sends your shell's own history (fc -ln -400, bash and zsh) to spark 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 r again cycles the matches. Ctrl-R is 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 s proposes the corrected command in your line -- the command and its exit code ride to spark line, which answers with a fix through the cmd path, and nothing runs until Enter. And a command 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 rm at the prompt earns its numbers beside the !. ? clean the build dir that comes back rm -rf build now 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 recursive rm (a glob or a plain delete says nothing), the walk is capped so a huge tree cannot hang the prompt (the count ends + then), and spark do shows the same line above its yes. The facts lead the hint so contract 4's cut eats the model's words, not the numbers.
  • spark ask survives 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 off still answered spark stop -- stopped, and the tune report signed itself spark tune; both verbs were removed in v1.17 and nothing looked at the strings that named them. tests/docs_test.py now reads the verb table out of bin/spark and refuses any %s <verb> -- line whose verb is not in it.

v1.17

  • spark uninstall no longer deletes the script it just told you to run. The undo pass is the one root step that runs bootstrap.sh rather 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.md and INSTALL.md no longer carry the shell layer or the apps: spark shell and spark bar moved to SHELL.md, and the apps to APPS.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, and tests/docs_test.py reads the app names from there.
  • The landing rule binds core documentation, and core documentation moves with a release. APPS.md and SHELL.md sit 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 help reads the same whatever the shell layer is doing. It had two versions of its interface block and picked one by reading site.env; with the gated verbs gone there is one block, and help no longer reads config at all. spark theme and spark font stay 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 ok and skip from ./bootstrap.sh and 18 more from install.sh; both now print what they changed, what needs you, and the summary -- so Nothing to do is usually the whole of it. --verbose brings every row back, and --dry-run is untouched, because that one is the report spark check and 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 update on a converged machine -- is never made to ask.
  • spark theme list is in spark help now, and spark ember list in both help and the cheatsheet: the grammar names four verbs that take list and the docs described two of them. The cheatsheet also names auto and none for spark model (it showed them for ember and hid them for model, whose default is auto), spells the mute the way help does (spark off | on), and writes <words> where it had WORDS.

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 ID keeps a round going, and a follow-up obeys the same law -- the moment a reply may assert, that is spark chat.
  • One switch vocabulary for both servers. spark serve used to start a server when a bare verb is supposed to show, and stopping it was a top-level spark stop, while the FORGE had on|off for the setting and start|stop for the same thing by hand -- four words for two states, and two grammars for one kind of thing. Now both are spark <verb> on|off, bare shows (spark serve reports the url, whether anything answers, and the model), and --force and --noreload are flags of off. spark stop and spark forge start|stop are 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, which spark check --chaos proves by running each remedy.
  • Memory lives under its own noun, like the soul. spark remember <words> and spark forget N were top-level verbs while the other half of the same identity was spark soul edit|reset: two shapes for two halves of one thing. It is spark memory add <words> and spark memory forget N now, beside the on|off and clear that were already there. The widget composes the new line too -- the fact Esc s offers to keep after a fix lands in your buffer as spark memory add '...'.
  • Three smaller harmonies. tune was 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]. The spark ledger tombstone 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, so spark theme and spark model no longer sign differently from spark shell and spark forge.
  • A mode is named for what spark does. ask meant spark answering your question, which read backwards next to spark ask, where spark is the one asking: the mode is answer now, and contract 10's edit-ask is edit-answer. ask is 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 way talk is read for chat, and /api/chat takes either.
  • The grounding law is one place now, lib/spark/text.py: anchor() checks a span, Ground.verdict() a whole note or question, and Gate is the stream that marks what it keeps and drops what it refuses. Anchors (contract 10's marker) is that gate with nothing refused, so spark edit ? behaves exactly as before -- its own tests are the proof. session.reading() and forge.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 ledger check row: sealed, 0600, what is in it and how much room is left before the oldest records go. The users row watches the ledger file too, so a plaintext one cannot sit unseen in a sealed store. spark check has 40 rows.
  • Contracts 11 (spark read) and 13 (spark drill) are written down and not built: their text is in ROADMAP.md and in lib/spark/read.py and lib/spark/drill.py, nothing dispatches to them, and their cases in tests/smoke.py are marked skipped with the reason.
  • spark check --chaos rehearses 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. --selftest proves a row can flip; --chaos proves 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.sh removed its partial file only on a sha256 mismatch; a curl that failed left a .part on the disk that was already full. --fetch U D S runs the download primitive alone, so that failure can be rehearsed.
  • Two spark update at once: the second refuses rather than race the first through a checkout. Both locks refuse alike now -- spark serve said 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 -- and Esc s on the empty line puts it back, already piped to explain; 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 from grep or diff, spark's own refusals and a multi-line command stay quiet. After the fix works, Esc s offers to keep what happened as a spark remember fact you edit before Enter. All of it is per pane, in shell variables, with no model call and no fork at the prompt; spark off silences the line with everything else, and the new failure check 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 edit on 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.py checks 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.py refuses 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() beside is_wsl() in both twins (lib/spark/__init__.py, bootstrap.sh) reads ID then ID_LIKE from os-release and answers debian, arch or nothing; SPARK_OS_RELEASE pins it in tests, the way SPARK_PROC_VERSION pins WSL 2. os_pretty reads 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.py is the one place python asks a package manager; bootstrap's pkg_installed, pkg_available and pkg_install are the one place sh does. spark uninstall reads the same file instead of bootstrap's source.
  • The apt and brew rows are one row, packages, in bootstrap and in spark check.
  • Arch: get accepts pacman and names its install line; packages come through pacman -S --needed, never -Sy alone (a name the database cannot find is a todo naming sudo pacman -Syu); the pending row counts checkupdates; gcc-libs is in base, 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.conf is yours) and spark quiet boot (no update-grub); the font and quiet rows say so, never fail (check.ARCH_ROWS, a sixth selftest pass). spark quiet login works there.
  • Two rows are generic now: the console palette's boot unit orders after systemd-vconsole-setup.service as well as console-setup.service, and the quiet login never creates a motd that was absent.
  • CI runs the new user's one-liner in an archlinux container 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 uninstall prints the plan, one row per thing, then asks for the word yes. 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 clone get made. Your soul, memory, sealed users and account keys, models.env, themes and privacy-terms stay unless --purge; the shell layer's packages are a question (--packages / --keep-packages answer it up front); --dry-run shows; --yes is 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 a todo row 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_profiles takes the spark profiles out of the preferences (a default or startup setting that named one falls back to Basic).
  • tests/uninstall_test.sh proves it against a real clone in a throwaway HOME: the plan changes nothing, a non-terminal refuses, --yes keeps exactly yours, --purge keeps 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 help reads top to bottom as spark (status, off|on, soul, memory, ver, chat, ask, do, edit, then a try: 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 edit on 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 theme and spark shell on|off redraw after sending it. The login screen is drawn before any shell, so bootstrap's new vt-palette row installs a one-shot spark-console.service (root, setvtrgb, the same sudo as the font) that sets the kernel's defaults at boot from console-colors.rgb, for every VT; none writes the VGA sixteen instead of a reset, and the theme row 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/spark and one Alt-s line. An editor becomes smart by being a client of spark edit; spark keeps the verb and its judge (the audition), never the plugin, the packages or the row. spark update hands back the links an older install made (the micro bootstrap row), then clone as the plugin's README says.
  • spark shell on installs 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 seeded settings.json, rendered only when micro is on PATH); spark shell off keeps settings.json -- it is micro's after the seed -- and drops only the colorscheme key it seeded. The rc files set EDITOR=micro only 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 font and spark quiet boot say so in one line and refuse to set; spark headless on refuses (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 help is 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_HOSTNAME is identity, not look. git and shellcheck leave the user's packages (core has git; shellcheck is a contributor's tool). The v1.3 stubs bootconfig and talk are gone; the page's two boot buttons run spark quiet boot on|off. One verb table drives dispatch and the near-miss hint. tmux's status line asks spark bar every 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.py reads. 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 setup asks three things, not four. The palette is no longer a question: spark ships wearing gruvbox-dark, and spark theme NAME or spark theme none changes 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. --theme and SITE_THEME pre-answer it.
  • A first run now leaves the machine looking exactly as it did. setup wrote the palette's runtime files whatever SITE_SHELL said, and the rc hook is core: it cats console-colors on a TERM=linux console, so a fresh install with the shell layer off repainted the VT at the next login. SITE_THEME is still recorded; the files, and the macOS Terminal profile, land with spark shell on or an explicit spark theme NAME.
  • A palette reaches the console it is typed on. spark theme wrote console-colors and left it for the next login, so a theme arrived late and turning one off looked stuck until a logout and a clear. One helper sends that file to a running Linux VT, and spark theme, spark shell on and spark shell off all use it. An emulator is never repainted.
  • spark theme no 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 reads console-colors alone. Only a running micro must be reopened.
  • spark shell off hands the terminal back: the palette goes with the layer that brought it, a running console is reset at once, and it prints the open a new shell (exec $SHELL) line that spark shell on always printed. SITE_THEME stays, so spark shell on paints it again. The theme check row reads na, not fail, for a palette nothing has painted yet.
  • spark font list on Linux names the Nerd Font. The console takes .psf faces from /usr/share/consolefonts; the JetBrainsMono Nerd Font that spark shell on unzips into ~/.local/share/fonts is a .ttf for 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 for sudo, git, curl and python3, then the one-liner). The runbook follows.
  • The page really does rebuild on a release. release.yml creates the Release with the job's own GITHUB_TOKEN, and GitHub raises no event for what that token does, so v1.8's release: published trigger never fired and the page went on naming v1.7. pages.yml waits 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_NAME and a guessed SITE_GIT_EMAIL take scutil --get LocalHostName on macOS instead of hostname: with HostName unset the kernel name follows whatever the network last said, so ~/.gitconfig and ~/.tmux.conf drifted from their templates on their own, the configs row failed, and install.sh would have rewritten the author line of every later commit. Linux is unchanged.
  • ./bootstrap.sh names a guessed git identity in its identity row 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 ember row reads na beside its other AI rows -- a client keeps no second model of its own, and spark ember NAME there is refused. spark ember list shows 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 serve passes --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 N sets a budget in MiB; the serve row 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 B answers about a selection in the light of the file around it; --thread ID keeps an exchange going, sealed like a chat thread; --decline --name NAME retires 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 -- q closes, Enter jumps to the quote, a applies a code block, d declines a note -- and ?? words goes on in it. ledger and ledger clear at the spark> prompt list or drop the file's declined notes; the shell verb spark ledger is gone.
  • A client stays a client: spark model, ember list and model budget on 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 off remains the way back.
  • Your own palettes: a ~/.config/spark/themes/<name>.env with the 21 THEME_* 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 NAME reaches everything at once: the open Terminal.app windows on macOS (profile, font and cursor), micro's colorscheme when micro had changed it, and tmux when it runs.
  • spark font on 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|off silences every sound spark makes; the audio row names the player it would use. 39 check rows.
  • tests/docs_test.py keeps the docs true (credits, counts, pages); the page shows a section above the newest tag as vX.Y (unreleased); tests/audition.py scores 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 carries MODEL_<NAME>_LICENSE; _TESTED=line marks a row proven on the line; _NOTE is one optional line. embers.env and community.env are gone; ~/.config/spark/models.env is still yours.
  • auto picks 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 list and spark ember list print the same table: license, line, a note under its row, u for yours. No more ? e marks.
  • spark model add URL works 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.md and ROADMAP.md are 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 N completes, <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-s opens spark> ; the new text is left selected -- Backspace discards, Ctrl-z undoes. setlocal spark.about "..." says what the text is; set spark false switches it off. No key is bound from inside (micro would detach the link).
  • Esc a became Esc s at the prompt: one gesture, shell and editor.
  • Check row editor (CAPABILITY, shell layer); 38 rows.
  • tests/micro_pty.py drives 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/users shows 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|off the only switch words; status = bare, list = the table; -h first, signed; one confirm shape, one spinner, one exit-code law (0, 1, 2, 78, 130).
  • spark quiet start|login|boot on|off replaces spark bootconfig; boot on is a genuinely silent boot (one GRUB drop-in).
  • Long output pages through $PAGER, plain when piped. spark help rewritten.
  • Chat: /resume [N], /clear, spark chat --thread N [words].
  • The line and chat prompts know spark's own verbs (?? how do I change the theme names spark theme).
  • TAB completion for verbs and their names, bash and zsh, offline; check row completion.
  • spark setup asks the theme; the palette lands on console, micro and tmux at once; check row theme; nord and tokyonight-night join.
  • spark shell off restores the rendered look from .bak; spark font leaves the shell gate; spark font list.
  • Fixes: did-you-mean at exit 2; spark update execs the fresh bin/spark; a restore never leaves an empty ~/.bash_profile (rc-login row); 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; keys n j k.
  • The config page's ember picker runs spark ember on the box; the headless switch is gone (it needs sudo).
  • /manifest.webmanifest and 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; q quits.

v1.1

  • The peer row understands a FORGE (/api/health), reporting forge <host> ok or up, its model loading|down.
  • spark check over plain ssh finds ~/.local/bin and Homebrew's bin.
  • The client shape: spark client URL|off; bootstrap skips the engine and the units, install.sh links no unit, the rows in CLIENT_ROWS read na; a fourth --selftest pass 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 to spark 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) and spark shell on (tmux, starship, micro, fzf, ...); shell rows na when off, a third --selftest pass proves it.
  • One model by default: SITE_EMBER_MODEL=none; the engine is the pinned llama.cpp tarball on both OSes (six flavours, check row engine); the table gains a speed column.
  • SITE_AI_BUILD=auto: vulkan when a GPU reports its memory, else cpu; auto picks 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 ver from 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 row models), spark model budget N (SITE_AI_BUDGET).
  • spark chat v2: wrapped replies, readline history, Ctrl-C stops a reply, /help /new /last /model /q.
  • CREDITS.md names 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 serve runs llama-server as a router (--models-dir, a rendered presets.ini); the spark role at context 4096 with reasoning off; the request's model field 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 row ember; /api/health gains models and roles; 31 rows.
  • Two tokens on the FORGE (admin and user), two faces on the page; spark forge token --new [--user].
  • spark chat is the conversation verb (spark talk dispatches 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 do never 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 render group (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]) and memory (spark remember|forget|memory); check rows soul, memory.
  • Threads: ? words starts one, ?? words continues the newest; spark talk, spark @FILE words, spark do <words> (one confirmed command at a time, yes for a destructive step).
  • spark headless on|off: linger, sleep masked, lid ignored (Linux); LaunchDaemons and pmset (macOS); check row headless. 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 throughput and gpu; spark model lists and switches.

v0.1

  • One command, spark: ? words, words?, Esc a, explain; spark serve|stop with a systemd unit and a launchd agent; spark check with a fixture selftest; spark bar; spark theme.
  • One bootstrap.sh, one install.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