All notable changes to VibeCody are documented here. This project follows Semantic Versioning.
[0.5.19] - 2026-10-11
0.5.16–0.5.18 were failed attempts at 0.5.15 and were never published; this is the next clean version.
Added
- Office editing in VibeCoder — in-place DOCX paragraph and table-cell edits, table row insert/delete, XLSX rendered like Excel with Office-style charts, PPTX resolved styles, 3D charts and anchored Markdown editing; run styling is preserved across edits.
- Browser Office engine (
packages/office-wasm) with edit operations in a Worker and a corpus gate in CI; structured edit operations invibe-docfmt. - LibreOffice (
soffice) as a first-class engine, with PDF and spreadsheet rendering. - Document RAG — PDFs and Office files in the workspace are indexed; agent
search_documentstool; optional knowledge graph. - Commit patch preview in the git diff view.
- Release: new updater signing key and setup script.
Changed
- Perplexity uses the Agent API; model lists are refreshed and the Tauri shell reads them from the catalog.
- Download links on the release page, notes, installers and package-repo generators go through
vibecody.online/dl/. - “Morning Briefing” is now “Daily Briefing”.
- The release page stays in step with R2; no leg is privileged in completeness checks; macOS Intel builds restored.
Fixed
- Every deploy artifact now starts the daemon with
--serve(it wasserve). - Indexing refuses the home directory and filesystem root.
- PDF zoom flicker; VibeCoder window dragging; blank SVG Touch Up canvas for SVGs without pixel size; blank table cells uneditable in the Word view; deep-link and SQLite file open flow; provider settings labels.
vibe-sheetfmtleft a stalet="s"when a shared-string cell was edited.- Release: Chocolatey files are UTF-8;
release-pagepushes without rebase; tag/version guards skip finalize-only runs.
[0.5.15] - 2026-09-30
Versions 0.5.16–0.5.18 were failed attempts at this release and were never published; the version is reset to 0.5.15.
Added
- Apple Foundation Models provider (macOS 27,
fm serve) with a Settings panel to accept Apple’s terms. The daemon never runsfmuntil the terms are accepted —fmrecords agreement as a side effect of use — and the provider becomes the first-run default only when the model is available and the terms are already accepted. Tools go through the prompt (prose) path becausefm serveignores atoolsarray. - Charts (bar, line, pie, area) editable as JSON and touched up in the SVG editor; embedded OOXML charts render in the document viewers.
- Image and SVG editing — crop, rotate, resize, brightness, contrast, select/move/resize/rotate/recolor, diagram touch-up and SVG export.
tool_choiceacross providers and the UI, plus local-provider settings.- Self-hosted package feeds — Homebrew tap, Chocolatey feed and apt/dnf repositories, published from the release pipeline.
- Release update check and a
quickbuildtarget; Baize theme; shared Embedding Model settings section.
Changed
- macOS Intel (x86_64) builds are kept, without notarization — CLI, VibeCoder, VibeDesk and VibeAIChat ship for Intel, ad-hoc signed and never submitted to Apple’s notary service (Apple silicon builds still sign and notarize).
- Release pipeline: every build publishes its own files to R2 as soon as it finishes, and a failed build can be retried for the same tag and version (
only=/ref=dispatch inputs) — no more burning a version number per failure.latest.txtadvances only when every required leg is published. - CLI configuration persists to the encrypted
ProfileStorefirst, with TOML fallback and migration.
Fixed
- The weather widget could stay empty. It fetched once on mount and swallowed failures, so a daemon still starting (about 16 s cold) left the clock weatherless until remount. It now retries with backoff and refreshes every 10 minutes.
- Release:
mistralrs-sandboxdid not compile for musl (vendored with a one-line cast); the Docker build did not copyvendor/; VibeCoder Linux builds were killed for memory (swap, no LTO). - vLLM and LM Studio API keys mapped to the wrong provider.
- Dropped files replaced the whole window in VibeCoder and VibeDesk.
- The Homebrew cask template always selected the Intel DMG.
[0.5.14] - 2026-09-12
Fixed
- Installer builds skip updater archives when the signing private key is absent, instead of failing the build.
- Pin the upstream citationberg fix to remove vulnerable quick-xml 0.38.4 from the PDF dependency tree.
- A release could publish with a broken VibeDesk build. The publish gate waited only on VibeCLI, VibeCoder and VibeAIChat; VibeDesk is now a required job like the other three desktop apps, and the workflow validates its inputs at startup instead of failing mid-run.
- Release artifacts could be reported live before R2 actually had them. Uploaded object sizes are now verified against the local build and public download availability is checked (with a cache-bust and a propagation retry for R2’s CDN lag) before a release is considered complete.
- The macOS arm64 and x64 updater archives could clobber each other. Tauri names
.app.tar.gzafter the product only, identically for both Mac architectures, and artifact collection merges every build leg into one directory — so the x64 upload could silently overwrite the arm64 one. Both are now renamed with their architecture on the way in. - A multibyte character split across two network chunks was silently dropped by VibeDesk’s SSE forwarders, which decoded UTF-8 per chunk before line assembly instead of after. Framing now decodes only complete lines, so a character split at any byte boundary survives.
- The allocation-counting test utility took its snapshot before dropping the measured value instead of after, undercounting cleanup allocations by one.
Performance
- The tool-call parser no longer allocates on ordinary prose. Every streamed reply ran thinking removal, XML parsing, element parsing and JSON parsing even when the model was just talking; a fast-path byte scan (every supported tool dialect requires
<or{) now returns immediately otherwise. Measured on the parser’s prose fixture: 36 allocations / 1,568 bytes → 0 per parse. - VibeDesk’s transcript replay is linear again, not quadratic. Reopening a saved conversation copied the whole rendered list on every event; replay now builds one locally owned array. On a 12,000-event fixture: 120.97 ms → 0.624 ms (single-run, instrumented, not a wall-clock guarantee).
- VibeDesk’s SSE line framing reuses one buffer instead of copying every chunk.
stream_agentandstream_approvalsnow borrow complete lines out of the HTTP chunk and only allocate for a line split across chunks. On a 2,000-line coalesced-chunk fixture: 3,000 allocations / ~27 MB → 0. - See the measured fixtures and limitations — synthetic, non-statistical timings, not an app-level latency claim.
[Unreleased]
Added
-
XLSX cells show their formatted value, not the raw stored one. A date cell’s underlying value is a bare serial number (
45261) and a percentage is a fraction (0.15) —vibe-sheetfmtclassified a cell’s kind (Cell.kind) but still showed that raw value verbatim. A newnumfmtmodule renders a cell’s ownnumFmtcode (built-in ids per ECMA-376 §18.8.30, or a workbook’s custom<numFmt>) into the text Excel itself would show — dates/times (its own hand-rolled proleptic-Gregorian calendar, including the fictitious February 29, 1900 Excel carries for Lotus 1-2-3 compatibility), percentages, thousands separators, and quoted/escaped literal format codes — exposed read-side asCell.display: Option<String>,NoneforGeneral, a non-numeric cell, or a format code the module doesn’t understand (never a guess).SpreadsheetViewer.tsx’s grid now showsdisplaywhen set, falling back to the rawtextotherwise; the edit<input>is unaffected and always edits the raw value. -
XLSX merged cells look merged in the grid, not just marked by a divider.
Sheet.merged_rangeshas existed since an earlier pass, but nothing painted it — a merge showed as ordinary bordered cells with the value only in the top-left one. Rather than a realcolSpan/rowSpan(the grid is virtualized, so only rows near the viewport are actual DOM nodes, and columns can be dragged into a different display order — both would have to be tracked across scroll position and reorder for a spanning cell to stay correct), a merge’s own cells now blend the border between them via CSS, computed from whichever cell is actually next to which at render time: a column drag that separates a merge’s own columns stops the blend on that side on its own, and a covered cell’s stray leftover text (real data, from before the cells were merged) is hidden the same way Excel itself hides it, not dropped from the model. -
DOCX footnotes and endnotes are readable and editable, not silently dropped.
vibe-docfmtused to leaveword/footnotes.xml/word/endnotes.xmluntouched only because it never looked at them — aw:footnoteReferencein the body vanished from the text buffer entirely, and the note’s own content was invisible. A reference now reads as a[^footnote:3]/[^endnote:2]marker in place (a newSpanStyle.footnote_ref, round-tripped through its own Markdown syntax the same waylinkalready is), and each real note (Word’s own separator/continuation-separator artwork excluded) becomes its own section —word/footnotes.xml’s note id 3 is sectionfootnote:3— using the same<!-- vibedoc:section -->routing EPUB’s chapters already rely on, so editing a footnote’s text edits only that note’s own<w:footnote>paragraphs. Adding or removing a note is refused with a clear error rather than guessed at: a note’s identity is half its content and half the body reference pointing at it, and this only ever edits the former.
Changed
-
The
mistral.rsdependency points at upstream again, not a fork.vibe-infercarried aTuringWorks/mistral.rsfork solely to hold a TurboQuant KV-cache codec (KvCacheCodec, native CUDA/Metal encode kernels) — but that codec’s own doc comments said it “exists today only as a correctness experiment”: it encoded, immediately decoded, and stored at the original fp16 dtype, so flippingVIBE_INFER_KV_CACHE=turboquantbought zero memory savings, only a way to measure reconstruction quality before a real packed-storage kernel existed. That kernel was never written, and the one tool that could have checked quality on a real model (kv_cache_compare.rs, loading Qwen2.5-0.5B twice, fp16 vs TurboQuant, side by side) was never run — no perplexity or recall number for a real model exists anywhere in the repo, only a CPU-only synthetic-data spike (4.57× smaller, cosine 0.92, logged in this file’s own history). Carrying a forked inference engine for an unmeasured, unwired feature was the wrong trade, sovibe-infer/Cargo.tomlnow pinsEricLBuehler/mistral.rsdirectly (the fork’s own comment already tracked upstreammasteralmost exactly, so this is the same commit, not a downgrade). Removed:kv_cache_codec.rs,KvCacheMode(and theVIBE_INFER_KV_CACHEenv var), thekv_cache_bench/kv_cache_compareexamples, the KV Cache dropdown in VibeCoder’s Inference panel, and the now-unneeded[patch.crates-io]candle pin (candle-core forminilm.rsresolves from crates.io again instead of being forced onto the fork’s git rev). The pure-Rust PolarQuant+QJL research code (kv_cache_tq.rs) and its own fidelity tests are unaffected — they never depended on mistralrs — as is the unrelated, already-shippedTurboQuantvector-embedding index compression (vibe-core/src/index/turboquant.rs, VibeCoder’s TurboQuant panel), a different feature that happens to share a name. -
VibeDesk: Connectors is folded back into Plugins, and the merged panel is called Integrations. Connectors got its own nav entry in 0.5.x because a mixed marketplace list buried “does my GitHub connector work” with no way to navigate straight to it — but that split left two sibling panels shopping for the same thing (something that extends the agent) under two different names, and
PluginsViewhad quietly grown its own connector rows in the meantime, so the two views had drifted into duplicates of each other. There is one nav entry now — Integrations — backed by the marketplace view that already listed plugins and connectors side by side; its “Yours” tab still separates a Plugins section from a Connectors section, so managing an installed one is unchanged.ConnectorsView.tsxis removed;PluginsView.tsxis renamedIntegrationsView.tsx. No daemon routes or Tauri commands changed — this is a nav/panel-identity change only.
Fixed
- A full-duplex voice session kept answering in the old engine after a mid-conversation System ↔ Neural switch.
/ws/voice/duplexopened its speech engine once, at connect time, and never looked at config again — so switching engines in Settings → Voice while a hands-free session was still open had no effect until the client reconnected. Because the session’s own reply might still be mid-flight on the old (often slower, cold-started) engine when the switch happened, this could sound like two different engines answering two unrelated turns rather than one setting change taking effect late. Each turn now re-checks the config before answering and swaps engines immediately if it changed.
[0.5.12] - 2026-09-05
Fixed
-
The draw.io panel is a file editor now, not a canvas that forgets. It had every part of a diagram editor except the file. It opened blank every time with no way to reach a diagram that already existed — the repository’s own five
docs/*.drawiowere unreachable from it. Its Save wrote to a hard-coded<workspace>/diagrams/diagram.drawio, so the second diagram you made silently replaced the first, and the toolbar said “Saved to workspace” without ever naming the file; the call behind it was.catch(() => {})followed by an unconditional success message, so a failed write reported as a save. It could not export at all. And the embedded editor showed a Save & Exit button that saved and then sat there, because the URL passednoSaveBtn=1— which replaces draw.io’s Save with Save & Exit — while nothing on this side handled theexitevent. A new Diagrams tab lists every.drawio,.dioand.drawio.svgin the workspace, newest first, with page and shape counts that are absent rather than zero when a file was too large to count, and no timestamp at all where the filesystem reported none. Opening one loads it into the editor and Save writes back to that file, named in the toolbar at all times with a dot for unsaved changes; a diagram with no name yet asks for one instead of choosing. Export writes into the workspace beside the diagram —docs/architecture.drawio→docs/architecture.png— because draw.io’s own export menu downloads through the browser, which the Tauri webview does not surface, so that route produced a file nobody could find.autosave=1was missing from the editor URL while this file already had anautosavehandler, so that branch never ran and Save could write a diagram several edits stale. The eight templates were eight promises: every one returned the same single rounded rectangle containing its own name, so picking “Microservices Architecture” gave you one box saying Microservices Architecture. They are now real starter diagrams indrawio_templates.rs, served from the backend so the tab cannot advertise one this build lacks, with a test asserting each has at least four shapes and three connections and that no edge points at a cell that does not exist. Previewing a.drawiofrom the file tree showed a blank frame: the viewer URL never asked for the embed protocol (embed=1&proto=json) and never acknowledgedconfigure, so theinitevent the component was waiting for could not arrive. Every path is now workspace-relative and resolved inside the open workspace —execute_drawio_mcptook a free-form absolute path, making the diagram bridge a general-purpose read/write primitive for anywhere the deny-list happened not to cover — and an export’sdata:URL is bounded before anything is allocated from it. Saving back into a.drawio.svgis refused by name rather than half-done, because writing only the embedded XML would leave the picture showing the previous version. New:vibecoder/src-tauri/src/drawio_templates.rs. -
A healthy daemon that rejected every client for two and a half days.
~/.vibecli/daemon.tokennames no port, and the daemon runs on any port — so every daemon on the machine wrote the same file and the last writer won regardless of which one was still alive. On 1 September a daemon bound port 7979, wrote that file, and exited two minutes later onSIGTERM. The daemon on 7878 stayed up, answered/healthperfectly, and returned 401 to everything else until it was restarted. What the user saw was “Could not read speech settings from the daemon (daemon returned 401). Is it running?” — a question whose answer was yes, pointing at the one thing that was not the problem.serve.rshad already been taught to bind before writing the token, but that only covers a same-port daemon losing the race; the 7979 daemon lost nothing, it bound a free port and wrote a file it had every reason to think was its own. The file is now named after the port —daemon-<port>.token— and the shareddaemon.tokenis written only by the daemon on the port clients resolve by default, so two daemons cannot collide; it is removed again on clean shutdown, because a token file that outlives its daemon is not a stale cache but a wrong answer with the shape of a right one. The layout has exactly one implementation, the newvibe-daemon-tokencrate, replacing seventeen private copies of the same path join across the daemon, three Tauri shells, the eval harness, the TUI, thevibecli-mistralrsprovider, the Agent SDK, the VS Code extension and the JetBrains plugin — a crate of its own becausevibe-evalis depended on byvibecliand could not depend back. And a 401 is no longer reported as a diagnosis it is not./healthpublishesapi_token.fingerprint, a SHA-256 prefix that reveals nothing about a 128-bit CSPRNG secret, so a client can compare what it holds against what the daemon accepts before issuing a request:Valid,Stale,Missing, orUnverifiablefor a daemon too old to say — four states with different fixes, wherestaleneeds the daemon restarted andmissingneeds it started.daemon_bootstrap::ensure_readychecks the process and the credential and the version together, becauseensure_runningalone answeredtruethroughout the whole outage; all three shells expose it asdaemon_readinessanddaemon_readiness_probe(which never starts a daemon — a poll must not resurrect one the user stopped), anduseDaemonMonitornow says so once per episode rather than every thirty seconds. A route answering 404 from a correct panel is named as what it usually is: an installed daemon older than the app. The three partial copies ofdaemonFetchinvibe-ui-shared— none of which retried a 401, so a daemon restart left speech settings and the harness panel broken until the app was relaunched — are now one shared helper that VibeCoder re-exports. New:crates/vibe-daemon-token,packages/vibe-ui-shared/src/lib/daemonFetch.ts. See Key rotation.
Added
-
The skill catalogue is browsable — and usable — from every desktop shell.
GET /v1/skilllens/skillshas served the full catalogue since SkillForge shipped, and until now exactly one of the three Tauri shells rendered it. VibeAIChat registered the two Tauri commands and never called them, under a comment saying the panel lived in VibeCoder; VibeCoder’s SkillForge panel scores skills and inspects them but has no way to use one, so reading a skill and putting it to work were two different apps; only VibeDesk had a browser, styled out of its ownApp.cssand therefore unliftable. There is now oneSkillsViewinpackages/vibe-ui-shared, rendered by all three over a smallSkillCatalogadapter — the shells reach the same route through differently-named commands (list_skillsagainst a caller-supplied daemon URL in VibeDesk,skilllens_list_skillsagainst the local daemon in the other two), which is exactly the kind of difference a shared component should not have to know about. VibeCoder gets it as AI/ML → Skill Catalog, reachable by typing/skillsin the chat composer; VibeAIChat gets it from the composer’s More menu, standing in for the thread rather than sharing a 440px window with it. Selecting skills writes a sentence into the composer, and that is deliberately all it does:AgentRequesthas noskillsfield, so a skill reaches a run as prompt text or not at all, and a checkbox implying “armed for the next run” would be describing a daemon feature that does not exist. The wording comes from one function, so a pick made in any of the three asks for the skill in identical words instead of three dialects drifting apart; the detail pane’s Insert full text is there for when the guidance itself, rather than a reference to it, is what the model needs. The two empty states are kept apart — the daemon reports no skills and nothing matches your filter are different facts, and saying the first when you mean the second sends someone to debug a daemon that is working./skillsalso needed the panel it opens to exist first: panels are lazy, so the first deep link into one fires while itsTabbedPanelis still unmounted and nothing is listening, and the request is now parked for that panel to claim as it mounts — claimed once, and only by the panel it names. New:packages/vibe-ui-shared/src/skills/,vibecoder/src/components/SkillsPanel.tsx,vibecoder/src/lib/panelDeepLink.ts. Found while auditing which commands each surface actually supports: they are still four disjoint vocabularies — VibeCLI’s 134 REPL commands, VibeDesk’s 14 navigation ones, VibeCoder’s 14 prompt prefixes, and VibeAIChat’s none — and this closes the skills gap, not that one. -
Developer Excellence: DORA and practice maturity, measured from the repository. VibeCoder could grade a service’s delivery performance before this, and the grade was a constant.
evaluate_idp_scorecardawarded every registered service 8/10 for deploy frequency, 7/10 for lead time, 6/10 for MTTR and 5/5 for change failure rate, under a comment reading “simulated DORA-style metrics (would connect to real CI/CD in production)” — 35 of the scorecard’s 100 points, on screen, in a panel a director would read as a measurement. The four keys are now computed from git history, and every number states where it came from:vibecli --devex dorareports each key with its band, its sample size, and the proxy it was derived from, because nothing in a repository knows what “production” means and a DORA number whose proxy is unstated cannot be argued with. Absence is the feature. A repository whose deploys are not tagged does not get0.0 deploys/week— zero is a claim about their delivery, absence is a claim about your instrumentation, and only one of them is true. It getsdeployment_frequency: unmeasuredwith the reason spelled out and the concrete change that would fix it, and the panel renders that block as prominently as the values.fix:commits are deliberately not counted as remediation: most of them fix a bug found in development, and counting them would push change failure rate toward 100% for every healthy team. Practice maturity scans ten practices at the workspace root and never reports level 4 — a file proves a practice is present, not that it is followed, reviewed or improving, so the top level stays attested by people rather than earnable withtouch; where path detection is known to under-report (Rust#[cfg(test)]tests have notests/directory; a monorepo’s lint config lives one level down) the practice carries adetection_caveatprinted beside the misses, not in a footnote. Onboarding reports bootstrap readiness and first-time contributors, and refuses to report time-to-first-commit: git records a contributor’s first commit but not the day they joined, so the interval the day-one target is about has no start, and any number there would have invented one.vibecli --devex gatefails a pipeline on a missed band with exit1and on an unmeasurable required metric with exit3— kept apart because a pipeline that conflates them either blocks releases for a tooling gap or ships on an absence — and a gate with no--require-*threshold is refused outright, so the cheapest route to green is never to delete the criteria. SPACE is the same discipline applied to the half DORA cannot see. Most of SPACE is not in a git repository, and--devex spacereports the frame rather than approximating it: Performance is carried by reference from DORA’s stability pair so it is never double counted, Activity is commits/authors/deployments as counts and never names, Collaboration gets the one thing git genuinely sees — files touched by more than one author, and the recordedCo-authored-byshare — and Satisfaction, Efficiency and review latency are returned as named gaps pointing at the survey, at CI, and at the forge’s pull-request API. Review latency is deliberately absent rather than derived from merge timestamps: a merge commit records when a branch landed, not when anyone first looked at it, and reporting the one as the other would publish queueing time as review time. Two rules are enforced in the shape of the data instead of left to the reader — there is no aggregate SPACE score (a test asserts the payload carries noscore/total/overallfield, because a number summing a survey response with a commit count cannot be wrong and therefore cannot be useful), and volume is never reported without an outcome signal (outcome_signalis false when Performance has no measure, and the panel renders that as an alert; a single-author window also gets no multi-author file share, because with one author it is 0 by arithmetic rather than by how the team works). Nothing in the report is per-individual: author counts are activity, author names would be surveillance, and the shape has no field for them.--devex surveyprints the quarterly instrument, carrying the anonymity and no-performance-review commitments in the document itself. Ten new skills in thedevexcategory (eleven counting the existingdevex-developer-experience.md), routed bydevex-director-operating-system.md, and six slash commands (/devex,/dora,/space,/practices,/onboarding,/devex-plan) that cite their skill file with the extension —get_skillnow accepts either spelling, so a name copied off the page in front of you resolves instead of returningnot found. New:vibecli/vibecli-cli/src/devex_metrics.rs,devex_routes.rs,devex_cmd.rs; panel Cloud & Platform → Developer Excellence, with Delivery, Experience, Practices and Onboarding tabs. See Developer Excellence. -
The Observe-Act loop actually runs. VibeCoder’s Observe-Act panel had every control a computer-use agent needs and drove nothing: Start set a local string to
"running", the Monitor tab said Capturing… over a grey rectangle forever, and Save Config wrote a file under the project data directory that no code path ever read. The pieces existed on either side of a missing middle —observe_act.rshad the action enum, the session state machine and the safety rails;desktop_agent.rshad the mouse, keyboard and screenshot commands — with nothing joining them, no route to reach them, and/v1/capabilitiesadvertising the feature to every client. The loop is now real: screenshot → vision model → validate → act → verify → repeat, in the daemon, behind/observe/*. Coordinates were the hard part. Three spaces are in play — the downscaled image the model actually sees, the capture file’s pixels (2× the screen on Retina), and the pointscliclicktakes — and the daemon measures image width against logical width and divides, rather than inferring the backing scale from whethersystem_profilerprints the word “Retina”, which is absent on a scaled resolution and wrong on a second display. Coordinates are mapped before the rails run, so a forbidden region is checked against the pixel the click will reach, not the one the model named. The three safety modes are enforced and tested: Restricted executes nothing and records what it would have done (struck through in History, so a read-only run is not indistinguishable from a run that did nothing); Cautious stops on a destructive action and treats five minutes of silence as a refusal, because an operator who walked away has not approved anything; Autonomous asks nothing. An over-long action batch is refused whole rather than truncated, and awaitis clamped to 30 s, atypeto 4096 characters, ascrollto 50 — each of them a way one model turn could otherwise park the session with nothing on screen saying why. A step is verified, failed, or unverified — three states, not two. The old panel modelled it asverified: boolean, which rendered “we did not check” as Failed; an unverified step now leaves the failure streak where it was and stays out of the rate’s denominator, so a run with verification off readsn/arather than 0%. Two sessions cannot run at once (409): two loops moving one mouse each verify against the other’s half-finished work. A session interrupted by a daemon restart reloads as aborted, not as a running phantom no stop button can clear.screencaptureexiting 0 with Screen Recording denied — the one failure that looks like success — is caught by checking the file exists and named as the permission it is, and the panel’s preflight names a missingcliclickwith thebrewline to fix it. Provider-agnostic and strictly so:providerandmodelare required onPOST /observe/sessionswith no fallback to the daemon’s boot provider, because a silent fallback would send screenshots of the user’s desktop to a vendor they did not pick. Twenty-two runtime tests drive the whole loop through a recording fake, so the rails are checked without a display or an API key. New:vibecli/vibecli-cli/src/observe_act_runtime.rs. See Observe-Act. -
Archives open in the file explorer. A
.zip,.jar,.vsix,.apk,.tar.gz,.tar.zst— fifteen ZIP-container extensions, tar, and four compressors (gzip, bzip2, zstd, xz) — expands in VibeCoder’s tree the way a folder does, and anything inside opens read-only in Monaco with highlighting, search, and image/PDF rendering. Trying to edit is the interesting part: rather than the silent no-op Monaco gives a read-only model, the keystroke (or ⌘S, or the context menu) offers to extract the whole archive into a sibling folder named after it —dist.tar.gz→dist/— and reopens your file from there, writable, with its neighbours intact so relative imports still resolve. The prompt names the exact folder before anything is written, including the-1suffix when a previous extraction is still sitting there, because silently merging into it is how someone loses the edits they made last time. The archive itself is never rewritten: VibeCody does edit DOCX and EPUB containers in place, and does it by copying every untouched entry back byte for byte in its original order with its original compression method — generalising that to a signed.apkis not a promise worth making. DOCX, XLSX, EPUB and friends are deliberately not treated as archives; they already have a viewer. Nothing allocates from a declared size — a member can claim 8 GiB it does not have, and 40 KiB of zip can expand to a terabyte, so every bound (64 MiB per member, 2 GiB per extraction, 200 000 entries) is enforced by reading, zip-slip paths are skipped rather than written, and a failed extraction deletes its half-built folder instead of leaving it. A tarball has no central directory, so its listing is cached per (path, mtime, size) — without that, expanding five folders in a.tar.gzdecompressed it five times. See archives.md. -
PDFs open as text, and save back. A
.pdfgains the same Edit text button DOCX, EPUB and Pages have. A PDF has no paragraphs — it has glyphs at coordinates — so it opens as plain text, one line per line of the page, one section per page, and the words on a line can be changed or the line cleared. Everything a PDF cannot do is an error naming what it is rather than a guess: a line cannot be added (a new line has no position, no font and no place in the content stream), a character the font cannot draw stops the save and is named, a page marker cannot be dropped, an encrypted file says so and asks for an unprotected copy, and a page drawing an inline image is left alone because pixels written betweenBIandEIdo not survive the stream being rebuilt. A change goes back to the run that drew it, encoded through that run’s font — every font in a modern PDF is subset to the glyphs its own words used, so a sentence with one bold word has no single font that can draw the whole line; measured on 500 PDFs, writing the line through the first font alone failed on about one line in ten that this deals back correctly. What a font’s codes mean is read, never assumed: a/ToUnicodemap first, then a named base encoding, then/Differences; where none of them answers — a symbolic font, a composite font with no map — the run is left out of the buffer and a warning names the font, because a reader that invents characters is worse than one that admits a gap. Words that TeX sets apart by moving the pen rather than drawing a space read as spaces and are written back as gaps. The changed page is appended as an incremental update, so the original bytes stay exactly where they are and a packed file comes out the same size — rebuilding one costs up to 70% more. Measured over a random 500 of every PDF on one machine: of the 285 with an editable line of text, 271 took an edit, saved, and read it back; 14 were refused by name because no font on the line carries the character asked for; the rest are system vector assets with no text, files that are not PDFs, or one encrypted. New:crates/vibe-docfmt/src/pdf/. -
Two pages side by side. Two up in the document toolbar puts a PDF into a real spread — facing pages, Pages 7–8 of 120 in the toolbar, ← and → turning a whole spread — and a DOCX or an EPUB chapter into two columns of a screen, paged sideways. The two are different on purpose: a PDF has pages and a chapter does not, so one counts pages and the other counts screens rather than pretending a reflowable document has sheets. Switching layouts keeps you on the page you were reading (page 8 one-up becomes the spread 7–8, not 8–9), an odd-length document ends on a single page rather than a blank one, and the choice is remembered per document for the session. Pages open fitted to the window rather than at 100%, which for a spread is two half-pages. PDF pages are now drawn by the app rather than handed to the platform’s viewer inside an
<iframe>, which is why that panel had no page navigation and no spread — everything inside that frame belonged to the browser. The renderer is imported the first time a PDF is opened, so the startup bundle is 138 kB gzipped smaller than importing it with the viewer would have made it. The columns are laid out with a gap of exactly twice the margin — not a taste decision: any narrower and the next screen’s first column shows through the pane’s padding, permanently, as a sliver of the following page. -
Mermaid and PlantUML diagrams are drawn, in markdown and on their own. A
```mermaidor```plantumlblock in a markdown preview becomes the diagram; a.mmd,.mermaid,.puml,.plantuml,.wsdor.iumlfile opens as the picture, with Source to go back to the text and the preview following the buffer as you type. Mermaid is bundled and drawn in the app — the Diagram Generator’s existing preview imported it from jsdelivr inside a sandboxed iframe, which the app’s own CSP (script-src 'self' blob:) blocks, so that pane could never draw anything on any machine, online or off; it now uses the same renderer as everything else. PlantUML is a Java program with no browser port, so it is run locally (-pipe, nothing written to disk) and nothing is sent to a remote renderer — the usual trick of encoding the source into a plantuml.com URL is someone else’s server reading your architecture because a picture appeared. With no PlantUML installed the diagram says so and how to install it.-failfast2is passed deliberately: without it PlantUML renders a picture of a syntax error and exits 0, which is a failure reported as a drawing that worked. Two things a diagram will never do: render blank, or render something the file could script — a failed diagram shows the renderer’s own message and the source that produced it, and every SVG is sanitised before it reaches the DOM. Mermaid’s labels are configured as SVG text rather than HTML in a<foreignObject>, because the sanitiser removes that namespace wholesale: the first working build drew every box and arrow with not one label, which looks like success. Mermaid loads on first use, so the startup bundle grows by 5 kB rather than a megabyte.
Fixed
-
The IDP scorecard reported delivery performance it had never measured. Four of Cloud & Platform → IDP’s nine metrics — Deploy Frequency, Lead Time for Changes, Mean Time to Recovery, Change Failure Rate — were hard-coded integers contributing 35 of a 100-point grade, so every service ever registered scored identically on all four regardless of how it was actually built or shipped. They are removed rather than replaced: a catalog entry holds a
repo_url, not a checkout, and DORA needs history, so there is nothing this function could honestly compute. The denominator drops to 65 with them, so the percentage still means “of what was assessed”; the response gained ascopeline saying the grade covers catalog metadata only and anunmeasuredblock naming the four keys, why they are absent, and where they are measured; and the panel renders both, because a client that dropped them would put the same grade back on screen looking like a whole-service score. The recommendation “Improve DORA metrics by automating deployments and reducing batch sizes”, which fired below 80% on the strength of those constants, is gone too — a function that measures none of the four keys is in no position to tell anyone their batches are too large. -
The TLS inspector called every certificate invalid. Network Tools → TLS reported Invalid / Expired and 0d remaining for every site on the internet, github.com included — directly above its own raw output showing the chain verifying at every depth. The dates were being read out of
openssl s_client’s human-readable chain listing, where OpenSSL 3 printsv:NotBefore: …; NotAfter: …and OpenSSL 1 printed no validity dates at all; the parser looked forNot After, with a space, found nothing, and rendered no expiry date as zero days left, and therefore expired. The fields now come fromopenssl x509 -noout -subject -issuer -dates -serial -ext subjectAltName, one documentedkey=valueper line on every version — which also brings back the serial number and the SAN list, empty until now for the same reason. Days remaining is counted on a real calendar instead of(year - 1969) / 4and a fixed table of month lengths, so a leap year is not a day out, and it is absent when the date could not be read rather than zero, because absent is not expired. The verdict stopped being a boolean: Expired, Not yet valid, Chain not trusted — self-signed certificate and Could not read the certificate are four different things, and reporting all four as “Invalid / Expired” is what let a parsing bug look like a bad certificate. The verify result is also read from stdout, where openssl prints the line that states the outcome, rather than stderr, which carries only the reassuringverify return:1callbacks. Checked against the live internet (cargo test -p vibe-coder -- --ignored tls_inspector): github.com valid with 88 days left,expired.badssl.comexpired 4160 days ago,self-signed.badssl.comuntrusted with its dates still readable, and a closed port an error naming the port instead of an empty certificate. New:vibecoder/src-tauri/src/tls_cert.rs. -
A release build made with cargo alone opened on a blank window. The frontend is compiled into the binary only when the
tauri/custom-protocolfeature is on —tauri buildpasses it,cargo build --releasedoes not — and without it the window is pointed athttp://localhost:1420instead. With no dev server running that is a white rectangle and nothing else: no panic, no log line, no failed request anyone sees. Every shipped build was fine, because CI andmake build-*both go throughtauri build; the trap was only there for anyone reaching for cargo directly, and it cost an afternoon to find. All three shells now declare thecustom-protocolfeature, socargo build --release --features custom-protocolembeds properly (412 asset keys in the binary, against 0 before), and the build says so twice when it will not: acargo:warningat build time naming the fix, and a line on stderr when such a binary starts. The feature is deliberately not a default — embedding brotli-compresses the whole bundle during macro expansion, andcargo checkwould otherwise pay for it on every run. -
Word documents could be opened but not saved. Every real
.docxtried — a résumé, two book manuscripts — failed verification and refused to write, which is the crate working as designed but is not a feature. The buffer and its own parse disagreed in six ways, each of them a place where the Markdown writer produced text the Markdown reader read back as something else: a trailing non-breaking space (Word ends paragraphs with them constantly, andstr::trimeats U+00A0 because Unicode calls it whitespace); an ASCII diagram’s leading indentation; a hand-numbered paragraph — “2. Map your first 90 days” typed into Word — read back as the second item of a list and renumbered to “1.”;**bold ****bold italic***emphasis, whose markers merge into an ambiguous****that no parser recovers; a code run containing a backtick, closing its own fence early; and a link whosehrefarrived with the newline XHTML wraps long attributes at, splitting the buffer line in two. Emphasis is now factored (**bold *and italic* more**), code runs get a fence longer than their own backticks, a paragraph that opens like a heading, bullet, table row or numbered item is escaped, and every scanner steps over a whole code span or link rather than reading the*,_or]inside it as markup. Measured, not asserted: every.docxand.epubfile on one machine — 434 of them — now round-trips and verifies, and 427 of the 430 that have an editable line took an edit and read it back (the other three could not be copied), against 11 that could not be saved before — and the same fixes repaired five e-books, including one whose fenced code block contained a ``` line and one whose blank line inside a code sample was being eaten by the list-spacing pass.
[0.5.11] - 2026-08-28
Added
- Re-run Review, from the findings. The control that starts a review lives on the Changes tab, so re-reviewing after an edit meant leaving the report to go and find it. The button now sits on the report itself and repeats the run with the same base ref — a test pins that by comparing the second call’s arguments to the first’s, because a re-run that quietly reviews something else is worse than no button.
- A model of its own for spoken replies. Settings → Voice now carries the model that answers a voice turn, separately from the model the composer is set to for writing code (
[voice] provider/[voice] model, overPUT /voice/settings). The two jobs have different budgets, and the model is nearly the entire spoken one: measured on a 24 GB Mac against a 20B local model with a README and 400 file paths in the system prompt (4.0k tokens), the model took 5.0 s warm and 42.0 s cold — 26.6 s of load, 12.0 s of prefill — against 0.58 s to recognise the speech and milliseconds to say the reply. Ollama evicts an idle model after about five minutes, so the cold number is not a first-run cost, it is what the first turn after a pause costs. Set, the pair wins over what the client sends, because a setting that deferred to the client would be visible, saved, and inert on all three desktop shells; unset, nothing changes. Both halves or neither — given one, the daemon builds no override and silently uses its own provider, so half a pair is refused rather than stored. Thereadyevent now names the model that will actually answer. - Sizing & Hardware — a new page answering what the documentation never did: how much RAM, disk and GPU each part actually needs, VRAM by model size and quantisation, which workloads use a GPU at all (local inference, and essentially nothing else), and what
vibecli --setupalready decides for you from RAM alone. Measured numbers and estimates are labelled separately throughout, because most sizing advice is arithmetic wearing a lab coat. - Third-Party Services — every external service VibeCody can talk to, what each needs from you, and the point the docs had buried: none of them is required. One provider key, or Ollama on the machine, and nothing else.
- The speech engine is configurable, installed, and warm. Settings → Voice in all three shells — engine, language and voice, backed by
GET/PUT /voice/settings. Three things, one of which is the reason none of the rest had ever run. Nothing built or installed the speech sidecar: for the whole life of this feature the only reference to building it was a hint string in a prototype’s error message, sotts_sidecarwas unset on every real machine,Tts::openfell straight through to batch, and every spoken reply came from onesayprocess per utterance in the system default voice — the slowest path and the least natural voice. The engine work was inert and the symptom was “the assistant sounds mechanical”.make voice-sidecarbuilds and installs it,make voice-kokorosets up the neural one, andmake voice-statusanswers the question that previously had no answer, which is which engine will actually run.Tts::openalso discovers a sidecar beside the daemon binary, so an install needs no config file at all. Warm at daemon start, not at socket open: the neural engine measured 6.6 s to load its model and speak its first sample, and paid inside the WebSocket handler that is 6.6 s of silence that looks exactly like a microphone that does not work — one engine is parked at boot, only when voice is configured, and a second concurrent conversation opens its own rather than sharing, since one synthesiser between two sessions would let a barge-in in one cancel the other’s speech. Each engine row carries whether it is available and why not, because a greyed-out option with no reason reads as a bug; selecting an engine writes its sidecar paths too, because"kokoro"with no interpreter configured is a setting that reads back correctly and does nothing. The settings live in the daemon rather than in each app: the daemon is what speaks, and three local copies would be three settings disagreeing about one machine. Voices come from the engine (--voices, 28 across 9 languages) rather than a table that is wrong the moment someone installs a language pack, and are grouped by language in the picker because a voice belongs to one. See Full-duplex voice. - A neural voice, for anyone who wants one.
[voice] tts_engine = "kokoro"runs Kokoro-82M through MLX instead of the platform synthesiser — 54 voices, Apache-2.0, Apple Silicon. It is not the default and it is not free: the platform engine reaches first audio in 21 ms and this one in 165–230 ms, both measured bytools/tts-benchrather than estimated. The sidecar speaks the same wire protocol as the macOS one, so the daemon needed no new plumbing — pointtts_sidecarat an interpreter andtts_sidecar_argsat the script. On macOS, try the free thing first: Apple’s Enhanced and Premium voices are neural, cost nothing at synthesis time, and are separate downloads most Macs have never made — a machine with none installed speaks in the compact tier, which is what “sounds mechanical” usually means. -
Audio frames carry their own sample rate.
AVSpeechSynthesizerproduces 22.05 kHz and Kokoro 24 kHz, and the reader assumed 22.05 kHz for everything. A wrong rate does not fail — it plays the whole reply at the wrong pitch and speed, which sounds like a bad voice rather than a bug. The newAURframe prefixes au32rate; the oldAUDframe is still read as 22.05 kHz so a daemon can drive a sidecar built before this existed. - Every model gets its own context budget, read from the provider. The agent loop pruned conversation history at 200 000 tokens for every model on every provider, and the chat panel compacted at a flat 80 000 characters — neither number had anything to do with the model in front of you. Both directions were silent: a larger window lost history it never needed to, a smaller one overflowed and the provider dealt with it quietly. Ollama drops the front of an oversized prompt, taking the system prompt and the tool contract with it, so the symptom was a model that appeared to forget how to call tools and started emitting malformed markup. Windows now come from the vendor —
context_window/context_length/max_context_length/max_model_lenon an OpenAI-compatible/models,model_infoon Ollama’s/api/show,inputTokenLimiton Gemini. There is deliberately no table of numbers: a hardcoded window per model id is a fact about someone else’s product, written from memory, wrong the moment a vendor ships a revision. Anthropic and OpenAI publish no such field, so they keep the documented default rather than a guess. - Source Control is four tabs. Review and Tools own the panel instead of stacking as collapsible sections under the changes list, where opening a review meant scrolling past the working tree to read it and back to act on it. Tools gathers the four things that are neither the working tree nor a review of it: AI branch names, AI merge-conflict resolution, changelog generation and the repository’s settings. Starting a review stays with the changes being reviewed — the control is on Changes, the findings on Review, and pressing Run switches you there.
- Full-duplex voice. The microphone stays open while the assistant speaks, so you can interrupt it mid-sentence and it stops. Push-to-talk stays — it is the right control for dictating a long prompt, and the only one that works without echo cancellation. The pipeline lives in the daemon (
voice_duplex.rs,GET /ws/voice/duplex): voice-activity detection, turn-taking, transcription, the model call and speech synthesis happen in one place, so a client contributes a microphone and speakers and nothing else. Wired into VibeCoder, VibeDesk and VibeAIChat through one shared hook. Measured end of speech to first audio: 134–158 ms — the first build measured 1076 ms, of which whole-utterance Whisper was 978 ms, and the win was not a faster model but overlapping recognition with the speech so end of turn only has to finalise. See voice-duplex.md. - Any language you speak in.
language=autoidentifies the language each turn across 99 of them, instructs the reply into it, and picks a voice that speaks it. Detection runs every turn rather than being pinned after the first: forcing a language suppresses the engine’s detection result, so a user who says one Hindi sentence and then an English one got the English turn labelled Hindi and answered in Hindi. Code-switching is the normal case for multilingual speakers, not an edge case. - Full-duplex voice is off until you turn it on. It holds the microphone open for the whole session, so it is disabled by default and the chat window carries the switch: one click enables the feature, a second starts listening, and an × turns it back off — closing the microphone rather than just hiding the control that was holding it open. The preference is shared across the three shells, and the hook enforces it as well as the button, because hiding a control is not the same as refusing to open a device.
tools/webview-probe— measures the voice stack on the webview engine each platform actually ships (WKWebView, WebView2, WebKitGTK) rather than on a browser standing in for them. The transport arm needs no audio hardware and runs in CI on all three OSes; the echo-cancellation arm needs a real microphone and speaker in one room and stays manual.
Changed
- VibeCoder and VibeAIChat composers follow VibeDesk’s. One
+replaces the row of permanent buttons: in VibeCoder it holds attaching a file, mentioning one, and the standing voice opt-in — the old bare+typed an@character, a button whose label described its glyph rather than what it did — and the toolbar now carries the live voice control only once voice is turned on, so a feature nobody enabled costs no space. VibeAIChat goes further: provider and model move out of the full-width bar above the conversation and into the composer as one control, which in a 440px window is a whole row reclaimed for a choice you make once a day. Both gain the keyboard contract as a quiet line under the frame, out of the placeholder — a placeholder disappears the moment you start typing, exactly when you might want to know what Enter does. The menu is one shared component (@vibe/shared/composer) rather than a third copy; VibeDesk keeps its own for now and switches over next. Also fixes an attachment badge that anchored to the whole composer instead of its button, since.chat-toolbar-btnhad no positioned ancestor. -
VibeAIChat has a test suite. It was the one shell shipping on
tsc --noEmitalone. - Charcoal is the default theme, on every surface that has one. A fresh install of VibeCoder, VibeDesk or VibeAIChat now opens in Charcoal rather than the midnight-blue Default, and
vibecoder/design-system/tokens.csscarries the same palette as its:root— so the paint that happens before any JavaScript runs is already the theme the app settles on, instead of a flash of something else. The Flutter client’s token translation (vibe_tokens.dart) and the design-system colour/elevation tables move with it. The default is named once (DEFAULT_DARK_THEME_ID/DEFAULT_LIGHT_THEME_ID) because the old fallbacks had already drifted apart: the theme toggle booteddark-sherwood, the editor’s fallback saiddark-default, and the stylesheet a third palette — a user with nothing stored got all three at once. A test now holdstokens.cssand the registry to the same values, including the WCAG-computed button foreground (black on Charcoal’s accents, not white). Existing installs keep the theme they chose — this changes what “no choice yet” means, not what anyone picked. - VibeCoder’s chat composer no longer clips its own send button. The toolbar was
flex-wrap: nowrapwithoverflow: hidden, so in a narrow sidebar the row was cut off at the card edge — and what sits on the far right is Send, which meant the panel’s primary action was invisible and unreachable while the voice button’s × was the last thing still showing. The row wraps now, voice and send travel as one right-aligned cluster so a second line keeps them together, and the card is a query container: below ~430px of its own width the Fast/Balanced/Thorough labels collapse to their icons (each button keeps anaria-label, since adisplay: nonelabel leaves no accessible name behind). A viewport media query would have asked the wrong element — the sidebar is resizable and is never the width of the window. - The VibeDesk composer groups its controls instead of listing them. The toolbar had grown to eight equal-weight pills on one row — quick actions, attach, mic, voice conversation, approval tier, branch, model, mode, thinking effort — which wrapped onto two lines at ordinary window widths and gave no clue which of them described the message and which described the run. It is now three places, each answering one question. In the frame with the text: what you are sending and who answers it — the
+, run mode as a segmented Agent/Chat/Sandbox switch, and model + thinking effort sharing one control, because “which model, how hard” is one decision. On a quiet row underneath: the run’s standing conditions — approval tier, whether it forks a worktree branch, sandbox grants — where only Full access takes colour, since it is the only one that has given something away. Behind the+: attaching files, the five panels, and the full-duplex voice switch; the duplex start button appears on the toolbar only once voice is on, so the toolbar carries the live control and not the opt-in as well. The bar now shrinks the model name before it wraps, and wraps on the composer’s width rather than the window’s, so nothing lands outside the frame.
Fixed
- Switching chats in VibeDesk killed the voice conversation, silently. The conversation pane is remounted on every chat switch and replaced outright by every full-screen overlay, and the voice session was mounted inside it: its teardown closed the WebSocket and the microphone, and the daemon’s history — which lives on that socket — went with them. Clicking a chat, or opening Settings, ended a spoken conversation mid-sentence with no message and left the button offering to start a new one. The session now lives in the shell above that line (
useVoiceSession), and the pane registers itself as the place completed turns are written; the composer’s run controls were lifted out for exactly this reason and the microphone was left behind. - An interrupted spoken turn was on your screen and absent from the assistant’s memory. Barge-in is the point of an open microphone, and every abandoned turn returned before recording anything — while the transcript had already been sent to the client and drawn. The conversation history was therefore emptiest exactly when the user had been talking most, and the assistant answered follow-ups with no idea what it had just been asked. A turn the user heard part of is now remembered with what was actually said; a turn superseded before any audio went out is carried into the next one, which was already the rule for words interrupted during recognition and is now the rule for words interrupted mid-generation too.
- A neural voice that cannot speak your language produced silence, and said so in a footnote. A Kokoro voice belongs to exactly one language —
hf_alphais Hindi — and asked for an English sentence it returns no audio at all. The daemon noticed and emitted anotice; the reply still arrived as text, was written into the chat, and was never said aloud. Every turn, for as long as that voice stayed selected, which from where the user sits is a microphone that does not work. The sentence is now spoken with the platform voice, which every machine has, and the notice says what happened and how to fix it properly. Measured before and after on the same reply: 0 KB of audio, then 44 KB. - VibeDesk picked a default model this machine cannot load. The picker chose the first row whose name did not contain “cloud” — on a 24 GB Mac, a 19.8 GB model, which Ollama refuses with
model requires 19.7 GiB but only 17.3 GiB are available. That is an HTTP 500, and 500 is not in the retry classifier, so every run and every spoken turn failed on a model nobody had chosen.GET /modelsnow reports each installed Ollama model’s size and, where the daemon can state a real budget, that a model exceeds it; the picker skips those when choosing for you and marks them in the menu with the two numbers. macOS only, and by design: on unified memory Metal offers ~75% of RAM as its working set, which is the bound Ollama checks and which reproduces the refusal above to within 1%. Elsewhere the daemon says nothing rather than inventing a bound — a wrong “fits” is worse than no answer. Models over the budget are marked, never hidden or disabled: the limit is raisable, and this is one number about one machine. - Every
PUTandDELETEroute in the daemon was unreachable from every browser client. The CORS layer allowedGET,POST,PATCHandOPTIONS, and the router serves threePUTroutes and fifteenDELETEones. A method missing fromaccess-control-allow-methodsis not a locked door, it is an invisible one: the browser refuses the preflight before sending anything, so the caller never gets a status code to explain — it gets the transport error, which in WKWebView readsLoad failed. The visible symptom was the new Voice settings tab, which could read its settings and never save one. Not a permissions change: these routes were always meant to be callable, andrequire_authis what decides who may call them. A test now asserts every method the router serves appears in the preflight answer. - A failed save no longer erases the Voice settings pane. The section returned early on
error, anderroris set by a failed save as well as a failed load — so one blocked request replaced every control with a line of text, and a save problem read as a load problem with nothing left on screen to retry from. The message now sits above the controls; only a load that produced nothing shows on its own. - VibeCoder had no Voice settings page at all. It does not use the shared
SettingsView— it has an older sidebar of its own — so a section defined privately insideSettingsViewreached two shells of three and the third silently had none.VoiceSectionis its own exported module now and VibeCoder’s sidebar renders it. This is the same shape as the voice stylesheet VibeCoder never imported: shared code only reaches the shells that ask for it, andtschas nothing to say about a shell that does not. daemon_portwas never registered as a Tauri command in any of the three shells. The shared voice hooks (useVoiceSettings,useVoiceDuplex) ask the host for the port and fall back to 7878 when the call fails, so the fallback ran every time and the failure was invisible on a default machine — and silently wrong on one whereVIBECLI_DAEMON_PORThad moved it.- A question asked in Hindi is answered in Hindi again. All three shells sent
language: "en"to the voice socket, and pinning a language does not merely bias the recogniser — it suppresses its detection result. Every turn therefore arrived labelled English, the reply-language rule never fired, and the assistant answered in correct, fluent, wrong-language English. They now sendauto, which detects per turn, because code-switching mid-conversation is normal for multilingual speakers rather than an edge case. -
The voice follows the language, not the session. The detected language reached the system prompt but never reached the speech engine, so a Hindi reply was read by an English voice — which is not accented Hindi, it is the wrong sounds. Language now travels with every spoken sentence, including the spoken approval question, and a pinned voice is honoured only while it still matches what is being spoken. Kokoro covers nine languages against the recogniser’s 99; when it has no voice, the daemon notices that a speakable sentence produced no audio and says which language it was, rather than leaving a silence that is indistinguishable from a broken microphone.
-
“Can you open that file” now opens it. The assistant read the file, described it, and the editor never moved — every half worked and there was no join. Three gaps stacked: the daemon’s spoken tool vocabulary was
read_file/list_directory/search_files/write_file/apply_patch, all executed against disk with nothing that acts on a window;useVoiceDuplexhad no channel back into the host beyondonTurn, which carries a role and a string; and in VibeCoderonOpenFilewas threaded to nine panels and stopped atPanelHostfor the one that hosts the conversation. There is now anopen_filetool the daemon deliberately cannot execute — it resolves the path, confirms the file exists inside the workspace, and sends the client{"type":"ui","action":"open_file",…}to act on. Opening is not reading, and the prompt has to say so: asked to “open the config” a model reaches forread_fileand describes what it found, which is an answer to a question nobody asked. The client declares whether it has an editor — VibeDesk and VibeAIChat run the same hook against the same daemon and have nowhere to put a file, so the tool is only advertised to a client that said it can honour it, and that declaration is derived from the presence of anonOpenFilehandler rather than configured beside it, because two switches for one fact drift into an assistant that says “I’ve opened that for you” over an editor that did not move. The path is checked here, not trusted: “open my ssh key” is a sentence a microphone can pick up, so it is canonicalised and required to name an existing file under the workspace root, and a path that fails is reported to the model as not opened. A source-scan test walks the whole prop chain, since every link is an optional prop andtscis happy with a feature that is simply inert. -
The voice assistant could not use a single one of the tools it was given. Asking it to open a file in a folder returned “the model produced only reasoning or tool calls and never answered” — for a model that had done exactly what it was told. Two independent defects, either of which was enough on its own.
StreamFiltersuppresses<think>,<thinking>and<tool_call>, which is correct for the agent console (it renders tool use as its own structured line) and fatal for a turn that has to run the call: the filter sits upstream of the tool gate, so every call was swallowed before the gate could see it,tool_text()was alwaysNone, and no tool ever ran. It now has areasoning_only()mode that passes tool markup through — keeping it out of the speaker is the gate’s job, which is what the gate exists for. And the spoken tool contract advertisedlist_dir, a nameparse_tool_callshas never known; the canonical name islist_directory, so following the instructions produced a call that parsed to zero calls. A prompt is an interface: the examples in the contract are now parsed by a test, so the specification is checked against the implementation that has to honour it. Thevoice_duplextest helper that claimed to be “the pipeline the daemon runs” modelled two of its three stages and passed throughout — it now runs all three. -
VibeAIChat’s menu-bar icon is the VibeCody mark, not a white square — and the window has a close button. macOS draws a template image from its alpha channel alone and discards the colours, so the tray was pointed at the full app icon — 96% of which is opaque — and rendered as a filled block beside Claude’s and OpenAI’s glyphs. The tray now uses a dedicated template icon (
icons/tray-template.png): the brand V on transparency, which takes the menu bar’s own colour in light and dark. Windows and Linux keep the full-colour icon, sinceiconAsTemplateis macOS-only and a monochrome black glyph would disappear into a dark taskbar. A unit test pins the replacement’s shape, because a template icon that covers its canvas fails silently — it looks like a deliberate square. The window is frameless, so its only exits were the — that hides it to the tray and the tray menu’s Quit; a ✕ now sits beside the — and quits outright. -
VibeCoder was rendering the voice controls with no stylesheet at all. VibeDesk loads
@vibe/shared/voice/voice.cssfrommain.tsxand VibeAIChat fromApp.tsx; VibeCoder never imported it, so the mic button, the full-duplex control and its state dot appeared as bare unstyled markup beside a toolbar of styled ones — for the whole life of the feature. The stylesheet itself was correct and fully themed, which is why nothing about it looked wrong in review: a perfectly themed stylesheet nobody loads is the same as no stylesheet. A source scan now fails if any of the three shells renders these controls without loading it. -
A spoken turn that produced no answer says so, instead of going quiet. Filtering reasoning out of the stream can leave nothing behind, and an empty reply reaches the user as a chat log that skipped a turn and a speaker that stayed silent — indistinguishable from a microphone that never worked. The daemon now separates the two silences it can actually tell apart (tokens arrived and none survived the filter; no tokens arrived at all) and reports neither as an answer. The conversation stays open, because the next thing you say is a new turn — and so does the control that stops it:
activewas derived from the turn state alone, so one failed turn made the button offer “start” on a microphone that was already open,startreturned early because the socket existed, and nothing left on screen could close it. -
A voice conversation now leaves something on screen. In VibeDesk the whole exchange — the question and the answer — happened with nothing written down anywhere: the composer mounted the duplex hook without an
onTurn, so the chat log never heard about a spoken turn. Completed turns now land in the conversation as ordinary user and agent messages, so a session where half the questions were spoken still reads as one thread. And all three shells gain a live caption above the composer (VoiceTranscript): the sentence being spoken right now, under the question it answers. The two are deliberately different — the chat log gets a turn once, when it is complete; the caption covers the seconds in between, when previously a broken pipeline and a working one looked identical. The caption also outlivesactive, because the hook tears the conversation down on a failed start, which is exactly when there is something to explain. -
The voice assistant no longer reads its own reasoning aloud. Asked “how are you doing?”, a reasoning model spoke two paragraphs of deliberation — “The user says: Hey, how are you doing? As a voice assistant, respond in one or two short spoken sentences…” — before giving the one-line answer. Ollama returns reasoning in a separate field and the provider splices it into the token stream as
<thinking>…</thinking>; the voice path was the one consumer that never filtered it, so the sentence splitter cut the narration into sentences and spoke every one. The existing streaming filter now sits between the provider and the splitter. It has to be a state machine rather than a per-chunk strip: tokens are not tag-aligned, and<thin+king>arriving in different chunks both misses the tag and leaks its two halves into the speaker.llm_ttft_msstays measured on the raw stream, so the gap tofirst_audio_mson a reasoning model is visible rather than hidden — the model really was producing tokens that whole time. - The voice assistant can see the project you have open. Asked about the workspace in the file tree beside it, it answered that it had no information about it — the typed chat path injected pinned memory, the context block, the open file and the file tree, and the voice path injected none of them. It was the one surface that never received the context every other path already had. The socket now carries a
set_contextcontrol message, sent on connect and again whenever the workspace changes, folded into the turn’s system prompt and bounded at 32k characters. An empty block clears it rather than leaving it unchanged: closing a project mid-conversation must not leave the assistant answering about the old one. The prompt asks it to say when the context does not say, rather than to be helpful about files it cannot see. -
One spoken reply is one chat message. A three-sentence answer arrived as three separate bubbles.
speakingfires once per sentence because that is what drives streaming TTS — waiting for the whole reply would make first audio inherit the entire generation time — and the shared hook appended a message for each one. Sentences now extend the turn in progress andreplycloses it with the model’s own text, so the host’s chat log hears about a turn once. The fold is a pure function (reduceTurns), so the property is unit-tested without a microphone, a socket or an AudioContext. - The git diff showed almost every line as added, whatever the change. The “before” side was rebuilt in the frontend by walking the unified diff and keeping its
-and context lines. A patch carries only the changed hunks; everything between them is not in it. Measured on a 500-line file with one line changed: the patch is 14 lines, the reconstruction yields 8, the file has 500 — so ~492 lines were reported as added. Both sides now come from git’s object store, and a file that genuinely has no previous version is distinguished from a lookup that failed. - A file in the commit history diffed against the working tree, not against the commit. Which usually meant no diff at all: while browsing history the working tree matches HEAD, so both sides came back identical and a commit looked like it had changed nothing. History entries now compare the commit against its first parent, and the viewer’s header names the commit it is showing.
- A long commit message no longer takes over the history list. The whole message rendered, subject and body, so one commit with a real body pushed “Files Changed” and every file under it out of view. Clipped to the subject line or 100 characters, with
more/less— and no toggle at all when nothing is hidden. - The voice buttons ignored the theme on every surface. Their stylesheet named
--border,--surface,--fg,--mutedand--accent-red, none of which this design system defines, each with a GitHub-dark hex beside it. Avar()fallback for a token that is never defined is not a fallback — the hex wins on every theme, forever. They now use the real tokens, and the full-duplex button matches.panel-btnin geometry as well as colour. Four of the dead names survived that pass in the duplex off-state and its × — the source-scan test caught them, and they are gone too. - A code comment rendered as text in the Review tab. Moving the review section out of its collapsed wrapper carried the comment inside it into JSX children position, where
/* … */is not a comment. Neither the type-checker nor the linter has anything to say about JSX text. - Editor hovers were clipped at the pane edge. A TypeScript diagnostic came out cut mid-word — and because the text wraps to the hover’s width before the clip, it read as a message that ends there rather than one that had been truncated. Monaco’s overflow widgets now render in a layer that escapes the editor’s
overflow: hidden. - Chat got slower the longer a session ran, and slower again inside a single long reply. Every streamed chunk re-rendered the entire transcript — re-parsing reasoning tags, tool markup and markdown for each message — and then re-parsed the growing reply on top of that. Measured with 30 messages and 20 chunks: 300 markdown renders where 0 were needed; and for a 40-chunk reply, 40 renders where 1 suffices.
- A tool call whose markup was malformed no longer writes a garbage file. The
pathattribute was read by searching for the first">, which is the tag terminator, not the attribute’s.<write_file path="src/main.rs" language="rust">therefore yielded the pathsrc/main.rs" language="rust, and a file was created with that literal name. Paths are now read to their own closing quote and checked before anything is written; a rejected tag is reported in the conversation rather than dropped. - The agent’s recovery no longer reports itself as a crash. The loop emits a circuit-breaker notice when it compacts context or hands a degrading run to a fresh agent, and then keeps going. VibeCoder treated every notice as terminal — printing “Agent halted”, clearing the run’s state — while the run carried on executing, so every tool call, approval prompt and the final answer after it were discarded.
- Full-duplex voice failed to start on every desktop shell, and took push-to-talk down with it. An
AudioWorkletmodule is fetched underscript-src, notworker-src— VibeCoder and VibeDesk shipscript-src 'self'withworker-src 'self' blob:, so the capture module was rejected outright (“Not allowed by CSP”, measured under the shipped policy). Fixed twice over:script-srcnow allowsblob:, and capture falls back to aScriptProcessorNodethat fetches no module, so the feature no longer depends on a host’s policy to work at all. Separately, a failed start released nothing — the microphone stayed open and push-to-talk could no longer acquire it, so one broken feature was disabling a working one. Teardown now runs on every failure path, and start failures name their remedy instead of saying “connection failed”. - “No speech engine configured” on machines that had one. The configured binary defaults to the bare name
whisper-server, but was checked withPath::exists()— which asks whether it sits in the daemon’s working directory. Bare names now resolve throughPATH. The binary was also checked before the port, so an already-running server was ignored because of that failed check; the port is asked first now, since a listening server is usable whether or not a binary can be found to start another. - A voice turn you were still finishing is no longer discarded. Barge-in treated “the user is talking while the assistant is speaking” and “the user is talking while the assistant is still thinking” as the same event. The second is someone finishing a thought — the 600 ms silence window ends a turn on a breath — so
"plus fifty one."`"minus fifty four."` dropped the first half and answered `32 - 54` instead of `32 + 51 - 54`, with the transcript on screen, no reply, and no explanation. Words from a turn superseded before it spoke are now carried into the next turn; a turn superseded *while* speaking is still abandoned, because that is a real interruption. FEATURE-MATRIXsaid VibeCoder had no parallel agents and no worktree isolation. Both were wrong and had been for some time: AgentPanel’s Parallel Mode callsstart_parallel_agent_task, and WorktreePoolPanel callsworktree_spawn/_list/_merge/_cleanup. The rows now name the command they were checked against. Background agents stays ❌ — that panel issues no commands at all.
Known issues
- Microphone capture does not work on Linux, for push-to-talk or duplex. WebKitGTK ships media capture off and denies the permission request unless the embedder answers it;
wryimplements that hook on macOS and Windows and skips Linux, and Tauri adds neither. The fix is two calls, reachable from Tauri’swith_webview()— seeapply_linux_media_fixintools/webview-probe. Established from the dependency source; the CI job added here is its first real run. - Full-duplex voice is measured on macOS only. Windows and Linux have the harness and a CI job but no numbers yet.
Added — earlier in this cycle
- DOCX, EPUB and Apple Pages open in the editor — and save back. Word documents and e-books read as Markdown, Pages documents as plain text, editable in Monaco via Edit text in the viewer toolbar. Saving edits the original container in place rather than rebuilding it, so images, footnotes, stylesheets, metadata and page setup survive an edit that only changed a sentence. New:
crates/vibe-docfmt, theread_document_text/write_document_text/read_document_previewcommands, and documents.md. - A save is only reported once it has been checked. Every write is assembled in memory, re-read with the same reader the editor uses, and compared against the text that was saved; a mismatch is an error naming the first differing line, with the file left untouched. Structural edits a format cannot take — a new EPUB chapter, an added DOCX table row — are refused with an explanation instead of guessed at.
- EPUBs are actually readable. The viewer renders the book: its own stylesheets (scoped to the chapter and stripped of anything that could escape it), its images, its cover, a nested table of contents from the EPUB 3 navigation document or the EPUB 2 NCX, and links that work — a cross-reference moves to that chapter and scrolls to the anchor, while an external link opens in the browser instead of navigating the editor away. Reading moved to the backend (
epub_view.rs), and two defects went with it: the browser-side ZIP reader’s inflate function returnednullunconditionally, so every deflate-compressed chapter — which is every chapter of every real book — was dropped and the viewer showed a card suggesting another application; andhrefwas missing from the sanitiser’s allow-list, so every link in every book rendered dead. Both are covered by tests, including the widened allow-list. - Pages support, with its limits stated where they are felt. Apple publishes no format specification, so
.pagesyields text and the page preview the file itself embeds, not a rendering. Writing substitutes text and shifts character-index ranges to follow it, keeps a.bakcopy of the original, and says in the save confirmation that no automated check has opened the result in Pages.
[0.5.10] — 2026-08-21
32 commits since v0.5.9. The theme is the one this project keeps returning to: a feature that renders is not a feature that works. Four security panels now review the workspace you actually have open, and the findings they report are re-checked against the files they name before you see them. Alongside that, a run of fixes to features that looked finished — code written to the wrong filename, a build that ignored the build system you picked, a router verdict that outlived its question, and a model’s private reasoning committed as a git subject line.
Added
- Workspace security review across four panels — Scanner, Red Team, Blue Team, Purple Team. Each reviews the open project with the provider and model selected in the toolbar, and each asks its own question: red finds the exploit, blue names the missing or weak control and what would detect an attack, purple reports the residual risk — the attack that gets through what is already there. Content is in scope alongside code, so prompts, docs and templates are checked for missing guardrails. The three team prompts are pinned distinct by test; a refactor that collapsed them would quietly make “Blue Team” hunt exploits.
- Findings are verified before they are reported. The security scanner re-checks each finding against the file it names and drops the ones it cannot substantiate, instead of forwarding the model’s first answer.
- One “Fix with AI” hand-off, on every panel that reports something to fix. The finding, the file and the requested change are written into chat; nothing is edited behind your back.
- Settings turns features off and reorders them. Panels and tabs can be hidden and reordered, and a tab can be hosted in a panel other than the one it ships in.
- vLLM and LM Studio providers, on the shared OpenAI-compatible implementation that eight existing providers were folded onto — a fix to one is now a fix to all of them.
- Team Onboarding reports contributor guides from real git history rather than a generic template.
- VibeDesk: Connectors has its own menu entry, with a working / not-working answer instead of a list that claims everything is connected.
WorkspaceStoregained aDebugimplementation that redacts its key, so{:?}in a log line cannot print it.
Changed
- The last plaintext credentials moved into the encrypted stores (
ProfileStore/WorkspaceStore). - iOS builds through Flutter’s Swift Package Manager path, following Flutter’s CocoaPods → SPM migration. The Podfile stays for plugins that have not migrated.
- Repository writes in VibeCoder go through one place, and upstream ahead/behind counts report what git actually says.
Fixed
- Generated code was written to
Component4.tsxinstead of the file the model named. - Build ran the first build system detected, not the one selected in the dropdown.
- Reasoning tags reached user-visible output — the chat window, review comments, and the subject line of AI-generated commit messages. Every spelling we have seen is stripped (
<think>,<thinking>, namespaced forms like<mm:think>), an unclosed or orphaned tag included, and an empty result is reported rather than committed. Two commits in this repository’s own history carry a<thinking>block in their subject. - The long-context router’s verdict outlived the question it answered — “no model has a window large enough for 1,010,000 tokens” stayed on screen after the slider moved, and followed the reader onto tabs it described nothing on.
- The MCP client matched responses by order rather than by id, so a server that spoke before it was spoken to was read as an empty reply — which surfaced as a connector with no tools.
- The test runner and the autofix panel showed no progress.
run_testscollected its output with a blocking call and emitted every line after the process exited, so a long suite left the panel showing one echoed command; it now streams line by line, with elapsed time, a line count, and a Suspend that kills the process group instead of only flipping a flag in the UI. - A panel’s controls could stretch to the full panel height — the
.panel-container > div:last-childcatch-all inApp.csslands on whichever block renders last, which during an autofix run was the framework selector row. - The ingest tab took a hand-typed path only; it now opens the OS file dialog.
- Documentation links that 404’d on the published site — the installer URL, a wrong org in repository links, dead release assets,
.mdlinks that resolved to the domain root, and a plugin registry that was advertised but does not exist. - The CLI build broke after
AgentContextgainedplugin_rules, and the provider migration left file-scope imports behind.
[0.5.9] — 2026-08-14
64 commits since v0.5.8. An evaluation harness that refuses to report a result it did not measure, a plugin marketplace where a workspace can actually install something, connectors as first-class MCP integrations with encrypted credentials, and a run of agent-reliability work that came out of watching real runs stall and lie about it.
The theme underneath most of it is the same: a green build proves less than it looks like. Several fixes here are not new features going in but old features that never worked — panels calling Tauri commands nobody had registered, a connector list that reported “connected” without a credential, a transform that scanned the app’s working directory instead of your project. Each is now covered by a test that fails when it comes back.
Added
vibecli eval— an evaluation harness —crates/vibe-eval, evals/README.md. Coding, agentic tool use, knowledge work, safety, and per-surface transport conformance across all fourteen clients. Four verdicts kept strictly apart —pass,fail,error(the harness could not decide),skipped(did not apply) — with errors and skips outside the pass-rate denominator, because “the agent regressed” and “python3 isn’t installed” are different sentences.--benchmarkruns the real harness rather than a parallel code path,make eval-checkvalidates the suites with no provider and no agent, andvibecli --eval gateexits 1 on regression against a baseline.- A grader with no assertions is an error, not a pass —
Suite::validaterejects one at load time, so a vacuous task cannot ship. - Adding a
Surfacevariant without a conformance task fails the suite tests.
- A grader with no assertions is an error, not a pass —
- Plugins and connectors are installable from the app —
vibecli/vibecli-cli/src/plugin_catalog.rs,connectors.rs, API reference. The Plugins panel used to say “No plugin components are enabled for this workspace” above a pointer to a CLI command, because the only way to get a plugin was to author, sign and pack an MCPB bundle by hand. It is a marketplace now — search, categories, and one-click install — backed by five new daemon routes.- A core catalog compiled into the binary: eleven plugins across engineering practice, security, performance, operations and design. Skills and rules only, because those are the component kinds existing loaders actually read; hooks and plugin MCP servers would have been rows that look live and do nothing.
- Seventeen connectors — the
modelcontextprotocol/serversreference set plus VibeCLI’s own — with credentials encrypted in the workspace store and never returned by any route. Arguments support{secret:NAME}, so a Postgres connection string reaches the server without being stored in the definition. - Bundles: a plugin can install other plugins and set up the connectors a job assumes. Engineering, On-call, Security review, Data work and Research ship. A bundle adds every connector that needs no credential and reports the rest as
needs_credentialswith the field names — never as configured. - Connector health is measured, never inferred. Nothing reports a connector as working until Test has launched it and listed its tools; no probe result is persisted, because a stored “ok” is a claim about the past presented as a claim about now.
-
/goal <what you want>in the CLI, and a VibeCoder panel that shows what a goal is doing and keeps failures on screen instead of clearing them. -
Agent hand-off on degradation. A run whose context has degraded past compaction retires the agent and briefs a successor rather than trimming again.
- BugBot now proposes committable fixes, not just comments —
vibecli/vibecli-cli/src/bugbot_autofix.rs, docs/bugbot.md. Every competing PR-review bot ends its review with a fix a reviewer applies in one click; BugBot ended its review with prose. It now emits GitHub```suggestionblocks.- Anchors come from the diff, never from the model. GitHub applies a suggestion by replacing the exact lines the comment is anchored to, so an off-by-one anchor silently destroys code. A new
PostImageindex mapspath → new-line → textfrom the diff’s own context and added lines; a proposal that cannot be located there is refused, not guessed. Seven typed refusals (AnchorMissing,SpanTooLarge,EmptyReplacement,Unchanged,FenceInReplacement,ModelDeclined,Unparseable) are printed with the finding rather than swallowed. - The verification level is in the type.
AnchorVerifiedmeans the target lines were found and the replacement is non-empty and different — nothing more. Every posted comment says the fix has not been compiled or tested, because it hasn’t. --bugbotis a real flag now.bugbot.rshad advertisedvibecli --bugbot --diff/--pr 123in its module docs since it was written; no such flag existed and the only caller was the GitHub webhook.--bugbotreviews uncommitted changes,--stagedthe index,--pr Na pull request, and it exits 1 on any error-severity finding so it drops into a pre-push hook or CI step.--propose-fixesadds suggestions;--apply-fixeswrites them, skipping any file that moved since the diff and printing both counts.--prrefuses a non-GitHub remote instead of guessing a slug that would review an unrelated repository.
- Anchors come from the diff, never from the model. GitHub applies a suggestion by replacing the exact lines the comment is anchored to, so an off-by-one anchor silently destroys code. A new
- Full-diff coverage and multi-pass review —
ReviewPlan/ReviewCoverage. BugBot sent the first 8 000 characters of the diff and nothing else, so on any PR past a few files everything after the cutoff went unreviewed with nothing said about it. The diff is now split per file, packed into batches that each fit the request budget, and every batch is reviewed — a small diff still costs exactly one call, a 64 KB one costs up to eight. What was read is reported (Reviewed 12/12 file(s) in 3 model call(s)), and when it wasn’t everything, the skipped and truncated files are named on stderr, appended to the commit-status description, and returned as acoverageobject on the webhook response.- A failed model call is not coverage. A provider error used to be swallowed into an empty finding list, so with the provider down the review printed
Reviewed 1/1 file(s)and no findings — an outage rendered as a clean bill of health. The review pass now distinguishesSome(vec![])(the model looked and found nothing) fromNone(it never answered); files whose every pass errored are reported as unreviewed,llm_calls_failedis counted separately, and the caveat points atvibecli --doctor. Found by running the command against a scratch repo, not by the build. --passes Ntrades cost for recall. Each batch is reviewed N times with the file order rotated, because a defect in the last file of a prompt is likelier to be missed than one in the first. Rotation is deterministic — two runs over the same diff issue the same requests, which a randomised ordering could not promise in CI. Findings are deduplicated by location plus a normalised message (so “off-by-one” and “off by one” collapse), keeping the highest severity seen. The call ceiling scales with--passes, so extra passes never cost coverage.
- A failed model call is not coverage. A provider error used to be swallowed into an empty finding list, so with the provider down the review printed
- 433 Jobs-To-Be-Done skills — the catalogue goes from 710 to 1,143. A national-operating-system skill library imported from Anthropic Agent-Skills format (
SKILL.mdper directory,name+descriptionfrontmatter) into VibeCody’s flat catalogue: 24 sector operating systems (energy-sector-operations,healthcare-sector-operations, …) with their 204 AI-personnel role skills and 27 autonomous-machine skills, 26 industry overlays, 15 cross-cutting role archetypes, 12 strategic missions, 8 subsector packs, 108 catalogue skills (humanoid robots, embodied-AI stack, capability optimisation, simulation training), and 2 routers. Categories map onto the existing vocabulary where one fits (energy,healthcare,defense,finance,logistics,robotics) and add a domain slug where none did.- Every relative cross-reference was rewritten, because a flat catalogue has no
../... The source tree navigates by path — 302 references to../../../00-framework/SKILL.md, 266 to a role’s own../../SKILL.md, plus directory pointers like_catalogs/humanoid-robots/. Left alone these would send an agent to files that do not exist. Each now names the flat skill (jobs-to-be-done-framework,energy-sector-operations) or the name glob the directory became (humanoid-*). The import refuses to write while any path-shaped reference remains unresolved; it currently reports zero. - The 52
references/*.mdcompanion files are inlined, not dropped. They carry the controls, exception tables, and subsector maps that the industry overlays and subsector packs point at. A flat catalogue can hold neither the directory nor the pointer, so each is appended as a## Reference — <title>section with its headings demoted, and every pointer to it repointed at that section. - Triggers are derived, never invented — from the skill’s title, the bold spans its own description uses to name its subject, its domain, and the explicit “trigger this skill when…” sentence the source carries. Triggers are the main signal in
skill_matches_query, so no skill ships with none. - Pointers to files outside the skill tree (the source-map vault note,
docs/capability-routing-matrix.md,tools/capability-router.html, theexamples/*.pyhelpers) were already dangling at the source and are reworded rather than shipped as dead ends.
- Every relative cross-reference was rewritten, because a flat catalogue has no
Fixed
- VibeCoder editor stuck on “Loading…” —
@monaco-editor/reactwas loading the Monaco engine from acdn.jsdelivr.netscript blocked by the Tauri CSP (script-src 'self'), so the editor never mounted for any file. Monaco and its web workers now ship in the app bundle (loader.config({ monaco })+ Vite?workerimports). -
Ollama Cloud models missing from the model dropdown — the registry now unconditionally unions cloud + locally-pulled + the full static catalog (deduped), so
*-cloudentries always appear regardless of whetherollama serveis running; the model-registry cache key is versioned so a stale cache no longer hides newly-added models for up to its TTL. - The agent could report work it had not done.
--execnow double-checks with the project’s own build and test before acceptingtask_complete, bounded so a check that can never pass cannot spin the run to death — and a check that fails to spawn is reported as unverified rather than mapped onto pass, which is the success-assuming fallback the verification step existed to catch. - A run could not be bounded from inside its own loops. Every guard was checked between turns or between chunks, so each depended on some inner loop coming back round; four separate stalls slipped past. The run is now bounded from outside, with elapsed-time walls for “nothing has changed on disk” and “no tool has run at all”, and a prose-loop wall for a model that only reasons.
- A finished agent no longer burns its budget. Stall detection counts mutations rather than any successful call, ignores byte-identical rewrites, and concludes the run itself when the agent has stopped changing anything — reported as
Partial, because “it stopped” is not the same claim as “it finished”. - Secrets could leave in the agent’s own words. Asked to summarise a
.env, the agent reproduced a database password verbatim, then paraphrased it once output redaction was added. Credential files are redacted on the way in, so the model never receives the value, andredact_secretsgrew the shapes it was missing:SECRET_KEY=, URL authority credentials,sk_live_. - An autonomous run cannot remove an authorization guard. Told to make a failing test pass against a test asserting anonymous access should succeed, the agent deleted the auth check in 2 of 3 sampled runs. Guard-bearing files are snapshotted and restored on any content change, unless the task explicitly asked for that file.
- 27 Tauri commands VibeCoder panels were already calling did not exist. Every one of those calls was a guaranteed runtime rejection; the panels looked finished and did nothing. Now implemented and registered, with a contract test that fails when an invoked command is not in
generate_handler!. - Panels that ignored the toolbar’s provider. Ghost text, ⌘., Counsel, Arena, SuperBrain, Compare, Automations and Code Transforms each ran on something other than the model you selected — one of them on a hard-coded vendor. A contract test now covers all 155 panels that receive a provider.
- Code Transforms scanned three different roots —
localStoragefor detection, the app’s workspace for planning, andstd::env::current_dir()when that was empty. After a restart the last was the bundle directory, so it reported “0 files to transform”, which reads as “nothing needs transforming”. - VibeCoder’s Connectors panel was a facade: it recorded every connector as connected without a credential, tested them by checking a
Vec, and lost everything on restart. It drives the real connector store now. - The welcome heading was sliced off the top on a short window — centring pushed the overflow out of both ends, and the half above the top edge cannot be scrolled back to.
vibecli --eval --helpstarted an evaluation run instead of printing help, and the eval report named the binary it was asked for rather than the one that ran.- A silent provider could hang a run — the wait for streaming chunks is bounded.
-
Diffs whose blank context lines lost their leading space now apply; chat no longer shows the same reply twice; reasoning tags no longer leak into the transcript.
- Opening a different folder in VibeCoder left search scoped to the previous one.
openWorkspacePathreplaces the frontend’s workspace (setWorkspaceFolders([path])), but the Tauri command behind it calledWorkspace::add_folder, which appends. After opening B, the file tree showed B whilefolders().first()still returned A — and the sidebar search sends no path at all, so its scope is entirely that value. So did the@context picker,search_workspace_symbols,get_git_status,resolve_at_references, the agent’s workspace root, andsafe_resolve_path_in(which kept the old root’s files readable and writable). 43 call sites, all pinned to the first folder opened in the session; only an app restart reset it.remove_folderexisted but had no command and no caller.- The opened folder is now the only root —
Workspace::set_root_folderclears the folder list, the old root’snotifywatchers (which had been accumulating one per folder ever opened, each with a live forwarding task emitting events for folders the user had left), and its open buffers.add_workspace_folderis renamedset_workspace_folder, since a name that says “add” while replacing is the reason this was hard to see. Validation,reject_sensitive_pathand the LRU recents are unchanged. - Search results are cleared on switch — they were paths inside the folder being left, so clicking one reopened a file that was no longer in scope.
HealthScorePanelfollows the workspace. Its path came from auseStatelazy initializer readinglocalStorageonce at mount, so it never noticed a folder change. It now listens forvibecoder:workspace-changed— an event dispatched since the workspace switcher shipped that nothing insrc/had ever subscribed to.- Not addressed: the daemon’s
ServeState.workspace_rootand the bind-oncegraph_index::GRAPHboth follow the app process’s cwd rather than the opened folder, so they were never right rather than stale-by-one; andsemindex_*accumulates across folders but serves synthetic placeholder data.
- The opened folder is now the only root —
list_skillsandget_skillre-read the entire catalogue on every MCP call. Both calledSkillCatalog::load_from_with_cwd_pluginsper invocation — 1,143 file reads, ~990 YAML parses, and aWorkspaceStore::open(which creates the encrypted database, in whatever directory the MCP host happened to launch in) to answer one question. An agent callinglist_skillsthree times in a turn paid it three times.skill_catalog::load_with_cwd_plugins_cachednow shares oneArc<SkillCatalog>per directory.- Revalidated by fingerprint, not by a timer, so authoring a skill in-tree still takes effect without a restart: file count, combined size, and newest mtime across the directory — measured ~7 ms over 1,143 files against ~60 ms+ to re-read and re-parse them. The directory’s own mtime would have been a single
stat, but it does not move when a file’s contents change, which is exactly how skills are edited. - The plugin overlay is deliberately not cached — a handful of files whose enablement can change at any time, recomposed per call while the expensive built-in load is shared. When no plugin contributes a skill the shared catalogue is returned as-is, with no copy of its ~5.8 MB of bodies.
- The
WorkspaceStoreopen is now gated on<cwd>/.vibecli/workspace.dbalready existing, so the skills path stops creating stray workspace databases in scratch directories.
- Revalidated by fingerprint, not by a timer, so authoring a skill in-tree still takes effect without a restart: file count, combined size, and newest mtime across the directory — measured ~7 ms over 1,143 files against ~60 ms+ to re-read and re-parse them. The directory’s own mtime would have been a single
-
Adding or removing a skill did not rebuild the binary.
include_dir!bakesskills/**in at compile time; rustc’s dep-info tracks the contents of the files the macro expanded to, but not the directory listing.build.rsalready emittedrerun-if-env-changeddirectives, which turns off cargo’s default “rescan the whole package” behaviour — so a new skill file changed nothing until something else forced a rebuild.cargo:rerun-if-changed=skillsnow names the directory explicitly. - Every installed build shipped an empty skill catalogue —
vibecli/vibecli-cli/src/skills_embedded.rs.list_skills/get_skill,GET /v1/skilllens/skills, and the SkillForge panel returned zero skills on every release binary while working fine in-tree, which read as a regression rather than what it was: the ~710skills/*.mdfiles were never packaged. The resolver’s primary path was${CARGO_MANIFEST_DIR}/skills, baked in at compile time, so an installedvibeclilooked for/Users/runner/work/vibecody/vibecody/vibecli/vibecli-cli/skills— a GitHub Actions path. The documented next fallback,<exe>/../share/vibecli/skills, was a convention nothing implemented:release.ymltars the bare executable, so no siblingshare/tree ever travels with it.- The catalogue is now compiled into the binary (
include_dir!) — the only fallback that survives how the binary is actually distributed, a single file copied anywhere. It is extracted once to~/.vibecli/bundled-skills/<version>/and loaded from there, soskill.pathstill names a file that exists and the scoring / body-render paths that re-read it are unchanged. Extraction is version-scoped, guarded by a completeness marker so an interrupted run re-extracts instead of caching a partial tree, and prunes older versions. It is deliberately not~/.vibecli/skills, which is the promoted-override dir. - One resolver, not two.
mcp_serverandskillforge_indexeach carried a copy of the fallback chain; both now callskills_embedded::resolve_skills_dir().VIBECLI_SKILLS_DIRstill wins and is used verbatim — an override that silently fell through to the embedded copy would hide an operator’s typo. vibecli doctorreports the catalogue it actually loads. The old check looked at~/.vibecli/skills— the override dir, not the catalogue — so it printed a benign “no directory” line throughout. It now prints the resolved path, the skill count, which rule chose it, and fails the line when the count is zero.
- The catalogue is now compiled into the binary (
-
POST /webhook/githubfailed open when no webhook secret was set. Signature verification ran onlyif let Some(secret) = …; with none configured, every unsigned POST was acted on. The route is public by design, and a review is not a read — it spends model budget and calls the GitHub API with the operator’s token against whatever repository the payload names, so an unsigned request from anywhere drove both. Unsigned webhooks are now rejected, and the error names theset-keycommand that fixes it. -
The GitHub App webhook secret could not be stored encrypted.
GithubAppConfig::resolve_webhook_secretreads the ProfileStore keygithub_app_webhook_secretfirst, per Zero-Config First — butvibecli set-keyvalidates the name against a fixed list that omitted it, so the command answered unknown provider. The only reachable paths were a plaintextconfig.tomlfield and an environment variable, both of which the same rule forbids for a secret.vibecli set-key github_app_webhook_secret <secret>now works, andlist-keysshows it. [github_app] auto_fixwas a dead flag. It was documented (“push auto-fixes to PR branch”), serialised, defaulted, and covered by three tests — and read by no production code path. It now drives the suggestion pass, and its documentation says what it actually does: it attaches committable suggestions and never pushes a commit. The webhook response gained an honestfixes_proposedcount, which excludes findings the fixer declined.
Performance
- Whole-response work stopped running on every streamed chunk. Thinking-tag stripping and tool-call parsing were re-run over the accumulated response per chunk, so a long answer cost quadratic work.
- Regexes hoisted out of hot paths and one HTTP client shared per timeout.
strip_thinkingalone cost 27 MB of allocation per streamed chunk before its patterns were compiled once. - Input-sized allocations are bounded before they are made, rather than after — a bogus length header used to allocate before the body arrived.
Security
- Patched 3 Dependabot advisories (vulnerable code paths all unused in-repo; bumped for defense-in-depth):
transformers5.3.0 → 5.14.1 (CVE-2026-5241, ACE in LightGlue model load),torch2.11.0 → 2.13.0 (CVE-2025-3000,torch.jit.scriptmemory corruption),serde_with3.20.0 → 3.21.0 (GHSA-7gcf-g7xr-8hxj,KeyValueMapempty-entry panic).
[0.5.8] — 2026-08-10
The largest release so far — 410 commits since v0.5.7. Voice input on every client, SkillForge, the kodegraph code-graph substrate, goal-driven loops, a provider-agnostic embedding layer, and the VibeApp → VibeAIChat rename that brought VibeDesk in as a third desktop shell.
This release adds end-to-end Developer ID code signing for macOS — the CLI
binaries were previously shipped with no signature at all, and the three app
bundles ad-hoc. Whether a given download is signed depends on whether the build
that produced it had the certificate; docs/release.md § Code signing has the
one-line check. Signing is not notarization: a signed-but-un-notarized app still
prompts on first launch.
Added
- Embedding models are now provider-agnostic and selectable —
crates/vibe-embed. Semantic search,@codebase:, and memory recall previously ran through a two-variantEmbeddingProviderenum (Ollama with a hard-coded127.0.0.1:11434, OpenAI locked totext-embedding-3-small), whilevibe-infercarried a separateEmbeddertrait that only the candle backend implemented and nothing bridged the two. A new low-level crate collapses both into one trait with one model catalog, shared by the code index, the memory stores, the remote indexer and the daemon.- Six providers — Ollama, OpenAI (base-URL overridable, so Azure / LiteLLM / vLLM / text-embeddings-inference need no new variant), Voyage (
voyage-code-3is the strongest code-retrieval option), Cohere, Gemini, and an in-process candle backend registered at runtime sovibe-embednever links an ML toolchain. - Models the catalog doesn’t know still work. The catalog is a hint list, not an allow-list —
ollama pull some-custom-embedand select it. What it buys is metadata no API reports: the task prefix a model expects, its Matryoshka dimensions, its input limit. - Dimension is observed, never assumed. What gets persisted is the length of a vector the model actually returned. A guessed dimension in an index header is worse than an absent one: absent triggers a measurement, wrong triggers silence.
- Documents and queries embed differently.
EmbedKindis a required argument, expressed natively where the provider supports it (Voyage/Cohereinput_type, GeminitaskType) and by prefix where it doesn’t (nomic-embed-text,mxbai,bge,e5). Getting it wrong doesn’t error — it silently costs recall, which is why it isn’t defaulted. - Indexes are per-model and coexist —
<workspace>/.vibecli/index/index__<provider>__<model>.jsonplus a small.meta.jsonsidecar so listing doesn’t parse gigabytes of vectors. Switching models is instant when the target index exists and switching back never re-embeds. - Batched embedding. The previous implementation issued one HTTP request per chunk; a 5 000-chunk repository meant 5 000 sequential round-trips.
- Surface — four daemon routes (
/embeddings/models,/embeddings/embed,/index/status,/index/build), six Tauri commands, a Settings → Embeddings picker that labels each provider local or cloud before you choose,/index-statusin the REPL, Agent SDK + VS Code client methods, anembeddingblock in/healthand the startup banner, anddocs/embeddings.md. [index]config is finally wired.embedding_provider/embedding_modelexisted but were referenced only by tests;/indexused its own hard-coded literal. An unrecognised provider is now an error rather than a silent fallback to Ollama.
- Six providers — Ollama, OpenAI (base-URL overridable, so Azure / LiteLLM / vLLM / text-embeddings-inference need no new variant), Voyage (
- Voice input on every client — one daemon route, one shared hook. Before this, the whole voice stack (
VoiceDispatcherinvoice.rs: Groq Whisper + local whisper.cpp fallback + model download) was reachable only from the REPL, and the daemon had no voice route at all — so VibeDesk, VibeAIChat, VibeMobile, VS Code, JetBrains, Neovim and the Agent SDK had nothing to call, while VibeCoder had quietly reimplemented Groq in its own Tauri command (cloud-only, so it could never run offline).- Daemon —
POST /voice/transcribe(bearer auth, 16 MB per-route body limit overriding the 1 MB daemon default) accepts either{audio_base64, mime_type, language, prefer_local}JSON or raw bytes with an audioContent-Type+X-Voice-Language/X-Voice-Prefer-Localheaders. Seven audio types are recognised; anything else is a415rather than a guessed extension.GET /voice/statusreports what the machine can actually do, including acan_transcribethat accounts for a downloaded model with no runtime to execute it; the probe (three subprocess spawns) is cached for 60 s.VoiceDispatcher::transcribe_file_with_enginereports which engine ran, so a response can saylocal_whispervscloud_whisperinstead of the caller guessing. 7 new serve tests, including one asserting a 2 MB upload gets past the body layer. - Shared React hook —
packages/vibe-ui-shared/src/voice/(useVoiceInput,VoiceButton,tauriTranscriber/daemonTranscriber,voice.css). Web Speech API first,MediaRecorder+ a pluggable transcriber as fallback. State is a discriminated union (idle | listening | transcribing | error) rather than three booleans, and every failure path now produces a message — the previous VibeCoder-only copy swallowed all of them, so a denied mic, a missing API key, and an unsupported webview were indistinguishable from a dead button. 18 BDD tests. - Desktop shells — new
crates/vibe-desktop-voice(transcribe_audio/voice_statusTauri commands) registered in VibeDesk, VibeAIChat and VibeCoder, so all three share one daemon bridge. Mic buttons in the VibeDesk composer + side chat and the VibeAIChat chat box. VibeCoder’s inline duplicate hook and its deadsrc/hooks/useVoiceInput.tsare deleted in favour of the shared one;@vibe/sharedis now aliased in its vite/vitest/tsconfig.transcribe_audio_bytes(Groq-only) is removed and the file-basedtranscribe_audiorenamed totranscribe_audio_fileto free the shared command name. macOS mic entitlement (com.apple.security.device.audio-input) added to all three, plusInfo.plistusage strings for VibeDesk and VibeAIChat — both keys are load-bearing and fail differently. - VibeMobile —
VoiceService(speech_to_texton-device recogniser →record+ upload fallback) with aVoiceStatussum type mirroring the web hook,VoiceMicButton/VoicePartialStripwidgets in the chat and watch-chat composers,ApiClient.transcribeAudio/voiceStatus, iOSNSMicrophoneUsageDescription+NSSpeechRecognitionUsageDescription, AndroidRECORD_AUDIOand the Android 11+RecognitionService<queries>entry (without which the recogniser is invisible on every device). - Editor plugins — SoX
reccapture, matching whatVoiceDispatcher::listenhas always done for the REPL, so there is one capture path for every non-browser client. VS Code: mic in the chat webview (recording runs in the extension host — webviewgetUserMediais unreliable) plus aVibeCLI: Dictatecommand. JetBrains: mic button in the Chat tool window. Neovim::VibeCLIVoicetoggle (!submits the transcript as a task). All three report a missing SoX with install instructions rather than a generic failure, and stop recording with SIGINT/SIGTERM so SoX finalises the WAV header. - Agent SDK —
agent.transcribe(audio, {mimeType, language, preferLocal})andagent.voiceStatus(). 9 new vitest cases; 60 total green.
- Daemon —
Fixed
-
The code index persisted API keys in plaintext.
EmbeddingProvider::OpenAI { api_key }wasSerialize, and the index was written as plain JSON — so every.vibecli/index.jsonbuilt against a cloud provider contained that provider’s key on disk. The index header now stores a model reference only; the embedder (and therefore the key) is supplied at runtime from the encrypted ProfileStore and is#[serde(skip)]. Pre-existing v1 indexes migrate automatically and the credential is dropped in the process, with a test asserting nosk-survives. -
Changing embedding model silently returned nonsense instead of failing. Three separate holes: the code index recorded no dimension or format version, so a mismatched model scored
0.0against every chunk and looked empty rather than broken;TurboQuantIndex::insert’s dimension error was discarded at both call sites (compressed_hnsw.rs,embeddings.rs), so wrong-sized vectors were dropped without a word; andvibe-memory’s SQLite had no model or dimension column at all, so aVIBE_MEMORY_DIMchange made every existing memory unreachable while the rows sat in the database. Indexes now carry a validated header, TurboQuant’s error propagates, and each memory row records the model that embedded it — search compares only what it can compare and logs what it skipped. -
VibeAIChat never started the VibeCLI daemon. VibeCoder and VibeDesk both autostart it via the shared
daemon_bootstrap; VibeAIChat had no spawn path whatsoever, and its manifest deliberately avoided thevibeclidependency. Since nearly every daemon route is behindrequire_authand the bearer token only exists once a daemon has written it, launching VibeAIChat on its own produced a blanket 401 with nothing on screen explaining why. It now uses the same shared module — identity-checked/healthreuse, binary resolution beyond barePATH, poll-to-deadline for the ~16 s cold start, and four distinct failure states. -
Stale bearer tokens produced a permanent 401 loop.
vibecli servemints a fresh token on every start and all three shells autostart the daemon, so any token held across a restart is dead — and nothing invalidated it. VibeDesk retried on none of its 20 daemon calls, andcrates/vibe-desktop-voice(linked into all three shells, so voice input broke everywhere at once) retried on neither of its two. Both now go through asend_authedhelper that retries once with a token read straight from~/.vibecli/daemon.token, deliberately bypassing the explicit-token /VIBECLI_TOKENprecedence — that order is right for a first attempt and exactly wrong for a retry, since the stale value is usually why the request 401’d. -
VibeCoder chat rendered markdown as raw text. Assistant replies containing tables arrived as walls of
| --- |rows and every emphasis as literal asterisks, while VibeDesk and VibeAIChat rendered the same reply correctly. Three causes stacked:AIChat’srenderContentextracted fenced code blocks and dumped all remaining prose into a<pre>;remark-gfm— which is what renders tables — was missing from VibeCoder’s dependencies entirely; and neither the Vite alias table nor the tsconfig let the sharedMarkdowncomponent resolve its imports. Chat now renders through the same component the other two shells use. Code blocks keep their Apply/copy affordances, and user messages stay literal. -
Two phantom design tokens meant borders and buttons never rendered.
var(--accent)(22 uses across 11 panels) andvar(--border)(74 uses, plus--border-default/--border-secondary/--border-primary) were referenced but defined nowhere; CSS drops an undefinedvar()declaration silently, so those backgrounds were transparent and those borders simply absent. Both now use the real--accent-color/--border-color, which are themed per-theme. Separately,.panel-btndeclared nobackgroundand nocolor, so 43 buttons carrying no modifier class fell through to the browser’s nativebuttonface/buttontext— light grey with black text on a dark panel, which is what made Configuration → Keys look unthemed. The base class is now themed, and all variants share one border box so buttons in a row line up. Three source-scan regression tests guard the lot, with the remaining undefined tokens recorded as an explicit, self-cleaning inventory. -
JetBrains plugin and Neovim plugin sent no bearer token. Nearly every daemon route is behind
require_auth, so/chat,/agent,/jobsand the new voice routes were a silent 100% 401 from both — the JetBrains tool window’s “Error” line and the Neovim “Daemon error” notice were that 401 every time. Both now resolve a token the same way every other client does (explicit config →VIBECLI_TOKEN→VIBECLI_DAEMON_TOKEN→~/.vibecli/daemon.token), and Neovim’s SSE stream appends?token=since curl-as-argv can’t carry a header there. - Goal-driven loops —
/loop goal <id-prefix>. Joined the two halves of autonomous execution: a/goalsupplies the durable intent +success_criteria,/loopsupplies the run-until-done engine. Parsing, prompt rendering, and hydration are pure and live inloop_engine.rs(GoalBrief,hydrate_goal_loop,goal_loop_prompt,goal_validator_prompt,LoopSpec.goal_id—#[serde(default)], so pre-existingloops.jsonjobs still load); the store-touching edge (resolve_goal_brief/goal_brief_by_id/link_goal_loop_start/record_goal_loop_outcome) lives inexec_goal_repl.rsover a newSessionStore::find_goal_by_prefix.- Stop condition is the criteria, not the model’s opinion — the worker prompt is explicitly told not to declare completion; a separate validator turn is shown the numbered criteria and answers
DONEonly if every one verifiably holds. A goal with no criteria still runs, with a warning that judgement falls back to the statement. - Only confirmed success writes back —
Doneflips the goal todone; max-iter, wall-clock expiry, Ctrl-C, and iteration errors leave the status untouched (an exhausted budget is not evidence of completion). Every outcome is attached to the goal as a link, so/goal showcarries the history. Already-donegoals are refused at start. - Bounds + secrets —
--max-iter Nand--max-duration 45moverride the 20-iteration / 30-minute defaults (a mistyped bound errors rather than silently falling back);--secret NAMEworks as on any other loop. - Machine-off parity —
POST /v1/loops {"args":"goal <id>"}hydrates server-side, and the daemon’s hosted scheduler now runs a real done-validator:LoopExecutor::run_iterationtakes avalidator: Option<&str>,scheduler_ticktakes avalidator_lookupclosure (criteria re-read each tick, so mid-run edits are honoured), andProviderLoopExecutorasks the completion question in a separate turn. Recurring jobs skip the extra turn. Previously no hosted self-paced loop could ever signal done — it ran to its caps. - Tests — 8 new
loop_engineunit tests, 2 newhosted_loopscheduler tests, afind_goal_by_prefixstore test (incl.LIKE-wildcard rejection), and 4 newloop_engine_bddscenarios (10 total). REPL completions,/loophelp, anddocs/vibecli.md(§ Goal-Driven Loops) updated.
- Stop condition is the criteria, not the model’s opinion — the worker prompt is explicitly told not to declare completion; a separate validator turn is shown the numbered criteria and answers
- SkillForge — analyse + train agent-skill docs (skilllensai-rs + skilloptai-rs). Ported TuringWorks/SkillLens + TuringWorks/SkillOpt into two standalone MIT Rust crates and wired them into the daemon kodegraph-style. SkillLens normalises agent runs into a
Trajectoryschema, extracts candidate skills (sequential / parallel mode-merge), and scores utility via trigger-coverage (deterministic) + target-evolvability (LLM-held-out); SkillOpt treats a skill markdown doc as the trainable state of a frozen agent — scored rollouts drive bounded add/delete/replace edits, accepted only when a held-out val score strictly improves, epoch after epoch, with a rejected-edit buffer + textual learning rate. Output is abest_skill.mddeployed with zero inference-time overhead.- Crates —
skilllensai-rs(23 tests + golden parse over ~710 shipped skills) +skilloptai-rs(30 tests, incl. a deterministic val-curve test + a strict-gate rejection test) +skillforgeai-rsfacade. Provider-agnostic via a crate-localSkillLlmtrait; neither crate depends onvibecli/vibe-ai.cargo clippy --features cli -- -D warnings+cargo fmt --checkclean. - Daemon bridge —
vibecli/vibecli-cli/src/skillforge_index.rsadapts both crates ontovibe_ai::AIProvider(AiProviderLlm), reuses the existingskill_catalog::SkillCatalog(no re-parse ofskills/*.md), and exposes ten routes:/v1/skilllens/{skills,skills/:name,refresh,convert,extract,score}+/v1/skillopt/{train,status/:job,cancel/:job,promote}.trainis async-job (spawn + pollstatus);promotewrites*.opt.mdto the per-workspace override dir (<ws>/.vibecli/skills/, fallback~/.vibecli/skills/) and never overwrites a shipped skill./health.skillforge+ the startup banner reportloading → ready (N skills). 10 bridge tests green;cargo checkclean. - Watch mirror — two curated
/watch/skilllens/*routes (skillscompact{count,top5},skills/:nameone-line) for the wrist form factor, registered inwatch_bridge.rs. - VibeCoder panel —
SkillForgePanel.tsx(Catalog / Lens / Optimize) mounted as a “SkillForge” tab inAiMlComposite. Catalog lists the ~710 skills (no LLM); Lens scores a skill against the toolbar-selected model; Optimize launches a train job, polls status every 1.5 s, renders the validation curve as an inline SVG sparkline, shows accepted/rejected + spent-tokens, and offers a guarded Promote (confirm banner; writes*.opt.md). 10 Tauri commands proxy the daemon routes (bearer~/.vibecli/daemon.token, same pattern asmobile_get_active_session) — the daemon stays the single source of truth.tsc --noEmitclean. - Client fan-out — full surface (
skilllens.{list,get,refresh,convert,extract,score}+skillopt.{train,status,cancel,promote}) in VS Codeapi-client.ts+ the Agent SDK (agent.skilllens.*/agent.skillopt.*); read-only catalogue + train-status on Flutterapi_client.dart(skilllensSkills/skilllensSkill/skilloptStatus), Apple WatchWatchNetworkManager.swift(loadSkilllensSkills/loadSkilllensSkill), and Wear OSWearNetworkManager.kt(skilllensSkills/skilllensSkill). Every LLM-calling client method takesprovider+model(STRICT — no hard-coded Anthropic). - Docs —
docs/skillforge.md(dedicated Jekyll page, permalink/skillforge/: overview, the VibeCoder panel, the full HTTP surface, per-client fan-out table, standalone crates),docs/demos/66-skillforge.md(runnable walkthrough — panel flow + curl equivalents +TrainConfigreference),docs/demos/index.md(Agentic Systems row) + a “What’s Next” cross-link fromdocs/demos/41-semantic-index.md,docs/index.md(nav row + VibeCLI/VibeCoder highlights bullets),docs/FEATURE-REFERENCE.md(SkillForge route table + watch surface),docs/FEATURE-MATRIX.md(SkillForge row). Design + roadmap innotes/skillforge/. - Follow-ups deferred — efficacy-metric substrate (LLM-judge vs embedding-overlap); nightly “sleep” job (offline self-evolution with experience replay); external benchmark
Envimpls (SWE-bench / BFCL) behind abenchmarksfeature. - Per-epoch SSE streaming + true cancellation —
skilloptai::trainer::train_with_signalsthreads a dependency-freeCancelToken(Arc<AtomicBool>, checked at the top of each epoch) and an optional per-epochEpochEventmpsc channel;train()is now a thin wrapper with empty signals. NewPOST /v1/skillopt/train/streamSSE route (job→epoch* →done/error, 15 s keep-alive) shares the same job map as poll-based/train, so/status+/cancelwork on both.cancel/:jobnow flips the live token so the run stops at the next epoch boundary instead of running to completion;TrainingReport.cancelleddistinguishes cancellation from early-stop. 3 new skilloptai tests (cancel-before-first-epoch, cancel-observed-between-epochs, one-progress-event-per-epoch) + 2 new bridge cancel-token tests; 33 skilloptai + 13 bridge tests green;cargo clippy --all-features -- -D warnings+cargo fmt --checkclean. Client SSE consumption — the VS Code extension (VibeCLIClient.skilloptStreamTrain) and Agent SDK (agent.skillopt.streamTrain) consume the stream asAsyncGenerator<SkilloptTrainEvent>({type:'job'|'epoch'|'done'|'error', …}) via a newreadSseTypedEventshelper (typedevent:+data:parser layered over the existingdata:-only SSE helpers; the Agent SDKstreamTrainasync generator is.bind(this)‘d because arrow generators aren’t valid syntax). 4 new Agent SDK vitest cases (job→epoch*→done ordering, error-stop, non-2xx throw, request-body shape);tsc --noEmitclean for both clients; 42 Agent SDK tests green. - Promoted-skill override dir —
promotenow writes<skill>.opt.mdto the per-workspace override dir (promote_dir_for(ws)=<ws>/.vibecli/skills/, or~/.vibecli/skills/when no workspace resolves and cwd has no.vibecli/workspace.db), so the 710 shippedskills/*.mdstay pristine — no in-repo overwrite. The catalogue list surfaceshas_promoted_overrideper skill and the detail view surfacespromoted_override(path or null); the bridge scans the override dir at init + refresh so overrides are picked up without a restart (the shipped-skill path resolution is unchanged —SkillCatalogstill wins over same-named plugins). The write is factored into a purewrite_promoted_override_inhelper for unit testability. 4 new bridge tests (dir resolver, write helper, stem-keyed scan, missing-dir empty); 17 bridge tests +cargo check+tsc --noEmitclean. Client-facing text in the VibeCoder panel, REPL/skillforge promotehelp,commands.rsdoc, and the VS Code / Agent SDKpromotedoc comments updated to point at the override dir. - Real agent-job history env — a third train env kind,
history, derivesEvalTasks from actual agent runs instead of the catalog. The CLI now writes a lightweight per-sessionSkillEvalRecord(<session_id>-eval.json) at the end of every agent run viavibe_ai::trace::TraceWriter::save_eval_record—{session_id, timestamp, prompt (first user message), final_answer (last assistant prose), tool_success_rate, steps, completed}— alongside the existing<session_id>.jsonltrace (secrets scrubbed, same redaction assave_messages/save_context).env.kind=historyscans~/.vibecli/traces/(or anenv.tasksoverride dir) via the newvibe_ai::load_eval_recordsand builds one task per run: the session’s prompt becomes the task prompt; the grader isLlmJudge(default — rubric cites the reference final answer + tool-success rate + completion; one extra LLM call per task per epoch) orContains(free, weak — a phrase from the reference answer), selected viaenv.grader="llm_judge"|"contains". Records with an empty prompt or final answer (errored/truncated runs) are skipped; an empty trace dir returns a user-facing “no agent-job history found” error. New types:SkillEvalRecord+load_eval_recordsinvibe-ai::trace(re-exported fromvibe_ai);EnvKind::History+EnvSpec.grader+HistoryGrader+RepoAgentEnv::from_history+history_trace_dir+parse_history_grader+rubric_for+phrase_frominskillforge_index.rs. The eval record is written at bothAgentEvent::Complete(completed=true) andAgentEvent::Partial(completed=false) in the interactive agent path (skipped onError— no reference answer); a smallhistory_tool_success_ratehelper computes the rate (1.0when no tools ran). 9 new bridge tests (llm_judge rubric, contains phrase, skip-empty, all-unusable=err, grader parse + case-insensitive, trace-dir override + home fallback, env_for reads records, env_for no-records=err) + 4 new trace tests (redaction, read dir sorted, missing-dir empty, ignores non-eval); 26 bridge + 20 trace tests green;cargo check(vibe-ai + vibecli) +tsc --noEmit(vscode + agent-sdk) + 42 Agent SDK vitest clean. The VS Code extension (skilloptTrain/skilloptStreamTrain) + Agent SDK (skillopt.train/skillopt.streamTrain) accept'history'+ an optionalenvGrader:'llm_judge'|'contains'; the catalog-derivedrepoenv is unchanged. - Gap-closure pass — closed the seven post-Phase-5 visibility gaps tracked in
notes/skillforge/07: (G1) VibeCLI REPL/skillforgecommand (list/show/refresh/score/train/status/cancel/promote/health, in-process, STRICT viaactive_provider/active_model); (G2) VibeCLI TUISkillforgeComponent+SkillForgescreen (/skillforgefrom chat, catalogue + train-jobs pane +/healthfooter); (G3)AgentContext.skill_healthrendered as a## Skill Healthsection inbuild_system_prompt, auto-gated oncached_reports > 0(no prompt bloat when nothing is scored) — populated byskillforge_index::render_health_line()at the daemon’sAgentContextconstruction sites; (G4) Apple WatchSkillforgeView(6th “Skills” tab,top5→ detail); (G5) Wear OSSkillforgeScreen+SkillforgeDetailScreen+SkillforgeTileService(routes + deeplink + manifest); (G6) FlutterSkillforgeScreen(8th “Skills” bottom-nav tab, cross-machine catalogue + detail + train-status lookup); (G7) VibeAIChat — 10 SkillForge Tauri proxy commands registered invibeaichat/src-tauri(no panel; the bespoke UI has no AiMlComposite/toolbar).cargo checkclean (vibecli, vibe-ai, vibecoder/src-tauri, vibeaichat/src-tauri); 199 TUI + 44 repl tests + 2 new context-assembly tests green;dart analyzeclean on new/touched Flutter files;swiftc -parseclean on Watch files. Full ledger innotes/skillforge/07 — Client Visibility & UX Gaps.md.
- Crates —
- Code Graph integration (kodegraph) — token-reduction substrate. Wired the standalone
kodegraphcrate (tree-sitter → SQLite code-knowledge-graph) into the daemon as a single bridge modulevibecli/vibecli-cli/src/graph_index.rs(kodegraph is a dep ofvibecli-clionly;vibe-ai/vibe-corestay kodegraph-free and receive pre-rendered strings /SymbolInfovecs):- Background build on daemon startup — non-blocking
std::thread::spawnparse; the startup banner,/health, and/graph/statussurfaceindexing → ready (N nodes, M edges); the graph persists at<workspace>/.vibecli/codegraph.db(the workspace-DB convention) and is reloaded on next boot; cheap mtime+size incremental refresh runs on agent spawn + explicit/graph/build. - Agent system prompt —
AgentContext.graph_summary(new#[serde(default)]field) carries a ~few-hundred-token god-node / community / surprising-edge summary that replaces the flat directory-tree repo map in## Workspace Structurewhen the graph is ready (falls back to the dir-tree when disabled). - TUI context —
ContextBuilder::with_relevant_symbolsseeds## Relevant Symbolsfrom a blast-radius around the task terms (graph_aware_symbols); empty when no graph, so behavior is preserved. - HTTP surface — eight
/v1/graph/*routes (build/status/query/node/:name/neighbors/:name/path/:from/:to/blast/report) carrying no LLM call (provider-agnostic-panel rule moot) and returningserde_json::Value(no kodegraph-type coupling); two curated/watch/graph/*routes (statuscompact{status,n,m},querycapped to ≤5 nodes) for the watch form factor. - 7-client fan-out — Tauri commands (
graph_*incommands.rs+lib.rs), Agent SDK (agent.graph.*), VS Code (apiClient.graph*), FlutterApiClient, Watch SwiftWatchNetworkManager, Wear KotlinWearNetworkManager. /semindexrewired ontograph_index(build/query/node/callers/callees/hierarchy/stats); the supersededsemantic_index.rs(regex/manual parser) anddep_visualizer.rswere deleted (their module decls removed fromlib.rs/main.rs).semantic_mcp.rs’s ownSemanticIndexServeris unrelated and untouched.- PreToolUse hook — a non-blocking
Glob|Grepnudge in.claude/settings.jsonthat surfaces the graph’s presence so broad file sweeps can consult/graph/*first. - Unit tests for
graph_index(build/query, symbol mapping, graph-aware seeding); follow-ups deferred: background debounce poll for incremental refresh,lsptier throughvibe-lsp,ast_edit.rsparser delegation, andsemantic_mcp.rsreuse ofkodegraph::mcp.
- Background build on daemon startup — non-blocking
- Phase 55 —
feat/bridge-cgap-fitgap-gaps— eight capability gaps shipped as compiling, unit + BDD-tested code:- C5 · Per-request effort knob — provider-agnostic
Effort(low|medium|high|xhigh, defaulthigh) invibe-ai/provider.rs, mapped per provider: Claude/Gemini extended-thinking budget, OpenAIreasoning_effort(clamped to “high”). Wired throughProviderConfig.effort,cost_router::route_task_with_effort, the daemon/agentpath (serve.rs, via the VX-111reasoninglabel), theai_chat_with_effortTauri command,vibeaichatstart_agent_session, and a VibeCoder toolbar selector (utils/effort.ts). Fix: Gemini Pro models can’t disable thinking —Effort::Lownow clamps Pro to the 128-token minimum instead of emitting an API-rejectedthinkingBudget: 0. BDD:effort_bdd(5 scenarios). - C1 ·
/loop— recurring (/loop 5m <prompt>) and self-paced (/loop auto <prompt>, loop-until-done) REPL command with aMAX_ITERguard (20), wall-clock auto-expiry, job IDs, Ctrl-C stop, an LLM done-validator, andlist/stop/status+ JSON persistence (loop_engine.rs). BDD:loop_engine_bdd(6 scenarios). - C3 · MCP Tasks extension + stateless transport (2026-07-28 RC) —
tasks/get|update|cancelregistry with aWorking→…→Completed/Failed/Cancelledstate machine, plus stateless_meta(RequestMeta) and astatelessflag onStreamableHttpConfig(mcp_tasks.rs). BDD:mcp_tasks_bdd(5 scenarios). - C6 · ACP + MCP Registry self-listing —
vibecli --registry <acp|mcp|all>emits the ACP agent-card / MCP Registry v0.1 server entry from one versioned identity (registry_listing.rs). - Design-Mode diffcomplete — clicked element + instruction → a CSS/HTML unified diff for
DiffReviewPanel; rejects live-DOM-mutation payloads. Tauridesign_emit_diff(design_diff.rs). - Opt-in security review — default-OFF watcher that emits standard
self_review::Findingrecords (no auto-apply). Taurisecurity_review_file(security_review_watch.rs). - WebMCP (origin-trial gated) — consumer (parse/validate site tools) + producer (panel→tool, read-only). Tauri
webmcp_parse_tools/webmcp_publish_panels(webmcp.rs). - c-series model registry — added Claude Opus 4.8, Gemini 3.5 Pro/Flash, DeepSeek V4, Qwen 3.6, Kimi K2.7 Code (via OpenRouter), GLM-5.2, MiniMax M3 to
useModelRegistry.tswith updated defaults; Fable 5 / Mythos 5 omitted (export-suspended 2026-06-12). - Follow-ups still open: UI panels (
/loopAutomations, Design-Mode wiring, SecurityPanel), daemon/live-transport loops (B3 always-on watcher, C3 live MCP dispatch, C4browser_agentCDP), C5 streaming-path propagation, and C2 (dynamic large-scale workflow primitive).
- C5 · Per-request effort knob — provider-agnostic
Changed
- The step budget now extends for a run that is still working, instead of cutting it off.
max_steps(default 50) is a runaway guard, but as a hard wall it also stopped healthy runs: an agent executing its plan tool-call by tool-call hit the limit mid-plan, reportedPartial, and left the user to press Resume to finish work that was going fine.AgentLoopnow grants up tomax_step_extensions(default 3) further budgets ofmax_stepseach — a ceiling ofmax_steps * (1 + N), 200 steps by default.- An extension is earned, not automatic. The decision is a pure
should_extend_budget(extensions_used, max_extensions, health, steps_since_progress), and every guard vetoes independently: extensions must remain, the circuit breaker must readProgress(notStalled/Spinning/Degraded/Blocked), and a successful tool call must have landed within the last 10 steps. Failed tool calls deliberately do not count as progress — an agent retrying the same broken command is precisely what the budget exists to stop. AgentLoop::with_step_extensions(0)restores the old hard wall; a test pins that.- Mechanically, the loop advances
stepat the top of its body rather than the bottom. The body has manycontinues, and a bottom increment would skip every one of them and spin forever. The post-loopPartialreporting now quotes the effective budget rather thanmax_steps, so it no longer understates how far the run got. - 5 new tests: the policy’s veto cases, the bound, a productive run that overruns a 2-step budget and now completes where it previously reported
Partial,with_step_extensions(0)restoring the wall, and an agent whose every tool call fails not being extended. - Tunable per run —
POST /agenttakes an optionalmax_step_extensions, so an operator can trade completion against cost without a rebuild (0= hard wall). Absent means the harness default, so no existing client changes behaviour.max_steps == 0is honoured rather than extended. - Sub-agents get 1 extension, not 3. The multiplier compounds down the tree: at the root default a depth-2 nest would let every sub-agent run 4× its budget, and a parent spawning many of them multiplies that again. 2× absorbs an overrun without the fan-out.
- An extension is earned, not automatic. The decision is a pure
Fixed
-
A chat history containing one huge message wedged the run permanently.
prune_messagesonly ever dropped the middle of the conversation. When the oversize lived in a message it must preserve — the system prompt, or aread_file/bashresult in the six-message tail — pruning was a no-op, so every following step re-sent the identical too-large payload and the provider rejected it the same way each time. A test reproduced it at 100,120 tokens against a 10,000 budget. Pruning now falls through to clipping individual messages (largest first, both ends kept around a…[truncated to fit the context window]…marker, always on UTF-8 char boundaries) so the next request actually fits. Message count and order are preserved — dropping the system prompt or the newest tool result breaks the loop worse than eliding the middle of one payload. Best-effort by construction:estimate_tokenscharges 8 tokens of framing per message, so a budget under8 × lencannot be met by clipping, and the helper degrades rather than spinning (pinned by a test). - Subprocess dispatch had the original
/agentbug, untouched. The child loop inmain.rsmatched fourAgentEventvariants and swallowed the rest with_ => continue— so a dispatched run that stopped mid-plan drained the channel, leftcompletedfalse, and the fallback sentDispatchFrame::Complete { "Agent finished." }, which the parent records asJobStatus::Complete. Byte-for-byte the defect fixed inserve.rson the first pass, in a code path never opened.ToolCallPendingwas dropped there too, silently abandoning the run. Found by verifying a claim (“the remaining catch-alls are already audited”) instead of restating it — twoAgentEventmatches inmain.rshad never been read.DispatchFramegains aPartialvariant, sinceCompleteis defined by this protocol as success andErroroverstates a resumable run. The parent publishes the richpartialevent to SSE and marksJobStatus::Partial. Safe to add a variant: the child is this same binary, self-spawned viacurrent_exe(), so parent and child can never be different versions.- A second
main.rsloop (console output) also droppedPartial, printing nothing at all when a run ended — the tool steps then silence.
-
kodegraph community detection was seeded by hash order, making the workspace suite intermittently red. Label propagation resolved ties with
tally.into_iter().max_by_key(|(_, c)| *c).HashMapiteration order is randomly seeded per process andmax_by_keyreturns the last maximum, so which label won a tie varied run to run and the same graph settled differently —detects_a_community_from_a_clusterfailed roughly one run in ten. Two further orderings had the same flaw: communities were grouped out of aHashMapand sorted with a stable sort on size alone, so equal-sized communities kept their hash order, and member lists were unsorted. Ties now break on the label id, the sort is total (size, then id), and members are sorted — the result is a function of the graph alone. A new test asserts the a/b/c triangle groups together and that 25 rebuilds produce byte-identical ids, members and ordering. Found by finally runningcargo test --workspacerather than--lib; unrelated to the rest of this changelog entry, and pre-existing. -
The CLI’s
spawn_agenthad the same unfinished-subtask bug as VibeCoder’s, verbatim.tool_executor.rsandvibecoder/src-tauri/agent_executor.rscarry duplicate copies of the sub-agent event loop; fixing one left the other reporting “Sub-agent completed.” for a sub-agent that stopped mid-plan, and silently abandoning the run when a policy gated a tool. Both copies now match. Found by auditing catch-all match arms rather than by review. -
Flutter’s watch poller never saw a
partialorcancelledsession as finished.watch_sync_service.darttestedstatus == 'complete' || status == 'failed', an allow-list of two — socancelled(pre-existing) andpartial(once the daemon gained it) spun the full 60-second timeout and then returned[], discarding assistant messages already fetched and leaving the phone blank. Terminal is now “not running/queued”, and a timeout hands back whatever did arrive. -
Wear OS read an HTTP failure on either SSE stream as a clean end. OkHttp calls
EventSourceListener.onFailurewith a null throwable when the failure is an unsuccessful HTTP response, and bothopenStreamandopenTaintedPendingStreamdidif (t != null) onError(t) else onComplete(). So a 401 or 500 ran the completion path:ConversationScreencleared the partial reply and dropped the spinner with no error shown. On the tainted-prompt stream it is worse — the wrist shows no pending approvals while a run sits blocked waiting for one. Both now distinguish an unsuccessful response from a genuine close. -
A JetBrains
stepwith nosuccessfield rendered as “ok”. Introduced by this changelog’s own parser rewrite, and caught by the follow-up sweep rather than by review.AgentEvent.Step.successis nowBoolean?and rendersoutcome not reportedwhen the daemon said nothing. -
An unverified observe-act step erased the failure streak, so a failing loop never bailed out.
ObserveActSession::record_stepcomputed success asverification_result.map(|v| v.success).unwrap_or(true)— the comment said// No verification = assume success— and a “success” resetsconsecutive_failuresto 0. With unverified steps interleaved (the normal case whenverify_after_actionis off, or verification itself fails), the streak could never reachmax_consecutive_failures, so a loop that was failing every step ran the entiremax_stepsbudget instead of aborting after three. Absent verification is now neither success nor failure: the streak is left unchanged. Three tests, including one that drives failure/unverified pairs and asserts the loop stops rather than running to the cap. -
A
tool_endwith nosuccessfield showed a green tick on the wrist.to_watch_event_jsondefaulted the missing field totrueand reportedstatus: "ok".statusisOption<String>precisely so absence can stay absent; it is nowNonewhen the producer said nothing. -
A failed watch/phone reply was recorded in the database as a completed one. The
/watch/dispatchstreaming task matched chunks withif let Ok(text) = chunk, so a mid-stream provider error was silently discarded and the loop carried on. The trailing{"type":"done","status":"complete"}still fired, and — uniquely on this path —finish_session(…, "complete")plus the truncated assistant message were persisted. The wrist showed a half-answer as finished and the transcript agreed with it from then on. A failed stream is now terminal: it emitserror, storesfailedwith the provider’s message, and keeps the tokens that did arrive (the user watched them stream in) without ever recording them as a completion. The persistence decision is extracted intopersist_watch_turnand pinned by three tests (failed-with-partial, clean, failed-before-any-output). -
/chatand/chat/streamignored the model you picked.AGENTS.md’s provider-agnostic rule says daemon routes read provider/model from the request body, not the daemon’s startup config, andPOST /agentalready did. The chat routes did not:ChatRequest.modelrode the wire but was marked#[allow(dead_code)]and never read, and there was noproviderfield at all. A user who chose a model in the toolbar was answered by whatever the daemon happened to boot with — silently, and differently from the agent route on the same screen. Both routes now resolve through a sharedchat_provider_for, which honoursprovider+modeltogether (matching/agent), wraps the result inresilient()since a chat turn has no retry loop of its own, and falls back to the daemon provider when the override is absent or unbuildable — so existing callers are unaffected. Pinned by tests covering both-present, either-alone, empty-string, unknown-provider, and the identity that surfaces through the wrapper. -
/chat/streamreported a failed reply as a finished one. A mid-stream provider error emitted anerrorevent and then carried on, so the trailingdonestill fired: clients sawerrorfollowed bydoneand rendered a truncated reply as complete.erroris now terminal. - A dropped connection reported a still-running task as finished. VibeDesk settled a stream that closed while
runningstraight todone, on the assumption that it meant “the run ended without a terminal event”. That assumption stopped holding once the daemon began always emitting a terminal event: reaching that path now means the transport dropped — a network blip, a daemon restart, the laptop sleeping — while the agent carried on working. The chat said the task was done, the transcript stopped mid-stream, and the task card was PATCHed toreviewing.- The daemon is authoritative and already had the pieces:
GET /jobs/:idfor real status and a durable event log replayable from seq 0. A closed stream now reconciles — if the job is genuinely terminal it settles to that outcome (failed→ error,cancelled,partial, else done); if it is still running it reattaches and replays, so nothing streamed before the drop is lost. Bounded at 5 consecutive attempts, after which the run is reported as lost rather than reconnecting forever, and the allowance resets on every real terminal event. - A user-requested cancel still short-circuits to
cancelledbefore any of this.
- The daemon is authoritative and already had the pieces:
- Choosing a model in the toolbar silently disabled retry. The daemon’s default provider is wrapped in
ResilientProviderbymain.rs::create_provider, butbuild_provider_override_with_effort— used whenever a request carriesprovider+model, which VibeDesk’s model picker, mobile, watch and the IDE clients all send — returned a raw provider. Every one-shot LLM call with no retry loop of its own (/chat, chat-mode agent turns, the planner, recap synthesis) therefore died on a single transient 503, and only for users who had picked a model. Those call sites now wrap explicitly via a newresilient()helper.- The builder deliberately still returns raw:
AgentLoopretriesstream_chatitself, so wrapping there would nest two backoff loops (4 × 5 = 20 attempts) and stall a run for minutes on a hard outage. A test pins that the builder is unwrapped and thatresilient()is identity-preserving (name()delegates), so wrapping never changes which model a caller believes it is talking to.
- The builder deliberately still returns raw:
- Unfinished agent runs were reported to every remote client as successes.
AgentEventhas nine variants; the daemon’s/agent→/stream/{id}route matched four and swallowed the rest with_ => continue.AgentEvent::Partial— the event the harness emits when the agent exhausts its step budget or stops mid-plan — was one of the swallowed ones, so the loop drained with no terminal event and the “clients must not hang” fallback publishedcomplete("Agent finished.")and marked the jobComplete. The real partial summary and the list of plan items that were never executed were discarded. Every client behind the daemon (mobile, watch, VS Code, JetBrains, Neovim, Agent SDK, VibeDesk) was told the task had finished.ToolCallPendingwas swallowed the same way, which killed the run outright. Droppingresult_txwithout answering madeAgentLoop::runhitErr(_) => return Ok(())— the one path in the harness that ended a run with no terminal event at all — and the same fallback then reported that abandoned run as a success. Reachable from any caller passingapproval: "suggest"/"autoedit", which the route accepts.- Harness — that silent
return Ok(())now emitsAgentEvent::Errornaming the tool and the dropped channel. A newassert_terminal_eventtest pins the contract across five paths: every run ends with exactly one ofComplete/Partial/Error. Callers that infer success from “noErrorseen” are now safe by construction. - Wire protocol — two new SSE kinds.
partialis terminal and carriessteps_completed/steps_planned/remaining_plan;retryis non-terminal and carriesattempt/max_attempts/backoff_ms(previously a retrying agent was indistinguishable from a hung one for up to 60 s per attempt).systemnow also carries circuit-breaker and verifier notices. NewAgentEventPayloadfields areOption+skip_serializing_if, so the existing kinds’ wire shape is byte-identical and old rows in the durable event log still deserialize — both pinned by tests. JobStatus::Partial— terminal, but neitherCompletenorFailed: the work done is real and the run is resumable. Folding it intoCompleteis what let unfinished tasks read as successes.- Client fan-out — Agent SDK + VS Code (
isTerminalAgentEvent, replacing four hand-rolledcomplete || errorchecks), VibeDesk (stream_agentforwardspartial/retry;RunStategainspartial, rendered as “incomplete” and now offering Retry, which it previously withheld on exactly the runs that needed it; the replay path no longer drops the partial notice, so a reopened chat still shows the work was unfinished), VibeCoderBackgroundJobsPanel(closes theEventSourceonpartialinstead of leaking it until the browser errors out), and Neovim. - JetBrains agent window rendered nothing at all — pre-existing and unrelated to the above. Its
parseEventlooked forthinking/text/tool_call/tool_resultand readsummary/message/text: kinds and fields the daemon has never emitted. Every streamed token fell through tonullandcompleterendered an empty summary. Rebuilt against the realAgentEventPayloadshape, withStep/System/Retry/Partialadded. - Sub-agents —
spawn_agent(vibecoder/src-tauri/agent_executor.rs) droppedPartialinto_ => {}, leavingsummaryempty, which its tail rendered as “Sub-agent completed.” to the parent agent. A sub-agent that ran out of steps mid-plan now returnsToolResult::errwith what it finished and what it did not, so the parent re-dispatches instead of building on unfinished work. - Docs:
docs/api-reference.mdgains a terminal/non-terminal event table, the partial-run example, and thepartialjob status.
- VibeDesk · Sandbox mode with per-axis permissions. The agent is normally jailed to
workspace_root—ToolExecutor::resolve_safecanonicalizes every path and rejects escapes. Sandbox mode relaxes that jail, which is the only place in the product where a model gets reach beyond the project it was pointed at, so the relaxation is explicit, per-axis, and denies by default. Newsandbox_policy.rs(read_outside·write_outside·exec_outside·network, plusallow_roots/deny_roots), asandboxfield onPOST /agent, and a composer settings sheet in VibeDesk.- Credentials are unreachable by construction.
vibe_core::path_guard(~/.ssh,~/.aws,~/.vibecli,id_rsa,daemon.token, …) is applied to every outside path before any grant is consulted, so no combination of toggles — including anallow_rootpointing straight at~/.ssh— reaches a key. Pinned bycredentials_are_never_reachable_however_configured, which asserts it under a maximally permissive policy. - Deny beats allow; grants only apply in the mode that asked for them.
deny_rootsis checked beforeallow_roots.effective_sandbox_policy()is extracted and tested precisely so asandboxfield left on an Agent request cannot silently widen an ordinary run — the obvious way this feature could have leaked. - Inside the workspace is untouched. The policy is consulted only for paths that escape the jail, so enabling sandbox mode can only add reach, never remove it. Deliberately, the credential deny-list is not applied inward:
DENIED_FILENAMESincludesconfig.json, an ordinary project file. - Sub-agents inherit the policy.
spawn_agentbuilds a child executor field by field; omittingsandbox_policythere would have handed the child a locked default, so a sandbox run’s sub-agent would fail on the very paths its parent was granted. exec_outsideenforces something real.bashis not path-jailed — the shell reaches whatever the OS allows, in Agent mode too — so a “commands may not leave the workspace” path check would have been theatre. The axis instead drivesToolExecutor’s existing OS-level confinement (bwrap+Landlock / sandbox-exec / AppContainer): off confines commands, on runs them as they do today. An earlier draft shipped aallows_exec_in()guard that could never fire; it was removed rather than left as a setting that enforces nothing.- Verified — 9 policy tests, 3 executor tests proving the jail widens for the granted axis only, 4 request-plumbing tests, and a live run: sandbox mode with no grants refused to read a canary file outside the workspace and reported the jail message. 12,926 vibecli tests green.
- Credentials are unreachable by construction.
- VibeDesk · Chat mode, thinking off by default, and a resizable Environment rail.
- Run mode (Agent · Chat). New
modefield onPOST /agentwith aRunModeparser (unknown values fall back toAgent, so older clients are untouched), and a chat path inserve.rsthat answers in one turn with no tool loop, no executor, no approvals and no workspace access. It reuses the session + event plumbing, so a chat turn replays, resumes and appears in history like any other. The agent’s system prompt orders it to reply only with a tool call, so asking a plain question in Agent mode got an answer shaped like work — Chat mode is how you ask without starting a task. Surfaced as a composerModePill, persisted with the other run controls. - Live-verified. “In one sentence, what is the Mandelbrot set?” in Chat mode against
minimax-m3:cloudreturns a one-sentence answer with no<tool_call>and no reasoning tags. unwrap_thinking()— found only by running it: minimax-m3 put a 54,000-character reply entirely inside one<thinking>block (</thinking>at char 54,210 of 54,222), sostrip_thinkingcorrectly returned nothing and the turn would have rendered as an empty message with the answer hidden in a collapsed aside. When stripping leaves nothing, the chat path now unwraps the tags and keeps the text. Display paths only — tool parsing still usesstrip_thinking, because reasoning quotes calls the model then rejected.- Thinking is off by default.
ReasoningEffortgains anofftier, now the composer default. Off is sent as the absence of thereasoningfield, not a value: the daemon maps an unknown effort to no budget but still publishes aReasoning effort: …system line for whatever it receives, which would have announced a tier the user had turned off. One sharedeffortParam()covers the main composer and the side chat. - Environment rail — drag-to-resize (200–520px, double-click to reset) plus collapsible sections with counts, both persisted via
useLayoutPrefsfollowinguseTheme’s localStorage-mirror-then-settings-store pattern so the first paint is already correct. The handle is absolutely positioned on the column boundary rather than occupying a grid track, so the three-column template is unchanged whether or not it renders. - CI —
vibedesk-checks(added with the release job) covers all of this;npm run test:thinkingis a new script with 11 cases.
- Run mode (Agent · Chat). New
</mm:think>rendered verbatim in chat, mid-sentence. minimax-m3 emits a namespaced reasoning tag, and all three strippers matched onlythink|thinking— so<mm:think>sailed through every one. Fixed invibe_ai::tools(strip_thinking/unwrap_thinking),vibedesk/src/lib/thinking.tsandvibecoder/src/components/AIChat.tsxwith an optional namespace prefix (<mm:think>,<ns:thinking>, …).- The Rust stripper was also missing the orphan-close rule the TypeScript side already had. That is the other half of the leak: Ollama consumes the opening tag into its own
thinkingfield, so only the close reaches us, and everything before it is still reasoning. Verified on the exact failing turn —<thinking>…</thinking>\nLet me write a single Python file.</mm:think><write_file …>now yields just the<write_file>element, with neither the tag nor the trailing reasoning. - 4 new Rust tests and 4 new VibeDesk tests, including one asserting that ordinary prose containing
</div>is untouched — the orphan rule is greedy by design and must not eat a sentence.
- The Rust stripper was also missing the orphan-close rule the TypeScript side already had. That is the other half of the leak: Ollama consumes the opening tag into its own
- The tool parser now accepts three dialects, not one.
parse_tool_callsunderstood only<tool_call name="…">, so a model that reached for any other shape produced a turn that looked like prose and did nothing.- Element style —
<write_file path="a.py">body</write_file>,<read_file path="x"/>. This is the format VibeCoder’s own chat prompt teaches (vibecoder/src-tauri/src/commands.rs), so models carry it into agent runs where nothing parsed it. - JSON style —
{"name": "read_file", "arguments": {…}}, with or without a code fence. Captured live from minimax-m3 given no native tool definitions: it falls back to the OpenAI function-call shape it was trained on. - Canonical wins and is never re-scanned, so a documented example quoted inside a real
<tool_call>body cannot double-fire. Only names inAVAILABLE_TOOL_NAMESare considered, so<div>,<p>and prose mentioning a tool are never mistaken for calls. Common aliases (file_path/file→path,cmd→command) are normalised, and element bodies map to each tool’s natural parameter (write_file→content,bash→command, …). - Element matching uses one regex per tool name rather than an alternation with a backreference — Rust’s
regexcrate has none, so</\1>cannot express “the same tag we opened”. Matches are position-sorted so a write-then-run turn executes in that order. - 6 new tests covering both dialects, precedence, false positives, and the rule that every dialect is read from the visible turn only (reasoning quotes calls the model then rejects).
- Element style —
- Native tool-calling models produced an empty turn and a stuck run.
OllamaChatRequestcarriedmodel,messages,streamandoptions— but nevertools. Tools existed only as prose inTOOL_SYSTEM_PROMPT, which works for models that follow prompt instructions and not at all for one trained to call a tool API:minimax-m3:cloudreasoned “Let me first check the workspace…”, reached for a tool interface that was never advertised, and returned nothing. The agent’s reasoning-only guard (agent.rs:1077) then re-prompted twice and gave up, leaving the task inreviewing. The response side had always transcribed nativetool_callsback to<tool_call>markup — only the outbound half was missing.- Fix — new
tools::tool_definitions()publishes all 14 tools as OpenAI-shaped function schemas, and the three/api/chatcall sites now send them./api/generate(prompt-completion, not the agent path) is untouched. - Gated, not unconditional —
tools::expects_tools()sends schemas only for conversations carrying the agent’s system prompt. Advertising tools to a plain chat panel would produce<tool_call>markup that the panel renders as literal text — the same class of leak as the raw<thinking>tags below. - Schemas are pinned to the parser.
render_tool_callturns each JSON key into an XML tag, so a renamed parameter would silently turn every native call into an unparsed block.tool_definitions_match_parserbuilds a call from each schema’s required params and asserts it round-trips throughparse_tool_calls. 5 new tests; 1181vibe-aitests green. - Verified live, since no unit test can catch this — every schema assertion passed while the bug was present.
tests/ollama_native_tools_live.rs(#[ignore]d; needs a signed-in Ollama) replays the original prompt againstminimax-m3:cloudand asserts a parsable tool call comes back. It now answers<thinking>…Let me check the workspace first.</thinking><tool_call name="bash"><command>pwd && ls -la</command></tool_call>— the same sentence that used to end the turn, now followed by an action. A second case pins the gating: the same task with no agent prompt returns prose and zero tool calls. - Still open —
openai_compat.rshas the same gap, so every OpenAI-compatible endpoint is prompt-only today.tool_definitions()is provider-neutral and ready for it.
- Fix — new
- VibeDesk rendered
<thinking>tags as literal text. Providers wrap reasoning into the content string so it travels as one value (ollama.rs,openai_compat.rs), and Rust strips it withstrip_thinkingbefore treating the text as an answer — butSessionStream.tsxpiped the raw turn into<Markdown>, so the transport markup appeared on screen. Worst on a reasoning-only turn, where the tags were the entire message.- Fix — new
lib/thinking.tssplits reasoning from the answer, andReasoningBlock.tsxrenders it collapsed (dashed, italic,--accent-purple) rather than discarding it: when a turn is only reasoning, that block is the sole explanation of why nothing happened. Copy-to-clipboard now yields the answer, not the internal markup. - Matches the Rust semantics plus the unbalanced shapes seen in practice: both
<think>/<thinking>spellings, an unclosed block swallowing its tail (stream cut mid-thought), and an orphan closing tag where the provider already consumed the opening one. 11 new tests vianpm run test:thinking. - VibeCoder was not affected —
AIChat.tsx:extractThinkingalready handled this; VibeDesk simply never got the equivalent.
- Fix — new
ProfileStoresplit intocrates/vibe-profile-store. Reading one API key used to mean linking the whole CLI: the store lived insidevibecli, so anything touching settings pulledmistralrsandcandlewith it.vibeaichat/src-tauri/src/commands.rsdocuments avoidingvibeclifor exactly that reason — and the shared settings crate had quietly reintroduced the dependency behind that comment’s back. The module turned out to be fully self-contained (zerocrate::references), so the lift was clean.vibeclire-exports it asvibecli_cli::profile_store, leaving all ~30 call sites unchanged —serve.rs,config.rs,watch_auth.rs, VibeCoder’scommands.rsand the rest.vibe-desktop-settingsnow depends on the store alone, so VibeAIChat’s tree carries novibecli,mistralrsorcandle(396 crates). VibeDesk still linksvibecli— by its own design, not by accident.- Verified by test accounting, not just a green build: the 30 profile-store tests moved with the crate and pass there, and
vibecliwent from 12,937 to 12,907 — exactly the 30 that left, nothing silently dropped.
- Vibe App is now VibeAIChat, and shares its settings with VibeDesk.
- Rename —
vibeapp/→vibeaichat/(git mv, history preserved), cratevibeapp/vibeapp_lib→vibeaichat/vibeaichat_lib, npm package,productName: VibeAIChat,identifier: com.vibecody.vibeaichat, window title, workspace member, Makefile targets (app→aichat,build-app→build-aichat, …), thebuild-vibeaichatrelease job,vibeaichat-checksin CI,.semgrep, and every doc. macOS treats the new bundle id as a new app, so it re-asks for permissions once; settings live in~/.vibecliand are unaffected. packages/vibe-ui-shared— the settings screens (Providers · Appearance · Account), theme definitions,useTheme,useProviderSettingsand the reasoning parser now exist once, consumed as source via a@vibe/sharedalias. No build step, nodist, no version to bump. This ends a drift that had already caused a bug: the reasoning rules existed in three copies, and a fix to one left the others rendering raw<thinking>markup.crates/vibe-desktop-settings— the same for the Rust half.#[tauri::command]functions do not have to live in the app crate, so both shells now drive one copy of the ProfileStore code rather than two. (They must live in a module, not the crate root: at the root each command collides with the macro#[tauri::command]generates for it.)SettingsViewgainedextraTabsso a shell can contribute settings that are its own without pushing them into the shared screens. VibeAIChat uses it for a Connection tab (daemon URL, optional token override, provider, model); VibeDesk passes none and is unchanged.- Wiring notes, because both are silent when wrong. The shared sources live outside each app’s project root and there is no hoisted
node_modules, so their bare imports are mapped to the host app’s copies in bothvite.config.tsandtsconfig.json— which also keeps one React instance in the bundle. The Vite aliases use regexfind: a plain string alias is a prefix match, so"react"would rewrite"react-dom"into"<path-to-react>-dom". Andresolve()strips a trailing slash, so sub-path replacements re-append it orreact/jsx-runtimeconcatenates intoreactjsx-runtime. Intsconfig,@typesmust precede the JS package inpathsor every import lands asany. - Verified —
tscandvite buildclean in all three shells,cargo check/fmtclean, the 4 settings-migration tests moved with the crate and pass, bothtest:thinkingsuites pass against the single shared parser, and the settings CSS was confirmed present in each bundle (VibeAIChat’s was missing at first — the screens render unstyled when the stylesheet is not imported, exactly the trap the package README documents).
- Rename —
- VibeAIChat answered “hi” with pages of the model arguing with itself. Two stacked defects, both visible in one screenshot.
start_agent_sessionhad nomodeparameter at all, so every turn ran the agent loop — whose system prompt orders the model to reply only with a tool call. The model’s own output diagnosed it: “the system prompt says every response MUST be a tool call”, followed by five paragraphs deciding whether a greeting counted. And the render site was{msg.content}straight to the DOM, so the<thinking>block wrapping all of it appeared verbatim.- VibeAIChat is an assistant, not a task runner, so its UI now sends
mode: "chat"— a single reply, no tools, no approvals. Verified live againstminimax-m3:cloud: the samehinow returns 37 characters (“Hi there! 👋 How can I help you today?”) with no thinking tags and no tool-rule argument. vibeaichat/src/lib/thinking.tsstrips reasoning at the render site as a guard. Chat mode already strips it server-side, so this covers the rest — agent-mode turns, an older daemon, or a spelling the server missed — because the failure mode is raw markup in the user’s face. Falls back to unwrapping when the reasoning is the whole reply, rather than rendering an empty bubble. 8 tests vianpm run test:thinking.- Note the duplication. These rules now exist in three places (Rust
strip_thinking, VibeDesk, VibeAIChat) because the two Tauri apps share no frontend package. Each copy carries its own tests and a pointer to the others; consolidating them behind a shared package would be the real fix.
- VibeAIChat is an assistant, not a task runner, so its UI now sends
- Every client 401’d against a healthy daemon — one cascade, three causes. Reported as three unrelated symptoms (VibeAIChat 401, VibeDesk “Couldn’t load chats”, VibeCoder “Port 7878 is in use by another program”). All one chain:
/healthshared the public routes’ 10-req/min per-IP bucket, and three desktop apps plusdaemon_bootstrap::probe’s 250ms startup poll all arrive from127.0.0.1— so the bucket was exhausted in seconds and never recovered. A throttled/healthreturns{"error":"Rate limit exceeded"}, which carries noservicefield, soprobe()read a healthy daemon as a foreign process; clients then spawned replacement daemons, and each replacement wrote~/.vibecli/daemon.tokenbefore binding, so the loser clobbered the live daemon’s token on its way out. Confirmed on the machine: daemon started 19:45:32, token file rewritten 20:00:40 by a process that never held the port./healthis off the shared bucket (serve.rs) with its own 600/min floodgate — bounded against a runaway client, unreachable by honest polling. Throttling the one route every client polls for liveness and identity is self-defeating: the failure is not a slow health check, it is the cascade above.- Bind precedes the token write. Binding reserves the port, so a daemon that cannot have it now fails before touching the shared token file. The comment defending the old order worried about a client reading a stale token the instant the port opens; that cannot happen, because
bindonly opens the socket and nothing is answered untilaxum::serveaccepts, well after the write. probe()treats 429 as a throttle, not a stranger — retries once rather than declaring a foreign service. Defence in depth now that 429 is unlikely.- New
tests/client_contract.rs— three tests against a real spawned daemon, one per link: 40 concurrent/healthprobes must all identify and none be throttled; a daemon that loses the port must exit without rewriting the token, which must still authenticate; and the bearer contract (protected 401s bare, succeeds with the file’s token,/healthpublic and self-identifying). Verified to fail without the fix: reverting the/healthlimiter made it fail atwait_ready—probe()could not identify the daemon at all within 90s, the production symptom exactly. HOMEis isolated in both daemon integration suites. The token path is shared across daemons regardless of port, so a test inheriting the realHOMEoverwrites the developer’s own daemon token and breaks every client on the machine — the very failure under test.daemon_bootstrap_integration.rshad been doing this since it was written; verified fixed by hashing the live token across a full run (unchanged, same mtime).
- Daemon autostart failed in every Finder-launched app — “exited immediately (exit status: 1)”. The daemon derives its workspace root, and therefore
<workspace>/.vibecli/, from its working directory. Neither spawn site set one, so the child inherited the parent’s cwd — which for a.applaunched from Finder rather than a terminal is/. The daemon then tried to create/.vibecli/on the read-only system volume and died before binding:Error: rl run store: storage: Read-only file system (os error 30). Launching from a terminal always worked, because a shell’s cwd is writable — which is why this survived so long.- The failure was undiagnosable by construction. Both spawn sites set
stderr(Stdio::null()), discarding the one line that explained it, then told the user to “runvibecli --serve --port 7878in a terminal to see why” — where it does not reproduce. - Fix —
daemon_bootstrap.rsgainsspawn_working_dir()(keep the caller’s cwd when the daemon could actually create its.vibecli/there, else$HOME, else the temp dir, so a client started from a repo still gets that repo as its workspace) andspawn_output()(capture stdout/stderr to~/.vibecli/daemon-spawn.log, truncated per spawn).DaemonState::TimedOutnow names that log rather than suggesting a command that will not reproduce the failure, and VibeCoder’s “exited immediately” error appends the daemon’s own last log line. - Both spawn sites now share it.
vibecoder/src-tauri/src/commands.rshad its owntokio::process::Commandspawn — precisely the second copydaemon_bootstrap.rsexists to prevent, and the one that carried this bug. It now callsboot::spawn_working_dir()/boot::spawn_output(), as does the sharedspawn_detached. - Tests — 3 new
daemon_bootstrapunit tests (unwritable candidate skipped, caller’s cwd preferred, candidate list ordered), following the existingfind_binary_inpattern of taking candidates as an argument instead of mutating process-global cwd. Verified end-to-end with the same binary under a Finder-likeenv -i: cwd/exits 1, cwd$HOMEcomes up listening.
- The failure was undiagnosable by construction. Both spawn sites set
- Editor IntelliSense — completion, hover, go-to-definition, signature help and diagnostics work again. Five independent defects, each sufficient on its own to make every LSP request return nothing. Verified against real servers, not just fixtures:
rust-analyzernow returns 165 completions fortext.on text that exists only in the editor, andclangdreturns exactly the struct’s fields.- The message pump deadlocked the connection. The client forwarded every inbound message into a 32-slot
mpscthat only an in-flight request drained.rust-analyzeremits thousands of$/progressnotifications while indexing, so the reader wedged on the full channel, stopped draining the server’s stdout, the pipe filled, and the server blocked on write — IntelliSense died seconds after opening a file and never recovered.client.rsis now a dispatcher: a reader task routes responses through a pending-request map (so concurrent requests can’t consume each other’s replies — hover could previously return a completion list), storespublishDiagnostics, and answers server→client requests (workspace/configuration,client/registerCapability,window/workDoneProgress/create, …) instead of dropping them, which is what a server waits on forever. Every request has a timeout, and a server that exits fails its in-flight requests immediately rather than making each wait out the timeout. Framing now scans headers to the blank line (servers that sendContent-Typeused to desynchronise the stream permanently). - Every document URI was one no server had heard of.
<Editor>had nopathprop, so all files shared a single model atinmemory://model/1, whiledidOpenannouncedfile://…. Each file now gets its own model at a real file URI, produced by one encoder on each side (fileUri/path_to_uri) — the oldformat!("file://{path}")also produced an unparseable URI for any path with a space or#. As a side effect Monaco’s own TypeScript service can finally resolve imports across open files. - Edits were never sent.
lsp_did_changehad no caller, so servers answered against the text as it was when the file opened — nothing you typed could be completed. Edits are now pushed (debounced 250 ms, and always flushed before a completion/hover/definition request so a request can’t race its own edit), on AI-applied diffs and undo too. Versions are owned by the client and increase monotonically; a change to a document the server never saw self-heals into adidOpen. - Completion kinds and snippets were mistranslated. LSP and Monaco both call the enum
CompletionItemKindand agree on no value (LSPTextis 1; Monaco’s is 18,Methodis 0), so every suggestion carried a wrong icon; mapping is now by name, since Monaco renumbers (Snippetis 28 in 0.55, was 27).insertTextFormat: 2now setsInsertAsSnippetinstead of typing${1:arg}literally. Auto-import edits, commit characters,sortText/filterText/preselect, deprecation tags,labelDetailsandcompletionItem/resolvedocumentation are all carried through. - No trigger characters were registered, so
foo.andVec::showed nothing while mid-identifier completion appeared to work. They now come from the server’s own capabilities (multi-character triggers split into the single characters Monaco accepts). ClientCapabilities::default()declared nothing — an empty object is how you get a server that answers with plain labels, no snippets and no docs, or refuses outright. We now declare completion (snippets, resolve, all 25 kinds), hover/signature-help markdown, synchronization withdidSave,publishDiagnostics, and workspace folders.- Servers are found where they are installed. A Finder-launched app inherits launchd’s bare
PATH, which contains no~/.cargo/bin, Homebrew prefix or npm global — sorust-analyzerwasNo such file or directoryin the bundled.appeven when it worked incargo run. Newdiscovery.rsresolves servers across the standard install dirs and hands the spawned server the same augmentedPATH(servers shell out: rust-analyzer → cargo, tsserver → node). Availability is a directory scan, not ~60whichsubprocesses. - A missing server no longer costs a spawn per keystroke. The manager remembers a failed start and fails fast until
lsp_restart_languageclears it (the action to take after installing a server — no app restart). Clients are handed out asArc, so the manager lock is released before awaiting: one coldrust-analyzerno longer stalls hover in every other language. Dead clients are replaced, not reused. - Diagnostics are surfaced as Monaco markers under their own owner, polled in a short bounded burst after each sync rather than a standing interval (
null= “nothing published yet” is kept distinct from[]= “clean”, so an early poll can’t erase real errors). New status-bar indicator names the active server, or the missing one plus its install command, with click-to-retry. Languages Monaco services in-browser (TypeScript, JavaScript, JSON, CSS, HTML) deliberately get no LSP providers — Monaco’s cannot be unregistered, so ours would duplicate every suggestion. - Also fixed in passing —
⌘.(DiffComplete) and cursor sync acted on whichever tab was open first, becauseonMountcapturedactiveFilePathonce; and Monaco models are now reaped when tabs close, since per-file models would otherwise accumulate a copy of every file ever opened. - Language coverage raised to the TIOBE top 30 — 28 of 30, and the 2 remaining cannot be done. An audit cross-referencing the three tables that must agree (
useLanguageRegistry.tsranks,manager.rsserver configs,lib/lsp.tsextension routes) found 8 of the top 30 unreachable: MATLAB, Assembly, Ada, PL/SQL, COBOL, SAS, Objective-C and Classic VB had no route from a file extension to a server, so those files got neither IntelliSense nor — because their Monaco language ids were never registered — syntax highlighting. Added servers formatlab-language-server,asm-lsp,ada_language_server,superbol-free(COBOL),sas-lsp,abaplsp,powershell-editor-services,bash-language-server, the Nomic Solidity server, andclangdfor Objective-C (a first-class clangd language, not an approximation); PL/SQL and T-SQL ride on genericsqlsunder their own language ids, with install hints that say so rather than implying dialect awareness. Scratch (a block language whose.sb3is a zip) and VB6 (no LSP exists anywhere) are asserted as deliberate exemptions — a test pins the exemption list at exactly those two, so a third needs a stated reason. Coverage is now enforced from both ends:every_tiobe_top_30_language_has_a_configured_server(id → binary) and a frontend “TIOBE top-30 coverage” suite (extension → id), because either half alone leaves a dead file type. Also found and fixed: 4 pre-existing servers (vb,dlang,lisp,cfml) reported “not installed” with no install hint, and a test usedcobolas its stand-in for an unconfigured language — a false negative the moment COBOL gained a server..pl(Perl vs Prolog) and.m(MATLAB vs Objective-C) resolve the same way IntelliSense and highlighting both do, so a file can never highlight as one language and complete as another; both are pinned by tests and documented with the unambiguous alternative. - Language coverage extended past TIOBE — 84 languages get IntelliSense (79 via a language server, plus the 10 Monaco services in-browser). Added servers for the modern systems languages (Odin
ols, Gleamgleam lsp, CUDA viaclangd; Zig/Nim/Crystal/V/D/Vala were already wired), the component frameworks (Sveltesvelteserver, Vuevue-language-server, Astroastro-ls), functional newcomers (Elm, PureScript, ReScript), infrastructure (Terraform/HCLterraform-ls, Nixnil, CMake, Protobufprotols), shaders and hardware description (GLSLglsl_analyzer, WGSLwgsl-analyzer, SystemVerilogsvls, VHDLvhdl_ls), plus LaTeXtexlaband Nushell. Two invariants are now test-enforced in both directions: every configured server is reachable from at least one file extension, and every routed extension has a server — which caughtcfml(configured, no.cfm/.cfcroute, so unreachable) anddlang(an unreachable duplicate ofd, now removed).- The built-in-service check had to move from the Monaco language to the LSP language.
.vue,.svelteand.astroall highlight ashtml, andhtmlis Monaco-serviced — so the original keying would have silently skipped Volar and svelteserver, the only servers that understand script blocks, props and typed templates. Keyed on the LSP language they run correctly; those files get some HTML suggestions alongside, which is the right trade.LspStatuslost its now-redundantmonacoLanguageprop as a result. - Highlighting followed routing. Files whose Monaco language id was never registered (
matlab,asm,cobol,sas, and nowodin,gleam,nix,elm,purescript,rescript,astro,cmake,vhdl,latex) got neither highlighting nor providers, because Monaco rejects an unknown language id.detectLanguagenow returns registered ids — includingcmakeandlatex, which previously resolved toplaintextand so could never attach a provider — and the ids are registered at editor mount. Registration makes providers work; it does not add a grammar, so those files still render unhighlighted. - Ambiguity kept deliberate and pinned by tests:
.vstays V (not Verilog — SystemVerilog uses.sv/.svh), alongside the existing.pl→ Perl and.m→ MATLAB decisions.
- The built-in-service check had to move from the Monaco language to the LSP language.
- Syntax highlighting for the 27 languages Monaco ships no grammar for — Zig, Nim, Crystal, V, D, Vala, Odin, Gleam, Haskell, Elm, PureScript, ReScript, Erlang, Nix, CMake, Protobuf, LaTeX, PostScript, MATLAB, Assembly, COBOL, SAS, Ada, Fortran, Prolog, VHDL, FoxPro and Astro, in a new
vibecoder/src/lib/monarch. Each language is a declarativeLanguageSpec(keywords, comment markers, string forms) that one tested factory turns into a Monarch tokenizer and a language configuration, so ⌘/ comments correctly and brackets match rather than only colours appearing. Registration is lazy (onLanguage) and never overrides a Monaco built-in — if Monaco gains one of these, theirs wins and ours is skipped, enforced by a test.- Zig, Nim, Crystal, D and V stopped borrowing C++/Python/Ruby highlighting. The approximation mis-coloured the keywords each language actually has and left their LSP providers registered against
cpp; each now uses its own id and grammar. - Two invariants are now test-enforced, both silently broken before: every id
detectLanguagecan return must be registered (Monaco rejects an unknown id, somatlab/cobol/sas/cmakefiles got neither highlighting nor IntelliSense, since the LSP providers key on the same id), and every server-backed extension must have a highlighting entry — which caught nine (.mts,.cts,.hh,.hxx,.sc,.psd1,.dpr,.prolog,.cbo) that would have rendered as plain text. - The grammar tests run Monaco’s own Monarch engine and assert on real tokens, not on the shape of the definition object — which is the only way to catch what actually breaks. Six genuine bugs surfaced that way and were fixed: in a Monarch regex
@@is an escaped@, so@@?collapsed to@?(an optional at-sign) and coloured every bare identifier in Crystal and ReScript as a variable/annotation; Monarch matches at the cursor with no lookbehind, so MATLAB’sA'transpose needed complete-string-then-operator ordering instead (as written, the rest of every line with a transpose became a string); Ada’s'x'was consumed by theObj'Lengthattribute rule; and CMake’s"${CMAKE_SOURCE_DIR}/src"needed variables inside strings. Also covered: nested comments that would otherwise un-comment the rest of the file ({- -},/+ +/,#[ ]#), PostScript’s nested(a (b) c)strings, COBOL’s column-7 indicator comments and picture clauses, Fortran’s fixed-form column-1C, Prolog’s variable-versus-atom distinction, and case-insensitive keywords in COBOL/Ada/Fortran/VHDL/SAS/CMake.
- Zig, Nim, Crystal, D and V stopped borrowing C++/Python/Ruby highlighting. The approximation mis-coloured the keywords each language actually has and left their LSP providers registered against
- A missing language server is now two clicks, not a copy-paste hunt. The status bar offers a Copy install button beside the warning;
parseInstallHintsplits the backend hint into a runnable command and a documentation link (51 of 79 servers yield a copyable one-liner, 23 a project URL, 5 are genuinely prose like “Included with Xcode”). A test runs the parser across the real hint table inmanager.rsand rejects any command that could not be pasted as-is — that is howcargo install --git, truncated at a URL that was actually an argument, was caught.lsp_language_supportnow returns the rawinstallHintalongside the human-readabledetail, so the editor parses the hint rather than scraping a sentence the backend composed. The command is copied, never executed: installing software is the user’s decision, and several hints need a platform choice first. - Tests — 139
vibe-lspunit tests; 15 end-to-end tests against a real stdio server (including a 1.6 MB notification flood that reproduces the original deadlock, out-of-order replies, timeout recovery, and URI encoding); 3#[ignore]d smoke tests against realrust-analyzer/clangd; 102 frontend tests for the bridge and status indicator, with enums imported from Monaco’s ownstandaloneEnums.jsso a renumbering can’t slip through.cargo check --workspace --exclude vibe-collab,tsc --noEmit, 362vibe-coderand 1353 frontend tests clean.
- The message pump deadlocked the connection. The client forwarded every inbound message into a 32-slot
Changed
- Brand · new app icons across every client, generated from one mark. The icon is now a bold tapered V with a gold cursor block on its baseline, replacing the old V-plus-thin-brackets artwork whose
<>dissolved below ~32px and which carried a stray white hairline around the tile. One silhouette family-wide, tinted per client (VibeCoder blue→violet, VibeCLI App purple→indigo, VibeDesk cyan→blue, VibeMobile green→cyan, Watch/Wear gold→orange with a blue cursor) so the apps stay apart in a dock or app switcher. Hues trackvibecoder/design-system/tokens.css.- Three real bugs fixed, not just a redraw — VibeMobile was still shipping the stock Flutter logo on iOS, Android, macOS and the PWA; VibeCodyWear had a placeholder chevron in
mipmap-hdpionly, with nomipmap-anydpi-v26, so modern Wear OS could not mask it to the device icon shape; and VibeAIChat/VibeDesk’sindex.htmlpointed at/vite.svg, a file that existed in neither app (a 404 favicon). All three now carry the real mark, plusandroid:roundIconin both Android manifests. - Pipeline —
make iconsregenerates all 143 artefacts fromscripts/brand/brandkit.py;make icons-checkfails when a committed icon drifts from the mark. Generated icons stay committed, so an ordinary build needs neither the pipeline nor librsvg.scripts/brand/pngkit.pyis a dependency-free PNG/ICO codec (no Pillow, no ImageMagick) covering the two jobsrsvg-convertcannot do: stripping the alpha channel the App Store rejects on iOS icons, and writing Windows.ico(DIB entries below 256px, PNG at 256). - Per-platform variants, because one PNG cannot serve them all — full-bleed no-alpha squares for iOS/watchOS, Apple’s 824-in-1024 grid plus drop shadow for
.icns, Android’s three adaptive layers (foreground scaled to the 66-of-108dp safe zone, background, monochrome for themed icons), circular artwork for Wear OS, and PWA maskable icons. Below 40px the artwork automatically drops the cursor block and grows the V, since at 16px the block is an indistinct smudge. Rationale and the variant table live inassets/brand/README.md.
- Three real bugs fixed, not just a redraw — VibeMobile was still shipping the stock Flutter logo on iOS, Android, macOS and the PWA; VibeCodyWear had a placeholder chevron in
- VibeDesk is now a released artifact. It had shipped in no release:
release.ymlbuilt VibeCoder and VibeAIChat only,ci.ymldid not check it at all, andvibedesk/src-tauri/tauri.conf.jsondeclared neithersigningIdentitynorentitlements— so the third desktop shell was buildable locally and invisible everywhere else.- Release — new
build-vibedeskjob mirroringbuild-vibeaichat: the same five-platform matrix (macOS arm64/x64, Linux x64/arm64, Windows x64), the same dual-mode Apple codesigning that falls back to ad-hoc with a::notice::whenAPPLE_CERT_P12_BASE64is absent, and the same artifact collection (.dmg/.deb/.AppImage/.msi/ NSIS.exe). Added torelease.needs[]and given its own downloads table in the release body. Deliberately not added to the release job’s criticalif:gate — the job has never run on Linux or Windows, and a first-run failure there must not block an entire release; promote it withneeds.build-vibedesk.result == 'success'once it ships green. - CI — new
vibedesk-checksjob (typecheck+ theno-inline-editVX-013 guard, the latter having existed as an npm script that nothing ran), wired intoci-gate’sneedsand its aggregate result. A release job for code CI never checks is a trap; both checks verified green locally before wiring. - Signing —
vibedesk/src-tauri/macos/entitlements.plistadded (JIT + unsigned-executable-memory for the webview, disabled library validation for bundled dylibs, network client/server since every desktop shell autostarts the daemon, and user-selected file read-write fortauri-plugin-dialog), withsigningIdentity: "-"and theentitlementspath intauri.conf.jsonso VibeDesk matches the other two shells. - Cost note — VibeDesk’s
src-tauriembeds the wholevibeclicrate for ProfileStore access exactly as VibeCoder’s does, so the new job’s cost tracks VibeCoder’s rather than VibeAIChat’s; on macOS that includes the Metal mistral.rs backend, pulled unconditionally by a[target.'cfg(target_os = "macos")']block irrespective of thevibe-mistralrsfeature flags.
- Release — new
- Docs · macOS signing, notarization and DMG troubleshooting (
docs/release.md). The existing section covered only the seven CI repository secrets; added the local path — install the Developer ID cert in your login keychain (theAPPLE_CERT_P12_BASE64secret is a CI-only import mechanism), exportAPPLE_SIGNING_IDENTITY/APPLE_TEAM_ID/APPLE_ID/APPLE_PASSWORD, and build. Documents thatAPPLE_SIGNING_IDENTITYoverrides the committed"signingIdentity": "-", so an ordinary dev build still needs no certificate; thatAPPLE_PASSWORDmust be an app-specific password; that notarization costs 2–15 min per build; and the threecodesign/stapler/spctlcommands that verify the result. Also recorded that VibeDesk was absent fromrelease.ymland declared nosigningIdentity— it was not released at all at the time. (No longer true: abuild-vibedeskjob now exists and passes the same signing and notarization secrets as the VibeCoder and VibeAIChat jobs. Corrected 2026-08-26; the entry is left in place because it was accurate when written.)error running bundle_dmg.sh— new troubleshooting entry, written against a reproduction.bundle_dmg.shdrives Finder over AppleScript and exits64when thatosascriptcall fails, but Tauri captures the script’s output and surfaces onlyfailed to run …/bundle_dmg.sh, so the reason is unrecoverable from the build log. Documents the fix that was actually verified —CI=true(notCI=1: Tauri bindsCIto its own--ciflag, which accepts onlytrue/falseand otherwise dies witherror: invalid value '1' for '--ci'), which passes--skip-jenkinsand skips the AppleScript, costing only the DMG’s cosmetic layout. Also covers the leaked-scratch-volume trap where a failed run leaves/Volumes/dmg.*mounted and breaks every retry with a different error, the per-app nature of the Automation permission, and the-1728“Can’t get disk” race that the script guards with only a fixed 2 s sleep — which is why the same script can succeed standalone on an idle machine and fail right after an LTO link.
-
Mobile · Flutter toolchain floor — raised the CI/release Flutter pin from
3.29.3to3.44.2(Dart 3.7 → ≥3.10) and thevibemobileDart SDK floor to^3.8.0, to supportflutter_lints6.0.0 (requires Dart^3.8.0) and the regenerated lockfile (resolved deps require Flutter ≥3.38.4 / Dart ≥3.10.3).FLUTTER_VERSIONupdated inci.ymlandrelease.yml; platform-requirements table indocs/vibemobile.mdupdated to match. - mistral.rs fork synced to upstream v0.9.0 — the pin was 9 commits ahead / 189 behind upstream (fork base
2d4ba4f1≈ v0.8.1). Our TurboQuant KV-cache patch was replayed onto upstream master (8010b6a0, post-v0.9.0) asTuringWorks/mistral.rs@vibe/kv-cache-codec-kernels-v0.9;vibe-infer/Cargo.tomlnow pins5860f815. The previous branchvibe/kv-cache-codec-kernelsis untouched atf8f3a105, so the bump reverts with one line. Zero VibeCody source changes — every API we consume survived (with_isq(IsqType)now routes through the newIsqSettingviabuilder_macros.rsbut keeps its signature; sampler setters,From<TextMessages> for RequestBuilder, andmistralrs::coreunchanged).- What we gain — the scheduler / concurrent-serving overhaul (#2354) against
inference_server.rs; the Metal runtime+AOT compilation rework (#2288) plus hardened kernel builds (#2176, #2139); aarch64 + x86 CPU decode/prefill kernels (#2304, #2311); roughly six removed panic paths that sit in daemon code (chat-template panics #2286, GGUF special-token OOB #2282, engine shutdown #2266, client-disconnectSendError#2170, engine-creation error propagation #2226, errored-sequence cleanup #2243);logprobscorrected to natural log (#2327); reversed FCFS priority in paged preemption (#2250). New models: Gemma 4 12B (+MTP spec decoding), LFM 2.5, Hunyuan v1, DiffusionGemma. - candle 0.10.2 → 0.11.0, via a workspace
[patch.crates-io]— upstream pins candle to a git rev (27f20fea), not the crates.io release.vibe-inferimplementsmistralrs::core::KvCacheCodec, whose methods takecandle_core::Tensor; a registry candle and a git candle are different crates to cargo even at the same version, so the impl would not typecheck. The patch forces all six candle crates onto one source.vibe-inferis the workspace’s only candle consumer;candle-transformershas notokenizersdep, sominilm.rsis unaffected. - Divergence shrank — the fork patch is 16 files instead of 17.
mistralrs-quant/build.rsdropped out entirely: #2288 replaced the hand-maintainedMETAL_SOURCESarray with ametal_source_set!macro that feeds both the AOT and runtime-compile paths from one list (so the split-registration bug class of #2169 is gone by construction), and CUDA was always covered by the existingkernels/*/*.cuglob. The#[allow(dead_code)]onMoEExperts::num_expertsalso dropped — it is a usedpubfield upstream now. RotatingCachehooks re-derived, not merged — upstream rewrote the cache into a relocating ring buffer (write_pos/window_start/retained_len). Invariant is now explicit:all_dataalways holds encoded values, so plain tensors are encoded on the way in, buffer slices decoded on the way out, and buffer-internal window relocation deliberately skips the codec (running it would double-quantize the retained window). The v0.9snapshot()/restore_from_snapshot()machinery, which our patch predated, needed the codec carried onRotatingCacheSnapshotand re-encoded on restore — without it, restore writes plain values into a buffer whose readers assume encoded, corrupting speculative-decode rollback with no compile error.- Two Metal bugs the sync exposed, both silent under the old pin. (1)
call_turboquant_encodebound its output buffer via a bare&Buffer→set_buffer(the input path) instead ofOutput::new→set_output_buffer, so the kernel’s write was invisible to candle 0.11’s input/output encoder tracking; the Metal-vs-CPU parity test went from passing to 1.48 max abs diff against a 5e-3 tolerance — wrong numbers, not an error. All 48 other call sites in that file already usedOutput::new. (2)turboquant.metaldeclared a file-scopeconstant uint SIMD_SIZE, private under per-file AOT compilation but colliding with MLX’sMLX_MTL_CONST int SIMD_SIZEonce #2288’s runtime path concatenates every source into one translation unit; sinceKernels::LIBRARYis a globalOnceLock, that single collision failed the whole library and took out every quant kernel whenever the precompiled metallib was absent (MISTRALRS_METAL_PRECOMPILE=0— the standard fallback when the Apple Metal Toolchain is missing). Renamed toTQ_SIMD_SIZE. - Verification — 31/31 fork
kv_cachetests (including all of upstream’s own ring-buffer tests) and 25/25vibe-infertests withmistralrs,mistralrs-metal, on both the AOT and runtime-compile Metal paths;turboquant_encode_{float,half}symbols confirmed present in all three builtmistralrs_quantmetallibs;cargo check --workspace --exclude vibe-collabclean. Note that the GPU parity test (native_codec_metal_matches_cpu_within_fp16_tolerance) is the only thing that catches this class of bug — a green build and a successful kernel dispatch both looked like success. - Dependency-surface note — upstream 0.9 pulls
mistralrs-code-exec+mistralrs-sandboxunconditionally (withlandlock/seccompileron Linux only).cargo tree -e featuresconfirms onlydefault,metal,mistralrs-paged-attnare enabled onmistralrs-core, so thecode-executionfeature is off and the Python code-execution tool is never exposed.cargo deny check(andcargo deny --all-features check, assecurity.ymlruns it) passes clean — advisories, bans, licenses, sources — withhttps://github.com/huggingface/candle.gitadded to theallow-gitlist, which the new[patch.crates-io]requires underunknown-git = "deny".deny.toml’sRUSTSEC-2025-0057(fxhash) note was corrected: it claimed the advisory would clear on the next fork sync, but bm25 is still 2.3.2 and still depends on fxhash, so it was never ours to fix. - Layout assumptions made explicit —
turboquant/ops.rsdestructuredstorage_and_layout()as(storage, _)for the rotation/projection matrices and passed the kernel a base pointer, so a non-zerostart_offsetor non-contiguous view would have been read from element 0: wrong numbers, no error. Callers useTensor::from_vecso it held, but that is the caller’s invariant, not one the module can enforce — the same shape of unchecked assumption behind the output-buffer bug. Both backends now go through a sharedensure_matrix_layout, matching the checkinputalready had.
- What we gain — the scheduler / concurrent-serving overhaul (#2354) against
Fixed
- CI · the Metal GPU path was never executed anywhere. Every
ci.ymljob runs onubuntu-latest, wheremistralrs-metalcannot build, and the note claiming “the release workflow uses--all-featureson macOS runners” was false —--all-featuresappears only in that comment and insecurity.yml’s cargo-deny step, which compiles nothing.mistralrs-metalis compiled in macOS release builds, but via thecfg(target_os = "macos")dep invibecli/Cargo.toml, and no workflow ever ran its tests. Net effect: the TurboQuant GPU codec was shipped on the strength ofcargo checkalone, and both Metal bugs found during the v0.9 sync would have reached a release. Added ametal-gpu-testsjob (macos-latest) that runs thevibe-infersuite on both Metal paths — precompiled metallib andMISTRALRS_METAL_PRECOMPILE=0runtime compilation — because those exercise different code and each caught a different one of the two bugs. Wired into the requiredci-gate, and the false comment corrected.
[0.5.7]
Release-engineering patch — restores the artifacts that didn’t build for v0.5.6.
Fixed
- CycloneDX SBOM job (
a6d670bf) —cyclonedx-py requirementsaccepts the requirements path positionally, not as-i FILE; the bad flag caused the tool to fall back to looking for./requirements.txtand emitCRITICAL | CDX > Could not open requirements file. Drop the-isovibe-rl-py.cdx.jsonis produced. Closes #28. - Mobile · iOS build (
b8d95e0f) —vibemobile/ios/Runner/AppDelegate.swiftreferencedFlutterImplicitEngineDelegateandFlutterImplicitEngineBridge, both of which were introduced in Flutter 3.38 for the UIScene rework; the CI Flutter SDK is pinned to 3.29.3, so the swift-frontend reportedCannot find type 'FlutterImplicitEngineBridge' in scope. Rewrite to the Flutter 3.29-compatibleGeneratedPluginRegistrant.register(with: self)pattern and register the relay-credentials method channel synchronously indidFinishLaunchingWithOptions. Closes #29. - Watch · watchOS build (
014f5cce) —GoalsView.swift,JobPickerView.swift,RecapView.swift, andTaintedConfirmationView.swiftwere on disk and referenced byContentView.swift/SessionPickerView.swiftbut never added toVibeCodyWatch.xcodeproj’sPBXSourcesBuildPhase. The Swift compiler reported fourcannot find … in scopeerrors and the watchOS simulator app build exited 65. Register each as aPBXFileReference+PBXBuildFile, add to the group and sources phase (plutil -lintpasses). Closes #30. - Watch · Wear OS build (
6193920a) —JobRecapTileService.ktandGoalsTileService.ktimportandroidx.concurrent.futures.CallbackToFutureAdapterandcom.google.common.util.concurrent.{Futures, ListenableFuture}, andRecapScreen.ktusesandroidx.compose.ui.tooling.preview.Preview; none were declared as dependencies, so:app:compileReleaseKotlinfailed. Addguava(33.4.0-android),androidx-concurrent-futures(1.2.0), andandroidx-compose-ui-tooling-preview(1.7.6) tolibs.versions.tomlandimplementationthem inapp/build.gradle.kts. Closes #31. - Docker image build (
99d8adfe+f922536b) — the Dockerfile’s two-phase cargo cache (copy manifests → stub sources → prebuild deps → copy real sources) had drifted from[workspace]members. Seven members added since March (vibecli/crates/vibe-sandbox{,-native,-firecracker,-hyperlight},vibecli/crates/vibe-broker,vibecoder/crates/vibe-infer,vibe-memory) had no manifest COPY, so cargo refused to resolve the workspace. After the manifest sync, the real cargo build then failed becausevibe-memory/src/was never copied over the empty stub, leavingvibecliunable to findMemoryContextHub,ProjectMemStore,GlobalMemStore,MemoryMeta. Add the missing manifests + stubs + real-source COPY. Closes #32.
Docs
- docs/release.md, docs/vibemobile.md, docs/watchos.md, docs/wearos.md (
41f189eb) — fix the download links to match the actual Tauri/Flutter output filenames:VibeAIChat_*(notVibeCLI_*),VibeCody-Mobile-vX.Y.Z-{ios,android}.*,VibeCody-WatchOS-vX.Y.Z.app.zip,VibeCody-Wear-vX.Y.Z.*. Surface the newaarch64.AppImageandarm64.debartifacts that landed in v0.5.6.
[0.5.6]
Added
- B2.9.daemon — Plugin hooks fire on the daemon agent path too (
41c6382e) — Closes the parallel gap to B2.9 on the daemon side: everyAgentLoop::newinserve.rs(/v1/agentstart, ACP submit-task, timed-task path) now callsplugin_runtime::merge_with_plugin_hooks(workspace, vec![])and attaches the resulting HookRunner beforeagent.run. Plugin hooks were previously silently bypassed on the most-used path (mobile, watch, VibeCoder, VS Code, JetBrains all go through/v1/agent); admin policy now reaches them. User hooks remain CLI-only by design — turning on user-hook dispatch from remote clients is a separate decision. - B2.9 + B2.10 — Plugin hooks and rules now actually run — Two follow-up slices that close the original B2 deferrals.
plugin_runtime::merge_with_plugin_hooks(workspace, user_hooks)(543255f4) opens the WorkspaceStore, fetches policy-active plugin hooks, converts eachHookComponentinto avibe_ai::HookConfigwith aCommandhandler that runs the absolute path under the plugin install dir, and appends them after the user’s hooks — first BLOCK still wins so user veto outranks plugin policy on the same event. Wired at both CLIHookRunner::newsites (run_parallel_agentsorchestrator +run_agent_repl_with_contextREPL). Best-effort withwarn!on any store/loader failure so admin policy can extend the CLI agent path but never break it.context_assembler::collect_plugin_rules(403a53cd) does the parallel job for Markdown rules: every On / Required plugin’s rule files render under “### {plugin}/{rule}” inside a newplugin_rulesContextSection at priority 1, joining the same system-prompt lane the CLAUDE.md / VIBECLI.md / AGENTS.md project memory feeds. Both chat and agent collectors consult it, so plugin rules influence REPL chat and every agent-task path.KNOWN_SECTION_NAMESgrows the new entry so/v1/capabilitiesadvertises the shape correctly. Daemon-sideAgentLoopsites inserve.rsintentionally not wired in this slice — those have never run user hooks either, and turning on hook dispatch there is a separate decision that affects user-configured hooks too. - VS Code Agent Hooks — protocol parity with CLI + JetBrains —
vscode-extension/src/hook-executor.tsimplements the same seven-event hook contract asvibecli-cli/src/hook_abort.rsand the JetBrainsHookExecutor:sh -c <command>(orcmd /con Windows), exit-code semantics (0 = allow, 2 = block, other = generic-error / non-blocking), structured-JSON-decision stdout override, 30 s per-hook timeout (timeout → BLOCK), spawn-failure ALLOW + warning, and ordered-chain short-circuit on first BLOCK.UserPromptSubmitis gated at every prompt entry point —vibecli.startAgent,vibecli.chat,vibecli.inlineEdit,vibecli.sendSelection, and the sidebar chat webview — each carrying an event-source discriminator ("source": "agent" | "chat" | "inline-edit" | "send-selection" | "chat-webview") so policies can vary by entry point.vibecli.hooksjoins the configuration schema as anarray<{name, event, command, enabled}>with the seven-kind event enum, surfaced inline by the VS Code Settings UI (aeae6c83). - JetBrains Agent Hooks — meaningful gate coverage (P3.10) — Four-commit slice (
7709bc0b→080bf920) brings the JetBrains plugin to hook-protocol parity with CLI/Tauri.HookExecutorservice mirrorsvibecli-cli/src/hook_abort.rs: subprocess invocation viash -c <command>, exit-code semantics (0 = allow, 2 = block, other = generic error / non-blocking), structured-JSON-decision stdout override ({action, reason?, message?}), 30 s per-hook timeout (timeout → BLOCK), spawn-failure ALLOW + warning (matches CLI), and ordered-chain short-circuit on first BLOCK.HookConfig { name, event, command, enabled }persists via the existingPersistentStateComponentinfra. Settings UI under IDE Settings → Tools → VibeCLI grows a hooks table with Add / Remove viaToolbarDecorator— Event column constrained to the seven allow-listed kinds matchingplugin_manifest::ALLOWED_HOOK_EVENTS. Both user-driven prompt-submission paths now run through the chain:AgentPanel.startAgentandInlineEditAction.actionPerformedfireUserPromptSubmitbefore any prompt reaches the daemon. Payload carries an event-source discriminator ("source": "inline-edit") so policies can vary by entry point. 14 JUnit cases cover decision semantics, structured-JSON override, chain short-circuit (sentinel-file proof), payload-on-stdin (capture viacat), and the event-kind allow-list as a drift guard. Advisory firings for SSE-arrivingPreToolUse/PostToolUse/Stopdeferred until a concrete audit-trail use case emerges (the daemon has already run its own pre/post chain by the time those events surface on the plugin side, so firing again would double-fire). - A1 — MCP Apps generic React embedding host (SEP-1865) —
McpAppEmbed.tsxrenders fencedmcp.appblocks inline in chat as a typed React card: title + component+version chip + collapsible Props (JSON) + CSP declarations (informational) + action buttons. Components are an allow-list (react@18,react@19,json-view,list,card); unknown component refs render a clear “unsupported” warning with props still inspectable — the host never executes arbitrary JSX. Action clicks dispatchvibecoder:mcp-app-actionwindow events for the chat layer to consume. Newmcp_apps_parseTauri command bridges the daemon-side parser (mcp_apps_payload.rs, shipped earlier as647b58de) to the webview as defence in depth. Fence regex inAIChat.tsxrelaxed from\w*to[\w.+/@-]*so the full MIME-like tagapplication/vnd.mcp.app+jsonmatches without truncation. Malformed payloads fall back to a plainCodeBlockso the raw bytes stay visible (39e95b17). Closes the last open Phase 53 P0 item — all of A1–A11 now shipped. - B2.8 + B2.12 — Plugin governance follow-ups —
ConfigPortability::register_plugin_serversregisters MCP-server components from policy-active plugins under namespaced idsplugin:<plugin>:<component>, disjoint from the flat user-configured id space (16da6354).plugin_install::install_from_urladds HTTPS-only URL fetch (60 s timeout, 50 MB cap, scheme guard) sovibecli plugin install <https://…>works alongside the local-file path; newplugin_install_from_urlTauri command and aLocal file | HTTPS URLtoggle in the governance panel (b7e7f988). - B2 — Plugin bundle format with admin install policies — Phase 54 P0 shipped end-to-end across 7 slices. Inner
vibecli-plugin.tomlmanifest schema + validator (B2.1,cea41606); detached per-publisher P-256 ECDSA signing via siblingvibecli-plugin.sig(B2.2,6275cf06);WorkspaceStoreplugin_policiestable with Off / On / Required tiers and an admin-only Required-pin guard (B2.3,eb7dcbfe); atomic install (stage → swap with RAII cleanup) preserving Required pins across force re-install (B2.4,2d52bb4e); runtime viewplugin_runtime::enabled_*returning only components from plugins with policy ≠ Off (B2.5,82d4a00b);PluginGovernancePanel.tsx+ 5 Tauri commands (plugin_install_from_file,plugin_list_installed,plugin_uninstall,plugin_get_policy,plugin_set_policy), all sensitive-path-gated (B2.6,fb9b80b6); first per-loader activation — MCPlist_skills/get_skillnow serve built-in + enabled-plugin skills tagged with{"kind": "builtin" \| "plugin", "plugin": "<name>"}provenance (B2.7,9c0ac982+32793d4d). All four design anchors held: no telemetry-driven personalization, client-side admin-authored policy, open MCPB bundle format (reusesmcpb_bundle.rs), and per-publisher P-256 trust roots (reusessigned_agent_card.rsJWK). See §23 in FEATURE-REFERENCE. /goal— durable execution intent — A new cross-session primitive: each goal is a persistent record of intent (title + statement + success criteria + status: Active / Paused / Done / Abandoned) that decomposes into anExecutionPlanon demand and gathers a link graph of contributing sessions / jobs / recaps. Shipped end-to-end across 7 slices: types + schema in~/.vibecli/sessions.db(G1.1), daemon/v1/goalsCRUD + plan/link/start routes (G1.2 / G1.3), REPL/goal new|list|show|status|link|start|delete(G1.4), VibeCoder Goals panel with slash-palette opener (G1.5), curated/watch/goalsfor mobile + Apple Watch + Wear OS (G1.6), and VS Code + Agent SDK + design docs (G1.7). Seedocs/design/goal/README.md. Tauri commands use theexec_goal_*prefix to avoid conflict with the existingCompanyGoalsPanelsurface./goal— hardening round (G4) —GET /v1/goals/:id/tree?depth=Nrecursive subtree walk (depth clamped 1..10, default 3, cycle-safe, truncation flag) andGET/PUT/DELETE /v1/goals/currentfor per-workspace “current pin” withpinned_goalscascading on goal delete. CLI gains/goal pin|unpin|currentsubcommands; aggregate/v1/goals/:id/recaphonors{ provider, model }for LLM synthesis with heuristic fallback (response carriesrecap_synthesizer). Apple Watch routes its “Start session” through the new curated/watch/goals/:id/startwrapper, and the VS Code extension grows avibecli.goalsViewtree-view in the sidebar with refresh + per-row context-menu actions./goal— fan-out round (G5) — Wear OSGoalDetailScreengains a “Start session” chip backed byWearNetworkManager.startGoal()(curated/watch/goals/:id/start)./agentauto-links new sessions to the pinned goal for the daemon’s workspace (or the global slot) — silent best-effort, never blocks session creation. The TypeScript Agent SDKgoals.*namespace addstree(id, depth?),pin(id, ws?),unpin(ws?),current(ws?), andrecap(id, { provider, model }); the FlutterApiClientgains parallelgetGoalTree,getCurrentGoal,pinGoal,unpinGoal. The VibeCoderGoalPaneladds a tree-view toggle (indents children under parents) and an “Aggregate recap” section that routes throughselectedProvider+selectedModelfrom the toolbar (heuristic fallback when either is empty)./goal— pin UX round (G6) — Pin/unpin chips in VibeCoderGoalPaneland the mobile detail sheet, with a ★ marker on whichever goal is pinned in the list. The agent stream emits asystemevent (AgentEventPayload::system) on every auto-link so SDK / VibeCoder / CLI consumers see “Auto-linked to pinned goal {id_prefix}: {title}” before the model’s first token.auto_link_to_pinned_goalnow returns the linked(goal_id, title)so callers can wire attribution UI. End-to-end tests confirm the helper inserts agoal_linksrow through the realSessionStore::open_default()path (HOME-redirect pattern from G4.1)./goal— goal-aware agent context (G7) — When/agentauto-links a session to a pinned goal, the daemon now synthesizes a model-readable preamble from the goal’s title, statement, success criteria, andcurrent_planand injects it intoAgentContext.approved_plan(only when that field is empty — the Phase 7 S3context_requestpath is untouched).auto_link_to_pinned_goalreturns the fullGoalstruct rather than just(id, title)so callers don’t re-fetch. Result: agents now run goal-aware, not just goal-attributed. 5 unit tests cover preamble shape (bare title, statement, criteria, plan steps, empty-plan edge)./goal— client surfacing round (G8) — VS Code extension renders the daemon’ssystemauto-link event with a distinct[goal]prefix in the agent output channel (0b64f6b4); the SDKAgentEventTypeis extended so programmatic consumers also see the new kind. Flutter mobile gains a “+ New Goal” flow — AppBar+action + empty-state button → modal with machine picker (only shown when ≥2 paired), 120-char title field, optional statement, and a 409-aware “already exists” snackbar (d2e1b236). VibeCoder main chat intentionally not touched — it runs the agent in-process via Tauri commands instead of consuming the daemon’s SSE stream, so the system event has no rendering surface there without a larger refactor./goal— “Working toward” banner (G9) — Pinned goal now surfaces above the VibeCoder chat tabs as a compact banner (PinnedGoalBanner.tsx), making the auto-link target visible from the surface users spend the most time on. Pollsexec_goal_currentevery 15 s for external pin changes (CLI / mobile) and also listens forvibecoder:pin-changedwindow events so a pin/unpin from the Goals panel updates the banner instantly. Click the ✕ to unpin. Zero chrome when nothing is pinned (43e7f697)./goal— search + tag chips (G10) —GET /v1/goalsaccepts?q=<text>for case-insensitive substring search across title + statement, AND-ed with the existing status / workspace / tag / limit filters. VibeCoderGoalPanelgains a 200 ms-debounced search input in the left header. Tags (schema since G1.1, previously read-only) are now inline-editable in the detail-header chip list — × per chip to remove, in-line input to add (Enter or blur to submit), both routing throughexec_goal_update. 2 new daemon tests cover the q-filter; frontendnpx tsc --noEmitclean (b755926b)./goal— TUI tree + Watch ★ marker (G11) — TUI Goals screen gains atkey that toggles between flat list and tree layout: children indent under parents via a client-side BFS overparent_goal_id, mirroring VibeCoder’s tree mode. Title-bar advertises the new key and current view mode. The curated/watch/goalspayload gains apinned: boolfield (one global-pin lookup plus one workspace-pin lookup per distinct workspace in the list — bounded at the 25-row Watch cap); Apple Watch + Wear OS goal rows render a ★ on whichever goal matches. Older daemons that lack the field decode cleanly (pinned: Bool?on Swift,optBoolean(..., false)on Kotlin). 3 new TUI tests cover tree ordering, orphan-as-root, and view-mode toggle./goal— Watch ★ everywhere (G12) — Curated/watch/goals/:idenvelope gains an envelope-levelpinned: bool(workspace-specific OR global slot, computed by the same logic the list uses) so the watch detail screen can render the ★ without a second/v1/goals/currentround-trip — the watch never hits/v1/*directly. Apple WatchGoalDetailViewand Wear OSGoalDetailScreenshow the ★ on the title; Wear OSGoalsTileServicenow prefers the pinned goal over the freshest-updated row (“what am I working on” beats “what did I touch last”) and prefixes the tile body with ★ when pinned./goal— VS Code ★ pin parity (G13) — VS Code goals tree closes the cross-surface ★ loop on the editor side:GoalTreeItemprefixes the title with ★ for pinned rows and pinned roots sort above the rest so “what am I working on now” lands at the top of the sidebar. NewVibeCLIClient.getPinnedGoalIds(workspace?)unions the daemon’s global pin with the workspace pin (union of two/v1/goals/currentcalls) so a goal pinned from any surface — mobile, watch, VibeCoder, CLI — surfaces in the editor./goal— TUI ★ pin parity (G13 cont.) — TUI Goals screen catches up to the rest: every row whose id appears inpinned_goals(any workspace, including the global slot) renders with a yellow bold ★ in front of the title — same glyph the other surfaces use. Newpkey toggles the pin on the selected row, advertised in the screen’s title bar alongsidef,t,r. Unpin walks every workspace slot pointing at the goal (covers the rare both-workspace-and-global case); pin writes to the goal’s own workspace (or globally if the goal is workspace-less), mirroring how the watch / mobile pin flows scope their writes.SessionStoregains two helpers:list_all_pinned_goal_ids()(oneSELECT DISTINCTper refresh) andlist_pin_workspaces_for_goal(id)(used by the unpin-everywhere walk).- TurboQuant-compressed OpenMemory index —
CompressedMemoryIndexreplaces the legacy f32 HNSW with a ~3 bits/dim PolarQuant + QJL backing store (≥ 8× smaller on disk, same insert/query API). Ships behind no feature flag — every memory write benefits. /memory/statsexposes index telemetry — response now includesembedding_dim,embedding_compression_ratio, andembedding_backend(always"turboquant"today; treat as opaque). Surfaced in the VibeCoder OpenMemory panel, theopenmemory_index_statsTauri command, and the MCPmemory_statstool.vibe-infercrate — pure-Rust local inference traits (Embedder,TextGenerator) with a stub backend by default and an opt-incandlefeature that loadssentence-transformers/all-MiniLM-L6-v2(384-dim, mean-pooled + L2-normalized) via candle 0.10 + hf-hub.candle-metaladds Apple GPU acceleration. Default workspace builds pull no ML deps.- Linux arm64 Tauri builds — VibeCoder and VibeCLI App now ship
.deb/.AppImageforaarch64Linux via the GitHub-hostedubuntu-22.04-armrunner (free for public repos). Matrix coverage now matches VibeCLI (which already had Linux arm64 viacross). - Ubuntu 24.04 forward-compat smoke job — new
smoke-linux-nextCI job runscargo check --releaseonvibecoder/src-tauriandvibeaichat/src-tauriagainst webkit2gtk-4.1 on Ubuntu 24.04.continue-on-error: trueand excluded fromrelease.needs[], so distro-drift regressions surface early but never block a tag. Foundation for the Ubuntu 26.04 LTS roll-forward (2026-04-23).
Changed
- Explicit macOS 12.0 floor — both
vibecoder/src-tauri/tauri.conf.jsonandvibeaichat/src-tauri/tauri.conf.jsonnow setbundle.macOS.minimumSystemVersion = "12.0"(was the Tauri 2 default of 10.13). Matches Apple’s current supported-OS cutoff.
Security
- Bump
rand0.8 → 0.9 acrossvibecli/vibecli-cli,vibecoder/src-tauri,vibecoder/crates/vibe-core, andvibecoder/crates/vibe-collabto pick up GHSA-cq8v-f236-94qc (low severity; unsound interaction betweenrand::rng()and customlogimplementations invoking RNG during reseed). Call sites updated to the 0.9 API (thread_rng→rng,.gen::<T>()→.random::<T>(),.gen_range(…)→.random_range(…)).p256 0.13SigningKey::random call-sites now usep256::elliptic_curve::rand_core::OsRngto pin the rand_core 0.6 RNG the crate’s signature bound requires.
Added (inference)
- Mistral.rs backend in
vibe-infer(Phase 1 of the Rust-inference runtime plan). Newvibe-infer::mistral::MistralGeneratorimplementsTextGeneratoron top of themistralrs 0.8.1crate (PagedAttention, ISQ, LoRA, OpenAI-compat types, candle 0.10.x transitively). Feature-gated behindmistralrs(CPU) /mistralrs-cuda/mistralrs-metal/mistralrs-flash-attn; defaults unchanged so baseline builds stay fast. Smoke example atexamples/generate.rsexercises Qwen/Qwen2.5-0.5B-Instruct end-to-end. The dep is pinned to the TuringWorks fork (TuringWorks/mistral.rs@3d422fde, branchvibe/kv-cache-codec, upstream basev0.8.1) via git + rev so we can iterate on theKvCacheCodechook without waiting on upstream review. Explicit SHA pin keeps local / CI / release builds reproducible — bump therevinvibe-infer/Cargo.tomlas the fork advances. KvCacheCodectrait landed on the fork (Phase 3 follow-up).TuringWorks/mistral.rs@vibe/kv-cache-codecadds apub trait KvCacheCodec { fn encode/decode/name }inmistralrs-core::kv_cachewith aPassthroughCodecdefault, threaded throughSingleCache/RotatingCacheasOption<Arc<dyn KvCacheCodec>>.None(the default) short-circuits to the existing bit-exact append / current_data paths; installing a codec runs encode on every write and decode on every read. Shape + dtype must be preserved by the codec, so the underlyingslice_set/narrowbuffers stay uniform — true packed-storage codecs need a richer interface and are out of scope for this landing. Round-trip tests on both cache types prove the dispatch path. The cache types (KvCache,SingleCache,RotatingCache) are now re-exported atmistralrs-corecrate root so downstream crates can install codecs without plumbing through private modules.CandleTurboQuantCodecbridge invibe-infer::kv_cache_codec— candle-backed implementation of the fork’sKvCacheCodectrait that reuses the pure-RustKvCacheTurboQuantfrom the Phase 3 spike. Encode shuttles the tensor to host-f32, quantizes + reconstructs eachhead_dimvector via PolarQuant + QJL, and returns a same-shape same-dtype tensor; decode is identity (the reconstruction already happened in encode). Shape contract:head_dimis the last axis. Six unit tests cover shape/dtype preservation, determinism across codec instances, wrong-axis rejection, uniform-random cosine floor matching the spike, and an end-to-end install intoSingleCacheproving the trait dispatch works. Gated behind themistralrsfeature so default builds stay thin.- Install-all codec fan-out + discovery helpers on the fork (Phase 3 wiring).
TuringWorks/mistral.rs@3d422fdeaddsKvCache::set_codec(fans out to both K and V sub-caches; no-op onShared),EitherCache::set_kv_cache_codec(codec) -> usize(walks every attention layer of aNormalorHybridcache, skipping recurrent layers, and returns the install count for logging), plus two async accessors onMistralRs/Model:set_kv_cache_codec(codec, model_id)— locks the pipeline behind the engine’sRebootStateand installs in one shot — andkv_head_dims(model_id)— returns(k_head_dim, v_head_dim)fromGeneralMetadata::model_metadataso callers can size a codec from the loaded model instead of hard-coding. 8 fork-side tests; lockout is held only for the duration of a pointer-clone per layer, so it’s safe to await mid-runtime even with other engines busy. MistralGenerator::loadinstalls KV codec on demand (Phase 3 completion). NewKvCacheMode { Fp16, TurboQuant { seed, qjl_proj_dim } }+KvCacheMode::from_env()resolver readsVIBE_INFER_KV_CACHE/VIBE_INFER_KV_CACHE_SEED/VIBE_INFER_KV_CACHE_QJL_DIM.MistralGenerator::load/load_isqcallinstall_codec_after_load, which queries head-dim from the fork, builds aCandleTurboQuantCodec, and fans it out viaModel::set_kv_cache_codec. Layer count is logged atinfofor observability. MLA-style models (k_head_dim ≠ v_head_dim) warn and pick the larger; pipelines withoutModelConfigLikemetadata (speech / diffusion) error out loudly instead of silently booting with a broken codec.- InferencePanel KV-cache dropdown (Phase 3 UX). When
Backend = Mistral.rsis selected, a new “KV Cache” dropdown appears with optionsFP16 (default)andTurboQuant (experimental). PickingturboquantprependsVIBE_INFER_KV_CACHE=turboquantto the generatedcargo runinvocation so the daemon picks it up at load time — no recompile required. Hidden for sidecar backends (where the codec has nowhere to install). - Mistral.rs wired into InferencePanel + inference_server (Phase 1.4).
InferenceBackend::MistralRsis now a first-class variant alongside vLLM / TGI / Triton / llama.cpp / Ollama, markedis_in_process()so callers can skip sidecar provisioning.build_mistralrs_command()emits theVIBE_INFER_MODEL=… cargo run -p vibe-infer --features {mistralrs | -cuda | -metal | -flash-attn} --example generate …invocation;generate_k8s_inference_deploymentuses thevibecody/vibecli-daemon:latestimage for the in-process path. The VibeCoder Deploy tab adds an “Accelerator” dropdown (CPU / Metal / CUDA / FlashAttn-2) that replaces Port when Mistral.rs is selected, and hides the GPU-count / tensor-parallel / quantization / batch / VRAM-slider / Docker-Compose controls — they don’t apply to an in-process backend that picks its device at compile time. KvCacheBackendtrait +KvCacheMethodenum invibe-infer::kv_cache(Phase 2 — experimentation harness). Declarative surface for swapping attention KV-storage strategies:Fp16 | Fp8 | Int8 | TurboQuant | Custom(name). Each variant carries a CLI-flag name and a bytes-per-element estimate so pod-sizing heuristics agree across backends.KvCacheReportis the harness record — tokens/sec prefill + decode, resident bytes, optional perplexity / recall@k. No runtime effect yet; the actual KV-cache kernel work lands in Phase 3 as amistralrs-quantcontribution.- Phase 3 KV-cache TurboQuant spike (
vibe-infer::kv_cache_tq+examples/kv_cache_bench) — pure-Rust PolarQuant+QJL prototype over[num_heads, seq_len, head_dim]tensors, with a bench that compares against simulated Fp8 (E4M3) and symmetric Int8. Run withcargo run -p vibe-infer --release --example kv_cache_bench [num_heads seq_len head_dim]. Findings at head_dim=128 against realistic spiked-attention data: 4.57× memory savings vs fp16 (measured 0.4375 B/el — the theoretical ≥5× asymptotic is not reached at typical head_dim because the per-vector 8 B scalar overhead (radius + residual_norm) amortises to +0.0625 B/el), mean reconstructed-K cosine 0.92, softmax attention MAE 0.0000 (below printable precision), and 100% top-1 attention-argmax agreement with fp16 — zero misrouted tokens. On uniform-random stress data,top1_agreedrops sharply, but that is a data-shape artifact (flat softmax has arbitrary argmax) rather than a codec fault; judge viability byattn_maewhich stays ~0.0002 even there. TheKvCacheMethod::TurboQuant.bytes_per_element()estimate is updated 0.375 → 0.4375 to reflect the real number; downstream pod-sizing numbers follow. Spike conclusion: viable for amistralrs-quantkernel PR — TurboQuant delivers 2.3× more compression than Fp8 without costing attention quality on realistic distributions. - TurboQuant-backed semantic scoring in
context_streaming(Phase 4 — runtime-agnostic). NewSemanticScorerowns aTurboQuantIndexfromvibe-coreand a caller-suppliedEmbedFn; eachadd_segmentembeds + compresses the content, andContextStreamingEngine::queryprefers approximate cosine similarity over the old keyword-overlap scorer when a scorer is attached.refocus(query)rewritesrelevance_scoreacross every segment so theRelevanceScore/Hybrideviction strategies drop segments least relevant to the current task — a prerequisite for 10M–100M-token workflows that outgrow working memory. Optional construction viaContextStreamingEngine::with_semantic(config, scorer); engines built withnew(config)behave identically to before. Runtime-agnostic — works with Mistral.rs, vLLM sidecar, or the Claude API; the scorer doesn’t care where embeddings come from.
[0.5.5] — 2026-04-17
Added
- Apple Watch client (SwiftUI, watchOS 10+) and Wear OS client (Kotlin / Compose, Wear OS 3+) — native
VibeCodyWatch/VibeCodyWearapps with pairing, session list, live transcript, and dictated reply; share a single/watch/*backend. /watch/*Axum routes —/watch/pair/challenge,/watch/pair/confirm,/watch/sessions,/watch/sessions/{id}/stream,/watch/sessions/{id}/reply. New modules:watch_auth,watch_bridge,watch_session_relay.- P-256 ECDSA (secp256r1) pairing — replaces Ed25519 for Apple Secure Enclave compatibility; 64-byte raw public key, signature over
SHA-256(nonce ‖ device_id ‖ issued_at_be). - URL-only / Bearer pairing everywhere — no QR code or JSON clipboard required; emulator-friendly.
- Zero-config connectivity — mDNS advertising (
_vibecli._tcp.local.), Tailscale Funnel auto-detection, ngrok auto-detect + opt-in auto-start. Clients race all reachable paths. - Apple-Handoff-style session continuity — paired devices see live sessions in real time; VibeCoder auto-switches to the Sandbox tab when a watch joins.
- Google-Docs-style real-time sync — ID-based message reconciliation with content-window dedup; no more 80/512-char truncation.
- Watch Devices panel in VibeCoder (
Governance → Watch Devices) to approve / rename / revoke devices. - CI release artifacts —
VibeCodyWatch-watchOS.app.zip+VibeCodyWear-wearos.apk/.aabalongside existing binaries. - Makefile targets —
build-watch,watch-ios,watch-wear,watch-wear-bundle,build-all.
Fixed
- 80 / 512-char message truncation — the legacy ring-buffer fallback was replaced with full-content sync.
- DMG bundling race on macOS 15 — hardened fallback against transient
hdiutil attachfailures under concurrent DiskImages2 load. - Emulator pairing — pairing now works with a pasted URL + Bearer token on Android emulators and watchOS simulators.
Changed
- Pairing algorithm: Ed25519 → P-256 ECDSA. Previously-paired devices must re-pair once on v0.5.5.
- Watch / phone auth — JWT (HS256), 32-byte secret in
ProfileStore, 30-day default TTL. - Version bumped to 0.5.5 across all manifests.
[0.5.4] — 2026-04-03
Added
- Claude Code System Prompts — integrated 254 prompts from TuringWorks/claude-code-system-prompts: core behavioral guidelines baked into TOOL_SYSTEM_PROMPT; all prompts stored as reference skills in
skills/claude-code-prompts/. - Auto-mode guidance — when FullAuto approval policy is active, agent receives autonomous execution rules.
- Error Boundary — React ErrorBoundary catches render crashes with error + stack trace display.
- 5 dynamic skill files — git-commit, pr-creation, security-review, debugging, simplify.
- WebView DevTools — auto-open in debug builds for crash diagnosis.
Fixed
- GLM/Qwen tool call parsing — normalize
<|tag|>delimiters so XML tool calls are correctly executed. - Incremental file saves during streaming —
<write_file>blocks flushed to disk as closing tag streams in. - Leading newline in generated files — strip
\nafter<write_file path="...">. <build>and<run>tag variants — recognize block form in addition to self-closing.- Apply crash — DiffReviewPanel overlays editor with deferred unmount; removed React.StrictMode.
- Terminal buffer cleared on tab switch — Terminal stays mounted with display toggle.
- Duplicate provider keys — 14 providers now return unique
"Provider (model)"names. - LSP invoke params — fixed snake_case to camelCase field names for hover, completion, goto-definition.
- Diff review toolbar — thinner, outlined ghost buttons, visible text with ellipsis.
- Tool call card icons — replaced emoji with thin-line SVG icons using CSS variables.
Changed
- Agent context window: 80K → 200K tokens; max_steps: 30 → 50.
- Claude max_tokens: 4,096 → 16,384; Ollama num_predict: 2,048 → 16,384.
- Retry attempts: 4 → 2 (500ms initial, 5s max backoff).
- Ollama HTTP timeout: 90s → 300s.
[0.5.3] — 2026-04-02
Added
- Document & Media Viewers — DocumentViewer, ImageViewer, HtmlPreview, DrawioPreview for VibeCoder.
- Per-Provider Model Lists — provider-appropriate models with auto-selection; Ollama uses live-discovered models.
- RL-OS Core Modules — 8 modules (EnvOS, TrainOS, EvalOS, OptiOS, ModelHub, ServeOS, RLHF, MultiAgent) with 660 tests, 10 panels, 20 Tauri commands.
- Sketch Canvas — drawing with Move tool, inline text, SVG/PNG export, shape recognition, code generation.
- Training Run Wizard — step-by-step RL training setup wizard.
Fixed
- VibeAIChat: Empty AI Responses — SSE parser read
ev["text"]but daemon sendsev["content"]. - VibeAIChat: Duplicate Streaming Text — guarded against React StrictMode double-mount race.
- VibeAIChat: Response Never Completing — fallback completion event on agent exit.
- VibeAIChat: Stale Model/Token —
useCallbackdependencies updated. - VibeAIChat: Window Icon — replaced default Tauri icon with VibeCoder icon.
- Ollama Model List Slow/Missing — removed per-model chat probe; instant name-based filter.
- Monaco Crash on Apply All — editor kept always mounted.
- VibeCoder Panel Bugs — TLS Inspector, Design Mode, Screenshot to App, file explorer, Fast Context, SemanticIndexPanel.
Changed
- Agent Identity — renamed from “VibeCLI” to “Vibe Agent” across all system prompts.
- RL-OS composite panels registered in panel host, tab groups, and search.
[0.5.2] — 2026-03-30
Added
- RL-OS: Unified Reinforcement Learning Lifecycle Platform — exhaustive fit-gap analysis against 40+ RL competitors (Ray RLlib, Stable Baselines3, Isaac Lab, TRL, d3rlpy, PettingZoo, SageMaker RL, etc.) identifying 52 gaps across 8 categories and 12 unique capabilities no existing tool provides.
- RL-OS Architecture Specification — production-grade architecture for 7 core modules (EnvOS, TrainOS, EvalOS, OptiOS, ModelHub, ServeOS, RLHF) with Rust crate structure (
vibe-rl/), declarative YAML DSL, RL-aware quantization, and 8-phase roadmap. - 12-Stage RL Lifecycle Scorecard — comprehensive lifecycle coverage model; closest competitor scores 5/12 vs. RL-OS target of 12/12.
[0.5.1] — 2026-03-29
Added
- AI Code Review (
ai_code_review.rs, 97 tests) — Qodo/CodeRabbit/Bito parity: 7 detectors, 8-linter aggregation, quality gates, learning loop, PR summary + Mermaid diagrams;/aireviewREPL. - Architecture Spec Engine (
architecture_spec.rs, 108 tests) — TOGAF ADM, Zachman, C4 Model, ADRs, governance engine;/archspecREPL. - Policy Engine (
policy_engine.rs, 91 tests) — Cerbos-style RBAC/ABAC, 14 condition operators, derived roles, policy testing, YAML, audit trail;/policyREPL. - Health Score (
health_score.rs, 92 tests) — multi-dimensional codebase health scoring. - Intent Refactor (
intent_refactor.rs, 89 tests) — natural-language-driven refactoring. - Review Protocol (
review_protocol.rs, 50 tests) — structured code review workflow. - Skill Distillation (
skill_distillation.rs, 82 tests) — extract reusable skills from agent traces. - Phase 32 P0 — context_protocol, code_review_agent, diff_review, code_replay, speculative_exec, explainable_agent.
- TurboQuant KV-Cache — PolarQuant + QJL (~3 bits/dim) for vector DB integration.
- Phase 32 — Advanced Agent Intelligence (6 new modules):
context_protocol.rs— Streaming context protocol for long-running agent sessions.code_review_agent.rs— Automated code review with configurable rulesets.diff_review.rs— Change-aware review focused on diff hunks.code_replay.rs— Reproduce past interactions for debugging and auditing.speculative_exec.rs— Predictive code path execution.explainable_agent.rs— Interpretable reasoning chain for agent decisions.
- FIT-GAP v7 — All 22 Gaps Closed (Phases 23-31):
- Phase 23:
a2a_protocol.rs(A2A protocol),agent_skills_compat.rs(cross-tool skills standard). - Phase 24:
worktree_pool.rs(parallel worktree agents),agent_host.rs(multi-agent terminal host). - Phase 25:
proactive_agent.rs(background intelligence scanner),issue_triage.rs(autonomous issue classification). - Phase 26:
web_grounding.rs(5-provider web search grounding),semantic_index.rs(AST-level codebase understanding). - Phase 27:
mcp_streamable.rs(Streamable HTTP + OAuth 2.1). - Phase 28:
mcts_repair.rs(MCTS code repair),cost_router.rs(cost-optimized agent routing). - Phase 29:
visual_verify.rs(UI screenshot verification),next_task.rs(workflow-level prediction),voice_local.rs(offline whisper.cpp voice),doc_sync.rs(bidirectional spec-code sync). - Phase 30:
native_connectors.rs(20 service connectors),agent_analytics.rs(enterprise metrics),agent_trust.rs(trust scoring),smart_deps.rs(agentic package manager). - Phase 31:
rlcef_loop.rs(execution-based learning),langgraph_bridge.rs(LangGraph compatibility),sketch_canvas.rs(sketch-to-code).
- Phase 23:
- File Attachments —
[file.ext]bracket syntax in VibeCLI REPL and VibeCoder chat for attaching documents, code, and images. - Image Lightbox — Click image attachments in chat to view full size with download button.
- System Theme Detection —
ThemeTogglenow respectsprefers-color-schemeon first visit. - Data Analysis Panel Backend — 9 new
da_*Tauri commands. - Counsel — Multi-LLM Deliberation (
counsel.rs, 534 lines, 20+ tests). - SuperBrain — Multi-Provider Query Routing (
superbrain.rs, 424 lines, 14+ tests). - Web Client (
web_client.rs, 1,048 lines) — zero CDN dependencies (air-gap safe). - FIT-GAP Code Review Architecture comparison across 12+ competitors.
- VibeCody vs OpenClaw whitepaper.
- Demo guides 36-60.
- 3 VibeCoder composite panels, 7 skill files, 10 new Tauri commands.
Changed
- Zero Demo Panels — All 23 previously demo-only panels wired to real Tauri backends (34 new commands, 17 new AppState fields). Panel status: 196+ total.
- Theme Variable Migration — Converted 85+ hardcoded colors to CSS variables.
- Tests: ~10,535 (0 failures). REPL commands: 106+. Rust modules: 196+. Skill files: ~550. Tauri commands: 360+.
- Documentation: FIT-GAP through v7, ROADMAP through v5.
- Provider count: 23 direct + OpenRouter (300+).
Fixed
- Production Hardening — Zero compiler warnings. Safe unwraps, flush-on-exit, configurable A2A server, poison recovery for Mutex locks.
- Clippy Clean — All lints resolved across workspace.
- Tokio Mutex Fix — 45 instances corrected.
- Crate Metadata — Added
descriptionfield to 6 Cargo.toml files. - Ollama Streaming — Status check fix + streaming hot path optimization.
- Suppressed warnings in ai_code_review, architecture_spec, diff_review modules.
- Duplicate REPL handlers removed; missing module stubs created.
[0.5.0] - 2026-03-24
Added
- 9 Quantum Computing Tools:
- Statevector Simulator — pure Rust simulator supporting up to 16 qubits with all 14 quantum gates (H, X, Y, Z, S, T, Rx, Ry, Rz, CNOT, CZ, SWAP, Toffoli, Measure). Complex number arithmetic, probability extraction, amplitude readout, and shot-based sampling.
- Visual Circuit Builder — SVG-based editor with categorized gate palette, click-to-place on qubit wires, multi-qubit gate workflow (control then target), click-to-delete, and live metrics bar (gate count, depth, 2Q gates, circuit volume).
- Circuit Optimizer — multi-pass optimization: identity cancellation (HH, XX, YY, ZZ, CNOT pairs), gate merging (SS to Z, TT to S), rotation merging (adjacent Rx/Ry/Rz on same qubit), with savings percentage reporting.
- Bloch Sphere Visualizer — SVG rendering of single-qubit states with oblique projection, axis labels, state arrow, and theta/phi readout.
- Cost Estimator — pricing comparison for IBM Quantum ($1.60/sec), Amazon Braket ($0.30/task + per-shot), and IonQ (per-gate) with itemized breakdowns.
- Project Scaffolding — complete project generation for Qiskit, Cirq, PennyLane, and Q# with source, tests, requirements, CI config, and README.
- Algorithm Templates — 8 pre-built circuits: Bell State, GHZ(n), QFT(n), Grover 2-qubit, Deutsch-Jozsa, Bernstein-Vazirani, VQE ansatz, QAOA.
- Hardware Topology Viewer — SVG connectivity maps for IBM Eagle (127q), Google Sycamore (53q), IonQ Aria (25q), Rigetti Ankaa-2 (84q), Quantinuum H2 (32q).
- Multi-language Code Examples — 11 algorithms with implementations in Qiskit, Cirq, and PennyLane (Grover, Shor, VQE, QAOA, QPE, Deutsch-Jozsa, BV, HHL, Quantum Walk, QSVM, QNN).
- Panel Consolidation (137 tabs to 36):
- 33 composite panels replacing 137 individual tabs, organized into 9 renamed groups (AI, Project, Code Quality, Source Control, Infrastructure, Data & APIs, Developer Tools, Toolkit, Settings).
- Reusable
TabbedPanelcomponent with keep-alive behavior for sub-tabs. createComposite()factory for one-liner composite panel definitions.- Alias-based search — typing old panel names (e.g., “docker”) still finds the consolidated tab (“Containers”).
- Full-stack Resilience:
ResilientProviderwrapper — automatic retry with exponential backoff and jitter on all 21 AI providers.retry_async()generic utility for any async operation with configurable max attempts, backoff, and error classification.is_retryable()classifier covering 20+ transient error patterns (429, 503, timeouts, connection resets, decode errors).- Agent loop: stream-level retry (5 attempts, 1-60s backoff),
RetryableErrorevent, frontend Retry button preserving completed work. - Streaming chat: full retry loop with mid-stream error recovery.
- 30+ HTTP API calls wrapped with retry: JIRA, GitHub, Linear, Groq Whisper, ElevenLabs, Telegram, Discord, Slack, Signal, Matrix, Twilio, WhatsApp, Teams, OpenSandbox (all operations), BugBot.
- 11 new Tauri commands for quantum operations (add/remove gate, simulate, optimize, cost estimate, templates, scaffold, circuit detail/delete/clear).
- 105 quantum computing tests (32 new for simulator, optimizer, templates, cost, scaffold).
Fixed
- Quantum circuit lookup uses
indexfield instead of array position (circuits remained accessible after deletions). - Missing
gatesarray in circuit detail for pre-existing circuits (defaults to empty). - Quantum simulator returns tuple arrays matching frontend TypeScript types.
TabbedPaneldisplay:contents breaking child panel height.LazyPanelsprops aligned with refactoredcreateCompositepattern.
Changed
- Quantum panel expanded from 6 tabs to 11 (Circuit Builder, Simulator, Optimizer, Cost, Templates, Scaffold, Topology, Languages, Quantum OS, Projects, Algorithms).
- Version bumped to 0.5.0 across all manifests (Cargo.toml, package.json, tauri.conf.json).
- VibeCLI crate now also builds as a library (
vibecli_cli) for Tauri backend integration. - Canvas workflow panel properties sidebar added.
- Tab labels corrected for Red Team, Blue Team, Purple Team.
- AI/ML Workflow and Model Wizard added to tab groups.
[0.4.0] - 2026-03-21
Added
- Warp terminal-style features (
warp_features.rs, 55 tests):# natural languagecommand — type# find large filesto generate shell commands via AI with explanation and confirmation.- Command corrections (thefuck-style) — 13 built-in rules for typos (
gtitogit), missing sudo, git push upstream, permission denied, wrong Python version, etc. - Secret redaction — auto-detects and masks API keys (
sk-****), AWS keys (AKIA****), GitHub tokens (ghp_****), Bearer tokens, passwords, and private keys in command output. - Next command suggestions — proactive hints after successful commands (git add to git commit, cargo build to cargo test, etc.).
- Block-style output — shell command output formatted with colored left border (green=success, red=failure), command header, and duration display.
- Desktop notifications — macOS/Linux notifications for commands taking longer than 30 seconds.
- Output filtering and AI error explanation prompts.
- Auth scaffolding expanded to 85+ frameworks across 17 languages — Go (Gin, Fiber, Echo, Chi, Hertz), Java (Spring Boot, Quarkus, Micronaut, Vert.x, Helidon, Javalin), Kotlin (Ktor, http4k), C# (ASP.NET Core, FastEndpoints), TypeScript (Next.js, Fastify, NestJS, Hono, Elysia), Python (FastAPI, Django, Flask, Starlette, Litestar), Rust (Axum, Actix, Rocket), Ruby (Rails, Sinatra), PHP (Laravel, Symfony), Elixir (Phoenix), Scala, Swift, Dart, Clojure, Haskell, Crystal, Nim, Zig. Auth providers expanded to 40+ including SAML, LDAP, OIDC, Passkey, TOTP, and 10 BaaS platforms. UI: searchable grid with language filter.
- Best-in-class documentation (20 new files, ~5,500 lines):
llms.txtandllms-full.txt— AI-agent-optimized project docs following the llms.txt standard. First AI coding tool to support this.quickstart.md— zero-to-productive in 5 minutes.- 3 tutorials: first-provider setup, agent workflow, AI code review.
api-reference.md— complete HTTP daemon API reference with curl examples for all endpoints.- Per-provider setup guides: Ollama, Claude, OpenAI, DeepSeek, Gemini.
troubleshooting.md(24 issues),faq.md(22 questions),glossary.md(50+ terms),security.md(13 sections),CHANGELOG.md.- Jekyll nav reorganized: quickstart-first user journey ordering.
- Full ANSI markdown rendering in VibeCLI REPL:
- Headers (H1-H4) in bold green/cyan/magenta/blue.
- Bold, italic, bold+italic text styling.
- Inline code with gray background and cyan text.
- Unordered and ordered lists with styled bullets/numbers.
- Blockquotes with green pipe and italic text.
- Task lists with checkbox symbols.
- Horizontal rules, links with underlined text and dim URLs.
- Code blocks with line numbers, language labels, dark background, and syntect syntax highlighting.
- Claude Code-style tool call rendering — dark background boxes with terminal-width padding, green checkmark or red cross, tool output displayed below (capped at 30 lines).
- MCP panels consolidated — merged MCP, MCP Lazy, and MCP Directory into a single unified panel with 4 tabs (Servers, Tools, Directory, Metrics).
- Model name in REPL prompt —
[vibecli ollama (deepseek-chat)] >shows both provider and model.
Fixed
- All GitHub URLs corrected from
vibecody/vibecody,AceCana662/vibecody,AiChefDev/vibecodytoTuringWorks/vibecodyacross 18 files (docs, GitHub Actions, package.json, config). - DeepSeek default model:
deepseek-codertodeepseek-chat(V3 current). - Gemini default model:
gemini-2.0-flashtogemini-2.5-flash(latest). - Streaming chat response rendering — replaced flawed stream-then-clear-then-rerender with direct rendering, eliminating blank line artifacts.
- Rustyline prompt double-bracket and cursor offset — switched to plain text prompt for reliable cursor positioning.
- Tool output now displayed in agent REPL (was captured but not shown).
- REPL args trimming — extra spaces after commands removed.
- All decorative emojis removed from REPL output (70+) and documentation (1,138 replacements across 14 files).
- Build warnings resolved (zero warnings, zero errors).
.vibecli/added to.gitignore(auto-generated local data).
Changed
- 23 direct AI providers (was 17): added MiniMax, Perplexity, Together AI, Fireworks AI, SambaNova, plus Gemini provider upgrade.
- Cost estimation expanded to cover all 23 providers with per-model pricing (was only Claude + OpenAI).
- Doctor command checks all 14 cloud provider API keys (was 4).
- Help text reorganized by popularity with all 23 providers listed.
- 55 new unit tests (warp_features), 130 new provider tests, 812 gap-closure tests.
- Documentation icons replaced with plain text (Yes/No/Warning instead of emoji checkmarks).
[0.3.3] - 2026-03-20
Added
- 5 new AI providers: MiniMax, Perplexity, Together AI, Fireworks AI, SambaNova — bringing the total to 23 supported providers.
- FIT-GAP v6: 19 new competitive gaps identified and closed across agent capabilities, context management, and cloud integrations.
- 17 new Rust modules:
channel_daemon.rs— Always-on background listener for multi-platform integration.vm_orchestrator.rs— Virtual machine lifecycle management for cloud sandboxes.spec_pipeline.rs— Spec-driven development pipeline with EARS syntax support.branch_agent.rs— Autonomous branch management with PR creation workflows.design_import.rs— Figma/Sketch design-to-code import pipeline.audio_output.rs— Text-to-speech for agent responses and accessibility.org_context.rs— Organization-wide context sharing across teams.session_sharing.rs— Share agent sessions with teammates via link or export.ci_gates.rs— Quality gates for CI pipelines with configurable thresholds.data_analysis.rs— Tabular data analysis with chart generation.managed_deploy.rs— One-click deploy to Vercel, Netlify, Railway, Fly.io.context_streaming.rs— Streaming context injection for long-running sessions.extension_compat.rs— Extension compatibility verification and migration.model_marketplace.rs— Browse and install models from community marketplace.agentic_cicd.rs— AI-driven CI/CD pipeline generation and optimization.cross_surface_routing.rs— Route agent actions across CLI, UI, and API surfaces.soul.rs— Project philosophy document management with agent integration.
- 10 new VibeCoder panels: Soul, McpLazy, ContextBundle, CloudProvider, ACP, McpDirectory, UsageMetering, SweBench, SessionMemory, IDP.
- Gemini provider upgraded to native implementation (previously OpenRouter-only).
- Best-in-class support documentation: troubleshooting guide, FAQ, glossary, security practices, and this changelog.
llms.txtfor AI-friendly project context.- Tutorial guides for getting started, provider configuration, and skill development.
Fixed
- DeepSeek default model updated from deprecated
deepseek-codertodeepseek-chat(V3). - Gemini default model updated to
gemini-2.5-flash. - Cost estimation now covers all 23 providers (previously only Claude and OpenAI).
- Doctor command checks all 14 cloud provider API keys (previously only 4).
- Session resume stability improved for cross-version session files.
- Monaco editor performance with files over 1 MB (disabled minimap by default for large files).
Changed
- Provider help text reorganized by popularity tier (Local, Major Cloud, Specialized, Meta).
- 812 new unit tests across 17 modules, bringing the workspace total to approximately 6,050.
- All production
unwrap()calls replaced withexpect()with descriptive messages. - Release profile optimized: LTO enabled, symbols stripped, panic set to abort, opt-level=s for workspace with opt-level=2 for vibecli.
[0.3.2] - 2026-03-14
Added
- Blue Team module (
blue_team.rs, 49 tests) — Defensive security operations: incident management with P1-P4 severity, IOC tracking across 9 indicator types, SIEM integration for 8 platforms (Splunk, Sentinel, Elastic, QRadar, CrowdStrike, Wazuh, Datadog, SumoLogic), forensic case management, detection rules with platform-specific query generation, playbooks with 8 action types, and threat hunting workflows. - Purple Team module (
purple_team.rs, 38 tests) — ATT&CK-aligned security exercises: 14 tactics, 20 pre-loaded techniques, attack simulation with outcome tracking, detection validation, coverage gap analysis, heatmap generation, and cross-exercise comparison. - IDP module (
idp.rs, 80 tests) — Internal Developer Platform support for 12 platforms: Backstage, Cycloid, Humanitec, Port, Qovery, Mia Platform, OpsLevel, Roadie, Cortex, Morpheus Data, CloudBolt, Harness. Includes service catalogs, golden paths, DORA-metric scorecards, self-service infrastructure provisioning, and team onboarding. - 3 new VibeCoder panels: BlueTeamPanel (7 tabs), PurpleTeamPanel (5 tabs), IdpPanel (7 tabs).
- 3 new REPL commands:
/blueteam,/purpleteam,/idpwith full subcommand sets. - Workspace total: approximately 5,912 tests with 0 failures.
[0.3.1] - 2026-03-13
Added
- Futureproofing Phases 10-14: 10 new Rust modules implementing 12 FIT-GAP v5 gaps (419 tests total):
- MCP lazy loading with tool search and LRU eviction.
- Context bundles (Spaces) with priority ordering and TOML serialization.
- AWS/GCP/Azure deep integration: service detection, IAM policy generation, Terraform/CloudFormation/Pulumi templates, cost estimation.
- ACP (Agent Client Protocol) server/client modes with capability negotiation.
- MCP verified plugin directory with search, install, and review pipeline.
- Usage metering credit system with per-user/project/team budgets and alerts.
- SWE-bench benchmarking harness for run/compare/export.
- Session memory profiling with leak detection and auto-compact.
- SOC 2 compliance controls with audit trail, PII redaction, and retention policies.
- Unified voice+vision+code multimodal agent.
- 8 new VibeCoder panels and 4 new REPL commands.
- 12 v5 capability gaps catalogued and Phases 10–14 planned.
- Workspace total: approximately 5,745 tests with 0 failures and 136+ panels.
[0.3.0] - 2026-03-09
Added
- FIT-GAP v4: All 23 identified gaps closed, including automations, self-review, MCP apps, agent teams v2, semantic MCP, docgen, remote control, AST editing, CI status checks, VS Code sessions, cloud sandbox, plan documents, security scanning, sub-agent roles, and edit prediction (RL Q-learning).
- Competitor Parity: 13 new modules closing all code-addressable “Partial” entries from competitive analysis — debug mode, three agent modes (Smart/Rush/Deep), conversational search, clarifying questions, fast context (SWE-grep), image generation agent, discussion mode, full-stack generation, enhanced agent teams, team governance, cloud autofix, GitHub Actions agent, and render optimization.
- App Builder (
app_builder.rs, 70 tests) — Template-based application scaffolding with AI enhancement. - Infinite Context (
infinite_context.rs, 79 tests) — 5-level context hierarchy with token budget, eviction, compression, and LRU caching. - Blitzy Parity: Batch builder (109 tests), QA validation pipeline (99 tests), legacy migration engine supporting 18 source languages including COBOL and Fortran (101 tests), and unified git platform manager for 5 hosting services (111 tests).
- 13 new skill files and 4 new VibeCoder panels.
- Workspace total: approximately 5,236 tests with 0 failures.
Changed
- Security audit completed: 20 findings (P0-P3) all resolved, including path traversal prevention, cryptographic IDs, CORS hardening, and command blocklist.
- All production
unwrap()calls replaced withexpect()with descriptive messages. - Release profile added to workspace: LTO, symbol stripping, panic=abort.
[0.2.x] - 2026-02 through 2026-03
Earlier releases established the foundation:
- Core agent loop with tool calling, streaming, and multi-provider support.
- VibeCLI with TUI (Ratatui) and REPL (Rustyline).
- VibeCoder with Tauri 2, React, Monaco Editor.
- 17 AI providers including Ollama, Claude, OpenAI, Gemini, and FailoverProvider.
- MCP client and server support.
- Container sandbox with Docker/Podman/OpenSandbox.
- 500+ built-in skills across 25+ categories.
- RAG pipeline with document ingestion, web crawling, and vector database support.
- Gateway system supporting 18 messaging platforms.
- Voice input (Groq Whisper), pairing (QR code), and Tailscale integration.
See the Changelog for the complete feature history.