GitHub Docs

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 in vibe-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_documents tool; 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 was serve).
  • 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-sheetfmt left a stale t="s" when a shared-string cell was edited.
  • Release: Chocolatey files are UTF-8; release-page pushes 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 runs fm until the terms are accepted — fm records 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 because fm serve ignores a tools array.
  • 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_choice across 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 quickbuild target; 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.txt advances only when every required leg is published.
  • CLI configuration persists to the encrypted ProfileStore first, 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-sandbox did not compile for musl (vendored with a one-line cast); the Docker build did not copy vendor/; 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.gz after 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_agent and stream_approvals now 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-sheetfmt classified a cell’s kind (Cell.kind) but still showed that raw value verbatim. A new numfmt module renders a cell’s own numFmt code (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 as Cell.display: Option<String>, None for General, a non-numeric cell, or a format code the module doesn’t understand (never a guess). SpreadsheetViewer.tsx’s grid now shows display when set, falling back to the raw text otherwise; 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_ranges has 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 real colSpan/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-docfmt used to leave word/footnotes.xml/word/endnotes.xml untouched only because it never looked at them — a w:footnoteReference in 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 new SpanStyle.footnote_ref, round-tripped through its own Markdown syntax the same way link already 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 section footnote: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.rs dependency points at upstream again, not a fork. vibe-infer carried a TuringWorks/mistral.rs fork 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 flipping VIBE_INFER_KV_CACHE=turboquant bought 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, so vibe-infer/Cargo.toml now pins EricLBuehler/mistral.rs directly (the fork’s own comment already tracked upstream master almost exactly, so this is the same commit, not a downgrade). Removed: kv_cache_codec.rs, KvCacheMode (and the VIBE_INFER_KV_CACHE env var), the kv_cache_bench/kv_cache_compare examples, the KV Cache dropdown in VibeCoder’s Inference panel, and the now-unneeded [patch.crates-io] candle pin (candle-core for minilm.rs resolves 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-shipped TurboQuant vector-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 PluginsView had 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.tsx is removed; PluginsView.tsx is renamed IntegrationsView.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/duplex opened 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/*.drawio were 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 passed noSaveBtn=1 — which replaces draw.io’s Save with Save & Exit — while nothing on this side handled the exit event. A new Diagrams tab lists every .drawio, .dio and .drawio.svg in 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=1 was missing from the editor URL while this file already had an autosave handler, 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 in drawio_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 .drawio from the file tree showed a blank frame: the viewer URL never asked for the embed protocol (embed=1&proto=json) and never acknowledged configure, so the init event the component was waiting for could not arrive. Every path is now workspace-relative and resolved inside the open workspace — execute_drawio_mcp took 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’s data: URL is bounded before anything is allocated from it. Saving back into a .drawio.svg is 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.token names 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 on SIGTERM. The daemon on 7878 stayed up, answered /health perfectly, 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.rs had 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 shared daemon.token is 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 new vibe-daemon-token crate, replacing seventeen private copies of the same path join across the daemon, three Tauri shells, the eval harness, the TUI, the vibecli-mistralrs provider, the Agent SDK, the VS Code extension and the JetBrains plugin — a crate of its own because vibe-eval is depended on by vibecli and could not depend back. And a 401 is no longer reported as a diagnosis it is not. /health publishes api_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, or Unverifiable for a daemon too old to say — four states with different fixes, where stale needs the daemon restarted and missing needs it started. daemon_bootstrap::ensure_ready checks the process and the credential and the version together, because ensure_running alone answered true throughout the whole outage; all three shells expose it as daemon_readiness and daemon_readiness_probe (which never starts a daemon — a poll must not resurrect one the user stopped), and useDaemonMonitor now 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 of daemonFetch in vibe-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/skills has 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 own App.css and therefore unliftable. There is now one SkillsView in packages/vibe-ui-shared, rendered by all three over a small SkillCatalog adapter — the shells reach the same route through differently-named commands (list_skills against a caller-supplied daemon URL in VibeDesk, skilllens_list_skills against 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 /skills in 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: AgentRequest has no skills field, 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. /skills also needed the panel it opens to exist first: panels are lazy, so the first deep link into one fires while its TabbedPanel is 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_scorecard awarded 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 dora reports 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 get 0.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 gets deployment_frequency: unmeasured with 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 with touch; where path detection is known to under-report (Rust #[cfg(test)] tests have no tests/ directory; a monorepo’s lint config lives one level down) the practice carries a detection_caveat printed 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 gate fails a pipeline on a missed band with exit 1 and on an unmeasurable required metric with exit 3 — 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 space reports 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 recorded Co-authored-by share — 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 no score/total/overall field, 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_signal is 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 survey prints the quarterly instrument, carrying the anonymity and no-performance-review commitments in the document itself. Ten new skills in the devex category (eleven counting the existing devex-developer-experience.md), routed by devex-director-operating-system.md, and six slash commands (/devex, /dora, /space, /practices, /onboarding, /devex-plan) that cite their skill file with the extension — get_skill now accepts either spelling, so a name copied off the page in front of you resolves instead of returning not 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.rs had the action enum, the session state machine and the safety rails; desktop_agent.rs had the mouse, keyboard and screenshot commands — with nothing joining them, no route to reach them, and /v1/capabilities advertising 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 points cliclick takes — and the daemon measures image width against logical width and divides, rather than inferring the backing scale from whether system_profiler prints 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 a wait is clamped to 30 s, a type to 4096 characters, a scroll to 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 as verified: 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 reads n/a rather 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. screencapture exiting 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 missing cliclick with the brew line to fix it. Provider-agnostic and strictly so: provider and model are required on POST /observe/sessions with 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 -1 suffix 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 .apk is 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.gz decompressed it five times. See archives.md.

  • PDFs open as text, and save back. A .pdf gains 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 between BI and EI do 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 /ToUnicode map 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 ```mermaid or ```plantuml block in a markdown preview becomes the diagram; a .mmd, .mermaid, .puml, .plantuml, .wsd or .iuml file 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. -failfast2 is 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 a scope line saying the grade covers catalog metadata only and an unmeasured block 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 prints v:NotBefore: …; NotAfter: … and OpenSSL 1 printed no validity dates at all; the parser looked for Not After, with a space, found nothing, and rendered no expiry date as zero days left, and therefore expired. The fields now come from openssl x509 -noout -subject -issuer -dates -serial -ext subjectAltName, one documented key=value per 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) / 4 and 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 reassuring verify return:1 callbacks. Checked against the live internet (cargo test -p vibe-coder -- --ignored tls_inspector): github.com valid with 88 days left, expired.badssl.com expired 4160 days ago, self-signed.badssl.com untrusted 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-protocol feature is on — tauri build passes it, cargo build --release does not — and without it the window is pointed at http://localhost:1420 instead. 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 and make build-* both go through tauri build; the trap was only there for anyone reaching for cargo directly, and it cost an afternoon to find. All three shells now declare the custom-protocol feature, so cargo build --release --features custom-protocol embeds properly (412 asset keys in the binary, against 0 before), and the build says so twice when it will not: a cargo:warning at 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, and cargo check would otherwise pay for it on every run.

  • Word documents could be opened but not saved. Every real .docx tried — 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, and str::trim eats 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 whose href arrived 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 .docx and .epub file 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, over PUT /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. The ready event 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 --setup already 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, so tts_sidecar was unset on every real machine, Tts::open fell straight through to batch, and every spoken reply came from one say process 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-sidecar builds and installs it, make voice-kokoro sets up the neural one, and make voice-status answers the question that previously had no answer, which is which engine will actually run. Tts::open also 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 by tools/tts-bench rather than estimated. The sidecar speaks the same wire protocol as the macOS one, so the daemon needed no new plumbing — point tts_sidecar at an interpreter and tts_sidecar_args at 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. AVSpeechSynthesizer produces 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 new AUR frame prefixes a u32 rate; the old AUD frame 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_len on an OpenAI-compatible /models, model_info on Ollama’s /api/show, inputTokenLimit on 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=auto identifies 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-btn had no positioned ancestor.
  • VibeAIChat has a test suite. It was the one shell shipping on tsc --noEmit alone.

  • 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.css carries 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 booted dark-sherwood, the editor’s fallback said dark-default, and the stylesheet a third palette — a user with nothing stored got all three at once. A test now holds tokens.css and 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: nowrap with overflow: 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 an aria-label, since a display: none label 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_alpha is Hindi — and asked for an English sentence it returns no audio at all. The daemon noticed and emitted a notice; 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 /models now 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 PUT and DELETE route in the daemon was unreachable from every browser client. The CORS layer allowed GET, POST, PATCH and OPTIONS, and the router serves three PUT routes and fifteen DELETE ones. A method missing from access-control-allow-methods is 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 reads Load 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, and require_auth is 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, and error is 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 inside SettingsView reached two shells of three and the third silently had none. VoiceSection is 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, and tsc has nothing to say about a shell that does not.
  • daemon_port was 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 where VIBECLI_DAEMON_PORT had 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 send auto, 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; useVoiceDuplex had no channel back into the host beyond onTurn, which carries a role and a string; and in VibeCoder onOpenFile was threaded to nine panels and stopped at PanelHost for the one that hosts the conversation. There is now an open_file tool 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 for read_file and 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 an onOpenFile handler 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 and tsc is 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. StreamFilter suppresses <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 always None, and no tool ever ran. It now has a reasoning_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 advertised list_dir, a name parse_tool_calls has never known; the canonical name is list_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. The voice_duplex test 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, since iconAsTemplate is 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.css from main.tsx and VibeAIChat from App.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: active was derived from the turn state alone, so one failed turn made the button offer “start” on a microphone that was already open, start returned 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 outlives active, 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_ms stays measured on the raw stream, so the gap to first_audio_ms on 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_context control 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. speaking fires 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 and reply closes 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, --muted and --accent-red, none of which this design system defines, each with a GitHub-dark hex beside it. A var() 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-btn in 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 path attribute 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 path src/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 AudioWorklet module is fetched under script-src, not worker-src — VibeCoder and VibeDesk ship script-src 'self' with worker-src 'self' blob:, so the capture module was rejected outright (“Not allowed by CSP”, measured under the shipped policy). Fixed twice over: script-src now allows blob:, and capture falls back to a ScriptProcessorNode that 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 with Path::exists() — which asks whether it sits in the daemon’s working directory. Bare names now resolve through PATH. 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-MATRIX said VibeCoder had no parallel agents and no worktree isolation. Both were wrong and had been for some time: AgentPanel’s Parallel Mode calls start_parallel_agent_task, and WorktreePoolPanel calls worktree_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; wry implements that hook on macOS and Windows and skips Linux, and Tauri adds neither. The fix is two calls, reachable from Tauri’s with_webview() — see apply_linux_media_fix in tools/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, the read_document_text / write_document_text / read_document_preview commands, 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 returned null unconditionally, so every deflate-compressed chapter — which is every chapter of every real book — was dropped and the viewer showed a card suggesting another application; and href was 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 .pages yields 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 .bak copy 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.
  • WorkspaceStore gained a Debug implementation 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.tsx instead 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_tests collected 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-child catch-all in App.css lands 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, .md links that resolved to the domain root, and a plugin registry that was advertised but does not exist.
  • The CLI build broke after AgentContext gained plugin_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. --benchmark runs the real harness rather than a parallel code path, make eval-check validates the suites with no provider and no agent, and vibecli --eval gate exits 1 on regression against a baseline.
    • A grader with no assertions is an error, not a pass — Suite::validate rejects one at load time, so a vacuous task cannot ship.
    • Adding a Surface variant without a conformance task fails the suite tests.
  • 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/servers reference 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_credentials with 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 ```suggestion blocks.
    • 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 PostImage index maps path → new-line → text from 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. AnchorVerified means 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.
    • --bugbot is a real flag now. bugbot.rs had advertised vibecli --bugbot --diff / --pr 123 in its module docs since it was written; no such flag existed and the only caller was the GitHub webhook. --bugbot reviews uncommitted changes, --staged the index, --pr N a pull request, and it exits 1 on any error-severity finding so it drops into a pre-push hook or CI step. --propose-fixes adds suggestions; --apply-fixes writes them, skipping any file that moved since the diff and printing both counts.
    • --pr refuses a non-GitHub remote instead of guessing a slug that would review an unrelated repository.
  • 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 a coverage object 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 distinguishes Some(vec![]) (the model looked and found nothing) from None (it never answered); files whose every pass errored are reported as unreviewed, llm_calls_failed is counted separately, and the caveat points at vibecli --doctor. Found by running the command against a scratch repo, not by the build.
    • --passes N trades 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.
  • 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.md per directory, name + description frontmatter) 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/*.md companion 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, the examples/*.py helpers) were already dangling at the source and are reworded rather than shipped as dead ends.

Fixed

  • VibeCoder editor stuck on “Loading…” — @monaco-editor/react was loading the Monaco engine from a cdn.jsdelivr.net script 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 ?worker imports).
  • Ollama Cloud models missing from the model dropdown — the registry now unconditionally unions cloud + locally-pulled + the full static catalog (deduped), so *-cloud entries always appear regardless of whether ollama serve is 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. --exec now double-checks with the project’s own build and test before accepting task_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, and redact_secrets grew 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 — localStorage for detection, the app’s workspace for planning, and std::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 --help started 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. openWorkspacePath replaces the frontend’s workspace (setWorkspaceFolders([path])), but the Tauri command behind it called Workspace::add_folder, which appends. After opening B, the file tree showed B while folders().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, and safe_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_folder existed but had no command and no caller.
    • The opened folder is now the only root — Workspace::set_root_folder clears the folder list, the old root’s notify watchers (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_folder is renamed set_workspace_folder, since a name that says “add” while replacing is the reason this was hard to see. Validation, reject_sensitive_path and 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.
    • HealthScorePanel follows the workspace. Its path came from a useState lazy initializer reading localStorage once at mount, so it never noticed a folder change. It now listens for vibecoder:workspace-changed — an event dispatched since the workspace switcher shipped that nothing in src/ had ever subscribed to.
    • Not addressed: the daemon’s ServeState.workspace_root and the bind-once graph_index::GRAPH both follow the app process’s cwd rather than the opened folder, so they were never right rather than stale-by-one; and semindex_* accumulates across folders but serves synthetic placeholder data.
  • list_skills and get_skill re-read the entire catalogue on every MCP call. Both called SkillCatalog::load_from_with_cwd_plugins per invocation — 1,143 file reads, ~990 YAML parses, and a WorkspaceStore::open (which creates the encrypted database, in whatever directory the MCP host happened to launch in) to answer one question. An agent calling list_skills three times in a turn paid it three times. skill_catalog::load_with_cwd_plugins_cached now shares one Arc<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 WorkspaceStore open is now gated on <cwd>/.vibecli/workspace.db already existing, so the skills path stops creating stray workspace databases in scratch directories.
  • Adding or removing a skill did not rebuild the binary. include_dir! bakes skills/** in at compile time; rustc’s dep-info tracks the contents of the files the macro expanded to, but not the directory listing. build.rs already emitted rerun-if-env-changed directives, 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=skills now 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 ~710 skills/*.md files were never packaged. The resolver’s primary path was ${CARGO_MANIFEST_DIR}/skills, baked in at compile time, so an installed vibecli looked 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.yml tars the bare executable, so no sibling share/ 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, so skill.path still 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_server and skillforge_index each carried a copy of the fallback chain; both now call skills_embedded::resolve_skills_dir(). VIBECLI_SKILLS_DIR still wins and is used verbatim — an override that silently fell through to the embedded copy would hide an operator’s typo.
    • vibecli doctor reports 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.
  • POST /webhook/github failed open when no webhook secret was set. Signature verification ran only if 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 the set-key command that fixes it.

  • The GitHub App webhook secret could not be stored encrypted. GithubAppConfig::resolve_webhook_secret reads the ProfileStore key github_app_webhook_secret first, per Zero-Config First — but vibecli set-key validates the name against a fixed list that omitted it, so the command answered unknown provider. The only reachable paths were a plaintext config.toml field and an environment variable, both of which the same rule forbids for a secret. vibecli set-key github_app_webhook_secret <secret> now works, and list-keys shows it.

  • [github_app] auto_fix was 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 honest fixes_proposed count, 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_thinking alone 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): transformers 5.3.0 → 5.14.1 (CVE-2026-5241, ACE in LightGlue model load), torch 2.11.0 → 2.13.0 (CVE-2025-3000, torch.jit.script memory corruption), serde_with 3.20.0 → 3.21.0 (GHSA-7gcf-g7xr-8hxj, KeyValueMap empty-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-variant EmbeddingProvider enum (Ollama with a hard-coded 127.0.0.1:11434, OpenAI locked to text-embedding-3-small), while vibe-infer carried a separate Embedder trait 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-3 is the strongest code-retrieval option), Cohere, Gemini, and an in-process candle backend registered at runtime so vibe-embed never 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-embed and 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. EmbedKind is a required argument, expressed natively where the provider supports it (Voyage/Cohere input_type, Gemini taskType) 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>.json plus a small .meta.json sidecar 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-status in the REPL, Agent SDK + VS Code client methods, an embedding block in /health and the startup banner, and docs/embeddings.md.
    • [index] config is finally wired. embedding_provider / embedding_model existed but were referenced only by tests; /index used its own hard-coded literal. An unrecognised provider is now an error rather than a silent fallback to Ollama.
  • Voice input on every client — one daemon route, one shared hook. Before this, the whole voice stack (VoiceDispatcher in voice.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 audio Content-Type + X-Voice-Language / X-Voice-Prefer-Local headers. Seven audio types are recognised; anything else is a 415 rather than a guessed extension. GET /voice/status reports what the machine can actually do, including a can_transcribe that 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_engine reports which engine ran, so a response can say local_whisper vs cloud_whisper instead 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_status Tauri 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 dead src/hooks/useVoiceInput.ts are deleted in favour of the shared one; @vibe/shared is now aliased in its vite/vitest/tsconfig. transcribe_audio_bytes (Groq-only) is removed and the file-based transcribe_audio renamed to transcribe_audio_file to free the shared command name. macOS mic entitlement (com.apple.security.device.audio-input) added to all three, plus Info.plist usage strings for VibeDesk and VibeAIChat — both keys are load-bearing and fail differently.
    • VibeMobile — VoiceService (speech_to_text on-device recogniser → record + upload fallback) with a VoiceStatus sum type mirroring the web hook, VoiceMicButton / VoicePartialStrip widgets in the chat and watch-chat composers, ApiClient.transcribeAudio / voiceStatus, iOS NSMicrophoneUsageDescription + NSSpeechRecognitionUsageDescription, Android RECORD_AUDIO and the Android 11+ RecognitionService <queries> entry (without which the recogniser is invisible on every device).
    • Editor plugins — SoX rec capture, matching what VoiceDispatcher::listen has 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 — webview getUserMedia is unreliable) plus a VibeCLI: Dictate command. JetBrains: mic button in the Chat tool window. Neovim: :VibeCLIVoice toggle (! 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}) and agent.voiceStatus(). 9 new vitest cases; 60 total green.

Fixed

  • The code index persisted API keys in plaintext. EmbeddingProvider::OpenAI { api_key } was Serialize, and the index was written as plain JSON — so every .vibecli/index.json built 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 no sk- 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.0 against 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; and vibe-memory’s SQLite had no model or dimension column at all, so a VIBE_MEMORY_DIM change 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 the vibecli dependency. Since nearly every daemon route is behind require_auth and 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 /health reuse, binary resolution beyond bare PATH, poll-to-deadline for the ~16 s cold start, and four distinct failure states.

  • Stale bearer tokens produced a permanent 401 loop. vibecli serve mints 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, and crates/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 a send_authed helper that retries once with a token read straight from ~/.vibecli/daemon.token, deliberately bypassing the explicit-token / VIBECLI_TOKEN precedence — 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’s renderContent extracted 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 shared Markdown component 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) and var(--border) (74 uses, plus --border-default / --border-secondary / --border-primary) were referenced but defined nowhere; CSS drops an undefined var() 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-btn declared no background and no color, so 43 buttons carrying no modifier class fell through to the browser’s native buttonface/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, /jobs and 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 /goal supplies the durable intent + success_criteria, /loop supplies the run-until-done engine. Parsing, prompt rendering, and hydration are pure and live in loop_engine.rs (GoalBrief, hydrate_goal_loop, goal_loop_prompt, goal_validator_prompt, LoopSpec.goal_id — #[serde(default)], so pre-existing loops.json jobs still load); the store-touching edge (resolve_goal_brief / goal_brief_by_id / link_goal_loop_start / record_goal_loop_outcome) lives in exec_goal_repl.rs over a new SessionStore::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 DONE only 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 — Done flips the goal to done; 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 show carries the history. Already-done goals are refused at start.
    • Bounds + secrets — --max-iter N and --max-duration 45m override the 20-iteration / 30-minute defaults (a mistyped bound errors rather than silently falling back); --secret NAME works 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_iteration takes a validator: Option<&str>, scheduler_tick takes a validator_lookup closure (criteria re-read each tick, so mid-run edits are honoured), and ProviderLoopExecutor asks 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_engine unit tests, 2 new hosted_loop scheduler tests, a find_goal_by_prefix store test (incl. LIKE-wildcard rejection), and 4 new loop_engine_bdd scenarios (10 total). REPL completions, /loop help, and docs/vibecli.md (§ Goal-Driven Loops) updated.
  • 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 Trajectory schema, 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 a best_skill.md deployed 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-rs facade. Provider-agnostic via a crate-local SkillLlm trait; neither crate depends on vibecli/vibe-ai. cargo clippy --features cli -- -D warnings + cargo fmt --check clean.
    • Daemon bridge — vibecli/vibecli-cli/src/skillforge_index.rs adapts both crates onto vibe_ai::AIProvider (AiProviderLlm), reuses the existing skill_catalog::SkillCatalog (no re-parse of skills/*.md), and exposes ten routes: /v1/skilllens/{skills,skills/:name,refresh,convert,extract,score} + /v1/skillopt/{train,status/:job,cancel/:job,promote}. train is async-job (spawn + poll status); promote writes *.opt.md to the per-workspace override dir (<ws>/.vibecli/skills/, fallback ~/.vibecli/skills/) and never overwrites a shipped skill. /health.skillforge + the startup banner report loading → ready (N skills). 10 bridge tests green; cargo check clean.
    • Watch mirror — two curated /watch/skilllens/* routes (skills compact {count,top5}, skills/:name one-line) for the wrist form factor, registered in watch_bridge.rs.
    • VibeCoder panel — SkillForgePanel.tsx (Catalog / Lens / Optimize) mounted as a “SkillForge” tab in AiMlComposite. 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 as mobile_get_active_session) — the daemon stays the single source of truth. tsc --noEmit clean.
    • Client fan-out — full surface (skilllens.{list,get,refresh,convert,extract,score} + skillopt.{train,status,cancel,promote}) in VS Code api-client.ts + the Agent SDK (agent.skilllens.* / agent.skillopt.*); read-only catalogue + train-status on Flutter api_client.dart (skilllensSkills / skilllensSkill / skilloptStatus), Apple Watch WatchNetworkManager.swift (loadSkilllensSkills / loadSkilllensSkill), and Wear OS WearNetworkManager.kt (skilllensSkills / skilllensSkill). Every LLM-calling client method takes provider + 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 + TrainConfig reference), docs/demos/index.md (Agentic Systems row) + a “What’s Next” cross-link from docs/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 in notes/skillforge/.
    • Follow-ups deferred — efficacy-metric substrate (LLM-judge vs embedding-overlap); nightly “sleep” job (offline self-evolution with experience replay); external benchmark Env impls (SWE-bench / BFCL) behind a benchmarks feature.
    • Per-epoch SSE streaming + true cancellation — skilloptai::trainer::train_with_signals threads a dependency-free CancelToken (Arc<AtomicBool>, checked at the top of each epoch) and an optional per-epoch EpochEvent mpsc channel; train() is now a thin wrapper with empty signals. New POST /v1/skillopt/train/stream SSE route (job → epoch* → done/error, 15 s keep-alive) shares the same job map as poll-based /train, so /status + /cancel work on both. cancel/:job now flips the live token so the run stops at the next epoch boundary instead of running to completion; TrainingReport.cancelled distinguishes 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 --check clean. Client SSE consumption — the VS Code extension (VibeCLIClient.skilloptStreamTrain) and Agent SDK (agent.skillopt.streamTrain) consume the stream as AsyncGenerator<SkilloptTrainEvent> ({type:'job'|'epoch'|'done'|'error', …}) via a new readSseTypedEvents helper (typed event:+data: parser layered over the existing data:-only SSE helpers; the Agent SDK streamTrain async 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 --noEmit clean for both clients; 42 Agent SDK tests green.
    • Promoted-skill override dir — promote now writes <skill>.opt.md to 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 shipped skills/*.md stay pristine — no in-repo overwrite. The catalogue list surfaces has_promoted_override per skill and the detail view surfaces promoted_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 — SkillCatalog still wins over same-named plugins). The write is factored into a pure write_promoted_override_in helper for unit testability. 4 new bridge tests (dir resolver, write helper, stem-keyed scan, missing-dir empty); 17 bridge tests + cargo check + tsc --noEmit clean. Client-facing text in the VibeCoder panel, REPL /skillforge promote help, commands.rs doc, and the VS Code / Agent SDK promote doc comments updated to point at the override dir.
    • Real agent-job history env — a third train env kind, history, derives EvalTasks from actual agent runs instead of the catalog. The CLI now writes a lightweight per-session SkillEvalRecord (<session_id>-eval.json) at the end of every agent run via vibe_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>.jsonl trace (secrets scrubbed, same redaction as save_messages/save_context). env.kind=history scans ~/.vibecli/traces/ (or an env.tasks override dir) via the new vibe_ai::load_eval_records and builds one task per run: the session’s prompt becomes the task prompt; the grader is LlmJudge (default — rubric cites the reference final answer + tool-success rate + completion; one extra LLM call per task per epoch) or Contains (free, weak — a phrase from the reference answer), selected via env.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_records in vibe-ai::trace (re-exported from vibe_ai); EnvKind::History + EnvSpec.grader + HistoryGrader + RepoAgentEnv::from_history + history_trace_dir + parse_history_grader + rubric_for + phrase_from in skillforge_index.rs. The eval record is written at both AgentEvent::Complete (completed=true) and AgentEvent::Partial (completed=false) in the interactive agent path (skipped on Error — no reference answer); a small history_tool_success_rate helper computes the rate (1.0 when 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 optional envGrader:'llm_judge'|'contains'; the catalog-derived repo env is unchanged.
    • Gap-closure pass — closed the seven post-Phase-5 visibility gaps tracked in notes/skillforge/07: (G1) VibeCLI REPL /skillforge command (list/show/refresh/score/train/status/cancel/promote/health, in-process, STRICT via active_provider/active_model); (G2) VibeCLI TUI SkillforgeComponent + SkillForge screen (/skillforge from chat, catalogue + train-jobs pane + /health footer); (G3) AgentContext.skill_health rendered as a ## Skill Health section in build_system_prompt, auto-gated on cached_reports > 0 (no prompt bloat when nothing is scored) — populated by skillforge_index::render_health_line() at the daemon’s AgentContext construction sites; (G4) Apple Watch SkillforgeView (6th “Skills” tab, top5 → detail); (G5) Wear OS SkillforgeScreen + SkillforgeDetailScreen + SkillforgeTileService (routes + deeplink + manifest); (G6) Flutter SkillforgeScreen (8th “Skills” bottom-nav tab, cross-machine catalogue + detail + train-status lookup); (G7) VibeAIChat — 10 SkillForge Tauri proxy commands registered in vibeaichat/src-tauri (no panel; the bespoke UI has no AiMlComposite/toolbar). cargo check clean (vibecli, vibe-ai, vibecoder/src-tauri, vibeaichat/src-tauri); 199 TUI + 44 repl tests + 2 new context-assembly tests green; dart analyze clean on new/touched Flutter files; swiftc -parse clean on Watch files. Full ledger in notes/skillforge/07 — Client Visibility & UX Gaps.md.
  • Code Graph integration (kodegraph) — token-reduction substrate. Wired the standalone kodegraph crate (tree-sitter → SQLite code-knowledge-graph) into the daemon as a single bridge module vibecli/vibecli-cli/src/graph_index.rs (kodegraph is a dep of vibecli-cli only; vibe-ai / vibe-core stay kodegraph-free and receive pre-rendered strings / SymbolInfo vecs):
    • Background build on daemon startup — non-blocking std::thread::spawn parse; the startup banner, /health, and /graph/status surface indexing → 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 Structure when the graph is ready (falls back to the dir-tree when disabled).
    • TUI context — ContextBuilder::with_relevant_symbols seeds ## Relevant Symbols from 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 returning serde_json::Value (no kodegraph-type coupling); two curated /watch/graph/* routes (status compact {status,n,m}, query capped to ≤5 nodes) for the watch form factor.
    • 7-client fan-out — Tauri commands (graph_* in commands.rs + lib.rs), Agent SDK (agent.graph.*), VS Code (apiClient.graph*), Flutter ApiClient, Watch Swift WatchNetworkManager, Wear Kotlin WearNetworkManager.
    • /semindex rewired onto graph_index (build/query/node/callers/callees/hierarchy/stats); the superseded semantic_index.rs (regex/manual parser) and dep_visualizer.rs were deleted (their module decls removed from lib.rs/main.rs). semantic_mcp.rs’s own SemanticIndexServer is unrelated and untouched.
    • PreToolUse hook — a non-blocking Glob|Grep nudge in .claude/settings.json that 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, lsp tier through vibe-lsp, ast_edit.rs parser delegation, and semantic_mcp.rs reuse of kodegraph::mcp.
  • 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, default high) in vibe-ai/provider.rs, mapped per provider: Claude/Gemini extended-thinking budget, OpenAI reasoning_effort (clamped to “high”). Wired through ProviderConfig.effort, cost_router::route_task_with_effort, the daemon /agent path (serve.rs, via the VX-111 reasoning label), the ai_chat_with_effort Tauri command, vibeaichat start_agent_session, and a VibeCoder toolbar selector (utils/effort.ts). Fix: Gemini Pro models can’t disable thinking — Effort::Low now clamps Pro to the 128-token minimum instead of emitting an API-rejected thinkingBudget: 0. BDD: effort_bdd (5 scenarios).
    • C1 · /loop — recurring (/loop 5m <prompt>) and self-paced (/loop auto <prompt>, loop-until-done) REPL command with a MAX_ITER guard (20), wall-clock auto-expiry, job IDs, Ctrl-C stop, an LLM done-validator, and list/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|cancel registry with a Working→…→Completed/Failed/Cancelled state machine, plus stateless _meta (RequestMeta) and a stateless flag on StreamableHttpConfig (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. Tauri design_emit_diff (design_diff.rs).
    • Opt-in security review — default-OFF watcher that emits standard self_review::Finding records (no auto-apply). Tauri security_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.ts with updated defaults; Fable 5 / Mythos 5 omitted (export-suspended 2026-06-12).
    • Follow-ups still open: UI panels (/loop Automations, Design-Mode wiring, SecurityPanel), daemon/live-transport loops (B3 always-on watcher, C3 live MCP dispatch, C4 browser_agent CDP), C5 streaming-path propagation, and C2 (dynamic large-scale workflow primitive).

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, reported Partial, and left the user to press Resume to finish work that was going fine. AgentLoop now grants up to max_step_extensions (default 3) further budgets of max_steps each — a ceiling of max_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 read Progress (not Stalled / 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 step at the top of its body rather than the bottom. The body has many continues, and a bottom increment would skip every one of them and spin forever. The post-loop Partial reporting now quotes the effective budget rather than max_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 /agent takes an optional max_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 == 0 is 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.

Fixed

  • A chat history containing one huge message wedged the run permanently. prune_messages only ever dropped the middle of the conversation. When the oversize lived in a message it must preserve — the system prompt, or a read_file / bash result 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_tokens charges 8 tokens of framing per message, so a budget under 8 × len cannot be met by clipping, and the helper degrades rather than spinning (pinned by a test).

  • Subprocess dispatch had the original /agent bug, untouched. The child loop in main.rs matched four AgentEvent variants and swallowed the rest with _ => continue — so a dispatched run that stopped mid-plan drained the channel, left completed false, and the fallback sent DispatchFrame::Complete { "Agent finished." }, which the parent records as JobStatus::Complete. Byte-for-byte the defect fixed in serve.rs on the first pass, in a code path never opened. ToolCallPending was dropped there too, silently abandoning the run. Found by verifying a claim (“the remaining catch-alls are already audited”) instead of restating it — two AgentEvent matches in main.rs had never been read.
    • DispatchFrame gains a Partial variant, since Complete is defined by this protocol as success and Error overstates a resumable run. The parent publishes the rich partial event to SSE and marks JobStatus::Partial. Safe to add a variant: the child is this same binary, self-spawned via current_exe(), so parent and child can never be different versions.
    • A second main.rs loop (console output) also dropped Partial, 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). HashMap iteration order is randomly seeded per process and max_by_key returns the last maximum, so which label won a tie varied run to run and the same graph settled differently — detects_a_community_from_a_cluster failed roughly one run in ten. Two further orderings had the same flaw: communities were grouped out of a HashMap and 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 running cargo test --workspace rather than --lib; unrelated to the rest of this changelog entry, and pre-existing.

  • The CLI’s spawn_agent had the same unfinished-subtask bug as VibeCoder’s, verbatim. tool_executor.rs and vibecoder/src-tauri/agent_executor.rs carry 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 partial or cancelled session as finished. watch_sync_service.dart tested status == 'complete' || status == 'failed', an allow-list of two — so cancelled (pre-existing) and partial (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.onFailure with a null throwable when the failure is an unsuccessful HTTP response, and both openStream and openTaintedPendingStream did if (t != null) onError(t) else onComplete(). So a 401 or 500 ran the completion path: ConversationScreen cleared 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 step with no success field rendered as “ok”. Introduced by this changelog’s own parser rewrite, and caught by the follow-up sweep rather than by review. AgentEvent.Step.success is now Boolean? and renders outcome not reported when the daemon said nothing.

  • An unverified observe-act step erased the failure streak, so a failing loop never bailed out. ObserveActSession::record_step computed success as verification_result.map(|v| v.success).unwrap_or(true) — the comment said // No verification = assume success — and a “success” resets consecutive_failures to 0. With unverified steps interleaved (the normal case when verify_after_action is off, or verification itself fails), the streak could never reach max_consecutive_failures, so a loop that was failing every step ran the entire max_steps budget 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_end with no success field showed a green tick on the wrist. to_watch_event_json defaulted the missing field to true and reported status: "ok". status is Option<String> precisely so absence can stay absent; it is now None when the producer said nothing.

  • A failed watch/phone reply was recorded in the database as a completed one. The /watch/dispatch streaming task matched chunks with if 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 emits error, stores failed with 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 into persist_watch_turn and pinned by three tests (failed-with-partial, clean, failed-before-any-output).

  • /chat and /chat/stream ignored 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, and POST /agent already did. The chat routes did not: ChatRequest.model rode the wire but was marked #[allow(dead_code)] and never read, and there was no provider field 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 shared chat_provider_for, which honours provider + model together (matching /agent), wraps the result in resilient() 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/stream reported a failed reply as a finished one. A mid-stream provider error emitted an error event and then carried on, so the trailing done still fired: clients saw error followed by done and rendered a truncated reply as complete. error is now terminal.

  • A dropped connection reported a still-running task as finished. VibeDesk settled a stream that closed while running straight to done, 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 to reviewing.
    • The daemon is authoritative and already had the pieces: GET /jobs/:id for 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 cancelled before any of this.
  • Choosing a model in the toolbar silently disabled retry. The daemon’s default provider is wrapped in ResilientProvider by main.rs::create_provider, but build_provider_override_with_effort — used whenever a request carries provider + 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 new resilient() helper.
    • The builder deliberately still returns raw: AgentLoop retries stream_chat itself, 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 that resilient() is identity-preserving (name() delegates), so wrapping never changes which model a caller believes it is talking to.
  • Unfinished agent runs were reported to every remote client as successes. AgentEvent has 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 published complete("Agent finished.") and marked the job Complete. 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.
    • ToolCallPending was swallowed the same way, which killed the run outright. Dropping result_tx without answering made AgentLoop::run hit Err(_) => 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 passing approval: "suggest"/"autoedit", which the route accepts.
    • Harness — that silent return Ok(()) now emits AgentEvent::Error naming the tool and the dropped channel. A new assert_terminal_event test pins the contract across five paths: every run ends with exactly one of Complete / Partial / Error. Callers that infer success from “no Error seen” are now safe by construction.
    • Wire protocol — two new SSE kinds. partial is terminal and carries steps_completed / steps_planned / remaining_plan; retry is non-terminal and carries attempt / max_attempts / backoff_ms (previously a retrying agent was indistinguishable from a hung one for up to 60 s per attempt). system now also carries circuit-breaker and verifier notices. New AgentEventPayload fields are Option + 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 neither Complete nor Failed: the work done is real and the run is resumable. Folding it into Complete is what let unfinished tasks read as successes.
    • Client fan-out — Agent SDK + VS Code (isTerminalAgentEvent, replacing four hand-rolled complete || error checks), VibeDesk (stream_agent forwards partial/retry; RunState gains partial, 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), VibeCoder BackgroundJobsPanel (closes the EventSource on partial instead 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 parseEvent looked for thinking / text / tool_call / tool_result and read summary / message / text: kinds and fields the daemon has never emitted. Every streamed token fell through to null and complete rendered an empty summary. Rebuilt against the real AgentEventPayload shape, with Step / System / Retry / Partial added.
    • Sub-agents — spawn_agent (vibecoder/src-tauri/agent_executor.rs) dropped Partial into _ => {}, leaving summary empty, which its tail rendered as “Sub-agent completed.” to the parent agent. A sub-agent that ran out of steps mid-plan now returns ToolResult::err with what it finished and what it did not, so the parent re-dispatches instead of building on unfinished work.
    • Docs: docs/api-reference.md gains a terminal/non-terminal event table, the partial-run example, and the partial job status.
  • VibeDesk · Sandbox mode with per-axis permissions. The agent is normally jailed to workspace_root — ToolExecutor::resolve_safe canonicalizes 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. New sandbox_policy.rs (read_outside · write_outside · exec_outside · network, plus allow_roots / deny_roots), a sandbox field on POST /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 an allow_root pointing straight at ~/.ssh — reaches a key. Pinned by credentials_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_roots is checked before allow_roots. effective_sandbox_policy() is extracted and tested precisely so a sandbox field 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_FILENAMES includes config.json, an ordinary project file.
    • Sub-agents inherit the policy. spawn_agent builds a child executor field by field; omitting sandbox_policy there 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_outside enforces something real. bash is 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 drives ToolExecutor’s existing OS-level confinement (bwrap+Landlock / sandbox-exec / AppContainer): off confines commands, on runs them as they do today. An earlier draft shipped a allows_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.
  • VibeDesk · Chat mode, thinking off by default, and a resizable Environment rail.
    • Run mode (Agent · Chat). New mode field on POST /agent with a RunMode parser (unknown values fall back to Agent, so older clients are untouched), and a chat path in serve.rs that 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 composer ModePill, persisted with the other run controls.
    • Live-verified. “In one sentence, what is the Mandelbrot set?” in Chat mode against minimax-m3:cloud returns 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), so strip_thinking correctly 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 uses strip_thinking, because reasoning quotes calls the model then rejected.
    • Thinking is off by default. ReasoningEffort gains an off tier, now the composer default. Off is sent as the absence of the reasoning field, not a value: the daemon maps an unknown effort to no budget but still publishes a Reasoning effort: … system line for whatever it receives, which would have announced a tier the user had turned off. One shared effortParam() 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 useLayoutPrefs following useTheme’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:thinking is a new script with 11 cases.
  • </mm:think> rendered verbatim in chat, mid-sentence. minimax-m3 emits a namespaced reasoning tag, and all three strippers matched only think|thinking — so <mm:think> sailed through every one. Fixed in vibe_ai::tools (strip_thinking / unwrap_thinking), vibedesk/src/lib/thinking.ts and vibecoder/src/components/AIChat.tsx with 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 thinking field, 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 tool parser now accepts three dialects, not one. parse_tool_calls understood 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 in AVAILABLE_TOOL_NAMES are 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 regex crate 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).
  • Native tool-calling models produced an empty turn and a stuck run. OllamaChatRequest carried model, messages, stream and options — but never tools. Tools existed only as prose in TOOL_SYSTEM_PROMPT, which works for models that follow prompt instructions and not at all for one trained to call a tool API: minimax-m3:cloud reasoned “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 in reviewing. The response side had always transcribed native tool_calls back 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/chat call 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_call turns each JSON key into an XML tag, so a renamed parameter would silently turn every native call into an unparsed block. tool_definitions_match_parser builds a call from each schema’s required params and asserts it round-trips through parse_tool_calls. 5 new tests; 1181 vibe-ai tests 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 against minimax-m3:cloud and 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.rs has the same gap, so every OpenAI-compatible endpoint is prompt-only today. tool_definitions() is provider-neutral and ready for it.
  • 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 with strip_thinking before treating the text as an answer — but SessionStream.tsx piped 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.ts splits reasoning from the answer, and ReasoningBlock.tsx renders 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 via npm run test:thinking.
    • VibeCoder was not affected — AIChat.tsx:extractThinking already handled this; VibeDesk simply never got the equivalent.
  • ProfileStore split into crates/vibe-profile-store. Reading one API key used to mean linking the whole CLI: the store lived inside vibecli, so anything touching settings pulled mistralrs and candle with it. vibeaichat/src-tauri/src/commands.rs documents avoiding vibecli for 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 (zero crate:: references), so the lift was clean.
    • vibecli re-exports it as vibecli_cli::profile_store, leaving all ~30 call sites unchanged — serve.rs, config.rs, watch_auth.rs, VibeCoder’s commands.rs and the rest.
    • vibe-desktop-settings now depends on the store alone, so VibeAIChat’s tree carries no vibecli, mistralrs or candle (396 crates). VibeDesk still links vibecli — 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 vibecli went 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), crate vibeapp/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, …), the build-vibeaichat release job, vibeaichat-checks in 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 ~/.vibecli and are unaffected.
    • packages/vibe-ui-shared — the settings screens (Providers · Appearance · Account), theme definitions, useTheme, useProviderSettings and the reasoning parser now exist once, consumed as source via a @vibe/shared alias. No build step, no dist, 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.)
    • SettingsView gained extraTabs so 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 both vite.config.ts and tsconfig.json — which also keeps one React instance in the bundle. The Vite aliases use regex find: a plain string alias is a prefix match, so "react" would rewrite "react-dom" into "<path-to-react>-dom". And resolve() strips a trailing slash, so sub-path replacements re-append it or react/jsx-runtime concatenates into reactjsx-runtime. In tsconfig, @types must precede the JS package in paths or every import lands as any.
    • Verified — tsc and vite build clean in all three shells, cargo check/fmt clean, the 4 settings-migration tests moved with the crate and pass, both test:thinking suites 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).
  • VibeAIChat answered “hi” with pages of the model arguing with itself. Two stacked defects, both visible in one screenshot. start_agent_session had no mode parameter 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 against minimax-m3:cloud: the same hi now returns 37 characters (“Hi there! 👋 How can I help you today?”) with no thinking tags and no tool-rule argument.
    • vibeaichat/src/lib/thinking.ts strips 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 via npm 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.
  • 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: /health shared the public routes’ 10-req/min per-IP bucket, and three desktop apps plus daemon_bootstrap::probe’s 250ms startup poll all arrive from 127.0.0.1 — so the bucket was exhausted in seconds and never recovered. A throttled /health returns {"error":"Rate limit exceeded"}, which carries no service field, so probe() read a healthy daemon as a foreign process; clients then spawned replacement daemons, and each replacement wrote ~/.vibecli/daemon.token before 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.
    • /health is 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 bind only opens the socket and nothing is answered until axum::serve accepts, 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 /health probes 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, /health public and self-identifying). Verified to fail without the fix: reverting the /health limiter made it fail at wait_ready — probe() could not identify the daemon at all within 90s, the production symptom exactly.
    • HOME is isolated in both daemon integration suites. The token path is shared across daemons regardless of port, so a test inheriting the real HOME overwrites the developer’s own daemon token and breaks every client on the machine — the very failure under test. daemon_bootstrap_integration.rs had 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 .app launched 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 “run vibecli --serve --port 7878 in a terminal to see why” — where it does not reproduce.
    • Fix — daemon_bootstrap.rs gains spawn_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) and spawn_output() (capture stdout/stderr to ~/.vibecli/daemon-spawn.log, truncated per spawn). DaemonState::TimedOut now 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.rs had its own tokio::process::Command spawn — precisely the second copy daemon_bootstrap.rs exists to prevent, and the one that carried this bug. It now calls boot::spawn_working_dir() / boot::spawn_output(), as does the shared spawn_detached.
    • Tests — 3 new daemon_bootstrap unit tests (unwritable candidate skipped, caller’s cwd preferred, candidate list ordered), following the existing find_binary_in pattern of taking candidates as an argument instead of mutating process-global cwd. Verified end-to-end with the same binary under a Finder-like env -i: cwd / exits 1, cwd $HOME comes up listening.
  • 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-analyzer now returns 165 completions for text. on text that exists only in the editor, and clangd returns exactly the struct’s fields.
    • The message pump deadlocked the connection. The client forwarded every inbound message into a 32-slot mpsc that only an in-flight request drained. rust-analyzer emits thousands of $/progress notifications 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.rs is 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), stores publishDiagnostics, 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 send Content-Type used to desynchronise the stream permanently).
    • Every document URI was one no server had heard of. <Editor> had no path prop, so all files shared a single model at inmemory://model/1, while didOpen announced file://…. Each file now gets its own model at a real file URI, produced by one encoder on each side (fileUri / path_to_uri) — the old format!("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_change had 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 a didOpen.
    • Completion kinds and snippets were mistranslated. LSP and Monaco both call the enum CompletionItemKind and agree on no value (LSP Text is 1; Monaco’s is 18, Method is 0), so every suggestion carried a wrong icon; mapping is now by name, since Monaco renumbers (Snippet is 28 in 0.55, was 27). insertTextFormat: 2 now sets InsertAsSnippet instead of typing ${1:arg} literally. Auto-import edits, commit characters, sortText/filterText/preselect, deprecation tags, labelDetails and completionItem/resolve documentation are all carried through.
    • No trigger characters were registered, so foo. and Vec:: 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 with didSave, 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 — so rust-analyzer was No such file or directory in the bundled .app even when it worked in cargo run. New discovery.rs resolves servers across the standard install dirs and hands the spawned server the same augmented PATH (servers shell out: rust-analyzer → cargo, tsserver → node). Availability is a directory scan, not ~60 which subprocesses.
    • A missing server no longer costs a spawn per keystroke. The manager remembers a failed start and fails fast until lsp_restart_language clears it (the action to take after installing a server — no app restart). Clients are handed out as Arc, so the manager lock is released before awaiting: one cold rust-analyzer no 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, because onMount captured activeFilePath once; 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.ts ranks, manager.rs server configs, lib/lsp.ts extension 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 for matlab-language-server, asm-lsp, ada_language_server, superbol-free (COBOL), sas-lsp, abaplsp, powershell-editor-services, bash-language-server, the Nomic Solidity server, and clangd for Objective-C (a first-class clangd language, not an approximation); PL/SQL and T-SQL ride on generic sqls under their own language ids, with install hints that say so rather than implying dialect awareness. Scratch (a block language whose .sb3 is 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 used cobol as 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, Gleam gleam lsp, CUDA via clangd; Zig/Nim/Crystal/V/D/Vala were already wired), the component frameworks (Svelte svelteserver, Vue vue-language-server, Astro astro-ls), functional newcomers (Elm, PureScript, ReScript), infrastructure (Terraform/HCL terraform-ls, Nix nil, CMake, Protobuf protols), shaders and hardware description (GLSL glsl_analyzer, WGSL wgsl-analyzer, SystemVerilog svls, VHDL vhdl_ls), plus LaTeX texlab and 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 caught cfml (configured, no .cfm/.cfc route, so unreachable) and dlang (an unreachable duplicate of d, now removed).
      • The built-in-service check had to move from the Monaco language to the LSP language. .vue, .svelte and .astro all highlight as html, and html is 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. LspStatus lost its now-redundant monacoLanguage prop as a result.
      • Highlighting followed routing. Files whose Monaco language id was never registered (matlab, asm, cobol, sas, and now odin, gleam, nix, elm, purescript, rescript, astro, cmake, vhdl, latex) got neither highlighting nor providers, because Monaco rejects an unknown language id. detectLanguage now returns registered ids — including cmake and latex, which previously resolved to plaintext and 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: .v stays V (not Verilog — SystemVerilog uses .sv/.svh), alongside the existing .pl → Perl and .m → MATLAB decisions.
    • 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 declarative LanguageSpec (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 detectLanguage can return must be registered (Monaco rejects an unknown id, so matlab/cobol/sas/cmake files 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’s A' 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 the Obj'Length attribute 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-1 C, Prolog’s variable-versus-atom distinction, and case-insensitive keywords in COBOL/Ada/Fortran/VHDL/SAS/CMake.
    • A missing language server is now two clicks, not a copy-paste hunt. The status bar offers a Copy install button beside the warning; parseInstallHint splits 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 in manager.rs and rejects any command that could not be pasted as-is — that is how cargo install --git, truncated at a URL that was actually an argument, was caught. lsp_language_support now returns the raw installHint alongside the human-readable detail, 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-lsp unit 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 real rust-analyzer/clangd; 102 frontend tests for the bridge and status indicator, with enums imported from Monaco’s own standaloneEnums.js so a renumbering can’t slip through. cargo check --workspace --exclude vibe-collab, tsc --noEmit, 362 vibe-coder and 1353 frontend tests clean.

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 track vibecoder/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-hdpi only, with no mipmap-anydpi-v26, so modern Wear OS could not mask it to the device icon shape; and VibeAIChat/VibeDesk’s index.html pointed at /vite.svg, a file that existed in neither app (a 404 favicon). All three now carry the real mark, plus android:roundIcon in both Android manifests.
    • Pipeline — make icons regenerates all 143 artefacts from scripts/brand/brandkit.py; make icons-check fails 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.py is a dependency-free PNG/ICO codec (no Pillow, no ImageMagick) covering the two jobs rsvg-convert cannot 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 in assets/brand/README.md.
  • VibeDesk is now a released artifact. It had shipped in no release: release.yml built VibeCoder and VibeAIChat only, ci.yml did not check it at all, and vibedesk/src-tauri/tauri.conf.json declared neither signingIdentity nor entitlements — so the third desktop shell was buildable locally and invisible everywhere else.
    • Release — new build-vibedesk job mirroring build-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:: when APPLE_CERT_P12_BASE64 is absent, and the same artifact collection (.dmg / .deb / .AppImage / .msi / NSIS .exe). Added to release.needs[] and given its own downloads table in the release body. Deliberately not added to the release job’s critical if: gate — the job has never run on Linux or Windows, and a first-run failure there must not block an entire release; promote it with needs.build-vibedesk.result == 'success' once it ships green.
    • CI — new vibedesk-checks job (typecheck + the no-inline-edit VX-013 guard, the latter having existed as an npm script that nothing ran), wired into ci-gate’s needs and 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.plist added (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 for tauri-plugin-dialog), with signingIdentity: "-" and the entitlements path in tauri.conf.json so VibeDesk matches the other two shells.
    • Cost note — VibeDesk’s src-tauri embeds the whole vibecli crate 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 the vibe-mistralrs feature flags.
  • 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 (the APPLE_CERT_P12_BASE64 secret is a CI-only import mechanism), export APPLE_SIGNING_IDENTITY / APPLE_TEAM_ID / APPLE_ID / APPLE_PASSWORD, and build. Documents that APPLE_SIGNING_IDENTITY overrides the committed "signingIdentity": "-", so an ordinary dev build still needs no certificate; that APPLE_PASSWORD must be an app-specific password; that notarization costs 2–15 min per build; and the three codesign / stapler / spctl commands that verify the result. Also recorded that VibeDesk was absent from release.yml and declared no signingIdentity — it was not released at all at the time. (No longer true: a build-vibedesk job 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.sh drives Finder over AppleScript and exits 64 when that osascript call fails, but Tauri captures the script’s output and surfaces only failed to run …/bundle_dmg.sh, so the reason is unrecoverable from the build log. Documents the fix that was actually verified — CI=true (not CI=1: Tauri binds CI to its own --ci flag, which accepts only true/false and otherwise dies with error: invalid value '1' for '--ci'), which passes --skip-jenkins and 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.3 to 3.44.2 (Dart 3.7 → ≥3.10) and the vibemobile Dart SDK floor to ^3.8.0, to support flutter_lints 6.0.0 (requires Dart ^3.8.0) and the regenerated lockfile (resolved deps require Flutter ≥3.38.4 / Dart ≥3.10.3). FLUTTER_VERSION updated in ci.yml and release.yml; platform-requirements table in docs/vibemobile.md updated 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) as TuringWorks/mistral.rs@vibe/kv-cache-codec-kernels-v0.9; vibe-infer/Cargo.toml now pins 5860f815. The previous branch vibe/kv-cache-codec-kernels is untouched at f8f3a105, so the bump reverts with one line. Zero VibeCody source changes — every API we consume survived (with_isq(IsqType) now routes through the new IsqSetting via builder_macros.rs but keeps its signature; sampler setters, From<TextMessages> for RequestBuilder, and mistralrs::core unchanged).
    • 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-disconnect SendError #2170, engine-creation error propagation #2226, errored-sequence cleanup #2243); logprobs corrected 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-infer implements mistralrs::core::KvCacheCodec, whose methods take candle_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-infer is the workspace’s only candle consumer; candle-transformers has no tokenizers dep, so minilm.rs is unaffected.
    • Divergence shrank — the fork patch is 16 files instead of 17. mistralrs-quant/build.rs dropped out entirely: #2288 replaced the hand-maintained METAL_SOURCES array with a metal_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 existing kernels/*/*.cu glob. The #[allow(dead_code)] on MoEExperts::num_experts also dropped — it is a used pub field upstream now.
    • RotatingCache hooks re-derived, not merged — upstream rewrote the cache into a relocating ring buffer (write_pos / window_start / retained_len). Invariant is now explicit: all_data always 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.9 snapshot() / restore_from_snapshot() machinery, which our patch predated, needed the codec carried on RotatingCacheSnapshot and 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_encode bound its output buffer via a bare &Buffer → set_buffer (the input path) instead of Output::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 used Output::new. (2) turboquant.metal declared a file-scope constant uint SIMD_SIZE, private under per-file AOT compilation but colliding with MLX’s MLX_MTL_CONST int SIMD_SIZE once #2288’s runtime path concatenates every source into one translation unit; since Kernels::LIBRARY is a global OnceLock, 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 to TQ_SIMD_SIZE.
    • Verification — 31/31 fork kv_cache tests (including all of upstream’s own ring-buffer tests) and 25/25 vibe-infer tests with mistralrs,mistralrs-metal, on both the AOT and runtime-compile Metal paths; turboquant_encode_{float,half} symbols confirmed present in all three built mistralrs_quant metallibs; cargo check --workspace --exclude vibe-collab clean. 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-sandbox unconditionally (with landlock / seccompiler on Linux only). cargo tree -e features confirms only default, metal, mistralrs-paged-attn are enabled on mistralrs-core, so the code-execution feature is off and the Python code-execution tool is never exposed. cargo deny check (and cargo deny --all-features check, as security.yml runs it) passes clean — advisories, bans, licenses, sources — with https://github.com/huggingface/candle.git added to the allow-git list, which the new [patch.crates-io] requires under unknown-git = "deny". deny.toml’s RUSTSEC-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.rs destructured storage_and_layout() as (storage, _) for the rotation/projection matrices and passed the kernel a base pointer, so a non-zero start_offset or non-contiguous view would have been read from element 0: wrong numbers, no error. Callers use Tensor::from_vec so 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 shared ensure_matrix_layout, matching the check input already had.

Fixed

  • CI · the Metal GPU path was never executed anywhere. Every ci.yml job runs on ubuntu-latest, where mistralrs-metal cannot build, and the note claiming “the release workflow uses --all-features on macOS runners” was false — --all-features appears only in that comment and in security.yml’s cargo-deny step, which compiles nothing. mistralrs-metal is compiled in macOS release builds, but via the cfg(target_os = "macos") dep in vibecli/Cargo.toml, and no workflow ever ran its tests. Net effect: the TurboQuant GPU codec was shipped on the strength of cargo check alone, and both Metal bugs found during the v0.9 sync would have reached a release. Added a metal-gpu-tests job (macos-latest) that runs the vibe-infer suite on both Metal paths — precompiled metallib and MISTRALRS_METAL_PRECOMPILE=0 runtime compilation — because those exercise different code and each caught a different one of the two bugs. Wired into the required ci-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 requirements accepts the requirements path positionally, not as -i FILE; the bad flag caused the tool to fall back to looking for ./requirements.txt and emit CRITICAL | CDX > Could not open requirements file. Drop the -i so vibe-rl-py.cdx.json is produced. Closes #28.
  • Mobile · iOS build (b8d95e0f) — vibemobile/ios/Runner/AppDelegate.swift referenced FlutterImplicitEngineDelegate and FlutterImplicitEngineBridge, 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 reported Cannot find type 'FlutterImplicitEngineBridge' in scope. Rewrite to the Flutter 3.29-compatible GeneratedPluginRegistrant.register(with: self) pattern and register the relay-credentials method channel synchronously in didFinishLaunchingWithOptions. Closes #29.
  • Watch · watchOS build (014f5cce) — GoalsView.swift, JobPickerView.swift, RecapView.swift, and TaintedConfirmationView.swift were on disk and referenced by ContentView.swift / SessionPickerView.swift but never added to VibeCodyWatch.xcodeproj’s PBXSourcesBuildPhase. The Swift compiler reported four cannot find … in scope errors and the watchOS simulator app build exited 65. Register each as a PBXFileReference + PBXBuildFile, add to the group and sources phase (plutil -lint passes). Closes #30.
  • Watch · Wear OS build (6193920a) — JobRecapTileService.kt and GoalsTileService.kt import androidx.concurrent.futures.CallbackToFutureAdapter and com.google.common.util.concurrent.{Futures, ListenableFuture}, and RecapScreen.kt uses androidx.compose.ui.tooling.preview.Preview; none were declared as dependencies, so :app:compileReleaseKotlin failed. Add guava (33.4.0-android), androidx-concurrent-futures (1.2.0), and androidx-compose-ui-tooling-preview (1.7.6) to libs.versions.toml and implementation them in app/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 because vibe-memory/src/ was never copied over the empty stub, leaving vibecli unable to find MemoryContextHub, 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_* (not VibeCLI_*), VibeCody-Mobile-vX.Y.Z-{ios,android}.*, VibeCody-WatchOS-vX.Y.Z.app.zip, VibeCody-Wear-vX.Y.Z.*. Surface the new aarch64.AppImage and arm64.deb artifacts 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: every AgentLoop::new in serve.rs (/v1/agent start, ACP submit-task, timed-task path) now calls plugin_runtime::merge_with_plugin_hooks(workspace, vec![]) and attaches the resulting HookRunner before agent.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 each HookComponent into a vibe_ai::HookConfig with a Command handler 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 CLI HookRunner::new sites (run_parallel_agents orchestrator + run_agent_repl_with_context REPL). Best-effort with warn! 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 new plugin_rules ContextSection 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_NAMES grows the new entry so /v1/capabilities advertises the shape correctly. Daemon-side AgentLoop sites in serve.rs intentionally 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.ts implements the same seven-event hook contract as vibecli-cli/src/hook_abort.rs and the JetBrains HookExecutor: sh -c <command> (or cmd /c on 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. UserPromptSubmit is 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.hooks joins the configuration schema as an array<{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. HookExecutor service mirrors vibecli-cli/src/hook_abort.rs: subprocess invocation via sh -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 existing PersistentStateComponent infra. Settings UI under IDE Settings → Tools → VibeCLI grows a hooks table with Add / Remove via ToolbarDecorator — Event column constrained to the seven allow-listed kinds matching plugin_manifest::ALLOWED_HOOK_EVENTS. Both user-driven prompt-submission paths now run through the chain: AgentPanel.startAgent and InlineEditAction.actionPerformed fire UserPromptSubmit before 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 via cat), and the event-kind allow-list as a drift guard. Advisory firings for SSE-arriving PreToolUse / PostToolUse / Stop deferred 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.tsx renders fenced mcp.app blocks 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 dispatch vibecoder:mcp-app-action window events for the chat layer to consume. New mcp_apps_parse Tauri command bridges the daemon-side parser (mcp_apps_payload.rs, shipped earlier as 647b58de) to the webview as defence in depth. Fence regex in AIChat.tsx relaxed from \w* to [\w.+/@-]* so the full MIME-like tag application/vnd.mcp.app+json matches without truncation. Malformed payloads fall back to a plain CodeBlock so 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_servers registers MCP-server components from policy-active plugins under namespaced ids plugin:<plugin>:<component>, disjoint from the flat user-configured id space (16da6354). plugin_install::install_from_url adds HTTPS-only URL fetch (60 s timeout, 50 MB cap, scheme guard) so vibecli plugin install <https://…> works alongside the local-file path; new plugin_install_from_url Tauri command and a Local file | HTTPS URL toggle 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.toml manifest schema + validator (B2.1, cea41606); detached per-publisher P-256 ECDSA signing via sibling vibecli-plugin.sig (B2.2, 6275cf06); WorkspaceStore plugin_policies table 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 view plugin_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 — MCP list_skills / get_skill now 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 (reuses mcpb_bundle.rs), and per-publisher P-256 trust roots (reuses signed_agent_card.rs JWK). 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 an ExecutionPlan on 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/goals CRUD + 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/goals for mobile + Apple Watch + Wear OS (G1.6), and VS Code + Agent SDK + design docs (G1.7). See docs/design/goal/README.md. Tauri commands use the exec_goal_* prefix to avoid conflict with the existing CompanyGoalsPanel surface.
  • /goal — hardening round (G4) — GET /v1/goals/:id/tree?depth=N recursive subtree walk (depth clamped 1..10, default 3, cycle-safe, truncation flag) and GET/PUT/DELETE /v1/goals/current for per-workspace “current pin” with pinned_goals cascading on goal delete. CLI gains /goal pin|unpin|current subcommands; aggregate /v1/goals/:id/recap honors { provider, model } for LLM synthesis with heuristic fallback (response carries recap_synthesizer). Apple Watch routes its “Start session” through the new curated /watch/goals/:id/start wrapper, and the VS Code extension grows a vibecli.goalsView tree-view in the sidebar with refresh + per-row context-menu actions.
  • /goal — fan-out round (G5) — Wear OS GoalDetailScreen gains a “Start session” chip backed by WearNetworkManager.startGoal() (curated /watch/goals/:id/start). /agent auto-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 SDK goals.* namespace adds tree(id, depth?), pin(id, ws?), unpin(ws?), current(ws?), and recap(id, { provider, model }); the Flutter ApiClient gains parallel getGoalTree, getCurrentGoal, pinGoal, unpinGoal. The VibeCoder GoalPanel adds a tree-view toggle (indents children under parents) and an “Aggregate recap” section that routes through selectedProvider + selectedModel from the toolbar (heuristic fallback when either is empty).
  • /goal — pin UX round (G6) — Pin/unpin chips in VibeCoder GoalPanel and the mobile detail sheet, with a ★ marker on whichever goal is pinned in the list. The agent stream emits a system event (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_goal now returns the linked (goal_id, title) so callers can wire attribution UI. End-to-end tests confirm the helper inserts a goal_links row through the real SessionStore::open_default() path (HOME-redirect pattern from G4.1).
  • /goal — goal-aware agent context (G7) — When /agent auto-links a session to a pinned goal, the daemon now synthesizes a model-readable preamble from the goal’s title, statement, success criteria, and current_plan and injects it into AgentContext.approved_plan (only when that field is empty — the Phase 7 S3 context_request path is untouched). auto_link_to_pinned_goal returns the full Goal struct 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’s system auto-link event with a distinct [goal] prefix in the agent output channel (0b64f6b4); the SDK AgentEventType is 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. Polls exec_goal_current every 15 s for external pin changes (CLI / mobile) and also listens for vibecoder:pin-changed window 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/goals accepts ?q=<text> for case-insensitive substring search across title + statement, AND-ed with the existing status / workspace / tag / limit filters. VibeCoder GoalPanel gains 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 through exec_goal_update. 2 new daemon tests cover the q-filter; frontend npx tsc --noEmit clean (b755926b).
  • /goal — TUI tree + Watch ★ marker (G11) — TUI Goals screen gains a t key that toggles between flat list and tree layout: children indent under parents via a client-side BFS over parent_goal_id, mirroring VibeCoder’s tree mode. Title-bar advertises the new key and current view mode. The curated /watch/goals payload gains a pinned: bool field (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/:id envelope gains an envelope-level pinned: 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/current round-trip — the watch never hits /v1/* directly. Apple Watch GoalDetailView and Wear OS GoalDetailScreen show the ★ on the title; Wear OS GoalsTileService now 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: GoalTreeItem prefixes 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. New VibeCLIClient.getPinnedGoalIds(workspace?) unions the daemon’s global pin with the workspace pin (union of two /v1/goals/current calls) 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 in pinned_goals (any workspace, including the global slot) renders with a yellow bold ★ in front of the title — same glyph the other surfaces use. New p key toggles the pin on the selected row, advertised in the screen’s title bar alongside f, 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. SessionStore gains two helpers: list_all_pinned_goal_ids() (one SELECT DISTINCT per refresh) and list_pin_workspaces_for_goal(id) (used by the unpin-everywhere walk).
  • TurboQuant-compressed OpenMemory index — CompressedMemoryIndex replaces 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/stats exposes index telemetry — response now includes embedding_dim, embedding_compression_ratio, and embedding_backend (always "turboquant" today; treat as opaque). Surfaced in the VibeCoder OpenMemory panel, the openmemory_index_stats Tauri command, and the MCP memory_stats tool.
  • vibe-infer crate — pure-Rust local inference traits (Embedder, TextGenerator) with a stub backend by default and an opt-in candle feature that loads sentence-transformers/all-MiniLM-L6-v2 (384-dim, mean-pooled + L2-normalized) via candle 0.10 + hf-hub. candle-metal adds Apple GPU acceleration. Default workspace builds pull no ML deps.
  • Linux arm64 Tauri builds — VibeCoder and VibeCLI App now ship .deb / .AppImage for aarch64 Linux via the GitHub-hosted ubuntu-22.04-arm runner (free for public repos). Matrix coverage now matches VibeCLI (which already had Linux arm64 via cross).
  • Ubuntu 24.04 forward-compat smoke job — new smoke-linux-next CI job runs cargo check --release on vibecoder/src-tauri and vibeaichat/src-tauri against webkit2gtk-4.1 on Ubuntu 24.04. continue-on-error: true and excluded from release.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.json and vibeaichat/src-tauri/tauri.conf.json now set bundle.macOS.minimumSystemVersion = "12.0" (was the Tauri 2 default of 10.13). Matches Apple’s current supported-OS cutoff.

Security

  • Bump rand 0.8 → 0.9 across vibecli/vibecli-cli, vibecoder/src-tauri, vibecoder/crates/vibe-core, and vibecoder/crates/vibe-collab to pick up GHSA-cq8v-f236-94qc (low severity; unsound interaction between rand::rng() and custom log implementations 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.13 SigningKey::random call-sites now use p256::elliptic_curve::rand_core::OsRng to 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). New vibe-infer::mistral::MistralGenerator implements TextGenerator on top of the mistralrs 0.8.1 crate (PagedAttention, ISQ, LoRA, OpenAI-compat types, candle 0.10.x transitively). Feature-gated behind mistralrs (CPU) / mistralrs-cuda / mistralrs-metal / mistralrs-flash-attn; defaults unchanged so baseline builds stay fast. Smoke example at examples/generate.rs exercises Qwen/Qwen2.5-0.5B-Instruct end-to-end. The dep is pinned to the TuringWorks fork (TuringWorks/mistral.rs@3d422fde, branch vibe/kv-cache-codec, upstream base v0.8.1) via git + rev so we can iterate on the KvCacheCodec hook without waiting on upstream review. Explicit SHA pin keeps local / CI / release builds reproducible — bump the rev in vibe-infer/Cargo.toml as the fork advances.
  • KvCacheCodec trait landed on the fork (Phase 3 follow-up). TuringWorks/mistral.rs@vibe/kv-cache-codec adds a pub trait KvCacheCodec { fn encode/decode/name } in mistralrs-core::kv_cache with a PassthroughCodec default, threaded through SingleCache / RotatingCache as Option<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 underlying slice_set / narrow buffers 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 at mistralrs-core crate root so downstream crates can install codecs without plumbing through private modules.
  • CandleTurboQuantCodec bridge in vibe-infer::kv_cache_codec — candle-backed implementation of the fork’s KvCacheCodec trait that reuses the pure-Rust KvCacheTurboQuant from the Phase 3 spike. Encode shuttles the tensor to host-f32, quantizes + reconstructs each head_dim vector via PolarQuant + QJL, and returns a same-shape same-dtype tensor; decode is identity (the reconstruction already happened in encode). Shape contract: head_dim is 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 into SingleCache proving the trait dispatch works. Gated behind the mistralrs feature so default builds stay thin.
  • Install-all codec fan-out + discovery helpers on the fork (Phase 3 wiring). TuringWorks/mistral.rs@3d422fde adds KvCache::set_codec (fans out to both K and V sub-caches; no-op on Shared), EitherCache::set_kv_cache_codec(codec) -> usize (walks every attention layer of a Normal or Hybrid cache, skipping recurrent layers, and returns the install count for logging), plus two async accessors on MistralRs / Model: set_kv_cache_codec(codec, model_id) — locks the pipeline behind the engine’s RebootState and installs in one shot — and kv_head_dims(model_id) — returns (k_head_dim, v_head_dim) from GeneralMetadata::model_metadata so 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::load installs KV codec on demand (Phase 3 completion). New KvCacheMode { Fp16, TurboQuant { seed, qjl_proj_dim } } + KvCacheMode::from_env() resolver reads VIBE_INFER_KV_CACHE / VIBE_INFER_KV_CACHE_SEED / VIBE_INFER_KV_CACHE_QJL_DIM. MistralGenerator::load / load_isq call install_codec_after_load, which queries head-dim from the fork, builds a CandleTurboQuantCodec, and fans it out via Model::set_kv_cache_codec. Layer count is logged at info for observability. MLA-style models (k_head_dim ≠ v_head_dim) warn and pick the larger; pipelines without ModelConfigLike metadata (speech / diffusion) error out loudly instead of silently booting with a broken codec.
  • InferencePanel KV-cache dropdown (Phase 3 UX). When Backend = Mistral.rs is selected, a new “KV Cache” dropdown appears with options FP16 (default) and TurboQuant (experimental). Picking turboquant prepends VIBE_INFER_KV_CACHE=turboquant to the generated cargo run invocation 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::MistralRs is now a first-class variant alongside vLLM / TGI / Triton / llama.cpp / Ollama, marked is_in_process() so callers can skip sidecar provisioning. build_mistralrs_command() emits the VIBE_INFER_MODEL=… cargo run -p vibe-infer --features {mistralrs | -cuda | -metal | -flash-attn} --example generate … invocation; generate_k8s_inference_deployment uses the vibecody/vibecli-daemon:latest image 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.
  • KvCacheBackend trait + KvCacheMethod enum in vibe-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. KvCacheReport is 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 a mistralrs-quant contribution.
  • 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 with cargo 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_agree drops sharply, but that is a data-shape artifact (flat softmax has arbitrary argmax) rather than a codec fault; judge viability by attn_mae which stays ~0.0002 even there. The KvCacheMethod::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 a mistralrs-quant kernel 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). New SemanticScorer owns a TurboQuantIndex from vibe-core and a caller-supplied EmbedFn; each add_segment embeds + compresses the content, and ContextStreamingEngine::query prefers approximate cosine similarity over the old keyword-overlap scorer when a scorer is attached. refocus(query) rewrites relevance_score across every segment so the RelevanceScore / Hybrid eviction strategies drop segments least relevant to the current task — a prerequisite for 10M–100M-token workflows that outgrow working memory. Optional construction via ContextStreamingEngine::with_semantic(config, scorer); engines built with new(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 / VibeCodyWear apps 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 / .aab alongside 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 attach failures 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 \n after <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 sends ev["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 — useCallback dependencies 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; /aireview REPL.
  • Architecture Spec Engine (architecture_spec.rs, 108 tests) — TOGAF ADM, Zachman, C4 Model, ADRs, governance engine; /archspec REPL.
  • Policy Engine (policy_engine.rs, 91 tests) — Cerbos-style RBAC/ABAC, 14 condition operators, derived roles, policy testing, YAML, audit trail; /policy REPL.
  • 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).
  • 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 — ThemeToggle now respects prefers-color-scheme on 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 description field 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 TabbedPanel component 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:
    • ResilientProvider wrapper — 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), RetryableError event, 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 index field instead of array position (circuits remained accessible after deletions).
  • Missing gates array in circuit detail for pre-existing circuits (defaults to empty).
  • Quantum simulator returns tuple arrays matching frontend TypeScript types.
  • TabbedPanel display:contents breaking child panel height.
  • LazyPanels props aligned with refactored createComposite pattern.

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 language command — type # find large files to generate shell commands via AI with explanation and confirmation.
    • Command corrections (thefuck-style) — 13 built-in rules for typos (gti to git), 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.txt and llms-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/vibecody to TuringWorks/vibecody across 18 files (docs, GitHub Actions, package.json, config).
  • DeepSeek default model: deepseek-coder to deepseek-chat (V3 current).
  • Gemini default model: gemini-2.0-flash to gemini-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.txt for AI-friendly project context.
  • Tutorial guides for getting started, provider configuration, and skill development.

Fixed

  • DeepSeek default model updated from deprecated deepseek-coder to deepseek-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 with expect() 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, /idp with 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 with expect() 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.