Repository navigation
v0.14.2
0.14.2 (2026-09-08)
base can be used at work. Every release up to and including 0.14.1 shipped under the PolyForm Noncommercial License 1.0.0, which permitted noncommercial use only, so a company running base internally, the audience it is built for, sat outside the grant. From this release the engine is licensed under the Functional Source License 1.1 with an Apache 2.0 future license (FSL-1.1-ALv2). You may run base inside a company of any size, modify it, and build and sell products that use it. The one thing withheld is selling base itself, or a service that stands in for base or for basemode. Each version becomes Apache 2.0 two years after it ships; that grant is irrevocable and written into the license text. Extensions and adapters written against base's documented interfaces (the extension manifest and API, command plugins, the hook protocol, the CLI surface, the on-disk graph format) are their author's own work under an explicit exception and may be licensed any way the author likes, including commercially. Versions already released keep the terms they shipped with; nothing is withdrawn. LICENSING.md carries the plain-English guide and the surface-by-surface table, and Cargo.toml now declares the SPDX identifier.
Every file the extractor parses is now a file in the map. Two lists decided this and they had drifted apart: the extractor parsed 66 extensions, the serialiser recognised 27 of them as files, and the 39 in the gap — .mjs, .cjs, .ps1, .vue, .md, .mdx, .json, .hpp, .cc, .cxx, .kts, .m, .mm, .groovy, .gradle, .bash, .luau, every Fortran and Pascal spelling — were parsed, reached the graph, and had every one of their entities attributed to the app root instead of the file they came from. Line numbers stayed right, so base ast query answered with a real line in the wrong file, and --file src/thing.mjs found nothing at all. On the tree that reported it, 6,495 of 21,979 entities sat on the project root; on a Windows tree here, 293 of 2,807. The two lists cannot drift again: in a whole-app extraction a node is a file when it is one of the files that were walked, which needs no list, and the extension set that covers single-file mode is now derived from the parser table rather than typed out beside it. Reported by @Elevation-mindset, who measured it, found the cause, and offered the patch.
.cjs is parsed at all now — it was in none of the three lists — and .hpp gains one more thing: it was already marked as a language whose records are structs, but never as a file, so a struct in a C++ header could not be typed as one. It can be. A map built before this release keeps its old paths until it is rebuilt; base sync --ast --target <app> does that in seconds, because the per-file extraction cache is still valid and only the serialisation step re-runs, and the Stop hook does it on its own the next time you work in that app unless the tree is over the size fuse — in which case run the command yourself. Nothing re-indexes automatically; the 0.14.1 fuse stands.
.baseignore directory patterns work on Windows. A pattern is written with forward slashes and the path it was compared against was backslash-separated, so archive/ never matched archive\deep\thing.ts and 139 files under an ignored directory were collected anyway. Both sides are normalised now, the way base already normalises paths everywhere else. Only patterns carrying a slash were affected — a bare archive and a glob like *.log matched on both platforms all along — and POSIX behaviour is unchanged.
The extractor says when it could not place an entity. A run that attributes entities to the app root because it found no file node for them now prints how many, every time rather than only in devmode, and a run with nothing to report prints nothing new. The number counts attributions rather than distinct nodes, and the two can differ: a symbol the extractor synthesises for something outside the tree, an inherited base class from the standard library say, is emitted once per place it is referenced and collapses to one node in the map, so a count of 89 can be 85 nodes. The count is the right tripwire either way — it is the number of times the fallback was taken. This is the tripwire that would have caught the drift above months ago. base sync --ast --yes captured the extractor's output so that a failed build could explain itself later, and on a successful build threw it away, including every "skipped this file" line. Those come back, on a sync you run yourself. A refresh started by a hook still prints to nobody: it is deliberately detached with its output discarded, and giving it a voice means writing these notices down for session start to read, which is not this release.
The sync line says what it counted. base domain sync, and the same line printed by base scaffold and base install, reported N rules for the rules it had imported from domains.toml (and from carl.json when one was named). That read as the domain's rule count, so a domain with five rules added by base rule add looked empty: sync never touches CLI rules, by design, and then printed a number that looked as if it had counted them, the same reading that produced #38 from base domain get. The line now says M rules imported from domains.toml, adds and carl.json and K decisions imported from carl.json only when a carl.json was named, and reads the same on all three prints; nothing about what sync counts changed, and base domain get still counts every rule. A record the domain block listed is not listed again. The [X CONTEXT] block served a domain's decisions and projects and the prompt-time walk then served the same records under <base-context> in the same prompt, because the block never marked them as served: the query did not project the record it read, and the list it kept was in a form the walk could never match. Both are fixed. A name the block listed still walks, because the block served its row and not what is attached to it, and it is never listed as its own record.
The base-help coach now answers the questions the 0.14.1 injection changes raised — keeping a domain out of automatic injection, how a path trigger matches, why a domain stopped loading and what base doctor says about it, why an add-trigger was refused, and exclude on an always-on domain — and the three pairs that described the pre-0.14.1 domain tier split as a live bug now say it was fixed.
The release download carries the license. Every asset up to and including 0.14.1 packed the binary and scripts/ and nothing else, so the first copy of base anyone receives arrived with no license text in it, while FSL-1.1-ALv2 asks that copies carry the terms or a link to them. The three tarballs and the Windows zip now also carry LICENSE.md, LICENSING.md, LICENSE-APACHE-2.0 and the retained LICENSE-PolyForm-Noncommercial-1.0.0.md, which is the grant those earlier releases shipped under and is kept so that a download can be compared against the terms it replaced. Nothing about an install changes: base update looks for the binary and for scripts/ by name and ignores everything beside them. The packing step is now read out of the release workflow and run by CI on every pull request, which is what would have caught this: the two archives are built differently, tar packs the members it is named while the Windows zip globs its staging directory, so a fix applied to one of them can leave the other three assets exactly as they were.
base installs in one line, and needs no toolchain to do it. The front door in the README was cargo build --release, and on Windows that additionally meant the MSVC developer environment plus LLVM and libclang, because the vendored RocksDB tree is built through bindgen. Binaries for all four platforms were published on every release and almost nobody found them: v0.14.1's four assets were downloaded 1, 2, 4 and 6 times, and across all 59 releases 1,068 times in total. install.sh and install.ps1 now detect the platform, pull the matching asset from the latest release, unpack it, and hand off to base install, which does exactly what it always did. The one-liner sits above the fold; the source build keeps its own section, unchanged, for anyone who wants it. BASE_VERSION pins a tag, arguments pass straight through to base install, and linux-aarch64 — which has no published asset — says so rather than failing on a download.
Running the installer end to end on a machine that had never had base turned up three things the binary does on a clean install that it does not do on yours, filed as #91, #92 and #93. The installers work around all three for now: they run the unpacked binary from inside its own directory so the AST extractor is found, they write base.exe alongside base so Windows can run it by name, and they say to rerun base install when there was no ~/.claude to wire hooks into. The fixes belong in the binary and are landing separately; the workarounds come out when they do.
A handoff or fork you registered stays registered. Until now two base processes writing the graph at the same moment silently destroyed each other's work. Every write loads the whole graph, changes it in memory, and writes the whole file back, and nothing made those writers take turns — so the one that finished second overwrote the first, and both printed success. That is what was behind rows that came back as open a minute after you archived them, an archive that reported archived and changed nothing, and a fork that vanished from fork list in every state right after it was created (#72, #73, #74 — one defect, not three). It was never rare: eight fork create commands starting together left three of eight rows on Windows and two of eight on Linux, with no error anywhere. Writers now take a lock on the graph file, one tier at a time, and readers never wait for it — base recall and the hooks are as fast as they were. The writers that take it are the ones you use: everything behind base learn, task, project, decision, rule, note, milestone, goal, reminder and relay ping, the handoff and fork commands, supersede, graph purge, workspace reconcile, and the hook that runs on every tool call. The dashboard's own writes are not locked and will not be: it is being deprecated. A handful of bulk operations (base sync, graph compact, doctor --repair, the migrations) are not locked yet either.
archive and snooze tell you what changed, and say so when nothing did. Archiving a slug that exists in neither tier used to print archived and exit 0. Now each command names the tier it changed, and a slug nothing holds is an error naming the slug and both places that were searched. A -g archive also stopped doing its work twice on the same file.
handoff create says which handoff it closed. Registering a handoff archives the previous open one for that project in the tier you are standing in — that has always been true, and the help text now says "in this tier" instead of promising something project-wide. What is new is that it tells you: the handoff it archived, or that there was none, and, when the other tier is holding an open handoff for the same project, its name and the exact command that would archive it. It still does not touch the other tier; a write acts on the tier you stand in.
Every language base parses now has a name, and a component's script block is read. Three lists decided what the extractor does with a file and only two of them were joined up in this release's first pass. LANG_MAP, which fills ops:language, was still typed out by hand: 22 extensions were parsed and reached the graph as unknown — .mjs, .cjs, .vue, .mdx, .kts, .gradle, .luau, every capitalised Fortran spelling, every Delphi and Lazarus form — while .psm1 and .zsh were listed as PowerShell and shell and were never parsed at all. The map is derived from the parser table now, the way the file-node set already is: a language belongs to an extractor rather than to an extension, and an extractor with no language stops the extractor at import instead of quietly filling the graph with unknown. .psm1 and .zsh are parsed for real rather than delisted, since the extractors for both were already there. Single-file components are the other half. A .vue or .svelte file wraps its code in <script> tags, the JavaScript grammar cannot read the markup around them, and what survived was decided by tree-sitter's error recovery: two files with identical script contents, one .vue and one .svelte, gave a function and nothing, and two .vue files differing by a single export let line did the same. The under-report was silent — the file appeared in the map with its functions missing. The script block is now isolated before parsing, with the markup blanked rather than removed so every line number still points where it did, and both extensions read the same way. A file that is plain JavaScript under a .vue name is unaffected. .astro has the same shape and is not covered here; it is tracked in #850.
A code map built in the background can finally say what it could not do. Every automatic refresh discards its child's output on purpose — the Stop hook, a first contact, the WSL delegate, and the git hook all do, because nobody is watching a detached process — so the counter above, and every "skipped this file" line, reached a person only on a sync they ran themselves. A tree could collect unreadable files for weeks and never mention it. Those notices are now written next to the map in .base-ast/.last-notices, exactly the way a failed build already leaves .last-error, and session start reads them back in one line. All four background paths run the same command, so the child records them whoever started it. A run with nothing to report deletes the file rather than leaving the last complaint standing, the same set is not repeated once it has been shown, and a changed one is shown again.
Hooks say things the model can now actually read. Claude Code adds a hook's plain stdout to the model's context on four events, and PostToolUse is not one of them — everywhere else stdout is written to the transcript and dropped. base already knew this and applied it to PreToolUse only, so the section AST context printed after a partial read, every extension inject nudge, and both directory-move blocks were written, logged as delivered, and never seen. All four now travel in the hookSpecificOutput.additionalContext envelope. session-start and user-prompt-submit, where the host does deliver plain stdout, are unchanged. Two fields the log had been keeping to itself, nudged and standards_injected, are now written out, because an extension nudge that fires and is recorded nowhere is invisible in both directions at once.
The end-of-turn relay nudge is gone rather than pretending. Stop has no channel that reaches the model at all, and the nudge was redundant besides: the same open task is re-announced the moment the session moves again. Printing it was a no-op that read as a clean pass. The block now goes out as systemMessage, which reaches the operator, and the operator is who it was useful to.
A hook event is logged in the workspace it happened in. The line recorded the cwd the host reported and then wrote itself to the tier of whatever directory the hook process was standing in, so an event could name workspace A and sit in workspace B — and base doctor's hook trail, which reads one tier, reported on the wrong one. A failing hook in A was invisible from A. The tier now comes from the payload on both the success and the failure arm, and each line carries cwd_source saying which input chose it.
Two pings in the same millisecond no longer overwrite each other. The slug was ping-<millis>, and it names both the inbox alert file and the graph IRI, where the write is a delete followed by an insert at a fixed address. So the earlier ping was replaced, with nothing on either side to show it had existed, while its sender saw a success. Eight pings fired at once left three. The slug now carries a per-process counter and the process id, because neither alone is enough: a counter cannot separate two processes and a pid cannot separate two pings from one.
Scripts check out runnable on Windows. Git for Windows turns on core.autocrlf by default and the repo shipped no .gitattributes, so every .sh arrived with CRLF endings including the shebang, and running one directly failed with /usr/bin/env: 'bash\r': Permission denied. It stayed hidden because CI runs on Linux, where the files are unaffected, and because bash script.sh ignores the shebang entirely. .sh and .py are now pinned to LF and .ps1 to CRLF — pinned rather than left alone, so no script has one identity on Windows and another on Linux. An existing Windows clone needs git add --renormalize . or a fresh checkout to pick this up; a git pull alone will leave its files exactly as they are.
Windows installs are runnable again. base install wrote the binary to ~/.local/bin/base with no extension, so a machine that had never run base before finished the install, printed correct PATH guidance, and then could not run base at all — Windows resolves executables through PATHEXT, and an extensionless file is not on it. Existing installs were unaffected because the update path already handled both names, which is why this only ever bit a first install. The binary's name now comes from one seam that carries the platform suffix. Install writes every name that seam lists, uninstall removes every one of them and, when it finds none, names the directory it searched and each name it looked for; the manifest records the name it actually wrote; and a guard test fails the build if any site outside the seam ever spells the name by hand.
Added
Fixed
- install: step 1 prints on one line like steps 2-7 (#110)
- verification: silent_no_ops_0142.sh could pass on a control it never read (#109)
- install: write the binary under every name Windows needs (#91) (#103)
- hooks: the silent no-ops - output channels, log tier, ping slug, CRLF (#75 #76 #77 #86 #95) (#102)
- ast: every parsed extension has a language, SFCs yield their functions, notices reach session start (#99) (#83, #84, #89)
- registry: serialize graph writes, and make archive/create say what they changed (#88) (#72, #73, #74)
- ast: every parsed extension is a file node, .baseignore dirs match on Windows (#66) (#81)
Changed
- changelog: the 0.14.2 tripwire paragraph ships once, not three times (#106)
- Ship the license files inside the release assets (#94) (#80)
- Serve each record once, say what sync imported, and correct the coach (#79) (#62, #65, #70)
- license: relicense engine to FSL-1.1-ALv2, add extension exception (#78)
Install
npx chrisaiOr download the binary for your platform below.
Docs: https://docs.basemode.ai