Changelog
v1.37
- The page, spark.forgewright.ai, is rendered outside this tree, at the newest signed release -- what
getinstalls -- and never from main.www/,pages.yml,tests/site_test.pyand the palette lockstep in docs_test leave with it; nothing here builds or publishes it any more. Releasing gains a step: the page's render is run by hand after the tag, or picks the tag up on its own within six hours. - The documents beside the core live in
docs/: TOUR.md, APPS.md, SHELL.md, IDEAS.md and TROUBLESHOOTING.md (a box that will not join the WiFi -- one WiFi daemon per card, then the logs -- which nothing had pointed to). Each says it is not tied to a release; docs_test checks that, that everydocs/X.mda doc names exists, that each is in CLAUDE.md's layout and pointed to from README, INSTALL, CHEATSHEET or ROADMAP, and that the root holds only the core docs.
v1.36
- The pre-commit hook reads the staged diff for the secret shapes
spark line --pasteholds (cli.SECRET_SHAPES, minus the two tuned for a paste and not a tree), names the shape and never the line, and skips a line markedspark:allow-secret; its py_compile runs-W error, so a SyntaxWarning refuses. - CodeQL (
codeql.yml) runs GitHub's static analysis over the python and the javascript on every push to main, every pull request and weekly, on GitHub's runner: the stdlib-only rule is untouched.ci.ymlgains aworkflowsjob -- zizmor, pinned, over the workflows themselves at medium and above -- and every checkout runs withpersist-credentials: false.advisories.ymlasks GitHub weekly whether llama.cpp published a security advisory after the engine pin's date and opens one issue when it did. Dependabot keeps every workflow's action pin current. spark ver --sbomprints what the tree depends on as one CycloneDX 1.5 JSON document (lib/spark/sbom.py): the llama.cpp engine per pinned flavour, every model with its sha256 and license, the distro packages, the python floor, the GitHub Actions the workflows pin. Deterministic but for the timestamp, the JSON alone so it pipes; every release carries it assbom.cdx.jsonbesideget.- The
pendingrow ofspark checknames the security upgrades among what the package manager holds back: apt's-securitysources,arch-auditon Arch (absent, the row says so without warning). One waiting is a warn with the family's upgrade line; none keeps the ok text plusno security upgrades pending. - What leaves is counted: every request records its bytes out and its destination (
host:port, orlocal);spark stats --sendsshows them by destination and day; thesendscheck row warns the day bytes go to a host that is not your configured server. - The FORGE's gates are probed from the wire:
tests/forge_probe.py URL(one line per gate, exit 1) and thehardeningcheck row ask contract 9's eight promises of the served FORGE, or of the peer on a client, every five minutes. spark dounderSPARK_DO_STDINsays so on stderr: a harness confirmed, not a person.- Every route the server answers is one row of
forgeserve.ROUTES(none,useroradmin), consulted by nothing else: a request off the table is 404. CLAUDE.md contract 9 prints the table,tests/docs_test.pyholds it equal to the code, and the newtests/policy_test.pysends three callers -- nobody, a user, the admin -- at every row. - A sealed audit trail: every admin action -- a command run from the page (its sha256 prefix and exit code, never its text), a verb run, a user added, removed or rotated, the admin token rotated -- is one record in the box account's
audit, numbers and names only.spark forge audit [N]reads the newest N (--porcelainfor machines); a trail that does not open is one signed line, exit 2, never written over. GET /api/configreturns no key matchingKEY|TOKEN|SECRET, whatever spark.env holds;GET /api/usersentries are exactly name, threads, last. The chat-history save skips a sealed file that does not open, one line on stderr, instead of writing over it.- Every non-human caller -- a script, an app, a CI job -- gets its own user with
spark user add NAMEand its own token; the admin token is the box's, never shared (INSTALL.md section 5).
v1.35
- A release is a signed tag.
spark updateandgetverify a tag's ssh signature against the tree'sallowed-signers(the principal isspark-release) and refuse any other in one line, with nothing moved;release.ymlverifies the signature before it makes the release. A newsignedrow inspark checknames the key a release clone's tag was signed by (37 rows).getasks forssh-keygen(openssh) beside git and python3 when it lands on a tag. - The one line comes from the release:
curl -fsSL https://github.com/forgewright-ai/spark/releases/latest/download/get | shin every doc and on the page front,getshipped as a release asset, so an unreleased push to main is never what a new machine pipes tosh. - The page's login goes through the write gate like every other POST --
X-Spark: 1, a Host this machine answers to, a matching Origin, a body sent asapplication/json-- so a page on another origin cannot log a browser in. - A cookie is a session id minted at random for that login, the admin's included, never derived from the token: logout drops it on the server, a token rotation ends it, and a restart sends every browser back to the login.
- A locked-out address gets 429 on a bearer as well, before any token is compared; a
SPARK_FORGE_TOKENshorter than 32 characters is refused when the forge starts. - The page's do console is held to its word on the server:
POST /api/do/runrefuses a command carrying a control character and runs a dangerous one only withconfirmed: true, which the page sends after its second click; the log names each run by a sha256 prefix and records its exit code. /v1/chat/completionstakes the bearer only and asks the model for at most 8192 completion tokens. The bind refusal covers every spelling of the unspecified address forspark forgeandspark servealike, and a public address binds with a warning. The page is served withX-Content-Type-Options: nosniffbeside its CSP.- A proof is asked for like a step:
spark dooffers it (Enter runs it), runs it on a 30 s leash, and sends the model its exit code alone -- a proof's output never rides a request.persona.proof_okreads argv, refuses a named list of writing options (--output,-o,--ext-diff,tail -f,git -c,-execand kin) and any control character, sogit diff --output PATHno longer truncates PATH. - A
spark docommand carrying a control character is refused whole, and each step's feedback -- the command that ran,edited fromthe proposal when it differs, its rc, the proof's rc -- lands on the thread the moment the step ran, the last step included. persona.is_dangerousflags a truncating>anywhere,sudoanything (aspark dosudo step needs the typedyes),sed -i,teewithout-a,shred,xargs rm,mv -fandcp -f,history -c,git stash drop;>>,2>,>&,/dev/nullandtee -astay plain.- The paste inspection looks before it sends: a paste shaped like a secret (a private key block, an AWS, GitHub, Slack or
sk-token, apassword=ortoken:line, 64 or more base64 characters) is one answer line naming the shape, and nothing is sent;persona.SENDSsays so. The widgets' Esc s hint after a failure says it re-runs the command. - A sealed ledger or memory that does not open (a flipped byte, a stale
account-key) is never written over: every writer refuses withthe ledger does not open -- spark user login again(the memory likewise) and readers still answer empty. A tampered iteration count in a key file is a refusal, never a stalled CPU; an append onto an empty or foreign sealed file is refused;spark user removevalidates the name likeadd; the plainstate/failsindex redactsNAME=valuewhere NAME smells of a secret. ci.ymlandpages.ymlpin every action to the commit of its release andci.ymlholdscontents: readonly;bootstrap.shdownloads are https only, redirects included;install.shnever overwrites an earlier back-up; the pre-commit privacy gate escapes each word and scans the staged file names and the branch name too, and CI's privacy hit names the file, never the line;spark model addsays when a sha came from Hugging Face's metadata; CREDITS points the llama.cpp pin atengine.env.
v1.34
- Granite 4.2 8B (IBM, Apache-2.0) replaces the Granite 3.3 row in
models.env, from IBM's own GGUF build: proven on the line and scored by the grounding audition on the box, soautomay pick it. The single-model server's--reasoning offkeeps its think tags out of the prompt line and the editor. - An
autotie among grounded rows at the same RAM goes to the earlier row of the list: the list is ranked, so a new row never takes the pick from the one above it (Granite 4.2 and Qwen3-8B both fit 7 GB). - The FORGE follows site.env.
spark model NAMErestarts spark-serve and not the forge, and the forge answered/api/modelsand/api/configfrom the config it started with, so a client'sspark modelkept the star on the old pick until someone restarted the forge. The forge now re-reads site.env and spark.env when either changes, the rule the forge-token already had; a forge_smoke case pins it. - A page title with one byte that was not UTF-8 crashed
spark readafter its answer: under the box's POSIX locale argv keeps such a byte as a lone surrogate, and the ledger's strict encode refused it with a traceback in the reader's face (seen through spark-w3m). The wire got its gate in v1.32; the stores have it now:text.cleanwalks every record bound for a thread, the ledger or a turn file, and the ledger normalises a name on the way in, so the same title lists and clears the same records. A smoke case drives it throughspark read --name. - The wait before a grounded answer is something to read. Every
read,ask,edit ?anddrillpays a reading pass first (the language and kind of the text, a few seconds on an 8B model), and the screen stayed blank until the first answer line. At a terminal stderr now saysreading ...while the pass runs and then what it named --reading ... English, essay-- one line; a pipe (an editor, spark-w3m, a test) sees nothing, so every contract's stdout and stderr stay as stated. A smoke case reads at a pty and in a pipe.
v1.33
- A client never mints an account.
spark setup --model noneused to mint this machine's own account and print its token -- an identity the FORGE it would answer from never knew, so the firstspark linethere saidtoken rejected, and a login with the box's token was refused because a local store already held the name. Now setup with no model skips the account row and says where the login comes from, the first thread write on a client without a login keeps nothing rather than minting (the FORGE is the account authority:spark user add NAMEthere,spark user login NAMEhere), thepeercheck row says whether the FORGE accepts this machine's login, and the login refusal on a machine whose store belongs to another name names the remedy (spark user remove NAME). Found by reinstalling the maintainer's laptop from the README one-liner. SPARK_YES=1answers everyyes/NOquestion, not only uninstall's:spark user remove NAMEand the license ask before a download take it too (a script's form).- No model chosen, no engine downloaded:
spark setup --model noneon a machine with no peer fetched the engine tarball for nothing; bootstrap skips the row now (spark model NAMEbrings the engine with it, and says so in its rows) andspark checkreadsnathere, not a failure.
v1.32
spark askasks real questions of a real page again. Over a few kB of source the model answered the nearest question-shaped line -- the task phrase itself, rephrased ("What does this text not answer about...?") -- and the gate rightly dropped it, so every page came back as a refusal (asked: 0on each one, in the turn numbers). The request's last line now restates the whole task after the source, in the reading's language; proven on the box against live articles in both shapes, bare and with words.- A page piped in with one byte that is not UTF-8 no longer kills the exchange with the engine's raw HTTP 500. Python keeps such a byte as a lone surrogate (stdin is surrogateescape under C.UTF-8, the box's locale),
json.dumpspasses it along, and the engine's JSON parser refuses the request. Every stdin gate now decodes the raw bytes as UTF-8 with the replacement mark (text.stdin_text; a strict locale crashed outright before), and the wire scrubs every string in a request the same way (text.utf8) as the last gate, whatever door the text came in by. Found on the box: a w3m article with one Latin-1 accent answeredHTTP 500 ... invalid string: surrogate. spark edit ? --sourceis the reading-discussion posture: the text is a published source you discuss -- a page, an article -- not a draft you edit. It answers the reader's question from the text and never suggests changes to it, while keeping every part ofspark edit ?(the thread, the ember, quotes checked and marked where the text does not hold them, never dropped). The reading apps pass it; the editors never do. Found on the box: asked whether a priced page named a price, the editor's brief reviewed the prose ("rephrase this", "consider adding") and concluded it named none while it named four. A ninth audition ground case (discuss-price) pins the answer to the fact and forbids the editor's verbs.spark statsshows a row per mode -- turns, the prompt cache hit rate, the median latency and, forspark read, the median wait to the first kept line (first_ms, a new number on the read turn). One cache rate over every turn hid the one that matters: the line's short prefix hits nearly always, a read carries a 16 kB source. This is the instrument for the roadmap's first item, the rerun that costs what the first run cost.--porcelainaddsmode_<name>lines.- The reading pass (
You read this as: ...) is greedy, temperature 0. It is restated above the source in the request that follows, so a word sampled differently on the same source threw the served prompt's cached prefix away right before the text, and the whole source was processed again. It is also a turn of its own now (modeedit-read, kindreading, numbers only), sospark statsshows what every grounded answer pays before it starts. - Every
systemctl --usercall spark makes --spark model,spark serve,spark update, theservicesrow,spark uninstall, and bootstrap's twin -- carries the user bus (XDG_RUNTIME_DIRand the session bus address, defaulted to/run/user/UID), so a plainssh box spark model NAMEreaches the unit instead of "Failed to connect to user scope bus". A unit start that fails now says so in onetodoline and returns, where before it reported success and waited 180 s on a server nobody had started, while the unit went on serving the old model beside the hand-started one. spark model NAMEdrops the brain cache after the restart, so the nextspark brainnames the model now served, not the one before (the cache is 60 s; a swap is faster).- Two more rows carry a grounding score, from one audition run each on the maintainer's box: Qwen3 4B
5/9and Gemma 3 12B5/9, beside the 8B's21/27. The 12B is slower (5.5 tok/s to the 8B's 8.7) and no better grounded; the 4B is fast and its judgment folds (a cold read over-generated and the gate dropped every line). The 8B is the box's model for reading.
v1.31
spark reveal [CPS]-- stdin at a reader's pace: at a terminal the text appears letter by letter (default 30 characters a second, SPARK_REVEAL_CPS or the argument set it), so a slow brain's line bursts read as a steady hand; a stall is never repaid as a burst. Piped anywhere else it is an exact copy, byte for byte. Purely local; the reading clients pipe through it.- spark-newsboat, the second client of spark read:
,sin newsboat asks the article you are on, on the terminal newsboat hands over -- the grounded answer line by line, Enter back to newsboat untouched. Its own repository, like every spark app. - The grounding audition gains a ninth case, read-about: "what is this text about" in words, where bare
spark read(the overview) already answers well. The model misses it -- it summarises in its own voice and the gate rightly refuses -- and three brief rewrites each traded that miss for false grounding on read-unanswered, so the brief stays as it was and the case stays as the measure of the gap. Found asking a web page "what is this page about" through spark-w3m; until a model or mechanism closes it, the overview is asked bare: Enter at the prompt, no words.
v1.30
The law:
- A quoted span must clear a floor to count as grounding evidence: two words or twelve characters after fold, not made entirely of stop words. Quoting "the" no longer grounds a claim;
spark readrefuses such a line,spark watchdrops it. - A span the model proposes (after
->or=>) is marked[proposed]where it stands, so a reader can tell a quote from a proposal. spark watchandspark recallanchor at word boundaries: "500" cannot ground against a window holding only "1500ms".spark recallkeeps only whole history lines -- a substring likerm -rf /insiderm -rf /tmp/buildis not a command that ran -- and a dangerous line carries the!mark into the prompt (Esc r shows "careful").spark watchdrains its input, so a burst of lines is one window, not one line per tick; matches end their line, so| while readworks; a rotated token or a broken server ends a watch (andspark edit --watch) in one signed line instead of a traceback.spark drill: no brain is one line, a missing terminal is a signed refusal (exit 2).- The danger set sees
rm --recursive/--force,find -delete,rsync --delete,git branch -D,chmod -Rand bare> filetruncation; the blast count reads only thermsegment of a compound line, resolves after a leadingcd, and expands~; the widgets classify every segment, socp x y && rm -rf xis never re-offered whole. - Escape sequences and control characters are scrubbed from the hint row and from every grounded stream: a model cannot retitle your window.
- The ledger takes a lock around every write (two editor panes cannot lose a decline) and an answered question suppresses itself however the gate printed it (numbered, anchor-marked).
- README's "What leaves this machine" lists every sender -- ask, read and recall included -- and the list is a test now.
The machine:
- Every
spark checkremedy names a live verb, and a lint keeps it true (spark forge start/stop, spark remember/forget and friends are gone). - CI's privacy gate: all three checks decide the step, not only the last one.
spark uninstall --packagessimulates first and refuses to take a desktop with the engine libraries; uninstall undoes only the root steps bootstrap recorded making (state/made), and--purgedeletes spark's files by name -- a file spark cannot name survives.spark model add --licenserefuses a value contract 3 would refuse, before it can poison the file; every config writer carries the guard.spark updaterestarts the loaded units after a converge that moved the tree, and the forge row warns when the running FORGE's version is not the tree's.- A client's bootstrap is root-free: packages, hostname, console, vt-palette and the quiet rows all skip.
spark quiet login offrestores only spark's own trace (/etc/motd.orig); a stock box is never written as root.spark serve onrefuses while the unit's server is loading instead of forgetting the running server's records (a new chaos scenario rehearses it).spark client URLstops and disables the server that ran here, removes its unit links, and says so.- Engine pins order by build number (b10689 beats b9999), and older pins are offered for removal.
- Drifts: lib/env.sh agrees with config.py on which file wins; --dry-run writes nothing; console font values are validated before the root sed; bf16 is documented; a freshly minted store key becomes the cached key in the same breath, and a store whose key is gone refuses with 78.
The audition:
- A model row can carry the grounding audition's score:
MODEL_<NAME>_GROUND="<kept>/<run> <YYYY-MM-DD>", written by hand from atests/audition.py --jsonrun the way_TESTEDcarries the line proof.spark model listshows it in the proof column (and--porcelaincarries it whole), the page's model table renders it, andautoprefers a grounded row when two fit at the same RAM.
The prompt, deeper:
- The command and the proof: a
? questionthat lands a command can land a third line too --proof<TAB>command, ONE read-only check that it did what was asked (test ! -d buildafterrm -rf build). A proof that is not read-only is refused before it prints. After the command runs, the hint row offers the proof on Esc s;spark doruns it after each confirmed step and shows the result. - Paste inspection: a multi-line paste into an empty prompt gets ONE verdict line -- what running it would do,
!when a line can destroy -- while the paste stays in the buffer and nothing runs until your own Enter. Over 8 kB nothing is sent at all;spark offdisables it. - Failure memory: the third time you hit a failure is not the same event as the first.
explainremembers the failure's shape (head word, exit code, first stderr line -- hashed locally), the fix that then worked is kept when you accept it (ledger kindfail), and the next failure of that shape saysfailed (N) -- last time the fix was: ...with NO model call: the prompt hook reads one plain index file and nothing else. A fix whose tool left PATH retires; records age with SPARK_HISTORY;spark historylists them.
Trust:
spark check --report: a block safe to paste into an issue -- version, OS and family, arch, backend, RAM and budget, the model stems, and every row's category, status and name. Never a value, a path, a hostname or a user name; the privacy word lists run over the report's own output and blank any hit. The bug template asks for it.- INSTALL.md states the threat model: what an attacker on the LAN, one with the disk, and one with a stolen phone can and cannot do, with the one remedy for each (rotation:
spark user token --new,spark forge token --new). docs_test keeps the section honest. - The tour is on the front door: TOUR.md renders on the page (tour/), linked from the front's stage 2 and the README's "Use it" head.
One identity, every door:
??across machines: on a logged-in client of a FORGE,??at the prompt continues the newest thread ON THE FORGE -- the same thread the box's prompt started and the phone's page shows -- and the turn lands there (POST /api/threads/<id>/append, the requester's own sealed store). Any trouble falls back to the local store, as before.
The FORGE:
- Logout revokes the session -- a replayed cookie is 401.
- A non-ASCII token is a wrong token (401 with the 1 s cost), never a 500.
- A client that leaves while queued pays no prefill and lands no thread (499); a reply cut mid-stream keeps its thread -- the error event names it and the page continues there.
- Prune reaches every store the server holds a key for, and drops header-only files from a failed first turn after a day.
--foregroundbinds before it writes its records: a failed second start no longer deletes the running FORGE's forge-url and pid.- The page strips any
#t=fragment, whatever its shape, and a non-empty token gets exactly one login attempt. - The handler carries a 30 s socket timeout and the wrong-login sleep is bounded, so a burst cannot hold a hundred threads.
- A sealed file renamed on disk is refused by name instead of read as another thread.
v1.29
- The chat owns the screen: the bottom tab bar is gone, navigation is one small menu button in the header on every screen size. A guest's page is now just the chat plus their account (palette, rotate token, clear chats, memory, log out) and help behind that button -- activity and do are the admin's alone, and redirect a guest to the chat. The soul editor leaves the guest's settings too.
- The page wears a transparent mark (
/static/mark.svg, the S with no ground, emitted byassets/icon-svg.py) in its header, login, greeting and message avatars, so the logo sits on any theme; the solid-ground favicon stays for the browser tab and the home screen.
v1.28
- The QR reads. v1.27 placed the format information transposed -- rows for columns, in both copies -- which this repo's own decode-back test could not see (it read the same wrong cells) and every camera could: the scan failed on a real phone. The cells now match the spec's figure and the encoder is verified cell-for-cell against an independent implementation across versions 1-5 and all 8 masks;
tests/qr_test.pypins the cell coordinates literally, so a transposition can never pass again. The half-block render also grows the spec's full 4-module quiet zone (the console's ASCII form keeps 1, to stay inside 80 columns).
v1.27
- The page, redesigned: a chat-first webapp instead of a terminal look-alike. Both roles land in the chat -- bubbles, a greeting with suggestion chips, a bottom tab bar on the phone and a sidebar on a desktop -- with activity, do (admin), settings and help beside it. The same five hash routes; keys 1..5 now start at chat.
- The page wears its own ember look, dark and light, sans-serif; the machine palette (gruvbox and friends) is now an opt-in choice under settings instead of the default. The login card no longer names the ember-token (gone since v1.4).
- Log in by QR:
spark forge --print-urlandspark user add NAMEprint, at a tty, a QR of the login link/login#t=<token>beside the token (--no-qrleaves it out). A phone's camera scans it and the page signs itself in -- the token rides the URL fragment, which never reaches the server or its log, and the page strips it before routing and tries it exactly once. The QR is the token drawn as squares: same custody. lib/spark/qr.py: a QR encoder written from ISO/IEC 18004 (byte mode, level L, versions 1-5, all 8 masks), pure stdlib, pinned intests/qr_test.pyto the spec's format table, the canonical Reed-Solomon vector and a full decode-back.spark setupno longer picks a look: it writesSITE_THEME=none, so a fresh machine keeps its terminal's own colours untilspark theme NAMEsays otherwise. It recorded gruvbox-dark before -- a choice the user never made, which the theme row's remedy then offered to paint.
v1.25
- A shared-engine box converges again. The
sharerow compared the token copy by contents, which the owner (not in thesparkgroup) cannot read, so everybootstrap/spark update/spark checkshowed a perpetual "1 to do" and re-copied the token. Freshness is now by mtime -- the copy is current when it is no older than the source -- readable by the owner, so the row (andspark share) settle took. A rotated token still re-syncs.
v1.24
spark shareno longer cries STALE at the owner. The shared token is0640 root:spark, so the owner (not in thesparkgroup) cannot read the copy to compare it -- and unreadable was being reported as stale. Now a content check runs only when the file is readable here; otherwise the perms are the health signal. A group member still gets the real check.- INSTALL's join one-liner named the wrong host (a 404); it now points at
raw.githubusercontent.com/forgewright-ai/spark/main/get, the same URL the page and README use.
v1.23
- Joining a shared engine is now userspace -- no sudo, no download. When a machine already runs a shared engine (
spark share on),spark setupdetects it and offers to join instead of the full first-run: it writes the client shape (no model,SITE_THEME=none), keeps the joining user's own soul and memory in their$HOME, and touches nothing that needs root. A second OS user sets up with the ordinary one-liner and answers from the one engine in seconds. Fixes v1.22, where a second user hit the model table, a second download, and a sudo wall they could not pass. spark share onnow also publishes the engine's address to/etc/spark/url(0644, beside the token), so a joining user finds it without reading the owner's home;spark sharereports it.bootstrap.shmakes a client of a shared engine truly root-free: it skips thesharesection for a client (no longer trying to remove the owner's token) and no longer asks for sudo up front when nothing needs it.
v1.22
spark share on|off: one engine for every OS user on a machine. Instead of each user loading the model again, the owner runs the engine (headless) and asparkOS group reads a0640copy of the api-token; a group member's spark answers from that shared engine as a client --spark client http://<host>:8080-- keeping their own soul, memory and threads in their own$HOME. One model loaded once; every user sovereign. The owner's own token stays0600; the shared copy is re-synced byspark share onand the newsharecheck row warns if it drifts. Explicit, not automatic: which engine answers is always the user's ownspark clientchoice. Linux (a shared workstation); macOS and WSL keep one user per box in this version.spark checkgains thesharerow (36 rows).
v1.21
spark drill(contract 13): a source on stdin becomes practice questions it answers. The model proposes a verbatim span of the source as each answer and a question that span answers; an item whose answer is not in the source is dropped before it is ever asked, and a source too thin to drill is one line, never padded from the model's own knowledge. Self-graded -- you see the source's own words and say whether you had it; the answers are graded on this machine and never sent.--namekeeps a schedule: a missed item comes back on a widening interval (1, 3, 7, 21, 60 days) until it is right twice in a row, the one ledger kind that schedules rather than suppresses.--ledger [clear]lists or drops it.spark watch(contract 14): a live stream on stdin -- a log tail, a build, a migration -- watched for the one thing you named. Silent until a line matches, then one line quoting it; the quote is checked against the stream, so it cannot report what is not there, and silence is the healthy state. A window is a few lines or a few seconds, cheap enough to leave running, and a brain that comes and goes underneath it is ridden out rather than fatal. The stream never leaves the machine.spark edit --watch FILE: the live form of the editor's?. It watches a draft on disk and comments on each stanza as you save it, reviewing the whole draft when you pause. Grounded like every?; the writer answers back through the Alt-s ask key, not a built-in chat.
v1.20
spark read(contract 11): the source on stdin -- a page, a message, a document -- a question in the words, and an answer that says only what the source says. Every line quotes the source and the quote is checked; a line whose quotes are not in it, or that quotes nothing, is dropped before the reader sees it. When the source does not answer, the reply is one line showing the source's own opening words, composed in code, never a model's guess. A source past 16 kB is parts (--part N), and the answer's first line names the part it read.--namerecords the question in the ledger (kindread, never suppressing -- a source does not change);--ledger [clear]lists or drops. The second verb through the gatespark asklanded.
v1.19
- The shell layer lives in its own repository: github.com/forgewright-ai/spark-shell. The tools, the starship and Nerd Font pins, the rc files, the terminfo entry and the rendered look (tmux, starship, btop, micro's colorscheme) install from there (
spark-shell on), read~/.config/spark/theme.env-- the palettespark theme NAMEwrites -- and re-render onspark-shell apply. spark keeps no shell code, no shell package, no gate: 35 check rows now, and a first install touches nothing but the AI. - What was appliance behaviour stayed, ungated:
spark quiet login|boot(the silent login and boot) and the bar line -- barespark barorspark bar lineprints the machine's one-line status for any status bar to run. - Moving off the old built-in layer is automatic: bootstrap's
shell-movedrow hands the rc files back from their.bakon the firstspark update, rendered files keep working where they are and spark-shell adopts them;spark shellandspark bar on|offanswer with a pointer for this one release. - The code now speaks the four value areas.
spark edit(contract 10) has its own module besidespark ask's;spark model/spark embermoved out of site.py into model.py; the shell layer's switch and hand-back live in shell.py; site.py keeps the site.env custody, the rc hook and the machine-shape verbs. One dispatch table in bin/spark lists every verb. No verb, flag or message changed. - One home for every decision. bootstrap.sh no longer computes its own answers in sh twins: lib/spark/facts.py prints the machine's facts (distro, build, WSL, memory, the engine's home and flavour, the model picks) from the same code the verbs use, and bootstrap eval's them. The llama.cpp pin moved to engine.env (version + one sha per flavour), read by both sides.
- Bring your own engine, on Linux too. On an architecture spark has no pin for, a
llama-serveralready on this machine ($PATH,/usr/local/bin,/usr/bin) is found and served with -- the engine check row reads(your build). macOS keeps its Homebrew probe; a pinned build still wins where one exists.
v1.18
- Intent search: describe a command you ran, and the line that ran comes back.
Esc rsends your shell's own history (fc -ln -400, bash and zsh) tospark recall <words>on stdin; the model matches meaning, and every candidate is checked against the history (text.anchor) so what lands in your prompt is always a line you actually ran, never invented.Esc ragain cycles the matches.Ctrl-Ris left to the shell and to fzf -- an instant key stays instant, and a dead brain costs nothing. No new contract: the grounding law is borrowed. Nothing is written; the turn record is numbers. - A command pasted from a page is rewritten for this machine. The prompt line's prefix now names this OS's side of the pairs a paste crosses most --
free/vm_stat, the package managers,systemctl/launchctl,xdg-open/open,ls --color/ls -G-- and tells the model to rewrite the other side and say so in the hint. This OS's half only, so the prefix stays byte-stable per machine and the prompt cache keeps hitting. - The failure moment goes further, twice. After the explain, a second
Esc sproposes the corrected command in your line -- the command and its exit code ride tospark line, which answers with a fix through the cmd path, and nothing runs until Enter. And acommand not found(exit 127) offers the line that installs it: a tool spark itself installs is named here with no model call (packages.package_for-- fd is fd-find on Debian, fd on Arch and macOS), the rest through the model. Both ride the same hook the widgets already keep, in both shells; no new check row. - Blast radius: a recursive
rmat the prompt earns its numbers beside the!.? clean the build dirthat comes backrm -rf buildnow reads<- 1,204 files, 3.1 GB, 2 tracked by git -- ...: the count of files, bytes and git-tracked files under the paths, worked out from the command spark already has -- nothing the model proposed is run. Only a recursiverm(a glob or a plain delete says nothing), the walk is capped so a huge tree cannot hang the prompt (the count ends+then), andspark doshows the same line above itsyes. The facts lead the hint so contract 4's cut eats the model's words, not the numbers. spark asksurvives a model that wraps its questions. The brief said to point at the text "by quoting it between double quotes", and small models read that as wrapping the whole question -- every line then ended in a quote mark and the law (it ends in ? or it is not output) dropped all of them: on the box, two different models produced zero questions. The brief now says the quotes go inside the question, never around it, and a line that still arrives as one wrapped question is unwrapped before the gate reads it -- its inner quotes are then exactly the spans the grounding law judges.- Signed lines name verbs that exist.
spark serve offstill answeredspark stop -- stopped, and the tune report signed itselfspark tune; both verbs were removed in v1.17 and nothing looked at the strings that named them.tests/docs_test.pynow reads the verb table out ofbin/sparkand refuses any%s <verb> --line whose verb is not in it.
v1.17
spark uninstallno longer deletes the script it just told you to run. The undo pass is the one root step that runsbootstrap.shrather than a command of its own -- bootstrap is what knows how to unmask sleep, drop the lid file and put the motd and GRUB back -- so when it cannot get sudo, its remedy is "run ./bootstrap.sh". The clone was then removed a few steps later, leaving an instruction nobody could follow and a machine still half spark's. The clone stays while that is outstanding, and says so.- The core documents are the core.
spark help,CHEATSHEET.txt,README.mdandINSTALL.mdno longer carry the shell layer or the apps:spark shellandspark barmoved toSHELL.md, and the apps toAPPS.md, each with one pointer line left behind. INSTALL loses two sections and renumbers; the app table the README carried is APPS.md's now, andtests/docs_test.pyreads the app names from there. - The landing rule binds core documentation, and core documentation moves with a release.
APPS.mdandSHELL.mdsit outside it: kept true as things change, no release waiting on them, nothing in them owed to help, the cheatsheet or a changelog entry. Both say so in their first lines and docs_test checks that they do, so neither drifts back under the rule unnoticed. spark helpreads the same whatever the shell layer is doing. It had two versions of its interface block and picked one by readingsite.env; with the gated verbs gone there is one block, and help no longer reads config at all.spark themeandspark fontstay in it: they are core, and a machine has a face with the layer off.- An install says what it changed, not what it checked. A converged machine printed 54 rows of
okandskipfrom./bootstrap.shand 18 more frominstall.sh; both now print what they changed, what needs you, and the summary -- soNothing to dois usually the whole of it.--verbosebrings every row back, and--dry-runis untouched, because that one is the reportspark checkand the tests read. - The sudo password is asked for once, at the start, before anything is touched. It used to be asked at whichever row first needed root, half way through a run that had already changed things. If sudo is not on the machine at all, that is one line and exit 1 up front rather than a failure later. Only at a terminal: over ssh or a pipe there is nobody to answer, and a run that needs no root --
spark updateon a converged machine -- is never made to ask. spark theme listis inspark helpnow, andspark ember listin both help and the cheatsheet: the grammar names four verbs that takelistand the docs described two of them. The cheatsheet also namesautoandnoneforspark model(it showed them foremberand hid them formodel, whose default isauto), spells the mute the way help does (spark off | on), and writes<words>where it hadWORDS.
v1.16
spark ask(contract 12): a plan, a draft or a decision on stdin, and at most three questions about it back -- one per line, nothing else. The shape is the law, not a request in a brief: a line that does not end in a question mark never reaches you, and neither does one whose every quoted span is missing from the text, one that could be asked of any plan, a repeat, or a question you have already answered (spark ask --answered --name NAME,--ledger [clear]). Three is a cap, never a target: when nothing survives, stdout stays empty and one line on stderr says so, exit 1. At most 12 kB in;--thread IDkeeps a round going, and a follow-up obeys the same law -- the moment a reply may assert, that isspark chat.- One switch vocabulary for both servers.
spark serveused to start a server when a bare verb is supposed to show, and stopping it was a top-levelspark stop, while the FORGE hadon|offfor the setting andstart|stopfor the same thing by hand -- four words for two states, and two grammars for one kind of thing. Now both arespark <verb> on|off, bare shows (spark servereports the url, whether anything answers, and the model), and--forceand--noreloadare flags ofoff.spark stopandspark forge start|stopare gone rather than kept as aliases: there is nobody to keep them for yet. The check rows that named the old verb name the new one, whichspark check --chaosproves by running each remedy. - Memory lives under its own noun, like the soul.
spark remember <words>andspark forget Nwere top-level verbs while the other half of the same identity wasspark soul edit|reset: two shapes for two halves of one thing. It isspark memory add <words>andspark memory forget Nnow, beside theon|offandclearthat were already there. The widget composes the new line too -- the factEsc soffers to keep after a fix lands in your buffer asspark memory add '...'. - Three smaller harmonies.
tunewas a flag and a verb at once (spark bench --tune,spark tune show); it is a sub-noun of the thing it belongs to now --spark bench tune [show|apply]. Thespark ledgertombstone from v1.7 is gone: it existed to tell people where the ledger went, and there is nobody to tell yet. And the signed first line uses one separator,spark <verb> -- <one line>as contract 8 says; the glyph separator is for fields inside a line, not for the signature, sospark themeandspark modelno longer sign differently fromspark shellandspark forge. - A mode is named for what spark does.
askmeant spark answering your question, which read backwards next tospark ask, where spark is the one asking: the mode isanswernow, and contract 10'sedit-askisedit-answer.askis left free rather than reused for contract 12 -- a string that changed meaning would make the turn records already on disk lie about themselves. The old names are read for one version, the waytalkis read forchat, and/api/chattakes either. - The grounding law is one place now,
lib/spark/text.py:anchor()checks a span,Ground.verdict()a whole note or question, andGateis the stream that marks what it keeps and drops what it refuses.Anchors(contract 10's marker) is that gate with nothing refused, sospark edit ?behaves exactly as before -- its own tests are the proof.session.reading()andforge.text_sha()/same_text()come out of the editor's path for the same reason: the next contract needs them, not a copy of them. - The ledger holds more than the editor's declined notes: one sealed file, one record shape, and a kind per contract -- and the rule that retires a record belongs to the contract that wrote it (
ledger.RULES), because a rule that generalised would fit none of them. A note declined in a draft retires when its quote leaves the text; a question you answered stays answered. - A
ledgercheck row: sealed, 0600, what is in it and how much room is left before the oldest records go. Theusersrow watches the ledger file too, so a plaintext one cannot sit unseen in a sealed store.spark checkhas 40 rows. - Contracts 11 (
spark read) and 13 (spark drill) are written down and not built: their text is in ROADMAP.md and inlib/spark/read.pyandlib/spark/drill.py, nothing dispatches to them, and their cases intests/smoke.pyare marked skipped with the reason. spark check --chaosrehearses the failures: it breaks a throwaway machine one known way at a time and proves the right row says so and the remedy that row prints heals it.--selftestproves a row can flip;--chaosproves the sentence under it is true. Each scenario on ports of its own, so a machine that is serving is left alone.- A reply cut off mid-stream is an error, not half an answer. A severed connection is not an end of stream: the read simply stops, so a killed server handed back a truncated answer looking whole, and exited 0. The turn still lands -- the question and the words that did arrive go on the thread, the way they do when you press Ctrl-C.
- A download that dies leaves nothing behind.
bootstrap.shremoved its partial file only on a sha256 mismatch; a curl that failed left a.parton the disk that was already full.--fetch U D Sruns the download primitive alone, so that failure can be rehearsed. - Two
spark updateat once: the second refuses rather than race the first through a checkout. Both locks refuse alike now --spark servesaid it on stderr and exited 1; a lock another process holds is a gate refusal, so it is signed and exits 2.
v1.15
- The failure moment: a command that exits nonzero prints one line above the next prompt --
* failed (1) -- press Esc s to ask why-- andEsc son the empty line puts it back, already piped toexplain; nothing runs until Enter. The command and its exit code ride along, so the answer can correct the command itself, and a command that failed in silence still gets an answer. A destructive head word (rm,dd,mkfs...) is seen but never offered a re-run; Ctrl-C, a no-match fromgrepordiff, spark's own refusals and a multi-line command stay quiet. After the fix works,Esc soffers to keep what happened as aspark rememberfact you edit before Enter. All of it is per pane, in shell variables, with no model call and no fork at the prompt;spark offsilences the line with everything else, and the newfailurecheck row (39 rows now) watches the hook through the liveness marker's fourth field (contract 6). - The hint above the prompt stopped cutting answers at 80 characters mid-word: an answer now carries up to 300 characters, cut at a word, and the widget trims it to the terminal's own width -- so a wide terminal shows the whole sentence. The ellipsis comes from the glyph table (
...on the Linux console, which cannot draw the Unicode one). - Five spark apps: spark-neovim and spark-vim join spark-micro with the whole prompt (one clone, one mapping: complete at the cursor, rewrite, ask in a pane, the ledger); spark-helix and spark-nano put
spark editon the editor's own prompt instead -- helix and nano have no cursor hook, so words rewrite or ask, and nothing completes. Each in its own repository, proven by its own pty test; any editor's filter is still a client with no plugin at all (INSTALL section 6). - The page front redesigned for onboarding: three stages (choose your OS with a picker, spark is live, spark apps), a light theme with a toggle, and a table of contents on the long pages. Every command on the front is a line of INSTALL.md or the README, and the page's palette stays in lockstep with the LAN page's -- both are tests now.
v1.14
The docs rewritten around three lines: choose your OS, one line, spark is live; spark apps, a tool that becomes smart as a client of spark edit; another contract when apps ask for it. The shell layer keeps working, off by default, and the docs stop presenting it as a third of spark.
- README: install in three steps, the verbs in one table, spark apps, what leaves the machine. The page front is spark and spark apps.
tests/docs_test.pychecks the voice: two nouns in what a new user reads, every spark app the README names in INSTALL, the cheatsheet, the credits and the page. - CLAUDE.md, AGENTS.md and CONTRIBUTING.md are framed by the same three lines; the shell layer is one principle, behind its gate.
- A new user is a new user: the word that held them at arm's length is gone from every doc, code comment and CI account name, and
tests/docs_test.pyrefuses it. - INSTALL.md in the new user's order, numbered and step by step (the machine, the one line, the verbs, the models, other machines, spark apps, the per-OS notes, keeping it, spark shell last). CHEATSHEET.txt in the same words, the shell block folded to its verbs.
v1.13
A second Linux family: Arch. One spark, one oracle, the package names as data.
distro()besideis_wsl()in both twins (lib/spark/__init__.py,bootstrap.sh) readsIDthenID_LIKEfrom os-release and answersdebian,archor nothing;SPARK_OS_RELEASEpins it in tests, the waySPARK_PROC_VERSIONpins WSL 2.os_prettyreads the same file.- The package names live in
distro/<id>.env, one file per family, the same eight keys (contract 3): the manager, its install line, the name the docs use, the five groups.lib/spark/packages.pyis the one place python asks a package manager; bootstrap'spkg_installed,pkg_availableandpkg_installare the one place sh does.spark uninstallreads the same file instead of bootstrap's source. - The
aptandbrewrows are one row,packages, in bootstrap and inspark check. - Arch:
getaccepts pacman and names its install line; packages come throughpacman -S --needed, never-Syalone (a name the database cannot find is a todo namingsudo pacman -Syu); thependingrow countscheckupdates;gcc-libsis inbase, so the AI layer asks for sudo only with a GPU. - What Arch lacks refuses in one signed line:
spark font(no console-setup:/etc/vconsole.confis yours) andspark quiet boot(noupdate-grub); thefontandquietrows say so, never fail (check.ARCH_ROWS, a sixth selftest pass).spark quiet loginworks there. - Two rows are generic now: the console palette's boot unit orders after
systemd-vconsole-setup.serviceas well asconsole-setup.service, and the quiet login never creates a motd that was absent. - CI runs the new user's one-liner in an
archlinuxcontainer too, with the shell layer on top; an Arch block in INSTALL sections 1 and 7; the Arch package names in CREDITS.md.
v1.12
A way out: spark uninstall takes spark off a machine and keeps what is yours.
spark uninstallprints the plan, one row per thing, then asks for the wordyes. Everything spark made goes -- units and timers, the shell layer's look (rc files and renders back from.bak), the spark line in your rc file, the console palette (VGA again, the boot unit removed), the Nerd Font and the terminfo entry,~/.local/bin/{spark,explain, starship}, the engine and every model,~/.config/spark,~/.local/state/spark, and the clonegetmade. Your soul, memory, sealed users and account keys,models.env, themes andprivacy-termsstay unless--purge; the shell layer's packages are a question (--packages/--keep-packagesanswer it up front);--dry-runshows;--yesis a script's form. Headless and the quiet login and boot are undone first through their own bootstrap rows; a root step whose sudo refuses is atodorow with its command. What spark could not record before changing it -- a hostname, pmset, a console font set before this release -- is named with the line that puts it back; the console font's original is kept from now on (console-setup.spark-orig).- Terminal.app:
theme.remove_profilestakes the spark profiles out of the preferences (a default or startup setting that named one falls back to Basic). tests/uninstall_test.shproves it against a real clone in a throwaway HOME: the plan changes nothing, a non-terminal refuses,--yeskeeps exactly yours,--purgekeeps nothing, a developer checkout is left.
v1.11
The help in the maintainer's own order; an empty buffer is a page to write on; the console palette paints the whole screen, and the login one.
spark helpreads top to bottom as spark (status, off|on, soul, memory, ver, chat, ask, do, edit, then atry:list of the prompt gestures), the FORGE: server/client, the machine (setup, check, update), the interface (shell, bar, theme, quiet, font) and "less often used"; the tail is the version alone (README carries what leaves the machine). 46 lines with the shell off, 48 on. CHEATSHEET follows the same order.spark editon an empty text: words write it from nothing (a new file in micro no longer answers "edit reads stdin"), the reply ending with a newline;?and Enter alone say what is missing in one infobar line.- The console palette paints the whole screen: a framebuffer console colours only what is drawn after a palette change, so
spark themeandspark shell on|offredraw after sending it. The login screen is drawn before any shell, so bootstrap's newvt-paletterow installs a one-shotspark-console.service(root,setvtrgb, the same sudo as the font) that sets the kernel's defaults at boot fromconsole-colors.rgb, for every VT;nonewrites the VGA sixteen instead of a reset, and thethemerow watches the boot palette.
v1.10
Three domains share the one command: spark, spark shell, and the smart apps in their own repositories; Windows through WSL 2, honestly.
- spark ships no app. The micro plugin moved to its own home, github.com/forgewright-ai/spark-micro, and installs micro's own way: a clone into
~/.config/micro/plug/sparkand oneAlt-sline. An editor becomes smart by being a client ofspark edit; spark keeps the verb and its judge (the audition), never the plugin, the packages or the row.spark updatehands back the links an older install made (themicrobootstrap row), then clone as the plugin's README says. spark shell oninstalls no editor: tmux, starship, fzf, zoxide, eza, bat, btop and the Nerd Font, and nothing else. A micro you have still wears the palette (its colorscheme and the seededsettings.json, rendered only when micro is on PATH);spark shell offkeepssettings.json-- it is micro's after the seed -- and drops only the colorscheme key it seeded. The rc files setEDITOR=microonly when micro is there; the core hooks export nothing of micro's.- Windows users have a way in: Ubuntu on WSL 2 is Linux to spark, minus what the VT console and GRUB own.
spark fontandspark quiet bootsay so in one line and refuse to set;spark headless onrefuses (the distro stops with its last window); the gpu, services and encryption rows say WSL 2; the status and the check header name it. Pinned by fixture (a fifth selftest pass, Linux only) -- CI has no WSL runner. spark helpis sectioned by domain -- spark, at the prompt, the FORGE, the server, the machine, spark shell -- and half as long: 51 lines with the shell on, no line wider than 80. The forms still live on each verb's-h. README opens with the three ways in; INSTALL is sectioned the same way, with a Windows walk and per-OS notes for three tribes; CHEATSHEET and CREDITS follow the layers.- The hostname row leaves the shell gate:
SITE_SET_HOSTNAMEis identity, not look. git and shellcheck leave the user's packages (core has git; shellcheck is a contributor's tool). The v1.3 stubsbootconfigandtalkare gone; the page's two boot buttons runspark quiet boot on|off. One verb table drives dispatch and the near-miss hint. tmux's status line asksspark barevery 15 s, not 5. - The docs' counts are tests: the category split, the roadmap's version and the page sources join the derived facts
tests/docs_test.pyreads. 38 check rows.
v1.9
A new user's first run: three questions, and a machine that looks untouched until it is asked to change.
spark setupasks three things, not four. The palette is no longer a question: spark ships wearinggruvbox-dark, andspark theme NAMEorspark theme nonechanges it whenever you like. Asking a first-timer to pick colours for tmux and starship -- the shell layer, which setup leaves off -- spent a question on nothing.--themeandSITE_THEMEpre-answer it.- A first run now leaves the machine looking exactly as it did. setup wrote the palette's runtime files whatever
SITE_SHELLsaid, and the rc hook is core: it catsconsole-colorson aTERM=linuxconsole, so a fresh install with the shell layer off repainted the VT at the next login.SITE_THEMEis still recorded; the files, and the macOS Terminal profile, land withspark shell onor an explicitspark theme NAME. - A palette reaches the console it is typed on.
spark themewroteconsole-colorsand left it for the next login, so a theme arrived late and turning one off looked stuck until a logout and aclear. One helper sends that file to a running Linux VT, andspark theme,spark shell onandspark shell offall use it. An emulator is never repainted. spark themeno longer asks for a shell restart it never needed: starship re-reads its config on every prompt, the widget draws no colour of its own, and the hook readsconsole-colorsalone. Only a running micro must be reopened.spark shell offhands the terminal back: the palette goes with the layer that brought it, a running console is reset at once, and it prints theopen a new shell (exec $SHELL)line thatspark shell onalways printed.SITE_THEMEstays, sospark shell onpaints it again. Thethemecheck row readsna, notfail, for a palette nothing has painted yet.spark font liston Linux names the Nerd Font. The console takes.psffaces from/usr/share/consolefonts; the JetBrainsMono Nerd Font thatspark shell onunzips into~/.local/share/fontsis a.ttffor a terminal emulator and can never appear in that list, so it is named under it with where to set it.- The setup table says it is not the whole list. Only rows proven on the line and under an open license are offered -- today the five qwen3 rows -- so the first run read as though spark served nothing else. One line counts the other 21 and points at
spark model list. - INSTALL.md opens with two numbered walkthroughs instead of prose: a machine from zero (the Debian image, the empty root password that earns you
sudo, the packages) and spark on a machine you have (one check forsudo,git,curlandpython3, then the one-liner). The runbook follows. - The page really does rebuild on a release.
release.ymlcreates the Release with the job's ownGITHUB_TOKEN, and GitHub raises no event for what that token does, so v1.8'srelease: publishedtrigger never fired and the page went on naming v1.7.pages.ymlwaits for the release workflow to finish instead, and publishes only when it succeeded.
v1.8
A rendered file stops moving with the network; the page stands on its own.
SITE_NAMEand a guessedSITE_GIT_EMAILtakescutil --get LocalHostNameon macOS instead ofhostname: withHostNameunset the kernel name follows whatever the network last said, so~/.gitconfigand~/.tmux.confdrifted from their templates on their own, theconfigsrow failed, andinstall.shwould have rewritten the author line of every later commit. Linux is unchanged../bootstrap.shnames a guessed git identity in itsidentityrow whether the key is absent or still the example's placeholder: a guess signs every commit, so it is said out loud.- A client's
emberrow readsnabeside its other AI rows -- a client keeps no second model of its own, andspark ember NAMEthere is refused.spark ember listshows what the peer offers. - The page rebuilds when a release is published, and its sign line links that release: the page and the GitHub release never disagree.
- The page loads no font from Google -- the local stack draws it, and a browser no longer waits on fonts.googleapis.com to render it.
v1.7
The editor grows up; the shell learns your palettes; a client stays a client.
spark servepasses--cache-ram 0: llama-server no longer keeps replaced prompts in host RAM (a 12B model on a 16 GB box had climbed from 32 % to 90 % in a day).SPARK_EXTRA_ARGS=--cache-ram Nsets a budget in MiB; theserverow reports it.spark edit ?checks every quote against the text: a span the text does not hold is followed by[not in the text].--sel A Banswers about a selection in the light of the file around it;--thread IDkeeps an exchange going, sealed like a chat thread;--decline --name NAMEretires a note so the next?does not raise it, until the words it quoted leave the file.- The micro plugin (1.3.0): a Lua error is an infobar line, never a dead editor; an answer over text that moved opens in a pane instead of being spliced. The pane has keys --
qcloses, Enter jumps to the quote,aapplies a code block,ddeclines a note -- and?? wordsgoes on in it.ledgerandledger clearat thespark>prompt list or drop the file's declined notes; the shell verbspark ledgeris gone. - A client stays a client:
spark model,ember listandmodel budgeton a client print the peer's table (GET /api/models), and the verbs that would make it a server are refused with one line;spark client offremains the way back. - Your own palettes: a
~/.config/spark/themes/<name>.envwith the 21THEME_*keys is listed, chosen, completed and checked like the six in the repository, and wins on a name clash.THEME_LOGO, optional, paints the banner's six rows in a palette's colours. spark theme NAMEreaches everything at once: the open Terminal.app windows on macOS (profile, font and cursor), micro'scolorschemewhen micro had changed it, and tmux when it runs.spark fonton macOS refuses a face the Mac does not have and lists the monospace faces installed; the default is the Nerd Font at 13, one face and size for every profile. On Linux the list spells sizes the way the command takes them, width by height.spark quiet audio on|offsilences every sound spark makes; theaudiorow names the player it would use. 39 check rows.tests/docs_test.pykeeps the docs true (credits, counts, pages); the page shows a section above the newest tag asvX.Y (unreleased);tests/audition.pyscores the editor's briefs against a live brain, outside the gate.
v1.6
One model list. The three lists (curated, embers, community) are one models.env, 26 rows, tested or not, any license named.
models.env: every row carriesMODEL_<NAME>_LICENSE;_TESTED=linemarks a row proven on the line;_NOTEis one optional line.embers.envandcommunity.envare gone;~/.config/spark/models.envis still yours.autopicks only among tested rows under Apache-2.0 or MIT; any row is yours by name; a row under another license prints it and asks first.- New rows: Qwen3-Coder-30B-A3B, Qwen2.5 7B/14B, Mistral 7B v0.3, Mistral Nemo 12B, Phi-4 mini and 14B, DeepSeek-R1 distills 7B/14B, SmolLM2 1.7B, gpt-oss-20b, Granite 3.3 8B, Llama 3.2 1B/3B, Llama 3.1 8B, Gemma 3 1B/4B/27B. Untested until someone posts the line proof.
spark model listandspark ember listprint the same table: license,line, a note under its row,ufor yours. No more? emarks.spark model add URLworks on huggingface.co again: the size and sha256 live on the redirect, not on the CDN it points at.- The page's models page is one table.
CHANGELOG.mdandROADMAP.mdare lists now, not essays; every doc drops the list words.
v1.5
The editor wave: micro is the first smart tool.
- Two arcs: OS -> spark -> smart shell and chat; tools -> spark -> smart tools.
spark edit(contract 10): text on stdin, raw text out;--at Ncompletes,<words>rewrites (12 kB cap),? [words]asks or reviews; hints--type --name --about --part. Works from a pipe.- Before a
?, a 20-token reading (language, kind) is restated to the model, so a Portuguese draft is answered in Portuguese. - micro plugin
home/.config/micro/plug/spark/:Alt-sopensspark>; the new text is left selected -- Backspace discards, Ctrl-z undoes.setlocal spark.about "..."says what the text is;set spark falseswitches it off. No key is bound from inside (micro would detach the link). Esc abecameEsc sat the prompt: one gesture, shell and editor.- Check row
editor(CAPABILITY, shell layer); 38 rows. tests/micro_pty.pydrives a real micro against a stub spark.Session(role=),ask_stream/ask_json(max_tokens=, timeout=),forge.clip,text.Fence.
v1.4
The multi-user wave. Break: the shared ember-token is gone.
spark user add|login|logout|remove|token --new|claim: named users, each with a personal token shown once and never stored.- Threads, memory and chat history sealed per user (ChaCha20-Poly1305 from RFC 8439, pinned to its vectors) under a key only that token opens; the box keeps a hash and a wrapped key. No reset.
- The FORGE is multi-user: forge-token = admin (the box account's store); every other caller is a user;
GET /api/usersshows names and counts, never a word. Browser logins live in memory (a restart asks again). [cwd]rides only the modes that propose a command (line, do, explain); a conversation sends no path.- Turns are numbers: one choke point strips every free-text field.
- Check row
users: 0700 dirs, 0600 keys, sealed magic; nags until the old ember-token and plaintext files are gone. .githooks/commit-msg: the privacy patterns over the message too.
v1.3
The CLI experience wave: one grammar, a machine that explains itself.
- The grammar (CLAUDE.md): bare = show;
on|offthe only switch words;status= bare,list= the table;-hfirst, signed; one confirm shape, one spinner, one exit-code law (0, 1, 2, 78, 130). spark quiet start|login|boot on|offreplacesspark bootconfig;boot onis a genuinely silent boot (one GRUB drop-in).- Long output pages through
$PAGER, plain when piped.spark helprewritten. - Chat:
/resume [N],/clear,spark chat --thread N [words]. - The line and chat prompts know spark's own verbs (
?? how do I change the themenamesspark theme). - TAB completion for verbs and their names, bash and zsh, offline; check row
completion. spark setupasks the theme; the palette lands on console, micro and tmux at once; check rowtheme; nord and tokyonight-night join.spark shell offrestores the rendered look from.bak;spark fontleaves the shell gate;spark font list.- Fixes: did-you-mean at exit 2;
spark updateexecs the freshbin/spark; a restore never leaves an empty~/.bash_profile(rc-loginrow); plain-text replies, no markdown. 36 rows.
v1.2
The page wears the brand.
- The FORGE page: the ember palette by default, dark and light, all mono; marks
* > !on transcript rows; a blinking caret; copy buttons; a stop button; keysn j k. - The config page's ember picker runs
spark emberon the box; the headless switch is gone (it needs sudo). /manifest.webmanifestand a drawn/apple-touch-icon.png: an app on an iPhone's home screen (Android opens a tab over LAN http).- The same page runs inside sparkapp (macOS and Windows): the login card asks the address too;
qquits.
v1.1
- The
peerrow understands a FORGE (/api/health), reportingforge <host> okorup, its model loading|down. spark checkover plain ssh finds~/.local/binand Homebrew's bin.- The client shape:
spark client URL|off; bootstrap skips the engine and the units,install.shlinks no unit, the rows inCLIENT_ROWSreadna; a fourth--selftestpass proves it.
v1.0
The ignition: one line, and a fresh Debian 13 or macOS has a private local AI at its prompt. spark is public, MIT.
get: clone or pull~/.spark, refuse what is not spark, never sudo, hand over tospark setup(name, user, model;--yes,--model,--name,--user; stdin not a tty takes every default).- Two layers:
SITE_SHELL=off(the AI only, one rc line appended) andspark shell on(tmux, starship, micro, fzf, ...); shell rowsnawhen off, a third--selftestpass proves it. - One model by default:
SITE_EMBER_MODEL=none; the engine is the pinned llama.cpp tarball on both OSes (six flavours, check rowengine); the table gains a speed column. SITE_AI_BUILD=auto: vulkan when a GPU reports its memory, else cpu;autopicks the largest model under the budget AND the build's speed cap (3 GB files on cpu, 6 on vulkan, 20 on metal); units warm the model after/health; launchd rows skip without a gui domain.- Privacy words leave the tree (
~/.config/spark/privacy-terms,SPARK_PRIVACY_TERMS); the tag is the release (spark verfrom git,spark update,release.yml); CI grows a Debian 13 container job. - Three model lists plus yours (folded back into one in v1.6);
spark model add URL [--sha256] --license,spark model verify(check rowmodels),spark model budget N(SITE_AI_BUDGET). spark chatv2: wrapped replies, readline history, Ctrl-C stops a reply,/help /new /last /model /q.CREDITS.mdnames every project spark downloads or installs.- Measured: a fresh Debian account over ssh reached its first answer in 10 min 19 s (a 4.7 GB download, 8.4 tok/s on vulkan); a fresh M4 Mac in 16 min 25 s (13.0 tok/s on Metal).
v0.4
The ember: one llama-server serves two models.
SITE_EMBER_MODEL auto|none|name;spark serveruns llama-server as a router (--models-dir, a renderedpresets.ini); the spark role at context 4096 with reasoning off; the request'smodelfield routes.- The rule: the prompt line is spark, every sentence is an ember; the identity rides only with the ember.
spark ember [NAME|auto|none|list]; check rowember;/api/healthgainsmodelsandroles; 31 rows.- Two tokens on the FORGE (admin and user), two faces on the page;
spark forge token --new [--user]. spark chatis the conversation verb (spark talkdispatches for one version); a generous quit grammar; one mark pair* !on both OSes.- A head-word guard: a command whose first word is not installed here is re-asked once;
spark donever offers it. spark do: a done summary whose numbers no output backs is marked unchecked; each turn records the model that answered.- Linux: the serving user joins the
rendergroup (GPU without a seat); the zsh widget empties the line before it speaks; downloads and restarts narrate both ends.
v0.3
The FORGE: spark is the seed, the FORGE is the agent it builds and keeps.
spark forge(lib/spark/forgeserve.py): a stdlib HTTP server in front of llama-server, one LAN address, a 0600 forge-token, cookie or bearer;/v1/chat/completions,/v1/models,/api/*, the page.~/.config/spark/soul(spark soul [edit|reset]) andmemory(spark remember|forget|memory); check rowssoul,memory.- Threads:
? wordsstarts one,?? wordscontinues the newest;spark talk,spark @FILE words,spark do <words>(one confirmed command at a time,yesfor a destructive step). spark headless on|off: linger, sleep masked, lid ignored (Linux); LaunchDaemons andpmset(macOS); check rowheadless. 30 rows.
v0.2
- Every turn records the server's timings;
spark last,spark status,spark stats(percentiles, cache hits, GPU). spark bench [--tune],spark tune apply(SPARK_FLASH_ATTN,SPARK_KV,SPARK_THREADS,SPARK_NGL).- Check rows
throughputandgpu;spark modellists and switches.
v0.1
- One command,
spark:? words,words?,Esc a,explain;spark serve|stopwith a systemd unit and a launchd agent;spark checkwith a fixture selftest;spark bar;spark theme. - One
bootstrap.sh, oneinstall.sh, Debian-family Linux and macOS; four palettes; five pinned models chosen by RAM; a privacy gate on every commit. Python 3.9+ stdlib and POSIX sh only. - After the first Debian 13 box:
libgomp1; ASCII on the console;spark theme NAME|list;spark ver;spark font;spark bootconfig.
source: CHANGELOG.md on GitHub -- this page is that file at spark v1.37