Changelog
0.22.0 — 26 Jul 2026
A large release — codebooks you can switch on and off, a self-contained offline report you can share as a single file, a more reliable AutoCode, and a pass over the whole report's visual system.
- Codebooks you can install, switch off, and trust to stay put. The Codebook Library is now a two-way surface — every tile has one button, Install or Uninstall (retiring the old Add / Remove), so an installed codebook is no longer look-only. Each codebook carries an on/off switch with a matching status dot in the sidebar (blue on, grey off); switch one off and its section folds to a one-line summary, its badges hide across the whole report, and it drops out of the tag sidebar and tag suggestions — but its tags stay on your quotes and every result is kept, so flipping it back on restores everything. "Off means off" is functional, not cosmetic. Uninstalling now also forgets a codebook's on/off state, so reinstalling one you'd switched off returns it on, not greyed-out. And the codebooks you have switched on now code newly-added sessions too — add interviews to a finished project and the existing codebooks catch up on the new quotes, rather than leaving them blank. Ships on PyPI and in the desktop app.
- Share an offline report — a self-contained leave-behind that opens from a file. Export a report as a single HTML file that runs the full app straight from
file://with the data inlined — no server, no internet, nothing to host. It opens read-only (the editing controls that can't persist offline are removed, not left as dead buttons), bakes in the researcher's UI language so it reads the same offline, and its coverage is guaranteed by construction: a build-time gate derived from the live API surface fails the build if any report data would be missing from the embed, so the download can't silently ship half a report. Security: the embedded data is now escaped against</>/&, closing a stored-XSS hole where a literal</script>in transcript text, a participant name, or a tag could break out of the data block in a shared file. Ships on PyPI and in the desktop app. - AutoCode stopped losing proposals — and a stalled run is recoverable. A run could cheerfully report "AutoCoded in 50s ✓ — 0 of 0 proposals" while the model had actually returned dozens (seen against the IKEA data with Bristlenose UXR and JJG's Elements: 38 proposals returned, every tag stuck at zero). The culprit was a cosmetic progress counter sinking the real payload: after each batch the job wrote its running quote-count with a read-then-write transaction, which SQLite refuses outright ("database is locked") the moment any other write is in flight, and because that write wasn't isolated its failure dragged the batch's proposals down with it. The progress write is now a bare, write-only update that waits its turn for the lock, and can no longer discard proposals if it does fail. Alongside it: re-apply proposals are de-duplicated per quote (no more perpetual silent re-fails), a cancelled or failed AutoCode job can be re-run instead of being stuck, start-time errors surface instead of looking like nothing happened, and orphaned jobs are reconciled on server startup. Ships on PyPI and in the desktop app.
- The report reads calmer. A pass over the whole report's visual system: one true heading per content zone instead of competing title weights, section dividers unified to a single full-width rule (the heavier capped rule is gone), balanced gutters on panes with no minimap, and a consistent corner-radius tier so nested cards stop inverting. The Analysis lens scrolls to the bottom properly again, transcript margin annotations use a cleaner grid layout with first-occurrence de-duplication, and the quotes-page minimap keeps its true proportion when quotes reflow into columns — it reads the live grid's column count rather than a second set of breakpoints. Ships on PyPI and in the desktop app.
- CLI: configure a provider without a system keyring, using the env vars the SDKs already expect.
configurenow persists keys to a config file when no OS keyring is available (headless boxes, some Linux setups), and Bristlenose reads the SDK-native, unprefixed environment variables for Gemini and Azure — so a key that already works with the official SDK works here without renaming. The no-provider guidance was reworked to point straight atconfigure(in the house cyan accent) and to name the right app store per platform. Ships on PyPI. - The Snap is now strictly confined. Converted from classic to strict confinement (home, network, network-bind, and removable-media interfaces), with
libblas3/liblapack3staged soffprobefinds its shared libraries and the frontend built under Node 22 (core24's Node 18 can't build Vite 8). Published to the edge channel. Snap users only. - Fixes worth calling out. Media probing now fails loud when
ffprobe/ffmpegerrors, instead of reporting an empty success and producing a silently blank transcript. The Sessions sidebar detects moderators correctly again (the stored role isresearcher, nevermoderator, so the old check silently never fired). And the thin-theme bucket is now plain "Uncategorised". Ships on PyPI and in the desktop app. - Desktop (reaches the alpha cohort with the next bundled build). The welcome screen is now illustrated — looping science-cell animations for the sentiment, quote, and theme concepts, a study-tools carousel introducing AutoCode and Codebooks one at a time, and a self-scaling book-shelf motif. The Settings window is resizable with animated per-tab height, a uniform width, and in-cell help subtitles. The menus fill in: dynamic Hide/Show Projects, Help ▸ Welcome to Bristlenose, a Sessions-contents toolbar toggle, a preference-gated Diagnostics menu, and debug windows no longer double into the Window menu. Distribution: a signed direct-download
.dmg(developer-id archive→export flow) now works for the expiring alpha alongside the App Store / TestFlight path.
0.21.0 — 16 Jul 2026
- "Out of credit" now says out of credit — not "rate limit reached." When a provider refused a call because the account had run dry, Bristlenose told you to wait. Credit exhaustion and rate-limiting both landed in the same
QUOTAcategory, which rendered as "rate limit reached" — advice that will never come true for a bankrupt key. Billing exhaustion is now its own non-retryable category (waiting doesn't fix it; topping up does), across every surface that reports a failed LLM call: the run-failure cause, the CLI banner, the desktop failed-run row, a new out-of-credit toolbar pill, and the Settings provider check. Detection is a single shared classifier with provider-scoped rules, and the wire is stranger than the documentation admits — Anthropic serves out-of-credit as an HTTP 400 carrying "credit balance is too low", not the documented 402, and OpenAI shares 429 between billing (insufficient_quota) and genuine rate-limiting — so the classifier reads the provider's own error code first, its message second, and the HTTP status last. Where a provider genuinely doesn't distinguish the two — Azure has no credit concept, and Gemini folds billing intoRESOURCE_EXHAUSTED— Bristlenose keeps calling it rate-limiting rather than inventing a top-up story the wire never told it. 33 tests built from real provider payloads. Ships on PyPI and in the desktop app. - Feedback from every state — and it can't be redirected off-host. The native feedback sheet arrived in 0.20.0 for the screen that needed it most (the status page after a failed run); it now opens from every state of the macOS app, so Help ▸ Send Feedback never dead-ends on whatever happens to be on screen. Alongside it, a hardening that applies to every feedback submission on every surface: the endpoint allow-list only ever checked the first request, and the HTTP client follows redirects by default — so a redirect from the feedback host would have quietly re-sent your typed message somewhere else. Redirects off
bristlenose.appare now refused outright. Ships on PyPI and in the desktop app. - The dashboard fills the window, and features up to five quotes. The Project tab still carried a 52rem reading-width cap that every other tab had shed, so on a wide display it sat in a column with the right-hand side empty. It now fills the width like the rest of the app — full-span panes stretch, and the two navigation lists share a flush row instead of trailing a ragged right edge. Featured quotes go from three to five: three across on a laptop, five across from 1400px up, with the visible count always matching the column count so a row never ends half-empty. Ships on PyPI and in the desktop app.
- Desktop: the welcome screen teaches you the app, a little at a time. The screen you land on with no project open was a single static panel — drop a folder, and that was the whole of it. It's now a grid that drip-feeds the things worth knowing: features you haven't found yet, the research science the analysis rests on, practical tips, help getting an LLM set up, and pointers into the documentation. The drip is the point. A wall of onboarding gets read once and dismissed, whereas the empty window is a surface you pass back through every time you finish a project — so it can keep handing you one more thing you didn't know, rather than one thing you've already ignored. Desktop-only (Swift); reaches the alpha cohort with the next bundled-sidecar build.
- Desktop: the shoal moves like a murmuration — and your analysis is what disturbs it. The word-flock that drifts during a run moved as though floating in treacle. The cause was a force clamp crushing every steering impulse — including the startle and attract behaviours — down to an idle nudge. Retuned, it banks and overshoots around a wandering attractor the way a real murmuration does. And it's no longer decorative motion running beside the work: the analysis drives it. Each batch of arriving words, themes and sentiments fires a matching disturbance through the flock — a cohort joining, a startle wave, a predator swoop — so the shoal reacts as your interviews are actually processed. Desktop-only (macOS); reaches the alpha cohort with the next bundled-sidecar build.
- Developer: a read-only database browser, in the direct-download beta only. The SQLAdmin browser now mounts at
/adminfor debugging a cohort user's data on a call — read-only over transcripts and PII, bound to localhost, and fail-closed by construction: it's gated on a compile-time flag set only by the direct-notarised beta build, so the App Store and TestFlight builds both hide it. A receipt check was considered and rejected — App Review runs under the StoreKit sandbox, so a receipt can't tell a reviewer from a tester, and the Debug menu would have been exposed to review. Read-only also disables table export, which was an unauthenticated full-table dump. Developer-facing. - Under the hood: the macOS app is sandboxed, hardened and accepted by App Store Connect. The Release build now runs under the App Sandbox with the Hardened Runtime, pinned to arm64 (the Python sidecar is arm64-only), with every nested binary signed the way App Store Connect requires — three separate upload rejections' worth of signing corrections. Not user-visible; it's what makes the macOS app distributable at all.
0.20.0 — 11 Jul 2026
- Your curation now survives re-analysis — and what's new is flagged. Re-run the pipeline on a project and the work you'd already put in stays put. Any quote you've starred, edited, or tagged is frozen with a durable identity and its exact wording, held exempt from the stale-cleanup that clears regenerated data — so a fresh analysis can never quietly drop a quote you'd marked, even one the model wouldn't surface again. Report sections keep their identity by membership, not by their heading text, so a re-run that renames or re-clusters a theme no longer scrambles the headings you'd edited; a theme name you've anchored to a starred quote persists across runs. And genuinely new material earns a dismissible green New badge on its section, so a returning researcher's eye goes straight to what changed without disturbing the sections they'd already curated. This is the persistence foundation for incremental analysis (adding sessions to a finished project); the manual re-assignment write path — moving a quote between sections by hand — lands alongside it. Ships on PyPI and in the desktop app.
- Desktop: the "Interviews" folder proxy reveals your source folder in Finder. The folder chip above the sessions table is now a real proxy for the interviews folder: click it in the macOS app and Finder opens with that folder selected — the shortest path from "the report on screen" back to "the recordings on disk," the exact move a researcher reaches for the moment they've recorded another session. It follows the same Show in Finder idiom as the project sidebar, and it's sandbox-safe by construction — the reveal is handed to Finder out-of-process, so it needs no extra file-access permission, no security-scoped bookmark, nothing new in the entitlements. In the browser (
bristlenose serve), where there's no native bridge to hand off to, the chip keeps its copy-the-path-to-clipboard behaviour, and both the action and its tooltip follow the surface ("Show in Finder" in the app, "Copy folder path" in the browser). The desktop reveal reaches the alpha cohort with the next bundled-sidecar build; the groundwork behind it — a newreveal-in-finderWKWebView bridge action and a Show in Finder label seeded across every language Bristlenose speaks — ships on PyPI. - Feedback that survives a failed run — and a native way to give it. When a run is cancelled or fails, the report gives way to a status page, and its "Send feedback" link used to lead nowhere: it pointed straight at the submission endpoint, so a browser GET got back a bare "Method not allowed." Now feedback works from that screen in every context. The macOS app opens a native feedback sheet — five-point rating, a message, Cancel/Send, no web chrome — while the browser (
bristlenose serve) opens an inline form on the status page itself. Both send the same anonymous{rating, message}, and both now report "sent" only when the server actually confirms receipt — a captive-portal or corporate-proxy page that answers200 OKwith its own HTML can no longer swallow your feedback and call it delivered. If a send can't be confirmed, your message is preserved and copied to the clipboard rather than lost. The reliability fix reaches the in-report feedback form too. Browser form + reliability ship on PyPI; the native sheet reaches the alpha cohort with the next bundled build. - Help opens the live docs — so you can read it beside the report, not over it. The in-app Help overlay is retired in favour of the online documentation, which supersedes it and stays current. Help → Bristlenose Help and Keyboard Shortcuts now open the matching page at
bristlenose.app/docsin your browser, and Acknowledgements opens the project's open-source credits. A panel that covers the very thing you need help with is the wrong shape for a tool you're actively using — the docs open in their own window (and the?key reuses a single tab), so the analysis stays in view while you read. Ships on PyPI and in the desktop app. - Jump from a session's journey straight to the moment it happened. The screen path under each row in the Sessions view — Beds Category → Bed Product Detail → Checkout — is now a row of live links. Click any step and Bristlenose opens that participant's transcript scrolled to the exact moment they first reached that screen, the segment briefly highlighted — the same deep-link the in-transcript journey navigation already performs, now reachable straight from the overview. The anchor is the participant's earliest quote on that screen, resolved to the nearest transcript segment so a step always lands somewhere real even when its first quote falls mid-segment. Plain click navigates in place; ⌘-click opens the moment in a new tab. Ships on PyPI and in the desktop app.
- Sessions view: play from the thumbnail, and a steadier Start column. The session thumbnail is now a play control — click it to open the pop-out video player at the start of that session, matching the filename link beside it (with the filename as a hover tooltip). And the Start column, which stacks the date above a sometimes-long screen path, gains a minimum width so it no longer collapses to one word per line as the window narrows — the path stays readable instead of shattering into a tall column. A fuller responsive pass on this dense, frequently-seen screen is tracked as follow-up. Ships on PyPI and in the desktop app.
- A calmer failure, and a proper landmark. A route-level error boundary now catches a crashing report view and shows a calm fallback instead of a blank page, and the routed content sits inside a
<main>landmark so a screen reader can jump straight to it. Ships on PyPI. - Drop loose interview files, not just folders — Bristlenose builds the project for you. The desktop app could only analyse a folder of interviews; dragging in individual files created a project that dead-ended on "this was created from individual files, so it can't be analysed." Now dropping loose files — onto the sidebar, or onto a new project you've just created — opens a native Create Project Folder sheet: name it, pick a location, and Bristlenose creates the folder, copies your interviews into it, and analyses them. Your originals stay where they are. It's a proper window-modal sheet — it slides from the title bar and follows the app's light or dark theme — not a stray floating panel, and single- vs multi-file drops read correctly. Desktop-only (Swift) — reaches the alpha cohort with the next bundled-sidecar build; the supporting interface strings ship on PyPI.
- Add more interviews to a project you've already analysed. Drop new files onto an analysed project — or use File → Add Files… (⇧⌘A) — and Bristlenose folds them in with an incremental re-run instead of starting over: a calm "Adding N interviews…" confirmation up front, then the normal stage ladder. Your starred, tagged, and edited quotes carry across (the curation persistence above). Desktop-only (Swift); the per-session caching that makes the re-run incremental ships on PyPI.
- Recover a stopped or half-finished analysis. Stop a run partway — or have one fail — and the interviews are already copied into the project's folder, so the project isn't a dead-end. Right-click it and choose Analyse to resume from the partial output already on disk. Files belong to the project: a stopped run is always visible and always retryable. Desktop-only (Swift).
- Two fixes worth calling out. Claude API-key validation now probes
/v1/modelsrather than calling a since-retired model, so a valid key no longer reads as invalid. And a project — with its cached inputs — is now identified by its resolved filesystem path rather than the spelling you happened to type, so the same folder reached two ways is one project, not two. Ships on PyPI. - Fix: your project's real name in the report, not "Untitled." A project analysed the moment it was added could have its report and dashboard title freeze at the placeholder "Untitled" — the database row behind the report is built in the split second before the pipeline writes the project's name to disk, and every later re-analysis healed that row's file paths but never its name. The importer now heals a stuck "Untitled" back to the pipeline's own project name (and its URL slug) on the next re-import, scoped tightly to the placeholder so it can never overwrite a name you've set on purpose. The desktop sidebar always read the right name — this repairs the report and dashboard heading, which reads from the database. Ships on PyPI and in the desktop app.
- Shoal, sized to your screen — a fuller flock, held smooth. The in-run word-flock now scales to the window it's drifting in: sparse in a laptop pane, a proper shoal across a 24″ external display, and it re-flocks live when you drag the window between screens — where before it drew the same fixed handful everywhere. It's capped at a steady 50 fps so the motion stays fluid instead of straining for a 120 Hz ProMotion refresh it can't hold (30 fps read janky; 50 was the sweet spot). The fix came from a better diagnosis: the flock's real cost is the main-thread flocking simulation — an O(n²) neighbour search every frame — not the GPU render we'd assumed, so 150 words only turned smooth once the frame rate was capped and the count was tied to the canvas area (clamped 24–200, still thickening early → late as analysis proceeds). Desktop-only (macOS); reaches the alpha cohort with the next bundled-sidecar build.
- Acceptance testing you can't fake-pass — a conformance tier that runs the real thing. Bristlenose's two automated test layers are hermetic by design: they mock every provider and binary so they stay fast and free, which leaves one whole class of bug invisible — the cross-seam kind where a setting reaches the wrong environment variable and every run silently falls back to a too-small model, green all the way down. This lands the first rung of a third tier that runs the real pipeline against real providers and asserts only shape, never content. Every input format we advertise now has a sub-second coverage check — all sixteen classify correctly, and thirteen media containers are produced and decoded through real ffmpeg, retiring the idea of a full transcription run per format. A provider matrix drives
bristlenose runacross Claude, ChatGPT, Azure, Gemini, and local Ollama and refuses to call a run successful unless it actually produced work: a completed-not-failed terminus, non-empty quote clusters, and the re-identification keys (llm-calls.jsonl,pii_summary.txt) provably absent from every export. The guards are themselves tested — an empty run, a missing artifact, a participant name that leaked past the anonymisation boundary all turn the check red — so a green run means something. The tier was designed against an adversarial multi-agent review and then fired for real, which immediately earned its keep: it caught a fail-closed bug in the harness itself (a missing artifact crashed the check instead of failing it — fixed), and proved the discipline end to end — an empty run fails, a model that timed out fails, a genuine Claude run passes. Developer-facing; no user-visible surface — everything lives indocs/testing/andscripts/acceptance/.
0.19.0 — 4 Jul 2026
- Nine new locales — Bristlenose now speaks nineteen languages. Dutch (
nl), Finnish (fi), Polish (pl), Russian (ru), Ukrainian (uk), Danish (da), Swedish (sv), Norwegian Bokmål (nb), and Turkish (tr) join the roster across the desktop app, web report, and CLI (Settings → Language) — nineteen locales in total. Each is a machine-seeded community preview, complete against all nine namespaces (common, cli, desktop, doctor, enums, pipeline, preflight, server, settings), offered so native speakers can refine on Weblate; the seed is fill-empty-only, so contributed translations always win the next sync. Slavic + Turkic plural rules are respected — Polish uses the 1 / few / many / other split, Russian and Ukrainian retain{{count}}in the_oneform because the shape recurs at 21/31/etc., Turkish is binary — and Scandinavian is one/other. Turkish register is formal (siz-and-imperative,Vazgeçfor Cancel rather than the abruptİptal). New tooling to catch regressions: a placeholder-union check across every locale, a_onecompleteness gate for East-Slavic, and a pytest guard that classifies unseeded strings against the English source. Norwegian ships as Bokmål only — theno → nbfallback path is an open follow-up. Ships on PyPI and in the desktop app. - Shoal — a live word-flock while your interviews are being analysed. During a run, a SpriteKit flock of the words the pipeline is currently extracting drifts across the detail pane in the desktop app. A Python-side sampler filters transcript words for stopwords and appends them to a
shoal-feed.jsonlsidecar as they surface; the SwiftShoalFeedpolls the sidecar every ~1.5 s and injects the batch into the scene, colour-graded by sentiment. The flock respects Reduce Motion system-wide and a dedicated Show animation while analysing toggle in Appearance → Animation; if the feed is stale by the halfway mark it logs once and falls back to a canned word pool so you never see an empty pane. A 0–600-word density rig with FPS + node counters ships behind the DEBUG build for anyone tuning the flock. Desktop-only feature; the sampler + i18n strings ship on PyPI so the SPA carries the plumbing. - Palette picker + Edo, end to end. Settings → Display picks between Default (re-grounded on Apple's system values — system-blue accent
#007AFF/#0A84FF, the neutral-grey AppKit selection capsule#EFEFEF/#2B2B2B, 6 px corners, native inactive-window dimming — so the seam with the desktop app's native chrome disappears) and Edo (a Prussian-blue accent sampled from the pigment's brilliant undertone rather than the dull masstone of aged prints, paper-white paper, passport-navy ink, with a warmer paper in dark mode so it stays legible). The picker is now a native macOS pulldown in Settings — no restart needed, the SwiftUI.tint()propagates through Settings/Debug scenes, and a WKWebView bridge repaints the report the instant you switch. Your choice persists locally and paints before first render (no flash of the wrong palette), validated against an explicitdefault/edoallowlist rather than a permissive regex. A--paletteCLI flag (renamed from the ad-hoc env var) letsbristlenose serve --palette edoboot straight into your preference. Translated across all nineteen locales; the picker's labels are provisional in the newer languages, pending native review. Ships on PyPI and in the desktop app. - Terminology fixes across four locales. A glossary-consistency pass corrected drift the multi-locale audit surfaced: Japanese (Quotes 引用 → 発言, the glossary-forbidden form), Korean (a 왜곡 mis-spelling, Accept 승인 over 수락, Signals 시그널 with 을/은 particle fixes), German (Sessions Sitzung → Interview, undo Rückgängig → Widerrufen, three informal du → formal Sie), and Spanish (dropping the over-literal Tubería for Pipeline). Values only, no key changes — verified against
check-locales.pyand the locale-key parity tests. Ships on PyPI. - Supply-chain hardening — CycloneDX SBOMs, signed and attached. Every release now attaches runtime-scoped CycloneDX SBOMs (Python + frontend) as durable GitHub Release assets, and binds the Python SBOM to the built wheel + sdist with a Sigstore attestation via
pypi-publish's attestation flow. A new gitleaks history-scan CI job checks the full git history on every push and PR (with three historical false positives allowlisted); a tracked-vs-gitignore guard catches the class of.env-shaped files being accidentally tracked while gitignored. Trusted Publishing (the OpenSSF baseline) was already in place — SBOM + attest close the falsifiable-property loop on "what shipped from what source." Ships in the release pipeline, no user-facing surface. - Lighter first paint. The report's tab views (Dashboard, Quotes, Transcript, Codebook, Analysis) now code-split into lazy per-tab chunks instead of being simultaneously dynamically and statically imported — a contradiction that quietly folded them all back into the main bundle. The landing bundle drops to ~213 kB (size-limit ceiling ratcheted 230 → 220 kB), and switching tabs fetches its island on demand behind a shared loading boundary. Invisible on a fast machine; a real saving on a cold load. Ships on PyPI.
- Smaller polish.
AccentColor.colorsetis finally filled (it had been silently empty, letting the system pick a nearby colour). AutoCode is hidden on auto-applied Sentiment framework so it stops offering itself when there's nothing to auto-code. Ano-fake-successpipeline acceptance test is added (slow, skip-in-CI) — the class of "green suite while the pipeline silently returned nothing" is now caught end-to-end. Translucent detail-column chrome merged from spike after full-screen inset handling was resolved. Ships on PyPI and in the desktop app.
0.17.0 — 30 Jun 2026
- Five new locales — Portuguese, Italian, and Traditional Chinese. Bristlenose now speaks Brazilian and European Portuguese (
pt-BR/pt-PT), Italian (it), and Traditional Chinese for Taiwan and Hong Kong (zh-Hant/zh-Hant-HK) across the desktop app, web report, and CLI (Settings → Language) — twelve locales in all. Portuguese and Chinese each ship as a deliberate pair rather than one catch-all, because a single code reads as foreign to one side: Brazilian and European Portuguese diverge on everyday words (arquivo / ficheiro, usuário / utilizador), register (você), and the progressive (Brazilian salvando vs. European a guardar); and Bristlenose's Chinese is Traditional only — a Taiwan-leaningzh-Hantplus a distinct Hong Kong variant — not Simplified. As with Czech, each locale is a complete machine-seeded baseline (all 8 namespaces +preflight) offered as a community preview for native speakers to refine on Weblate; the seed is fill-empty-only, so a contributed translation is never overwritten and Weblate's own work always wins on the next sync. Apple-HIG action verbs and the Bristlenose research-method glossary (Codebook, Quotes, Sessions, Themes…) seeded per language so reviewers start from an agreed taxonomy. New guard tests pin locale-key parity,{{placeholder}}consistency, and plural-form completeness across every locale.
0.16.1 — 28 Jun 2026
- Send to Miro becomes a native macOS sheet, and shows exactly where the board will land. On the desktop app the export moves out of the in-report panel into a native sheet with its own Miro connection — paste your access token once and it's held in the macOS Keychain — and the sheet now names the connected account, workspace, and organisation plus the destination team, so you can confirm where the board lands before you push. The web report's Miro panel gains the same connected-account / workspace identity, so
bristlenose serveusers see it too. Desktop sheet reaches the alpha cohort with the next bundled-sidecar build; the Miro-panel changes ship on PyPI. - Desktop Quotes toolbar: a native search field and a starred-only toggle. The Quotes view's search box and "starred only" filter are now native macOS toolbar controls rather than web widgets, debounced through a single-writer bridge to the report, and the redundant tag dropdown is removed. Desktop-only — reaches the alpha cohort with the next bundled-sidecar build.
- Fix: the native Miro export no longer 401s after you switch projects. The serve process now resets its bearer auth token on a cold start, so the desktop app's token stays in sync with the freshly-booted server and the Send-to-Miro request authenticates instead of failing 401. The
servechange ships on PyPI; the symptom was desktop-only. - Desktop embedded report suppresses the web sidebar rails and close-×. In the macOS app the native sidebar owns project navigation, so the SPA's own sidebar rails and close button are hidden when the report runs embedded in the app. Theme/CSS change ships on PyPI but only takes effect in the embedded (desktop) context.
- Contributor tooling: a dev-only Run Inspector and a native Debug menu.
bristlenose serve --devgains a Run Inspector (/api/dev/run, dev-gated + auth-enforced) that visualises instrumentation the pipeline already captures —llm-calls.jsonl,pipeline-events.jsonl, timing — without adding new logging paths, and the macOS DEBUG build gains a Debug menu (Run Inspector at ⌃⌘R, a live diagnostic-fixtures submenu, reveal-.bristlenose/ open-log / copy-build-provenance). A build-time sidecar-staleness gate fails the desktop build loudly when the bundled sidecar is stale versus the Python source. Developer-facing; no change to the report or exports.
0.16.0 — 27 Jun 2026
- Send to Miro — push your analysed quotes onto a Miro board. A new export builds a first-draft research wall: a fresh board each time, one column per section then per theme (the report's left-to-right order), pink section headers and yellow quote stickies stacked session-then-time, coloured by sentiment. Connect once by pasting a Miro access token (guided setup at
bristlenose.app/docs/send-to-miro.html), name the board, choose colour-by, optionally link quotes to hosted clips, and preview the whole board credential-free before pushing. Participant display names never leave your machine — stickies carry speaker codes (p1, p2) only, and hidden quotes are excluded. Experimental, phase 1 (paste-token; one-click browser connect comes later). In the SPA export menu now; the macOS native menu arrives with the next bundled build.SECURITY.mddocuments Miro as an opt-in sub-processor. - Desktop: native SF Pro typography, with a Typography setting. The macOS app now renders on a calibrated SF Pro type scale aligned to Apple's text styles, and a new Typography setting switches between SF Pro and Inter. The type scale ships in the theme (so the in-app report matches, via WKWebView type-parity fixes); the setting is the macOS app.
0.15.19 — 24 Jun 2026
- Desktop: the macOS project sidebar is rebuilt on native AppKit. The sidebar moved off SwiftUI's
List— which had hit selection and tap dead-ends on macOS 26 — onto a nativeNSOutlineView, bringing the row affordances the old list couldn't carry: two-line project rows (icon · name · session count, with a live status subtitle beneath), native activity and copy rings with hover-to-cancel on the row itself, status glyphs for can't-find and iCloud-evicted projects, the failure → diagnostic popover opened from the row, project and folder context menus, and Finder folder-of-videos drops wired into the sidebar. The migration came with its own test seams — pureProjectCellSpec,OutlineNode, andDropRoutinghelpers with unit coverage — so the AppKit layer isn't an untested blind spot. - Desktop: the five report views become a lens rail, with a native window title + session subtitle. Project · Sessions · Quotes · Codebook · Analysis move out of the toolbar picker into a lens rail at the top of the sidebar; switching lens updates a native window title and a lens-contextual session subtitle (the old toolbar title pill is gone). Desktop-only (Swift) — both land in the alpha cohort with the next bundled-sidecar build; the PyPI / Homebrew / Snap package gains the supporting interface strings across all seven languages.
0.15.18 — 21 Jun 2026
- Desktop: copy progress moved onto the project row, and switching back to a recent project is now near-instant. Two macOS-app refinements. (1) When you drag files into a project, the copy progress — a determinate ring, a "Copying · N%" subtitle, and hover-to-cancel — now rides that project's sidebar row instead of a separate toolbar pill, so per-project status lives where the project lives (the standalone copy pill is gone). (2) Switching back to the immediately-previous project re-points to its still-running ("parked") server instead of tearing it down and restarting, so the boot wait disappears for the common back-and-forth switch — the "warm-sidecar pool." Localised across all seven languages.
0.15.17 — 18 Jun 2026
- Default Claude runs no longer break when Anthropic retires a model. Bristlenose's built-in Claude default pointed at
claude-sonnet-4-20250514, which Anthropic retired on 15 June — so from that day a plainbristlenose runon the Claude provider (with no--modelset) failed at the topic-segmentation stage with a404 model_not_found. The defaults now track current aliases (claude-sonnet-4-6, plusclaude-opus-4-8where Opus is offered), updated across every place a model id is named — provider registry, config, and pricing — so the next retirement is a one-line change rather than a forty-file hunt. Pin a model with--modelorBRISTLENOSE_LLM_MODELand nothing changes. Ships on PyPI. - Quote cards pack tighter on the report, where the browser supports it. The quote grid now uses CSS Grid Lanes masonry (Safari 26.4+ and the desktop app's WebKit) so variable-height cards settle into the shortest column instead of leaving ragged gaps — a progressive enhancement that falls back cleanly to the existing grid everywhere else, with cards kept in reading order for keyboard and screen-reader users.
- The desktop sidebar shows real progress on cached and first-time runs, instead of a frozen "Analysing…". The per-stage verb ladder (Identifying speakers → … → Rendering) was driven only by the timing estimator, so two run types never advanced it — cache-verified / transcript-only runs (the only per-stage signal lived inside the "stage actually ran" branch), and every new user's first few runs (the estimator stays silent until it has ~4 runs of history, so day-one runs showed nothing). An estimator-independent signal now fires at the entry of each analysis stage, carrying the last-known ETA so warm runs are unchanged (the determinate ring doesn't regress) and cached / cold-start runs advance the verb with the ETA simply absent. Python-only — ships on PyPI and in the desktop sidecar; no change to the report or exports.
0.15.16 — 10 Jun 2026
- Quote extraction no longer fails on dense interviews when the model's output limit is small. Some models cap how much they can write in a single response — ChatGPT's gpt-4o stops at 16,384 tokens, small local models far less — and on a long, quote-rich transcript the quote-extraction stage could hit that ceiling mid-response: the JSON came back truncated and the whole run failed. We were seeing this on roughly one run in three against dense ChatGPT transcripts (caught 8 Jun during a real interview-analysis session). Bristlenose now detects the truncation, splits the transcript at a natural topic boundary — falling back to even halves when no confident boundary exists — extracts quotes from each piece separately, and merges the results, recursing up to three levels deep (≤8 chunks) for the densest material. Duplicate quotes that straddle a split are removed by matching their verbatim text, and the split is all-or-nothing per interview so a partial failure never leaves a half-analysed session. A new typed
TruncatedResponseErrorcarries anOUTPUT_TRUNCATEDcause through the pipeline (mirrored on the desktop side so a truncation reads honestly, not as a generic failure), and the cloud LLM client now retries up to six times on rate-limit bursts. The split-point picker was deliberately kept to two tiers — a high-confidence topic boundary from the segmentation stage, then mechanical halves — after a third "moderator question pivot" tier was dropped: topic shifts in skilled interviews emerge from semantic drift over many turns, not from lexical signposts. Verified on Claude with a forced low cap (A/B), on a full real-interview pipeline, and against the live gpt-4o SDK; every quote still appears in exactly one report section. Ships on PyPI — benefits every provider with a tight output cap.
0.15.15 — 9 Jun 2026
- Čeština — Bristlenose now speaks Czech, and it's the first language our users asked for. Czech (
cs) joins Spanish, Japanese, French, German, and Korean across the desktop app, web report, and settings (Settings → Language → Čeština). The backstory is the nice part: a volunteer signed up to translate Bristlenose into Czech on Weblate before we'd shipped the language — the first organic demand signal for a locale we've had. So rather than hand them a blank slate, we machine-seeded a complete Czech baseline (all 8 namespaces +preflight, ~950 strings) with proper Czech four-form CLDR plurals —one/few/many/other— for them to react to and refine. The seed is fill-empty-only (it never overwrites a contributed string), and Weblate's own translations win on the next sync, so the human work is always safe. Apple-HIG Czech glossary terms added (Uložit, Zrušit, Zavřít, Kopírovat…). - A coverage sweep so more of the app actually translates — settings, codebook, transcripts, accessibility labels, the desktop pipeline — plus the first Czech-correct plurals. Adding Czech surfaced how many strings still hard-coded English and so reached none of the seven languages; this pass wired them through i18n. Newly translatable: the Settings → Config reference (72 category + setting labels), the Codebook's "New group" / "New code" defaults, the transcript page's loading / error / empty states, ~30 React
aria-labels and tooltips across eleven components (so screen-reader users on a non-English locale hear their own language), and the desktop pipeline-activity status — "Analysing…", "Stage 3", "Queued", "Elapsed", the popover header and the Stop button. The headline fix is grammatical: every desktop count-bearing string used a hardcodedcount == 1 ? one : otherselector that could never reach Czech'sfewform (counts 2–4), so a newI18n.pluralCategory()computes the real CLDR category and both the diagnostic-overflow text and the three sidebar chrome counts (interview / unanalysed / missing) now render the right form — proven by Swift tests against the live Czech locale. New guard tests pin locale-key parity,{{placeholder}}consistency, the CLI↔GUI string mirror, and four-form completeness. Translation quality for the machine-seeded strings remains the native-speaker community's to refine on Weblate; this pass is the mechanical groundwork. Mostly internal / UX — no API change.
0.15.14 — 7 Jun 2026
- Desktop runs the LLM provider you actually chose — and an LLM failure no longer reads as "Transcription failed". Four diagnosed defects in the desktop provider-resolution path, closed together. (1) The AI-consent resolver no longer flips a deliberately-chosen provider back to Claude when its cached verdict is merely absent (absent ≠ invalid), so a configured ChatGPT / Gemini / Azure account survives the consent sheet. (2) Spawn-time
overlayPreferencesinjects provider and model as a matched pair instead of a bare global model — the bug that rangpt-4ounder the Anthropic endpoint and 404'd. (3) The failure classifier checks LLM-error markers before Whisper markers and names the provider in its human summary, so an out-of-credit or bad-key LLM failure stops masquerading as a transcription failure;degradedBodyno longer hides the captured cause. (4) The Settings status board is now eager — every provider row paints from cache the moment Settings opens and silently reconfirms, retiring the lazy dashboard and the activation no-op. Out-of-credit (402) becomes its own sticky state shown through a cached green, with activation decoupled from strict-online viacanActivate; a pure, unit-testedresolveStatuscentralises the cache-vs-masking decision. Reviewed by six agents + William. - Desktop status polish — copyable help, ⌘, from any focus, honest offline caption. Provider status explanations now render as markdown (backtick
command→ inline monospace) and are selectable, so every explanation is copyable; embedded shell commands (today Ollama'sollama pull …/ollama serve) get a one-click monospace copy row reusing the diagnostic popover's silent copy idiom (no "Copied" flash — the Finder / Safari pattern), backed by a pureLLMValidator.shellCommands(in:)extractor.⌘,now opens the native Settings window from report focus too —BristlenoseWebViewwas claiming the key equivalent while it hosted the first responder, so Settings previously opened only from native chrome (⌘1/2/3, ⌘F, ⌘⇧E unaffected). The offline status caption softens "Your key is fine" → "Your key was fine" — offline we can only vouch for the last known-good check, not the current key; 402 / 429 messages keep present tense because there the key authenticated. Status-indicator vocabulary settled on colour + label with no per-state glyphs (the blue selected-tick vs green OK-tick collision was decisive), and the provider / keychain design docs are trued to the shipped data-protection-keychain + eager-status-board reality. Newdesktop.llmSettings.copyCommandstring across all 6 locales. Desktop-only; reaches the alpha cohort with the next bundled-sidecar build.
0.15.13 — 4 Jun 2026
bristlenose pipelineview gains per-(provider, model) grain (feature rung v2). The pipeline view added in 0.15.11 now renders a row per (provider, model) pair instead of one row per provider: each LLM stage shows Claude / ChatGPT / Gemini / Azure with their individual models, per-model editorial quality ratings, and arecommendedaxis split fromdefault(the first build where they diverge — Opus 4 and gpt-4o are recommended over the defaults).llm_summarydeleted in favour of per-stage rendering; payloadschema_version3 → 4 with a_migrate_schemaplaceholder hook. CLI and the React Settings → Pipeline tab both render the sectioned-flat matrix with quality glyphs, inline notes, and a compact symbol key (Availability / Quality subheadings group the glyphs; italic = from-your-settings). Column alignment fixed across every stage table (table-layout: fixed+ shared colgroup so transcription / anonymisation line up with the LLM tables); the current/in-use row carries the app's selection wash +aria-current+ sr-only label rather than a loud badge, and collapsed-provider rows render flush-left at provider level so ChatGPT / Gemini / Azure read as peers of Claude, not its children.list_models(timeout)useserrors="replace"so a wedged Ollama binary can't breakprobe_host()'s never-raises contract. Provider naming stays product-names; the account-vs-product rename + logos are parked post-TestFlight. Feature rungs andschema_versionare decoupled both ways (internal payload restructure with no UX change; render-only rungs over data the schema already carries) — documented indocs/design-pipeline-view.md. Newtest_models.pydeclaration invariants +_collapse()unit tests; brittle model-id pins dropped. Ships on PyPI (CLI surface).- Desktop: on-device (Ollama) setup is now a model-first "flow B", and consent activates the chosen provider. Two desktop landings. (1) Flow B setup: a toolbar pill drives phase popovers — choose model → get Ollama → wait → download → finishing — replacing the old
OllamaSetupSheet. Honesty rule throughout: a static hourglass while Bristlenose is waiting on the human (download Ollama, daemon spin-up), a moving bar/spinner only while Bristlenose is actually doing work, red reserved for genuine failure — no timeout-to-failure, no animation when nothing is moving. The pill sits in the toolbar.statuszone (its own trailing slot, not absorbed into macOS 26's primary-action capsule). Cancellation guards on every late daemon-snapshot write so a re-entry or cancel can't be clobbered by a stale async result. 43-key locale layer across all 6desktop.json(German uses formal Sie). (2) Consent activation: the AI-consent sheet's Continue/Done path never wroteactiveProvider, so a validated cloud key silently didn't take effect and the pipeline routed to a stale backend. Continue now runs the same activation the Settings toggle does, via a testedConsentActivation.resolvehelper that only switches when the active provider is local or unconfigured (never overrides a deliberate cloud choice). Stay-local applies the RAM-aware default and pulls the model ambiently through an app-level download surfaced in the pill, so the fetch survives sheet dismissal. Review fixes: failure pill error glyph/colour (xmark.circle.fill/.red), VoiceOver label + value on the pill, consent relabel + download-size hint, redundantUserDefaultsmirror writes dropped (AppStorage already persists), consent audit-trail (DPIA anchor) documented in the desktop security audit. DEBUG-only menu-bar "Debug" harness cycles every pill/popover state for live QA — menu-bar, because macOS swallows.contextMenuon toolbar items with its own "Customize Toolbar" menu. Desktop-only; reaches the alpha cohort with the next bundled-sidecar build. - CI hardening — bounded jobs, pinned actions, least-privilege tokens. Three classes of fragility closed after the 2026-06-03 e2e hang burned toward the 6-hour ceiling: every job now carries
timeout-minutes(e2e + perf-gate) with a bounded-retry wrapper around the Playwright install so a hung CDN download fails fast instead of cascading to sibling workflows; the three third-party actions that touch secrets (peter-evans/repository-dispatch,snapcore/action-build,snapcore/action-publish) are SHA-pinned; every workflow lacking one gets a read-only defaultpermissions:block. Methodology documented indocs/design-ci.mdas a six-class fragility map with standing audit targets andverify-pypias a load-bearing fence./usual-suspectsgains a CI workflow lens routingsecurity-review+silent-failure-hunterat any diff touching.github/workflows/**. Workflow + docs only — wheel unchanged.
0.15.12 — 23 May 2026
-
Anthropic credit-exhausted is now reported as "out of credit", not "model unavailable". Preflight's BadRequestError classifier was substring-matching on
credit_balance_too_low(anerror.typetoken that doesn't appear instr(exc)) — the live SDK message isYour credit balance is too low to access the Claude API. ..., so the branch silently missed and fell through to the model-unavailable copy. Hit on 21 May on a cleanbristlenose runafter credit ran out; user saw "The requested model is not available for your Claude account" and was directed at the wrong fix. Substring widened tocredit balanceto match what the SDK actually emits, aligning with the runtime path atpipeline.py:339(runtime classification was already correct; this was a preflight-only miss). Regression-pinned with the real SDK message intests/test_preflight_api_key.pyplus a sibling test asserting that the underscored-only token does NOT match, so any future "tidy-up" back to the original substring fails loudly. Broader LLM-error taxonomy work (16-kindLLMErrorKindenum, runtime-classification rewire, per-provider classifiers) parked as a separate branch — over-built for current evidence; let user reports drive what gets distinguished. -
Desktop: ⌘⌫ no longer beeps on placeholder projects or multi-selection. The Project menu's
hasProjectpredicate conflated "is a project selected" with "does the bridge handler hold a serve-able path" — so ⌘⌫ on a brand-new placeholder from+ New Project(empty path) and ⌘⌫ on any multi-selection both hit AppKit's disabled-shortcut beep instead of removing the row(s). Receivers inContentView.swiftwere already total over empty/mismatched selection (they guard onsole/ iterate the selection set), so the gates were duplicating work the handlers already do. RemovedhasProjectand the four.disabled(!hasProject)sites; sharpened Show in Finder to justselectedProjectRevealablePath.isEmpty(the real predicate). William adjudication on the usual-suspects review added back two surgical gates so single-only operations dim correctly rather than silently no-op on multi-select — Rename and Move-to-folder submenu now.disabled(selectedProjectPath.isEmpty), matching the Finder / Notes / Mail / Things 3 indie-consensus pattern. Remove-from-Sidebar stays ungated. Desktop-only; doesn't affect CLI / PyPI users until the next bundled-sidecar build reaches the alpha cohort.
0.15.11 — 21 May 2026
bristlenose pipeline+ read-only Pipeline tab in Settings. New CLI verb (bristlenose pipeline, with--jsonand--stage <id>filter) and matching last-position tab in the React Settings modal that render the mixture of models Bristlenose currently uses across its pipeline stages: MLX/faster-whisper for transcription, the chosen LLM provider × model for speaker identification / topic segmentation / quote extraction / quote clustering / thematic grouping, Presidio (or "Off") for anonymisation, and an Apple Foundation Models row that returnsUnknown from CLIuntil the Swift-side probe ships. Two-column definition-list layout; deterministic ingest/audio/parse/merge stages omitted (they don't participate in the mixture story). Host context strip at the bottom shows OS · arch · RAM · keys present · ollama status. Read-only on purpose — to change a backend you still edit settings or .env. Reuses_resolve_backendfroms05_transcribe.pyfor the transcription row; no parallel resolver. Cross-language schema lock attests/fixtures/pipeline-view-contract.jsonround-tripped by Python tests and the React tab. New modulebristlenose/pipeline_view/(renamed mid-branch frompipeline/after the collision with the existingpipeline.pyorchestrator surfaced); the underscore in the package name keeps the user-facing CLI verb intact. 28 new tests; full suite green. Design captured indocs/design-cli-improvements.md§Captured design.
0.15.10 — 17 May 2026
- Release pipeline actually fixed (test-only — wheel unchanged). PyPI had been stuck at 0.15.3 since ~10 May — six tag pushes (0.15.4-0.15.10) reached GitHub but
ci/perf-gatefailed on every one, blocking the release workflow. Root cause: the v0.15.4-era server-rendered status page interceptor (bristlenose/server/status_page.py) returns "Nothing to see here, yet." whenapp.state.last_run[1]has no terminus event, and the smoke-test fixture'sbristlenose-output/had never carried apipeline-events.jsonl. Every Playwright run against that fixture got the status page instead of the SPA,#bn-app-rootmounted empty, and perf-gate'sDOM nodes — Quotestest timed out 30s intowaitForPageReady— surfacing the failure five layers downstream of the actual cause and silently cascading six other perf-gate tests as "did not run". Fix: ship a fixedRunCompletedEventline for project 1 in the smoke fixture's.bristlenose/pipeline-events.jsonl. Test-infrastructure only —tests/doesn't ship in the wheel, so the v0.15.10 source artifact is byte-identical before and after this fix. Tag re-pushed after the fix landed on main; PyPI sees 0.15.10 for the first time. - Structural protections so the next silent-mount regression fails loudly. Three new layers:
tests/test_server_status_page.py::TestSmokeFixtureMountsSPAasserts the on-disk smoke fixture boots into the SPA, not the status page (pytest layer — the cheapest place to catch the regression class).e2e/tests/spa-mounts.spec.tsis a standalone Playwright mount-precondition gate across four primary routes, running in the e2e job in parallel with perf-gate, withconsole.error/pageerrorsurfaced on failure so future mount issues land with stack traces instead of a bare 30s timeout.perf-gate.spec.tsitself gains abeforeEachbrowser-error capture for the same reason. - Post-push PyPI verification step in CLAUDE.md release flow. A tag push reaching GitHub is not the same as a release reaching PyPI. New release-flow step polls
https://pypi.org/pypi/bristlenose/jsonfor up to 15 minutes aftergit push origin main --tagsand applies the v0.15.0 debouncing workaround (git push --delete origin v<X.Y.Z> && git push origin v<X.Y.Z>) if the workflow didn't fire. Five versions of silent-gap masking should not be repeatable. - SIGTERM lifecycle subprocess test resilience.
tests/test_run_lifecycle.py::test_subprocess_sigterm_writes_run_cancelledand the SIGINT sibling have post-signal_wait_for_eventtimeout bumped 30s → 60s. Heavy-load Python 3.12 ubuntu-latest runners under full CI matrix contention have been observed missing the 30s window — flake on top of the perf-gate failure, picked up opportunistically while the release pipeline is open. --codebook=<slug>flag +bristlenose codebookssubcommand. CLI plumbing for selecting an AutoCode codebook framework from the command line.bristlenose codebookslists the nine available templates (sentiment,uxr,cli-ux,garrett,morville,nielsen,norman,yablonski,plato) with title and author;--codebook=<slug>onrunandanalyzevalidates the slug against the YAML inventory and exits cleanly with the available list and a pointer to the subcommand on a typo (preflight-abort convention, exit 2, matches the v0.15.8 banner mapping). Validation only — the pipeline doesn't auto-run AutoCode against the selected codebook yet; the flag stores the preference onBristlenoseSettings.codebookfor a follow-up branch to wire the consumer side. C1 from the brew-first-run + IKEA-call cluster; the reframe (11 May) deliberately deferred the "auto-run default codebook" UX moment to a later desktop codebook-picker chunk, gated on cohort calls. Reviewed via three agents (code-review, james-bach, william-of-ockham); Rich-markup escape applied at the interpolation site even though no current YAML triggers the gotcha, future-proofing per the CLAUDE.md audit-on-write convention. Newlist_available_templates()+list_available_slugs()helpers inbristlenose/server/codebook/give the enabled-filter a single source of truth across the validator and the subcommand. 11 new tests; full suite 2681 passed.
0.15.9 — 16 May 2026
-
Multi-project Phase 2 #11 + #14 — drag-onto-folder copy machinery + folder watcher foundation. Two big landings on top of the v0.15.8 multi-project foundation. Drag-onto-existing-project now goes through a copy path (
CopyMachinery) instead of reference-in-place — progress pill with per-file estimate,NewFilesSheetstub for post-copy review, regression-pin tests inCopyMachineryTestscovering collision-rename and folder-name preservation. Folder watcher:NSFilePresenter+ SQLite ingested-set table (opened withimmutable=1for the read path) tracks what's been seen per project; count pill on the row surfaces the delta against the project's current input set; clicking opens a sheet for triage. Two review passes ran on top — first pass: F1 iCloud-evicted detection, F3 debounce scans, F6 SQLite busy-handling; second pass: F49 cloud-evicted glyph swap (icloudoutline, noticloud.and.arrow.down— that arrow connotes download-in-progress, wrong meaning here), F50 pointing-hand cursor on the delta button with no underline (native idiom, not web anchor), F52 session count uses.footnote(function-correct per Apple's text-style scale, not "12pt eyeballed"), F53 subtitle text uniformly.secondary(state via glyph, not via graduated text colour), F59 / F62 i18n + comments.NewFilesSheetflagged as TF scaffolding for post-TF SPA migration — data views belong in the React project dashboard, not native chrome. Row reshape: session count moved onto the title row, deltas moved into the subtitle, Schema A dates throughout. -
Sidebar honesty wave 2 —
.readygated on disk evidence + per-reason cantFind glyphs. v0.15.8 sealed the "fake-success" class of bug at the pipeline layer; this pass closes the matching surface in the sidebar. A re-opened project that had completed analysis weeks ago briefly showed "analysing…" because the row state was readingis_runninginstead of inspectingbristlenose-output/for completion artefacts..readynow requires disk evidence — a completed run record plus at least one canonical output file — so re-opens read "Ready" immediately. cantFind glyph specialises per reason:.moved→questionmark.folder,.cloudEvicted→icloud(outline),.permissionDenied→lock.shield,.removed→folder.badge.minus. Volume remount no longer regresses to.movedwhen the bookmark resolves cleanly to the original path. AppleDouble (._*) sidecars and.DS_Storeskipped across every directory scanner (is_os_metadata()inbristlenose/utils/fs.py, applied atdiscover_files,load_transcripts_from_dir, and the server importer scan sites) — hit 16 May 2026 when a project was copied to an ExFAT SD card and._s1.txtlooked like a transcript to the pipeline and crashed utf-8 decode withUnicodeDecodeError: byte 0xb0. SwiftProjectFolderWatcheralready filters via.skipsHiddenFiles+name.hasPrefix("."), so the Python side closes the symmetry. Newtests/test_appledouble_filter.pypins the behaviour. CLAUDE.md now flags the trap for future scan sites. -
Pipeline subtitle i18n across 6 locales. Sidebar row subtitles for in-flight pipeline state (
Transcribing…,Extracting quotes…,Clustering themes…, etc.) now render in the user's locale — previously English-only because the strings were synthesised in Swift from raw stage keys. Translations across es / fr / de / ko / ja matched against the Apple glossary where possible;dt()overrides where the desktop register diverges from the CLI's. New keys flow through the existing six-localedesktop.jsonshape so subsequent additions stay symmetric. -
Keychain hardening — biometric ACL on stored API keys + dev-build persistence. API keys held in the macOS Keychain now require Touch ID (or password fallback) to read, via biometric access control on the Keychain item — adds a "you, at the keyboard" gate to the key escrow even if the device is unlocked and another app is malicious. Distinct from disk encryption; it raises the cost of a piggybacked-process attack. Debug builds switched from Apple Distribution to Apple Development signing because Distribution-signed debug builds were burning Keychain ACLs on every rebuild — every debug Run prompted for Touch ID and asked the user to re-grant access, making the feature un-dogfoodable. Hardware-validated on the dev machine across multiple build cycles; the underlying keychain entry survives across rebuilds, restarts, and worktree switches.
-
HIG corpus mirror + citation discipline for review agents. Local Apple Human Interface Guidelines mirror added so
what-would-gruber-saycan cite HIG sections by path + paragraph anchor instead of paraphrasing from training data — same epistemics asdocs/design-test-philosophy.mdforwhat-would-james-bach-say, but for native-feel reviews. The scraper that built the corpus is written generic from day one so the public release (planned at github.com/cassiocassio/what-would-gruber-say) is a copy operation not a rewrite, mirroring the william-of-ockham pattern (MIT, 1 May 2026). Citation discipline is now expected for every Mac-app review finding the agent surfaces. -
Misc polish.
Window > Bristlenosemenu item to reopen the closed main window — Mac default that every document-shaped app has; ours didn't. Session start time no longer floored to midnight on the dashboard — display was roundingstarted_atto the date boundary, losing same-day session ordering when multiple sessions were transcribed within one calendar day. In-flight switch modal: "Continue analysing" reframed as the cancel role so Esc and the default action keep the analysis running rather than killing it — HIG-correct default is the non-destructive option, and the Stop and Switch action carries the destructive style.
0.15.8 — 14 May 2026
- Honesty everywhere — the pipeline, the install path, the sidebar all stop faking success. v0.15.7 was a pipeline-only meta-release; this is the substantive follow-up. The walks-fix-walks gate (7 May quality reset) demanded that "did this work?" be answerable everywhere in the app. Three currents land together:
- CLI honesty (A-stream brew/IKEA cluster).
bristlenose doctornow hard-errors with a brew/pipx/pip-aware fix message when[serve]extras are missing (was a silent runtime crash later inbristlenose serve); the message is zsh-glob-safe quoted to keep'bristlenose[serve]'from being eaten as a shell pattern. The Richconsole.printmarkup-eating gotcha —[serve]rendered as''because Rich parsed it as a style tag — fixed at three print sites by addingmarkup=Falseto the relevant calls; a CLAUDE.md gotcha now flags any newconsole.printthat interpolates package-spec / file-glob / version-range text.bristlenose run/serve/analyzeall gateserve_depsin preflight before stage 1; when the pipeline abandons mid-run,PipelineAbandonedErrormapsCause.categoryto a researcher-facing banner (QUOTA, API_REQUEST split from QUOTA, AUTH with provider name + key-shape suffix, NETWORK, API_SERVER, DISK, MISSING_INPUT, MISSING_BINARY) instead of a generic "Failed". The static-render naming is finally sealed:bristlenose renderremoved (replaced by a hidden Typer catch-all stub that redirects torun/servewith a friendly message),--staticdeleted as arunoption.--no-serverestored as a hidden flag — A3's first pass deleted it alongside--static, the doc sweep caught it being aliased on the macOS sidecar'sPipelineRunner.swiftcall site separating two semantically-distinct concerns (suppress static render vs suppress auto-serve), so it came back without the misleading--staticalias._QUOTA_REsplit into QUOTA + API_REQUEST;_AUTH_REextended to cover real Anthropic / OpenAI SDK 401 strings + tightened to drop bare-word false positives. Doc truing across man page, README, manual, CLAUDE / CONTRIBUTING / DEVELOPMENT / SECURITY, and 20 design docs in the same release. - Stage-cache honesty (A4). Abandon-check now fires BEFORE
mark_stage_complete(was after — the bug atpipeline.py:1308that produced fake-success states where an LLM-failed stage cached its empty output and on rerun was skipped as "already done").s08PII removal added abandon-check.s10(quote extraction) ands11(quote clustering) flipped from soft-fail to hard-fail:StageFailureis now emitted at the LLM call site BEFORE fallback runs, so the upstream cache and downstream pipeline both know the truth.mark_stage_completerefuses to recordcontent_hashfor empty content (defence-in-depth: even if a stage misbehaves, the cache stays honest). Privacy contract oncause.message— class name + provider + stage only, neverstr(exc)(exception strings can carry PII / API keys / file paths). Whisper-zero-segments treated as success (silent interviews are a real research signal, not a failure).topicsbucket added toPipelineSummary; cross-branch contract fixture bumped to v5. 470 lines of new tests acrosstest_pipeline_abandon.py/test_manifest.py/test_run_lifecycle.py. -
Long-audio quality (B1). Whisper-loop band-aids:
collapse_adjacent_repeats()post-processor catches Whisper looping on the same word ("thanks thanks thanks", "facebook facebook") while preserving natural interjection doubling ("No. No. No.", "yeah yeah"). Three Whisper params tuned for mlx-whisper:condition_on_previous_text=False(stops decoder context-poisoning across silent gaps),no_speech_threshold=0.85(raises bar for hallucinated transcript in silence),compression_ratio_threshold=1.8(catches repetition loops). Speaker propagation surface: regression-pin unit tests for moderator/participant heuristic +pct_words(defends against silent flip on future refactors); design-doc clarification + INFO log surfacing the architectural limit ("LLM splitter forward-propagates last label from 5–8 min sample window past the window"). Pre-implementation diagnostic against preserved interview artefacts caught two of three reported bugs as already-fixed or by-design before any code was written. -
Multi-project foundation (Phase 0 + Phase 1) — the sidebar starts telling the truth about each project's state. Phase 0 schema lock:
ProjectAvailabilityenum (ready/not-found/cloud-evicted/permission-denied) withlast_seen_attimestamps inprojects.json, propagated throughBridgeHandlerand the sidebar'sProjectRow. Cross-branch contract fixture v5 documents the schema for follow-up branches. Phase 1 UX wins: removing a project from the sidebar is now undoable via a 5-second toast (Cmd-Z works during the window) — undo restores both the row position and any in-flight scroll state; cancelling commits the removal. "Locate…" flow fornot-foundprojects walks the user through Spotlight one-shot search → NSOpenPanel fallback → cancel with grace; the chosen path is captured via security-scoped bookmark and stored alongside the existing bookmark, so the next launch resolves cleanly even after another move. Spotlight one-shot uses a 2-secondNSMetadataQuery(returns top match by display name; tie-break by depth then mtime); query cancels cleanly if the user clicks Browse first or dismisses. -
Multi-project sidebar switching (Phase 2 #1/#2/#3). Sidebar-clicking a different project now tears down the current sidecar (SIGINT → 50ms poll → SIGKILL after 2s) and spawns a new one pointed at the picked project. In-flight pipeline runs prompt a confirm sheet (Stop and Switch / Stay) before cancelling — HIG-correct default-action "Stay" across all 6 locales, with Apple's negated-form translations (es No cambiar / fr Ne pas changer / de Nicht wechseln / ko 그대로 두기 / ja 変更しない).
ServeManager.shutdown(timeout:)escalating teardown;switchProject(to:)orchestrator. Server/api/projects/*getsCache-Control: no-storeas belt-and-braces against WKWebView HTTP cache surviving the.id()rebuild.ProjectBookmarkLease(Mini-spec 3) ships as foundation with tests; the next-branch wiring (#14 folder watcher) consumes it. Reviewed by 6 agents + William's parsimony pass; finding 30 (in-flight cancel atomicity) parked — cost-bounded by existing manifest+resume; durability hardening is post-beta. Phase 3 (cloud-evicted single-state) remains open. -
Pipeline-failure trust-UX (re-run failure ≠ broken project). Cohort-style QA on multi-project switching surfaced a fake-success cousin: clicking Run on a project that already has
bristlenose-output/producedFailed (unknown)+ sidebar badge "Something went wrong" + Retry-that-just-fails-again in the popover. Cohort testers would read "the project is broken" — but the re-run attempt failed, not the prior analysis. NewPipelineFailureCategory.outputExists(matches PythonCauseCategoryEnumsnake_case rawValue);categoriseFailureregex matches "output directory already exists" first in the chain so the broader whisper/model heuristic can't hijack it. Human summary in researcher register: "Already analysed — re-analysing would replace the existing results." Sidebar badge: "Already analysed." The popover swaps Retry → "Re-analyse…" which opens a destructive-confirm alert ("This will delete the existing analysis and run from scratch. Any researcher edits in this project will be lost.") before spawning with--clean.PipelineRunner.start(project:clean:)overload threads the flag through; queue path doesn't preserve it (queue + Re-analyse interaction is a follow-up if observed). -
Drag-onto-project to add files, with project-onto-project rejection. Plan §11 data-integrity guard. Dragging a Finder folder onto an existing project's row in the sidebar adds its files via the existing
addFilespath (reference-in-place, not Copy — adequate for typical cohort flow where source folders are stable on local volumes). Guard: when the dropped folder is itself a Bristlenose project (folderLooksAnalysed—bristlenose-output/with at least one canonical artefact), the drop is rejected with a non-modal toast ("'%@' is already a Bristlenose project — drag its source files instead"). Started life as a single-button Apology-Alert; cohort QA — "alerts for things users won't want to do anyway? that's not how Mac apps work" — flipped it to a toast per HIG (alerts are for decisions, not explanations). Self-drops silently ignored. Six locales updated; out-of-scope deferred to chips: actual file copying, progress pill / Cancel / rollback for cross-volume multi-GB drops, "folder that contains a project" rejection (rare nesting), disk-space precheck, hover affordance polish. -
Sidebar drop hit-test refactored to the native SwiftUI pattern. Original implementation hand-rolled hit-testing on top of
.onDrop+DropDelegate+GeometryReader+PreferenceKeyplumbing in a custom coordinate space. The macOS coordinate-space mess (.globalreturns bottom-left Cocoa coords;.named/.localreturns top-left SwiftUI coords;DropInfo.locationdocumented as local but empirically inconsistent across SwiftUI versions — Apple forum thread 732076) produced two real bugs in cohort QA: drag-highlight followed the cursor at a constant y-offset (rows lit up one off the cursor's actual position), and the project-onto-project reject alert was silently unreachable because the hit-test missed the target. Two patches tried to fix the coordinate-space math (record frames in.global, then self-healing hit-test that prefers direct match) before the native-pattern refactor replaced ~209 lines of bespoke machinery with per-row.dropDestination(for: URL.self, action:isTargeted:)(since macOS 13). SwiftUI now manages coordinate-space concerns internally; URLs come pre-loaded — noNSItemProviderasync dance. Polish:.onDisappearclears the hover-target state as insurance against the macOS intermittent bug whereisTargeted=falsedoesn't fire when the cursor drag-leaves the window entirely. Chipped for follow-up:FolderRowaccepting Finder URL drops directly, internal-project payload as aTransferablenewtype (custom UTType app.bristlenose.project-id) instead ofString, drop-affordance VoiceOver label. -
Locales now reach the host bundle under sandbox. The Xcode "Copy Sidecar Resources" script phase only declared
en/common.jsonas an input/output anchor; editing any other locale file (en/desktop.json, the five non-en locales) didn't trigger the phase to re-run — Xcode's dependency analysis saw no relevant input change. The rsync inside the script never fired and the bundle kept yesterday's copy of every locale file. Symptom: newdesktop.chrome.*keys rendered as raw key strings in alert titles/messages under the default scheme (sandbox=on, sidecar=bundled). Closes a longstanding "i18n locales not reaching host bundle under sandbox" gotcha. Fix:alwaysOutOfDate=1on the phase, same pattern Generate Build Info already uses; the rsync body is idempotent and the cost is ~50ms of syscalls per build. -
Two pre-existing UX bugs surfaced by cohort QA.
common.cancelrendered as a raw key string in three alert dismiss buttons because the key actually lives undercommon.buttons.cancelandcommon.activity.cancel— none at the top level. Fixed all three call sites. File-subset projects (projects analysed against an explicit file selection rather than a whole folder) gated their detail pane oninputFiles != nilalone, so even file-subset projects with completed analysis showed theUnsupportedSubsetViewinstead of their report; same principle as the trust-UX work above — run state shouldn't block viewing what's already there. AddedpipelineHasViewableData(true for.readyand.partial), ANDed with theinputFilescheck. -
HIG: trim ellipsis from "Choose Icon" menu label. Opens a popover, not a dialog — ellipsis is wrong per HIG. Dropped from all 6 locales. Full flyout/submenu UX (trim palette, hybrid Finder-tag-colour, custom-grid Menu) captured for later.
-
Swift 6 main-actor isolation fix.
SpotlightOneShot()as a default argument was evaluated in the caller's isolation context, but the initializer is@MainActor-isolated. Swift 6 strict concurrency rejects this even thoughLocateFlowitself is@MainActor. Defaulting toniland constructing inside the body keeps the call MainActor-scoped without forcing every caller into an async path. Was blocking Xcode build on main. -
CI hygiene.
.tool-versionsnow pins node 24.6.0 + python 3.12.13 (mise / asdf / rtx all read this; CI workflows derive Node + Python versions from the same file). No more "works on my machine but fails on CI because mise gave me 24.8.0 and CI runs 24.6.0" drift. Branch-protection-styleno-red-CI-mergespolicy documented inCONTRIBUTING.md— main branch only accepts green merges; two pre-existing CI failures surfaced by the gate are noted in TODO with reproduction steps. Release-pipeline fix from v0.15.7 stays in (the Node 24 measurement-artefact 210 → 215 kB ceiling re-baseline;frontend/CLAUDE.mddocuments the call: runtime currency / security / dep coherence outweighs sub-1% gzip headroom). -
Workflow tooling for parallel Claude sessions. Three skills land:
/end-sessionsign-off sentinel +/close-branchdrift check —/end-sessionwrites.claude/last-end-session.json(gitignored) recording phase outcomes + HEAD sha./close-branchreads it: warns before archiving a branch that was never end-sessioned, or that has drifted (commits added) since sign-off. Stops the "I thought I closed that" class of mistake./new-feature --print-launch-urlflag + Step 14 hand-off — branch-creation can now emit aclaude://deep link the user can hand to the next session; the spawned session reads the canonical handoff path automatically. Pairs with the existing handoff convention.-
/sitrepskill — situation-report with backward truth-check (plan-docs vs git over a rolling window) + forward candidate ladder. Sprint-aware grid format: prose preamble → sprint frame + contents tables → recent ships → in-flight branches → forward ladder (with sprint-tag column) → recommendation → suggestions. William of Ockham agent triaged the first review pass to 7 spec edits before the skill landed. -
Agent ecosystem. New
what-would-james-bach-sayreviewer agent — test-placement reviewer in the Bach / context-driven-testing tradition: catches tests at the wrong layer, tests that lock implementation rather than invariants, coverage gaps for invariants the user would actually notice, brittle exemplars that future agents will imitate, "while we're here let's also test X" scope creep. Sibling to code-review / perf-review / security-review — this one is testing taste and proportion, not correctness. Readsdocs/design-test-philosophy.mdfor the project's house position. Reminder slated to audit aggregate impact after a month. -
/true-the-docsv2. New--claude-pointersmode reconciles the references-and-pointers layer of CLAUDE.md files specifically (which can drift independently of substantive prose). Phase 5 sweep landed: 3 historical docs archived, CLAUDE.md bloat trimmed, source-aware archival pattern documented in the public archive README. Sibling to A3's design-doc sweep — same epistemics, different doc class. -
Sidecar build fixes. PyInstaller bundle now ships
inflect+typeguard.pysources alongside their.pyc— both packages callinspect.getsource()on their own functions at runtime, which fails on a bytecode-only bundle. Fix:collect_data_filesfor both packages. Sidecar resource-copy step also switched torsync -aL(wascp -RL) to dereference symlinked sources cleanly —cp -RLwas breaking the mlx metallib's internal rpath symlinks under macOS sandbox. -
Frontend bundle ceiling 210 → 215 kB. Node 20 → 24 zlib upgrade (1.2.13 → 1.3.1) compresses the live SPA bundle slightly differently; the 0.75 kB "overage" against the 210 kB ceiling was a measurement artefact, not a regression. Re-baselined: current live SPA is ~210.5 kB gzipped against the new 215 kB ceiling.
frontend/CLAUDE.mddocuments the call.
0.15.7 — 12 May 2026
- Release-pipeline fix that unblocks v0.15.4–6 reaching PyPI + Homebrew. Three independent CI failures had been silently stacking up since 10 May, leaving PyPI and the Homebrew tap pointed at 0.15.3 while the source tree was on 0.15.6. No user-facing source change in this version; the entire v0.15.4–6 changelog (preflight block, server-rendered status page, SPA auto-refetch, sandbox-safe Export, structured failure causes, etc.) is what users actually pick up when
brew upgrade bristlenose(orpip install -U bristlenose) catches up to 0.15.7. tests/test_pipeline_abandon.py— the ffmpeg preflight introduced in v0.15.5 aborts on Linux CI runners (no ffmpeg on PATH). The test already patched the Whisper preflight; the same bypass is now extended to ffmpeg, with a reminder for future preflights.tests/test_serve_auth.py—test_spa_html_sets_cookiehit/report/, which returns 500 (fail-loud, by design since C3) when the React bundle is absent. CI's Pythontestjob doesn't runnpm run build. Switched to the existing_VITE_INDEX_HTML+_STATIC_DIRpatch pattern fromtest_serve.py— a 5-line fixture change rather than restructuring CI to build the bundle in 8 matrix cells..github/workflows/{ci,release}.yml— Node 20's zlib 1.2.13 compressed the live SPA bundle to 210.75 kB (over the 210 kB limit); Node 24's zlib 1.3.1 compresses identical bytes to 204.4 kB. The 0.75 kB "overage" was a measurement artefact of the CI Node version, not a real regression.frontend/CLAUDE.mdalready documented Node 24 LTS as required; CI is now in sync.
0.15.6 — 11 May 2026
- Server-rendered status page for runs the SPA can't render —
bristlenose serve's/report/*catch-all now serves a server-rendered page (no React mount) when the project has no completed run, or the latest run failed or was cancelled. The SPA's invariant becomes: it only mounts when there's data to render. Three states surfaced — no-run-yet (CLI vs desktop copy viaBRISTLENOSE_PLATFORM), failed (with cause + log tail in a<details>block, sourced frompipeline-events.jsonl), cancelled. Reuses the five-kindMessageKindtaxonomy frombristlenose/ui_kinds.py— no sixth kind invented. Page styling lives atbristlenose/theme/templates/status-page.cssand uses the design system's tokens exclusively (sibling toreport.css/transcript.css/print.css/export.css). The event-watcher startup seed inbristlenose/server/app.pyalso widened to seedfailedandcancelledtermini, not justcompleted, so non-happy-path runs survive a server restart. English-only in v1; locale fills for es/fr/de/ko/ja batched into the next translation pass
0.15.5 — 11 May 2026
- First-run preflight block: Whisper / ffmpeg / API-key / spaCy, all before stage 2. Eight-slice landing of
docs/design-cli-just-works.md. Triggered by a 2-hour first-run call with a friendly CTO on 9 May where four blockers fired in sequence (silent ~1.5 GB Whisper download, Ctrl+C resumption opacity,--redact-piihard-failing on missingen_core_web_sm, API key with no billing balance burning six minutes of transcription before stage 8 noticed). Every gap now surfaces up front: bristlenose/preflight/whisper.py— Whisper-model preflight with backend-aware cache probe (Systran/*→model.bin,mlx-community/*→weights.npz; tolerates HF Hub leavingconfig.jsonfrom an interrupted download). Framed banner above (model name,~1.5 GB, "Cancellable with Ctrl+C; resumes cleanly next run."), HF Hub's native progress in the middle (Option B from the design-doc Debate 1 — we step aside; the Rich spinner stops + restarts around the fetch),✓ Whisper model ready [Ns]below.WHISPER_SIZE_HUMANis the single source of truth — referenced by both the banner and thedoctor --fetchhelp text. Newbristlenose <run|transcribe|analyze> --no-fetchaborts cleanly with the recovery message instead of fetching;bristlenose doctor --fetchpre-warms the cache for offline runs.bristlenose/preflight/ffmpeg.py— distro detection from/etc/os-release(macos/ubuntu/fedora/arch/other). Per-distro install table with the matching distro row first, padding computed at print time so translations don't misalign. On macOS with Homebrew on PATH andbrew --prefixwritable, an explicit[y/N]offersbrew install ffmpeg; default flipped to N so a hurried Enter never installs system software. The brew subprocess runs natively with the Rich spinner stopped,✓ ffmpeg installed [Ns]after.bristlenose/preflight/api_key.py+bristlenose/llm/billing_hints.py— paid-call validation against billing. Single-token/messagesrequest (locked literal prompt".", never user-derived;User-Agent: bristlenose/<version>) to the configured Anthropic or OpenAI account — costs ~$0.0001 and exercises the credit-balance path that a free probe misses. Anthropic 400credit_balance_too_low→billing_empty; OpenAI 429insufficient_quota→billing_empty;AuthenticationError→invalid_key;BadRequestError(other) →model_unavailable;RateLimitError→rate_limit. Each maps to a translatable wrapper around the code-ownedProviderBillingregistry (console_url/keys_url/billing_url/minimum_noteper provider). Azure / Gemini fall through to the generic "Provider says: …" copy per finding 24 in the design doc. Result cached for 24 h per provider instate.json(mode0o600, atomic write viaos.open+O_TRUNC) at~/Library/Application Support/Bristlenose/state.json(macOS) /$XDG_DATA_HOME/Bristlenose/state.json(Linux). Non-TTY runs still validate (CI catches expired keys); only the first-run source-attribution banner is suppressed in non-TTY mode.BRISTLENOSE_SKIP_PREFLIGHT=1is the explicit escape hatch for spoofed-TTY CI runners. Network / TLS / corporate-proxy faults surface with a dedicatednetworkerror class rather than collapsing to the generic provider-error copy.bristlenose/stages/s07_pii_removal.py— spaCyen_core_web_sm(~12 MB) lazy-fetches on the first--redact-piirun viapython -m spacy download. Single inline line, no banner ("fetch UX scales with fetch size"). The spaCyConsole()instantiation moved into_ensure_spacy_model()so terminal-width detection isn't frozen at module-import time (sandbox-piping concern).MessageKind.SUCCESS/MessageKind.ERRORglyphs used for the done/failed lines.bristlenose/utils/package_install.py— two install helpers (ensure_spacy_model,ensure_hf_model) with frozen-sidecar guards so a desktop-bundle accidentally hitting these paths raisesFrozenSidecarErrorinstead of silently failing against read-onlysite-packages. (ensure_wheelshipped in an interim draft, deleted in the post-review pass when it had no callers.)- Front-loaded as a single block in
bristlenose/pipeline.py— all preflights fire between ingest (stage 1) and audio extraction (stage 2). After the block, the pipeline printsNo more questions. ~X min (±Y min) to your report. Ctrl+C anytime.(estimate from the existing Welford estimator when warmed up; degrades gracefully without the parenthesis when it isn't). No prompt fires for the rest of the run. - Unified
PreflightAbortedErrorinbristlenose/preflight/__init__.pyfor all three preflights; CLI catches once at therun/analyze/transcribe-onlycall sites and exits 2 with the recovery message. - i18n — new
preflight.*namespace underbristlenose/locales/<loc>/preflight.jsoncovering banner copy, install-table labels + commands, four error-class recovery wrappers, source attribution, closing line. English is the source of truth; es / fr / de / ko / ja currently mirror en and will get native review in the next translation pass. - Documentation truing — design doc updated with a post-implementation Status table mapping each item to SHIPPED / DEFERRED / PARKED; sections that drifted (state path, Whisper stage placement, lint-rule promise, no-account-yet flow) now carry inline corrections + deferral banners. New §"i18n implications" subsection covers namespace shape, code-owned vs translatable surface, translator workflow.
SECURITY.mdadds a fourth "Data leaves your machine only when" bullet covering the preflight call (one full stop, ~$0.0001, no transcript content, 24 h cache), a "What Bristlenose writes to your machine" appendix, and an HF Hub LFS-hash integrity caveat.docs/platform-text-map.mdcarries a CLI-only row for the new namespace. Man page (bristlenose/data/bristlenose.1, symlinked fromman/bristlenose.1) gains anOptions for doctorsection, documents--no-fetch/--fetch/--yes/--static/--dev(the last three were pre-existing gaps), recordsstate.jsonunder FILES, adds exit code 2, and theBRISTLENOSE_SKIP_PREFLIGHTenv var.scripts/bump-version.pynow refreshes both version and ISO date in the.THline on every bump (no more stale "March 2026" on a May release). - Deferred to follow-up (captured in
.claude/plans/cli-just-works.mdDecisions block +TODO.md): the no-account-yet branch (numbered URL flow + getpass + Keychain persist) — material UX work depending on the front-load collector;pipeline-events.jsonlCauseentries on preflight abort; whitelist enforcement for read-only commands; Keychain-source attribution refinement.
0.15.4 — 10 May 2026
- SPA: auto-refetch on pipeline completion — new
GET /api/projects/{id}/last-runendpoint andLastRunStore(module-level, single timer, 3 s poll, visibility-paused, in-flight skip, mount baseline). All four content tabs (QuotesTab,SessionsTab,ProjectTab,CodebookTab) acceptrefreshKey?: number; islands re-fetch when the store detects a newrun_id.announce(t("announce.pipelineCompleted"))fires onrun_idtransition (skipped on first-poll startup-seed). Closes the gap where the browser SPA went silent after the pipeline finished — users now see fresh content within ~3 s without reloading. Import-then-publish race fixed on the server side - SPA: trust-UX layer on top of auto-refetch — manual refresh button in the NavBar (serve-mode only; hidden in the desktop app's embedded mode, where the OS toolbar owns chrome and 3 s auto-poll carries the trust signal alone). Refetch overlay (opacity dim + pointer-events-none) on Quotes / Sessions / Dashboard while a refetch is in flight, so the screen no longer snaps silently. Post-zero-quotes empty state on the Quotes tab: "Analysis finished but found no quotes. Check the source files or try a different LLM provider." Pre-pipeline empty state deliberately out of scope here — that surface belongs to the upcoming generic-failure-surface work and the desktop
WelcomeView - Desktop: Export downloads route via
WKDownload+NSSavePanel— sandbox-safe Export. Previously the browser/Vite serve path leaned on a synthetic<a download>click that the App Sandbox silently dropped — users got "Export" feedback but no file. The desktop shell now intercepts the Export request, hands the response toWKDownload, and prompts the user viaNSSavePanelfor a user-chosen destination. Six localedesktop.jsonfiles updated for the save-dialog strings. Edge case: round-tripping the locale JSON throughjson.load/json.dumprewrites every\uescape (or every literal Unicode, depending onensure_ascii); fixed via targeted text-replace with per-valuejson.dumps(value, ensure_ascii=True). Gotcha now captured inCLAUDE.md -
Desktop: surface structured
Causefrom events log in the failure pill —EventLogReadernow exposes the categorisedCausefrompipeline-events.jsonl(10 categories:MISSING_INPUT,MISSING_BINARY,LLM_ALL_FAILED, etc.). The failure pill in the project sidebar renders the category-appropriate glyph + short label instead of the generic "Failed" badge. Pairs with the schema-coupledPipelineSummaryrollup that landed earlier in this version. NewPipeline.runintegration test exercises the transcribe-all-fail abandon path end-to-end. Review-pass fixes: tightenedpytest.raisesassertions to specific exception types, copy-button now confirms with brief visual feedback, "human" category label refined -
CLI: glyphs + colours sourced from
MessageKindtaxonomy — the_print_step/_print_warn_step/_print_error_step/_print_cached_stepfamily inbristlenose/pipeline.pynow delegates to a single_print_stage(message, kind, elapsed)helper that consultsbristlenose.ui_kinds.cli_prefix(kind)for the leading glyph + colour. New_say(kind, message)helper inbristlenose/cli.pyreplaces ~20 ad-hoc tintedconsole.print("[colour]...[/colour]")calls so glyph/tint comes from one table (CLI / popover / toast all in sync). Doctor + status icon dispatch also routes throughcli_prefix. Decorative[dim]…[/dim]annotations untouched. No user-visible behaviour change beyond a few borderline-coloured lines now showing the correct kind glyph - Pipeline: structured per-stage failure summaries + abandon path for empty-data runs — every terminus event (
run_completed/run_failed/run_cancelled) now carries an optionalsummaryfield with aPipelineSummaryrollup (per-stage attempted / succeeded / failed counts plus categorisedCauseper failure). When every transcription session fails or every quote-extraction LLM call fails, the pipeline now raisesPipelineAbandonedErrorinstead of silently writing a fake-empty report — the run terminates cleanly with no report file on disk. Two newCause.categoryvalues (MISSING_INPUT,MISSING_BINARY); bare-nameFileNotFoundError(e.g.'ffmpeg'under sandbox) now categorises asMISSING_BINARYinstead ofUNKNOWN. Backward-compatible: legacy events log lines without the newsummaryfield deserialise cleanly withsummary=None. Schema-coupled with the upcoming desktop diagnostic-pill work but not runtime-coupled. Seedocs/design-pipeline-resilience.md2026-05-07 changelog entry - Sidecar: prepend bundled FFmpeg dir to
$PATHsomlx_whisperresolves under macOS App Sandbox —mlx_whisper.audio.load_audioshells out to bare"ffmpeg"viasubprocess.run, bypassing ourbundled_binaryhelper. Under the sandbox the inherited PATH doesn't include Homebrew, so transcription failed with[Errno 2] No such file or directory: 'ffmpeg'. Newprepend_bundled_to_path()inbundled_binary.pyruns frombristlenose/__init__.pyso the bundled directory is on PATH before any submodule imports. No-op for CLI / pip / Homebrew installs (no bundle dir to prepend). Same fix transparently coversfaster_whisperand any other transitive bare-name shellouts - Sidecar: avoid
/bin/psexec under macOS App Sandbox —run_lifecycle._ps_start_time()now reads process start time via libprocproc_pidinfo(PROC_PIDTBSDINFO)on Darwin (same call Activity Monitor uses) instead of subprocessing/bin/ps -o lstart=. The exec was blocked by the sandbox and crashed the bundled sidecar at startup before serving a single byte. Linux unchanged (no sandbox blocker; keeps the/bin/psfallback). Third libproc swap in the family (proc_pidpath,proc_listpids+proc_pidfdinfo, nowproc_pidinfo) - Sidecar: serve React bundle via in-memory
read_bytesunder macOS App Sandbox — Starlette'sStaticFiles(FastAPI mount path) opens files withmmap, which the App Sandbox can reject for bundled assets read from inside the.app. The serve-mode static handler now reads the React bundle into memory and returns it directly, bypassing the mmap path. Linux/CLI users see no change (path-existence + size-cap guards keep the read bounded). New regression coverage intests/test_static_serving.py - Desktop: i18n sweep — LLM helpers, transcription settings, build-info, default project name — extracted hardcoded English literals missed by earlier passes:
LLMProvider.swiftactivation toggle, status labels, console-link labels, Ollama description;TranscriptionSettingsViewBackend / Model / Auto / hint / model-recommended interpolated label;BuildInfoSheetClose button; default "New Project" name. All six localedesktop.jsonfiles updated (en/es/fr/de/ko/ja) - Desktop: delegate locale to System Settings — removed the in-app language picker on macOS. Locale selection now flows through System Settings → Apps → Bristlenose, matching the macOS norm.
I18n.swiftreadsBundle.preferredLocalizationson every launch (Apple's BCP 47 matcher);INFOPLIST_KEY_UIPrefersShowingLanguageSettings=YESensures the per-app language section is visible even for users with a single preferred language. Web/CLI serve picker unchanged. Seedocs/design-locale-negotiation.md
0.15.3 — 4 May 2026
- Desktop: provider-switching hardening end-to-end — the "Change provider" flow now actually activates the new provider across Keychain, sidecar env injection, and UI. Four fixes landed together: Ollama choice persists to
UserDefaultsbefore the consent log writes (defeats a race where a crash between the two left the app in an inconsistent state); the Change-provider button activates the row first then falls back to a selector if activation didn't take; sidecar API-key injection is scoped to the active provider only (was leaking other providers' keys into the child env); LLM Settings lazy-loads Keychain reads to the selected row only (was prompting on every row render). Desktop-only — no PyPI surface change - Prompt-injection sentinel-tag boundary (Phase A) — adds a defence-in-depth boundary against prompt-injection attempts hidden in interview transcripts. Sentinel tags wrap the user-supplied content so the LLM can structurally distinguish data from instructions. Phase A is the boundary itself; later phases will tighten the prompt contract around it
- Sidecar bundle trim (S1 + S2 + S3) — three rounds of PyInstaller bundle slimming: S1 excluded
mlx_whisper.torch_whisperfrom the bundle (PyTorch was being pulled in transitively); S2 introduced a dedicated build venv so the bundle no longer carries the contributor's full.venvgraph; S3 evictedtorchandonnxruntimeand switched mlx-whisper packaging tocollect_allso itsassets/ship correctly. Net: significantly smaller sidecar, faster TestFlight uploads - Doctor:
mlx-whisperrecognised as a complete backend on Apple Silicon —bristlenose doctorno longer reports a missing whisper backend on Apple Silicon when onlymlx-whisperis installed (the bundled-sidecar configuration). Other platforms unchanged - Desktop: log-tail trust on sidecar exit-1-after-success — when the sidecar exits with code 1 after a successful pipeline run (rare but observed during sandbox shutdown), the desktop app now trusts the log tail rather than misclassifying the run as failed
- Desktop: sandbox-on Debug i18n + resizable window — Debug builds with the App Sandbox entitlement enabled now load locales correctly and the window resizes properly on macOS Tahoe (was locking vertical resize)
- Frontend dependency updates — minor/patch bumps to
i18next,react-router-dom,eslint,eslint-plugin-react-hooks,typescript,typescript-eslint,vite,vitest(#91);react-i18next16.6.6 → 17.0.6 (#90). The v17 breaking change intransKeepBasicHtmlNodesFordoesn't apply — no usage in this codebase - Test: relax
maxHeightassertion for jsdom 29 compatibility — jsdom 29 normalisesstyle.maxHeight = "0"to"0px"on read-back (matching real browsers); the previoustoBe("0")assertion inAnalysisPage.test.tsxwas leaning on jsdom 27's non-spec echo-the-input behaviour. Switched totoMatch(/^0(px)?$/)so the assertion passes on both versions, unblocking the upcoming jsdom 27 → 29 dependabot bump (#89, pending) - Tooling:
/new-featureskill grew optional flags for self-contained branch handoff (handoff doc + invocation in one paste-able command)
0.15.2 — 2 May 2026
- Bundled FFmpeg/ffprobe discovery for sandboxed sidecar — new
bristlenose/utils/bundled_binary.pyresolves FFmpeg and ffprobe from a bundle-relative path before falling back to$PATH. Lets the sandboxed desktop sidecar find ship-with-app binaries that aren't on the user's shellPATH.audio.py,video.py, andclip_backend.pyroute through the helper;bristlenose doctorreports the resolved path. CLI/PyPI users see no behaviour change —$PATHlookup is still tried bristlenose/server/lifecycle.py— new module factoring out serve-mode lifecycle handling (parent-death detection, sandbox-friendly bind(0), graceful shutdown). Used by Track A A6 sandbox-native sidecar work; CLIservecallers get the same lifecycle path- PipelineRunner:
SidecarMode.resolvemigration (PR #96) — desktop sidecar invocation goes through the resolved-mode path, with logged mode + clearer error copy when the resolution fails - TLS sidecar redirect to certifi under sandbox (PR #97) — bundled sidecar uses
certififor TLS root certs when the system trust store isn't reachable from inside the App Sandbox. Desktop-only impact - Frontend size budget: re-baselined to live-SPA gzipped (lazy locales excluded from the budget figure since they're loaded on demand). Budget remains 320 kB
- Docs: manual + README reconciled with shipped bundled-binary-helper; pricing reframed per hour of audio rather than per study; HANDOFF.md branch-handoff convention surfaced at worktree root via gitignored symlink
0.15.1 — 1 May 2026
- Desktop alpha-readiness landed (Sprint 2 Track C C2–C5) — sandbox-safe API-key injection (Swift Keychain → env vars; Python no longer execs
/usr/bin/security), libproc-based zombie cleanup (proc_listpids+proc_pidfdinfo+proc_pidpath— replaceslsofand/bin/ps, both blocked by App Sandbox),os.Loggerwith privacy redaction throughoutServeManager, key-shape stdout redactor (Anthropic / OpenAI / Google formats) as defence-in-depth against accidental key leakage in subprocess output,SidecarMode.resolve+ three Xcode schemes for the dev escape hatch, privacy manifests for host + sidecar (TestFlight unblock), per-binary signing infrastructure, supply-chain provenance (THIRD-PARTY-BINARIES.md+ auto-regen script). Desktop-only — no PyPI surface change - Desktop first-run experience (Sprint 2 Track B Branch 1) —
BootViewunified boot/loading/failed surface,WelcomeViewtwo-variant pattern (.firstRun+.noSelection), Beat 3 round-trip API key validation inLLMSettingsView, Beat 3bOllamaSetupSheetstate machine (idle / installingOllama / waitingForDaemon / downloadingModel / finishing / failed) for local AI install,I18n.findLocalesDirectory()worktree-aware#filePathderivation,boot.*andwelcome.*translation blocks added to all 6 desktop locale files (English drafts; native review owed for es/fr/de/ko/ja). Desktop-only bristlenose servefall-back to bundled theme CSS — projects that have never run the pipeline have no per-project rendered CSS at<output_dir>/assets/. The SPA's stylesheet link would 404 and the entire UI rendered as raw HTML inside the WKWebView. Server now registers a typed CSS route that prefers per-project CSS if it exists and falls back to the bundled theme otherwisebristlenose doctor --self-test— new flag that verifies sidecar bundle integrity (spec → bundle file presence). Catches the "data files declared in.specbut missing from the built bundle" class of bug. Runs indesktop/scripts/build-all.shstep 2abristlenose servefail-loud on missing React bundle — returns a "Build incomplete" 500 page instead of silently falling back to the static render. The static render is officially vestigial scaffolding now, not a fallback- Japanese (
ja) locale completion — bulk-translated ~614 stub strings across 8 namespace files inbristlenose/locales/ja/, seededglossary.csvwith 83jarows. Alpha gate cleared - i18n: tag sidebar — extracted hardcoded strings, added 11 keys across all 6 locales
0.15.0 — 26 Apr 2026
- Pipeline resilience: Phase 1f / 4a-pre — run-level event log + honest run state — pipelines now write a single-source append-only
pipeline-events.jsonlrecording how each run ended (started / completed / cancelled / failed) with a structuredCauseobject capturing category, code, message, provider, stage, signal. Replaces the inference path the desktop app previously used (which mis-classified interrupted runs as.ready). Survives crashes, recovers cleanly from torn writes, refuses concurrent runs against the same project. Seedocs/design-pipeline-resilience.md§"Run outcomes and intent" - Structured failure causes — exceptions categorised into 10 cases:
auth/network/quota/disk/whisper/unknown(existing) plususer_signal/api_request/api_server/missing_dep(new). Same case names on Python and Swift sides for lossless JSON round-trip; word-boundary regex matchers anchor the categoriser to avoid false positives (e.g."credit"no longer matches"credentials") - Cost as honest estimate — pipelines now stamp
cost_usd_estimate+price_table_version+input_tokens+output_tokensonto every terminus event. UI surfaces viaformat_cost_estimate()as"~$0.46 (est.)"— never bare dollars. Token counts are real (provider returns them); USD is best-effort.SessionRecordextended additively with the same fields - Python-side PID file with start-time check —
<output>/.bristlenose/run.pidcontaining(pid, start_time, run_id)for cross-process liveness. Defeats PID reuse on busy macOS; mode 0o600 +O_NOFOLLOW. Lives inside the project folder so App Sandbox doesn't need extra entitlements - Stranded-run reconciliation — if a prior run died mid-flight (kill -9, OOM, hard reboot), the next
bristlenose runnotices the strandedrun_startedevent and synthesises arun_failedfor it before starting a new run. Honest fallback: cause isunknown, message is "Analysis stopped unexpectedly." - Desktop:
EventLogReader.deriveState— Swift consumer of the events log. NewPipelineStatecases.partial(kind, stagesComplete)(transcribe-only completed) and.stopped(stagesComplete)(user cancelled) join the existing set. UI verb wiring (Resume / Retry / Re-analyse…) deferred to a separate desktop UX iteration - Desktop:
BristlenoseTeststarget wired up —xcodebuild testnow runs 90 Swift tests (Tab, I18n, LLMProvider, KeychainHelper, ProjectIndex, plus newEventLogReaderTests). Pre-existing test files marked@MainActorat suite level to satisfy Swift 6 strict concurrency - Test totals: 2328 Python (4 new test files, 80 new tests) + 90 Swift, all green
0.14.6 — 18 Apr 2026
- CI: e2e gate re-enabled as a blocking check — cleared the three P3 items parked during v0.14.5. Autocode-status 404 allowlisted in
e2e/tests/network.spec.ts(kept REST semantics; frontend atCodebookPanel.tsx:609already null-tolerant). Codebook-route console subresource 404 allowlisted ine2e/tests/console.spec.tswith aTODO(S3)pointer; root-cause fix deferred to the next frontend sprint._BRISTLENOSE_AUTH_TOKEN=test-tokenwired into the main e2e workflow step so Node-side fetches can authenticate - Fix: Analysis page "Show all N quotes" toggle — the expand/collapse toggle on signal cards was rendered as an
<a>withouthref, which fails a11y (not keyboard-focusable) and trips link validators. Converted to a proper<button type="button">with minimal CSS reset inanalysis.css. Discovered during the e2e gate-flip verification on this branch - Fix: Playwright config shell-quotes —
playwright.config.tsinterpolated${BRISTLENOSE}and${FIXTURE_DIR}unquoted, so worktree paths with spaces (likebristlenose_branch ci-cleanup) broke the webServer launch. Paths now quoted. Pre-existing bug, surfaced by the branch naming convention - Security docs:
SECURITY.mdcorrected — prior text claimed the auth token was "random at startup" and "memory only" unconditionally. Reality:_BRISTLENOSE_AUTH_TOKENin the process environment overrides the random token (required for CI fixtures and for uvicorn--reloadsession continuity across code saves). Spec updated to describe the actual behaviour; a future hardening task will gate the override behindBRISTLENOSE_DEV_MODE=test - Doctor: new
Auth tokencheck —bristlenose doctornow warns if_BRISTLENOSE_AUTH_TOKENis set in the shell environment. Helps catch accidental bleed from a previous CI run or dotfile that would otherwise silently pin the serve token - CI: allowlist register — every deliberate suppression in the Playwright e2e gate now has an entry in
e2e/ALLOWLIST.mdand a// ci-allowlist: CI-A<N>marker in code. Three categories:infra(stack artefact, never fixable),by-design(intentional correct behaviour),deferred-fix(real bug with tracker link). Prevents silent accumulation of "we ignored that failure because" in the gate. Validator script + staleness gate deferred until ~10 entries
0.14.5 — 17 Apr 2026
- CI: unblock release pipeline — v0.14.0–v0.14.4 releases all failed in CI for three independent reasons that had compounded since early April. No user-facing changes; cutting this release to prove the pipeline is green end-to-end before resuming normal feature work
- Fix:
eslint-plugin-react-hookspeer-dep mismatch — 7.0.1 capped the eslint peer at major 9, but the project ships eslint 10.npm install(local) is permissive;npm ci(CI) is strict and rejected it. Bumped to 7.1.0 which accepts eslint 10 - Fix: pin
jsdomto 27.x — a dependabot auto-merge bumped jsdom 27 → 29, which stopped shimming Web Storage API methods. Combined with Node 25, 140+ frontend tests failed atlocalStorage.*. Reverted; reattempt once jsdom 29 + Node interaction has a proper polyfill - Fix: stale Vitest mocks after
api.tsand i18n changes —useKeyboardShortcuts.test.tsmissedapiGetin itsvi.mockfactory (PlayerProvider now imports it);CodebookSidebar.test.tsxasserted on a raw template title that the component now passes throught("codebook.sentimentTitle"). Both updated - Fix: e2e install —
e2e/package-lock.jsonregenerated afterlighthousewas added toe2e/package.jsonwithout runningnpm install. Samenpm installvsnpm cidivergence pattern as the eslint peer-dep issue - CI: exclude
perf-stress.spec.tsfrom default Playwright discovery. The spec has a CI guard that refuses runs under the defaulttrace: 'on-first-retry'config; it runs via the dedicatedplaywright.stress.config.tsinstead - CI: e2e gate temporarily informational — three P3 e2e findings (console subresource 404 on
/report/codebook/, noisy autocode status 404 when no job exists, perf-gate auth-token env wiring) parked to sprint 2. Re-enable the e2e gate once all three clear
0.14.4 — 16 Apr 2026
- Pipeline resilience: input change detection — phase 2c of pipeline resilience. Detects when input files have been added, removed, or modified between runs. Stale sessions are quarantined and the affected stages re-run automatically. SHA-256 content hashing with size-based short-circuit for large files
- Alembic migration infrastructure — replaced manual
_migrate_schema()with Alembic for schema migrations. Existing databases auto-stamp to the baseline revision on first serve. New migrations go inbristlenose/server/migrations/ - CI: multi-python matrix — test suite now runs against Python 3.10, 3.11, 3.12, and 3.13. Lint job split from test jobs for faster feedback
0.14.3 — 27 Mar 2026
- Export: video clip extraction — starred and featured quotes extracted as trimmed video clips via FFmpeg stream-copy. Human-readable filenames (
p1 03m45 Sarah onboarding was confusing.mp4), adjacent merge, async progress toast,clips_manifest.jsonaudit trail. Two-backend architecture (FFmpeg now, AVFoundation future) withClipBackendProtocol. 64 new Python tests - Fix: video player 401 —
<video>elements can't send Authorization headers;/media/routes exempted from bearer token middleware. Also fixedPlayerContext.tsxandMinimap.tsxusing rawfetch()instead ofapiGet() - Fix:
safe_filename()path traversal — single-pass..removal could reassemble from"..../". Now useswhileloop - Refactor:
pick_featured_quotes()— moved fromdashboard.pytoexport_core.pyfor shared use by dashboard and clip extraction
0.14.2 — 24 Mar 2026
- Localhost bearer token — defence-in-depth API access control for serve mode. Per-session
secrets.token_urlsafe(32)validated via Starlette middleware on/api/*routes. Token injected into SPA HTML and desktop app WKWebView - Fix: TOC sidebar empty — left-hand contents navigation on the Quotes tab was broken by the auth middleware (raw
fetch()lacked the bearer token). Switched toapiGet()helper
0.14.1 — 24 Mar 2026
- French, German, Korean locales — machine-translated all 8 namespace files (common, settings, enums, cli, pipeline, server, doctor, desktop) for fr, de, ko. Cross-checked against Apple macOS glossaries and ATLAS.ti/MAXQDA research terminology. Native reviewer docs added for German and French
- Contextual left-panel labels — desktop app toolbar and View menu items now show tab-specific labels (Contents on Quotes, Codes on Codebook, Signals on Analysis) instead of generic "Navigation". View menu items renamed from "Toggle X" to "Show X" per Apple HIG
- Tags sidebar shortcut —
⌘⌥Ttoggles the tag sidebar in the desktop app - Keyboard shortcuts design doc — full inventory of desktop app shortcuts with conflict matrix
0.14.0 — 20 Mar 2026
- Inspector panel — DevTools-style collapsible bottom panel in the Analysis tab. Heatmap matrices (section × sentiment, theme × sentiment, codebook group) live in a resizable pane that opens via click, drag-up, or the
mkeyboard shortcut. Signal card selection (blue wash) syncs bidirectionally with inspector source tabs — click a card to drill into its matrix, click a tab to highlight its card. Drag-from-collapsed grows the panel smoothly from the 28px handle bar. Tooltip repositioning flips above when near viewport bottom. Footer auto-hides to maximise matrix space. New files:InspectorPanel.tsx,InspectorStore.ts,useVerticalDragResize.ts,inspector.css. 4 feature commits, 696 new test assertions - Codebook sidebar — left-hand navigation panel now available on every tab, not just Quotes. The TOC sidebar becomes a codebook-tree sidebar showing tag groups, counts, and micro-bars for the current view. Tabs without a right sidebar get a symmetric spacer rail so the center column stays balanced. Inert rail layout ensures non-sidebar tabs render cleanly without sidebar state leaking
- Finding flags on signal cards — each sentiment-based signal card carries an optional finding flag (Win, Problem, Niggle, Success, Surprising, or Pattern) classifying the signal's direction and weight. Flags are computed from sentiment valence, participant breadth, and intensity. Conservative thresholds mean cards that don't clear the bar stay unflagged. Four tunable thresholds ready for calibration via a future dev-mode HUD. Two-tone micro-bars visualise signal composition. Flows through all three output paths: static render, sentiment API, and tag API. 27 new tests. Design exploration:
docs/design-finding-weight.md - Settings modal — gear-icon /
⌘,modal with sidebar navigation shell (ModalNav organism). Reusable across modals — Settings and Help share the sameModalNavcomponent with their own sizing classes - Help modal — replaces the About page route and keyboard shortcut overlay with a sidebar-nav modal. Seven sections: Help, Shortcuts (keyboard grid with platform-aware badges), Signals, Codebook, and About group (Developer, Design, Contributing). Triggered by ⓘ navbar icon or
?key./report/about/URL gracefully redirects to/report/ - About tab section components — modular React components for each About section (AboutSection, CodebookSection, ContributingSection, DesignSection, DeveloperSection, SignalsSection). Worktree launch paths fixed for serve mode
- Activity chips persist across tab navigation — AutoCode progress toasts survive React Router navigation via a new
ActivityStore. Action link uses realhreffor Cmd+click, label changed to "View Analysis", hidden when already on Analysis tab - New framework codebooks — Nielsen's 10 Usability Heuristics and Yablonski's Laws of UX join the built-in codebook library. Import either via Codebook → Add Framework
- Fix: sidebar not showing on codebook tab when URL had a trailing slash
- Fix: codebook grid fills available width — removed max-width cap that left empty space on wide screens
- Fix: autocode toast — "View Report" link now opens the threshold review modal instead of always being visible; toast auto-dismisses on click
0.13.7 — 16 Mar 2026
- Spanish locale — machine-translated all 102 UI strings across 8 locale files (3 frontend, 5 backend). Covers navigation, settings, enums, CLI messages, doctor, pipeline progress, and server errors. Ready for native-speaker review and polish
0.13.6 — 16 Mar 2026
- Single tag focus mode — click a tag count in the sidebar to solo that tag (mixing-desk style). All other sidebar controls dim to 50%; the focused tag row stays at full brightness. "You are here" blue wash highlight matches the TOC sidebar's active link style. Click again or press Escape to exit and restore the previous tag filter
- Fix circular dependency crash —
SidebarStoreimportingsetTagFilterfromQuotesContextat module scope created a circular dependency that caused "Cannot access uninitialized variable" in production builds. Refactored to passsetTagFilteras a callback parameter - Used/unused tag filter toggle — "Used" link in tag sidebar header filters to show only tags that appear on at least one quote, hiding zero-count tags for a cleaner view
- Sidebar tag click-to-assign — select quotes then click a tag badge in the sidebar to apply that tag to all selected quotes. Flash animation on each newly tagged quote, toast notification when tagged quotes are off-screen
- Tier 1 logging instrumentation — persistent log file with stage-level timing, session counts, and LLM token usage. PII hardening: file paths, API keys, and participant names are scrubbed from log output
- Multilingual UI infrastructure — i18next integration with
useTranslationhook, Python i18n module, browser language auto-detection on first visit. Translation keys for all user-facing strings (not yet translated) - Responsive signal cards — signal framework cards use CSS grid layout with narrow-screen stacking for readable display on mobile viewports
- Cache
system_profilerresults — 24-hour TTL cache for macOSsystem_profileroutput, avoiding the slow 2-3s startup penalty on everybristlenose doctororbristlenose run
0.13.4 — 14 Mar 2026
- Fix import FK constraint — delete
ProposedTagrows (AutoCode proposals) before removing stale quotes during re-import, preventingFOREIGN KEY constraint failederrors onbristlenose servestartup
0.13.3 — 14 Mar 2026
- Help modal polish — platform-aware keyboard shortcuts (⌘ on Mac, Ctrl on Windows/Linux), sans-serif keycaps with deeper shadow and min-width for visual consistency, typography tokens and spacing overhaul, entrance animation, dark-mode keyboard styling. Phase 3 adds custom tooltips with keyboard shortcut badges across the UI
- Bulk actions on multi-selection — star, hide, and tag operations now respect click-based multi-selection: clicking a starred quote in a selection unstars all, clicking an unstarred one stars all. Shift+click range selection no longer triggers browser text selection. Tag commits apply to all selected quotes
- Sidebar animations — push animation on keyboard shortcuts (
[,],\) and click, 4× faster open/close transitions, two-row header layout. Rail drag-to-open with 60px commit threshold and pointer-tracking overlay. Drag handle hover-intent (80ms delay) filters casual mouse traversal - TOC overlay clip-path animation — smooth reveal/hide for the table-of-contents overlay panel, with playground toggle for overlay style
- Scroll spy Safari fix — two-phase click-intent override (immune phase 0–600ms, sticky phase >600ms) resolves Safari's smooth-scroll
getBoundingClientRect()timing glitch. Minimap reordered to grid column 4 (between center and tag sidebar) - Pipeline error display — failed stages show red ✗, partially successful stages show yellow ⚠ in CLI output
- Speaker badge fix — correct indent when speaker badge wraps to a new line
- Scroll-margin fix — section headings in React SPA now scroll to the correct position, accounting for the sticky navbar
- CLAUDE.md refactor — extracted frontend, pipeline, and LLM gotchas into dedicated child files (
frontend/CLAUDE.md,bristlenose/stages/CLAUDE.md,bristlenose/llm/CLAUDE.md), keeping the root file focused on architecture and conventions
0.13.2 — 11 Mar 2026
- Render refactor — broke up the 2,903-line
render_html.pymonolith intobristlenose/stages/s12_render/package with 8 focused submodules:theme_assets.py(CSS/JS loaders, Jinja2 env),html_helpers.py(escaping, timecodes, badges, video map),quote_format.py(quote HTML, badges),sentiment.py(sparklines, rewatch/friction),dashboard.py(project tab, featured quotes, coverage),transcript_pages.py(standalone + inline transcripts),standalone_pages.py(codebook, analysis),report.py(orchestrator). No behaviour change — same HTML output, all 1,872 tests pass - Static render deprecated —
render_html()now emitsDeprecationWarning.bristlenose serveis the canonical path;bristlenose renderstill works but is frozen - Updated ~99 documentation references across 34 files to point to the new submodules
0.13.1 — 10 Mar 2026
- Responsive layout playground — dev-only responsive design tool (
--devmode) with device presets, type scale previews, and a HUD overlay for testing quote grid layouts across breakpoints. FAB toggle button,PlaygroundStorewithuseSyncExternalStore - Sidebar TOC overlay mode — left sidebar TOC switches to hover-triggered overlay on narrower viewports.
useTocOverlayhook manages open/close transitions with mouse-enter/leave - Minimap component — document minimap for the quotes page, showing a scaled overview of quote sections with viewport indicator
- Responsive quote grid CSS — CSS-only responsive grid for quote cards, adapting column count to available width
0.13.0 — 10 Mar 2026
- Codebook-aware tag autocomplete — TagInput now groups suggestions by codebook section with colour-coded pills and section headers, matching the IDE-style autocomplete pattern. Highlighted suggestion takes priority over ghost text. Active pill uses white text, z-index fix prevents clipping under sidebar, deeper dropdown accommodates more suggestions
- Sentiment tags unified into codebook framework — sentiment tags (positive, negative, neutral, mixed) are now a proper codebook framework (
sentiment.yaml) imported through the standard codebook system, replacing the ad-hoc UXR sentiment group. Tag colours use the design-system colour sets. Importer handles migration of existing sentiment tags to the new framework - Tag provenance tracking —
QuoteTag.sourcecolumn tracks"human"(manual adds) vs"autocode"(LLM-suggested). Provenance is preserved across bulk tag operations (snapshot before delete, restore on re-insert). Exposed in the quotes API viaTagResponse.source. Case-insensitive duplicate guard prevents adding the same tag twice - Hidden-group tag UX — eye-toggled codebook groups show a grey closed-eye icon in TagInput autocomplete. Accepting a tag from a hidden group auto-unhides the group so the badge is immediately visible.
allTagNamesprop prevents invisible re-adds from hidden groups - Quick-repeat tag shortcut (
r) — pressron a focused quote to re-apply the last-used tag without opening the tag input. Shown in help modal - Context expansion fix — fixed infinite re-render loop when clicking the up chevron to expand context on a quote card.
FocusContextandQuotesContextupdated to break the render cycle - Doctor offers MLX install on Apple Silicon —
bristlenose doctornow detects Apple Silicon Macs without MLX and offers interactive installation for GPU-accelerated transcription. Install helpers usesys.executableto target the correct Python. Skipped in non-interactive contexts - Sidebar UX accessibility — focus management, ARIA attributes, and
prefers-reduced-motionsupport for sidebar drag-resize animations - Dashboard Cmd+click fix — stat cards now use
<a>elements with realhrefattributes, so Cmd+click opens in a new tab as expected. Normal clicks still use React Router navigation
0.12.2 — 2 Mar 2026
- Footer feedback restored in React serve mode — the footer once again exposes both "Report a bug" and "Feedback" in the served React app. "Feedback" now opens a React-native modal (sentiment picker + optional message), matching the legacy flow rather than reusing the keyboard-shortcuts help modal
- Feedback submit + fallback parity — feedback sends
{version, rating, message}JSON to the configured feedback endpoint when available over HTTP(S). On failure (or non-HTTP contexts), it falls back to copying a plain-text feedback payload to the clipboard and shows toast guidance - Health API contract extended —
GET /api/healthnow returnslinks.github_issues_urlandfeedback.{enabled,url}alongside existingstatusandversionkeys (unchanged) - Export parity with serve mode — exported reports embed the same
health.links+health.feedbackshape so footer behavior is consistent between serve and export - Config via environment variables — optional server-side overrides:
BRISTLENOSE_GITHUB_ISSUES_URL,BRISTLENOSE_FEEDBACK_ENABLED,BRISTLENOSE_FEEDBACK_URL
0.12.1 — 1 Mar 2026
- Word-level transcript highlighting — during video playback, individual words in the transcript highlight in sync with the audio, karaoke-style. Whisper captures per-word start/end timestamps during transcription; these flow through the pipeline (
session_segments.json→ SQLitewords_json→ transcript APIwordsfield) and render as<span class="transcript-word">elements withdata-start/data-endattributes.PlayerContext.updateGlow()scans word spans at 4 Hz and toggles.bn-word-activeon the current word. Three-tier rendering fallback: word spans (Whisper sessions) →html_textwith<mark>quote highlighting (non-Whisper with quotes) → plain text. CSS usescolor-mix(in srgb, accent 30%, transparent)withprefers-reduced-motionunderline fallback. VTT/SRT-imported sessions gracefully degrade to segment-level glow only. Glow brightness rebalanced: paragraph glow dimmed 50%, word glow brightened 50% — the active word pops against a subtler segment background - Design doc:
docs/design-word-highlighting.md— pipeline diagram, data flow, frontend architecture
0.12.0 — 1 Mar 2026
- Dual sidebar for Quotes tab — left sidebar: table of contents with scroll-spy (sections + themes, active heading highlights on scroll). Right sidebar: tag filter with codebook tree, eye toggles for badge hiding (persisted to localStorage). 5-column CSS grid (
rail | sidebar | center | sidebar | rail), drag-to-resize with snap-close thresholds, keyboard shortcuts ([left,]right,\both,⌘.tag sidebar). Quotes-tab-only — other tabs see no grid or rails. New components:SidebarLayout,TocSidebar,TagSidebar,TagGroupCard,TagRow,EyeToggle. New hooks:useDragResize,useScrollSpy. Module-levelSidebarStorewithuseSyncExternalStore. Backend: tag-group-with-quotes API endpoint, admin panel registration. 845 Vitest tests (60 files), 1856 Python tests - Frontend CI — ESLint, TypeScript typecheck, and Vitest added to GitHub Actions (
frontend-lint-type-testjob on Node 20). ESLint step informational pending fix of 84 pre-existing errors
0.11.2 — 1 Mar 2026
- Self-contained HTML export — download button in the NavBar bundles all API data as embedded JSON, blob-URL'd JS chunks, and a hash router for
file://— recipients open the report in any browser without Bristlenose installed. Optional anonymisation checkbox strips participant names before download. Read-only CSS + JS guards hide mutation UI in exported files. 26 Python tests, 20 frontend tests - About panel redesign — sidebar layout (Claude-settings style) with 5 sections: About (per-screen product guide), Signals (sentiment taxonomy, concentration metrics, academic references), Codebook (sections/themes, sentiment tags, framework codebooks with author refs), Developer (architecture, stack, APIs, contributing), Design (design system, dark mode, component library, typography)
- Configuration reference panel — read-only grid in Settings tab showing all 63 configurable values across 12 categories with defaults, file badges, clickable env var names (copy to clipboard), and valid options
- Morville honeycomb codebook — Peter Morville's User Experience Honeycomb: 7 groups (useful, usable, desirable, findable, accessible, credible, valuable), 28 tags with discrimination prompts and cross-codebook references. Codebooks now sorted by browse order via
sort_orderfield
0.11.1 — 28 Feb 2026
- Fix video player — double URL encoding broke playback for files with spaces (
%20→%2520). Removed Python-side_url_quote()from_file_to_media_uri(); JSencodeURIComponent()now handles encoding once. Also fixed subdirectory loss:merge_transcript.pynow stores relative paths (e.g.interviews/show and tell 40.mov) instead of just the filename
0.11.0 — 28 Feb 2026
- Full React SPA in serve mode — React Router (
react-router-domv7) replaces vanilla JS hash-based tab navigation. SingleRouterProviderroot with pathname routes (/report/,/report/quotes/,/report/sessions/:id, etc.).AppLayoutwrapsNavBar+Outlet. 8 page wrappers compose existing island components. Backward-compat shims (window.switchToTab,window.navigateToSession) delegate to React Router for any remaining callers - Player integration —
PlayerContextmanages popout video player lifecycle,seekTo(pid, seconds), glow sync via DOM class manipulation (not React state).buildGlowIndexkeys by session ID from URL pathname. Progress bar via--bn-segment-progressCSS custom property.player.jsbails out in SPA mode - Keyboard shortcuts —
FocusContext+useKeyboardShortcutshook. j/k navigation, multi-select (Shift+j/k, x), bulk star/hide/tag,?help modal,/focus search, Escape cascade (modal → search → selection → focus). Data-derived visible quote IDs replace DOM queries - React app shell —
Header(logo, project name, subtitle),Footer(version,?for Help),HelpModal(keyboard shortcuts overlay). Serve mode serves Vite-built SPA directly — no more_transform_report_html()marker substitution. Route extraction:app.pyrefactored to route modules - Vanilla JS retired from serve path —
_strip_vanilla_js()removes all 26 modules from the IIFE while keepingwindow.*globals (BRISTLENOSE_VIDEO_MAP,BRISTLENOSE_PLAYER_URL,BRISTLENOSE_ANALYSIS). Static render path unchanged - Video player links on sessions page and dashboard open the popout player
- Importer finds source files in one-level subdirectories (mirrors ingest scan pattern), fixing video 404s when files are in
interviews/ - Speaker display names in sessions grid use normal font size (matching date/duration columns)
- Word-level timing data plumbed through pipeline to transcript API (
wordsfield on segments)
0.10.3 — 21 Feb 2026
bristlenose statuscommand — read-only project status from the manifest, shows stage completion with session counts, intermediate file validation, and-vper-session detail- Pre-run resume summary — one-line status message before pipeline output when resuming an interrupted run (e.g. "Resuming: 7/10 sessions have quotes, 3 remaining.")
- Split speaker badges — two-tone pill with speaker code (left, mono) and participant name (right, body font). Settings toggle: "Show participants as: Code and name / Code only", persisted in localStorage. Applied across all surfaces: quote cards, session table, dashboard, transcript pages, user journeys, friction points
- Em-dash removed from quote attribution — replaced with
margin-left: 1remgap between quote text and speaker badge - Always-on sticky transcript header — session selector dropdown always visible on transcript pages (not gated on journey data). Single-session pages show a plain label as page title
- Serve-mode session routing — session links now navigate to standalone transcript pages (React island) instead of inline Jinja2 drill-down. Inline Jinja2 transcript pages and vanilla JS subnav stripped in serve mode
- AutoCode frontend — complete lifecycle for LLM-assisted tag application: ✦ button on framework codebook sections triggers AutoCode run, progress toast with 2s polling and cancel support, threshold review dialog with confidence histogram and dual-threshold slider (accept/tentative/exclude zones), per-row override, proposed badges on quotes (pulsating dashed border, hover accept/deny with brightness flash animation). Tag colour pipeline carries
colour_set+colour_indexfrom CodebookGroup through to accepted user tags - Threshold review dialog — confidence-aware triage: 20-bin histogram with tag colours, three proposal zones, bulk accept/deny with max_confidence filter, per-quote override before committing
- Activity chip and chip stack components — lightweight status indicators for background jobs with multi-stage progress, cancel button, and auto-dismiss on completion
- Transcript page improvements — greedy slot layout for annotation span bars (no overlapping when multiple quotes span the same region), suppress repeated label+sentiment annotations (show only on topic change), speaker badges use
bn-person-badgestyling consistent with sessions table - Journey chain: full sequence with revisits — transcript sticky header now shows the complete user journey including revisited sections (e.g. Cat Nav → Prod List → Cat Nav → Checkout), not the deduplicated summary. Index-based tracking distinguishes repeated labels — clicking the second "Cat Nav" jumps to the second occurrence, scrolling highlights only the current one. Sessions table retains deduplicated labels unchanged
- Journey label active state — new
journey-labelCSS atom with--bn-colour-hoverpill background on the active step, extracted from transcript styles into the atomic design system. Fixed CSS specificity bug whereall: unseton buttons suppressed the active background - Resilient transcript discovery — serve-mode importer now searches four locations in priority order (cooked → raw/output → raw/project → transcripts/project) instead of only
transcripts-raw/, fixing empty transcript pages when pointing serve at non-standard output layouts - Generic analysis matrix and signals — reusable computation engine for cross-tabulating any labelled data (not just quotes), with API routes for serve mode
- Man page and docs updated for
statusandservecommands - Fix: serve-mode navigation escape — transcript page back link pointed to
/report/{filename}.html(raw static HTML without React islands) instead of/report/(serve-mode route with React injection). Clicking "← Research Report" from a transcript now stays in serve mode
0.10.2 — 21 Feb 2026
- Pipeline crash recovery — interrupted runs resume where they left off instead of starting over. Kill mid-analysis, re-run the same command, and only the unfinished sessions get LLM calls. Completed sessions are loaded from cache in milliseconds
- Per-session tracking for topic segmentation and quote extraction — the manifest records which sessions finished within each stage, so a crash after 7 of 10 sessions only re-processes the remaining 3. Cached + fresh results are merged transparently
- CLI resume guard — re-running into an existing output directory now detects the pipeline manifest and resumes automatically. No
--cleanneeded, no "output directory already exists" error.--cleanstill available for full re-runs - Pipeline resilience design doc — CS foundations research (build systems, event sourcing, WAL, CAS, sagas) and phased implementation plan for crash recovery, data integrity, and incremental re-runs
0.10.1 — 19 Feb 2026
- Desktop app API key onboarding — first-run setup screen prompts for Claude API key, stores in macOS Keychain via
securityCLI, sidecar picks it up automatically via_populate_keys_from_keychain(). Settings panel (⌘,) for viewing, changing, or deleting the key .dmgpackaging —build-dmg.sharchives via xcodebuild with ad-hoc signing, packages with hdiutil (orcreate-dmgfor drag-to-Applications).build-all.shchains sidecar + ffmpeg + whisper + dmg into one command- Serve mode after pipeline — desktop app auto-launches
bristlenose serveafter pipeline completes, opens report athttp://127.0.0.1:8150/report/with full React islands - Deployment target updated to macOS 15 Sequoia (was 14 Sonoma)
- Codebook tag templates — pre-built tag sets via new API endpoints and UI
0.10.0 — 18 Feb 2026
- Desktop app v0.1 — SwiftUI macOS launcher (
desktop/Bristlenose/) with folder picker, drag-and-drop, pipeline output streaming, View Report in browser. 4-state UI (ready → selected → running → done), ANSI escape stripping, report path detection from both OSC 8 hyperlinks andReport:text fallback. Xcode 26 project, 840 KB .app, macOS 14+ deployment target - ProcessRunner —
@MainActorObservableObject that spawnsProcess(), reads stdout viaTask.detached, streams lines to SwiftUI, extracts report file path - FolderValidator — scans directories recursively for processable file extensions (mirrors
models.pyextension lists) - Back-to-folder navigation on Done screen — re-validates folder state for re-render/re-analyse without starting over
0.9.4 — 17 Feb 2026
bristlenose servecommand — FastAPI local dev server that serves the HTML report over HTTP with SQLite persistence, React islands, and live JS reload in dev mode. Auto-renders before serving, auto-opens browser, prints clickable report URL- React islands architecture — 5 islands (SessionsTable, Dashboard, QuoteSections, QuoteThemes, CodebookPanel) mount into static HTML via comment markers and
re.subat serve time; 16 reusable React primitives (Badge, PersonBadge, TimecodeLink, EditableText, Toggle, TagInput, Sparkline, Counter, Metric, JourneyChain, Annotation, Thumbnail, MicroBar, ConfirmDialog); 182 Vitest component tests - Codebook CRUD — React island replaces vanilla JS codebook: drag-and-drop tags between groups, inline editing of group titles and subtitles, tag merge with confirmation, create/delete groups and tags, pentadic colour system, MicroBar frequency bars, ConfirmDialog for destructive actions. 9 API endpoints, 36 Python tests
- Dashboard island — Project tab as React composition: 8 clickable stat cards, compact sessions table, featured quotes with reshuffle, section/theme navigation with cross-tab anchor links. 43 API tests
- Data API — 6 fire-and-forget PUT endpoints sync researcher state (hidden, starred, tags, edits, people, deleted-badges) from localStorage to SQLite. 94 tests (37 happy-path + 57 stress)
- 22-table SQLAlchemy domain schema with instance-scoped people/codebook and project-scoped sessions/quotes/themes;
assigned_bytracks pipeline vs researcher authorship; idempotent upsert importer - Desktop app scaffold — SwiftUI macOS shell with folder picker, pipeline runner, sidecar architecture (self-contained in
desktop/) - Renderer overlay (dev-only, press D) — colour-codes report regions by origin: blue for Jinja2, green for React, amber for vanilla JS
- Visual diff page (dev-only) — side-by-side, overlay, and toggle comparison of Jinja2 vs React sessions table
0.9.3 — 13 Feb 2026
- Interactive dashboard — Project tab stat cards are clickable links to their target tabs (audio→Sessions, quotes→Quotes, sections/themes→Quotes anchors, AI tags→Analysis, user tags→Codebook); featured quote cards open video player or fall back to transcript; session table rows drill into Sessions tab; section/theme names switch to Quotes tab and scroll to the anchor
- New
--bn-colour-hoverdesign token withlight-dark()support - Speaker code lozenge attribution on featured quotes
- Reusable JS helpers (
scrollToAnchor,navigateToSession) and Python helpers (_timecode_html,_session_anchor) - Fix: logo dark/light swap on appearance toggle
0.9.2 — 12 Feb 2026
- Sessions table redesign — speaker badges with colour-coded IDs, user journey paths below start dates, video thumbnail placeholders (96×54px, 16:9), per-session sentiment sparkline mini-bar charts
- Appearance toggle — system/light/dark mode switcher in settings tab
- User journeys — derived from topic-segmentation screen clusters, shown in sessions table and sortable journeys table
- Time estimates — upfront pipeline duration estimate after ingest (
~8 min (±2 min)), recalculated remaining time after each stage, Welford's online algorithm for per-metric running stats, hardware+config keyed profiles persisted to~/.config/bristlenose/timing.json - Clickable logo — Bristlenose logo in report header navigates to project tab
- Fix:
llm_max_tokenstruncation causing silent 0-quote extraction — providers now detect truncation viastop_reason/finish_reasonand raiseRuntimeErrorwith actionable.envfix - Fix: sentiment sparkline bars now align with video thumbnail baseline (removed inline height override that capped sparkline container at 20px instead of the intended 54px)
0.9.1 — 11 Feb 2026
- Moderator and observer names shown in Project tab stats row (Oxford comma lists, observer box only when observers exist)
- Fix: clicking [+] to add a tag on a quote now tags that quote, not the previously-focused quote
0.9.0 — 11 Feb 2026
- Tab navigation — tabs remember their position across page reloads via URL hash (
#codebook,#analysis, etc.); browser back/forward navigates between tabs; deep-linkable tab URLs - Analysis tab — inline signal cards and heatmaps in the main report (previously a placeholder);
BRISTLENOSE_ANALYSISdata injected into the report's script block alongside the standaloneanalysis.html - Codebook tab — fixed empty grid caused by
_countQuotesPerTagfunction name collision betweencodebook.jsandtag-filter.jsin the concatenated JS bundle; codebook panel now renders correctly in the main report - Removed dead toolbar button handlers from
analysis.jsandcodebook.js(replaced by navigation tabs)
0.8.2 — 9 Feb 2026
- Transcript annotations — per-participant transcript pages now highlight which segments were selected as quotes, with margin labels showing sentiment, colour-coded span bars connecting multi-segment quotes, and a citation toggle to show/hide annotations; playback-synced glow on both transcript segments and report quote cards when video is playing
- Gemini provider —
--llm geminifor budget-conscious teams (~$0.20/study, 5–7× cheaper than Claude or ChatGPT);bristlenose configure geministores your key in the system credential store; interactive provider picker now includes Gemini as option [4] - Jinja2 template extraction — report renderer migrated from f-strings to Jinja2 templates (13 templates extracted across two phases); pure refactor, no output changes
- Platform-specific credential language — doctor and configure now show the actual store name: "Keychain" on macOS, "Secret Service" on Linux, instead of generic "credential store"
0.8.1 — 7 Feb 2026
- Hidden quotes — press
h(or click the eye-slash button) to hide volume quotes you want to keep as evidence but need out of your working view; per-subsection badge shows count with dropdown of truncated previews; click a preview to unhide with highlight animation; bulk hide via multi-select +h; hidden state persists in localStorage and survives search, tag filter, and view switching - Codebook — standalone
codebook.htmlpage (opens in a new window via toolbar button) with interactive panel for organising tags into groups; drag-and-drop reordering within and between groups; inline editing of group names; add/delete groups with confirmation; toggle AI tag visibility per-tag; colour-coded tag badges with 24-colour palette; shared data model across report and codebook via localStorage - Toolbar redesign — unified round-rect button styling with 3-state border progression (rest → hover → active); tag filter and view switcher use dual-class pattern for consistent appearance
- Python 3.14 compatibility —
check_piiin doctor now catchesException(not justImportError) to handle pydantic v1 crash when importing presidio on Python 3.14
0.8.0 — 7 Feb 2026
- Azure OpenAI provider —
--llm azurefor enterprise users with Microsoft Azure contracts; usesAsyncAzureOpenAIfrom the existing OpenAI SDK (no new dependencies); configure withbristlenose configure azureorAZURE_OPENAI_API_KEY+AZURE_OPENAI_ENDPOINTenv vars - Install smoke tests — new CI workflow (
install-test.yml) verifies install instructions work on clean Linux and macOS VMs; runsbristlenose doctorandbristlenose renderfrom pre-built fixtures with no API key needed; weekly full-pipeline run with real API key catches integration regressions
0.7.1 — 6 Feb 2026
- Bar chart alignment — sentiment and user-tag charts use CSS grid so bar left edges align within each chart; labels hug text with variable gap to bars
- Histogram delete — hover × on user tag labels in the histogram to remove that tag from all quotes (with confirmation modal)
- Surprise placement — surprise sentiment bar now renders between positive and negative sentiments
- Quote exclusivity in themes — each quote assigned to exactly one theme (pick strongest fit)
0.7.0 — 5 Feb 2026
- Multi-select — Finder-like click selection (click, Shift-click, Cmd/Ctrl-click) with bulk starring (
skey) and bulk tagging; selection count shown in view-switcher label; CSV export respects selection - Tag filter — toolbar dropdown between search and view-switcher filters quotes by user tags; checkboxes per tag with "(No tags)" for untagged quotes; per-tag quote counts, search-within-filter for large tag lists, dropdown chevron, ellipsis truncation for long names
0.6.15 — 4 Feb 2026
- Unified tag close buttons — AI badges and user tags now use the same floating circle "×" style
- Tab-to-continue tagging — pressing Tab commits the current tag and immediately opens a new input for adding another tag (type, Tab, type, Tab, Enter for fast keyboard-only tagging)
- Render command path fix —
bristlenose render <input-dir>now auto-detectsbristlenose-output/inside the input directory
0.6.14 — 4 Feb 2026
- Doctor fixes — improved Whisper model detection and PII capability checking
0.6.13 — 3 Feb 2026
- Keychain credential storage —
bristlenose configure claude(orchatgpt) validates and stores API keys securely in macOS Keychain or Linux Secret Service; keys are loaded automatically with priority keychain → env var → .env;bristlenose doctornow shows "(Keychain)" suffix when key comes from system credential store;--keyoption available for non-interactive use
0.6.12 — 3 Feb 2026
- File-level transcription progress — spinner now shows "(2/4 sessions)" during transcription
- Improved Ollama start command detection — uses
brew services start ollamafor Homebrew installs,open -a Ollamafor macOS app, platform-appropriate commands for snap/systemd - Doctor displays "(MLX)" accelerator — when mlx-whisper is installed on Apple Silicon, doctor now shows "(MLX)" instead of "(CPU)"
- Whisper model line fits 80 columns — shortened to "~1.5 GB download on first run"
- Provider header fix — pipeline header now shows "Local (Ollama)" instead of "ChatGPT" when using local provider
- Improved fix messages — doctor fix messages now use
pipx injectfor pipx installs, proper Homebrew Python path for brew installs (PEP 668 compliance) - Retry logic catches ValidationError — local model retries now also handle Pydantic schema validation failures, not just JSON parse errors
0.6.11 — 3 Feb 2026
- Local AI support via Ollama — run bristlenose without an API key using local models like Llama 3.2; interactive first-run prompt offers Local/Claude/ChatGPT choice
- Automated Ollama installation — offers to install Ollama automatically (Homebrew on macOS, snap on Linux, curl script fallback); falls back to download page if installation fails
- Auto-start Ollama — if installed but not running, bristlenose will start it for you
- Provider registry — centralised
bristlenose/providers.pywithProviderSpecdataclass, alias resolution (claude→anthropic, chatgpt→openai, ollama→local) - Ollama integration —
bristlenose/ollama.pywith status checking, model detection, and auto-pull with consent - Retry logic for local models — 3 retries with exponential backoff for JSON parsing failures (~85% reliability vs ~99% for cloud)
- Smart cloud fallback hints — fix messages for Ollama issues now check which API keys you have and only suggest providers you can actually use
- Doctor integration for local provider — shows "Local (llama3.2:3b via Ollama)" status, helpful fix messages for Ollama not running or model missing
0.6.10 — 3 Feb 2026
- Output directory inside input folder —
bristlenose run interviews/now createsinterviews/bristlenose-output/to avoid collisions when processing multiple projects - New directory structure —
assets/for static files,sessions/for transcript pages,transcripts-raw//transcripts-cooked/for transcripts,.bristlenose/for internal files - Report filenames include project name —
bristlenose-{slug}-report.htmlso multiple reports in Downloads are distinguishable - Coverage link fix — player.js no longer intercepts non-player timecode links
- Anchor highlight — transcript page segments flash yellow when arriving via anchor link
0.6.9 — 3 Feb 2026
- Transcript coverage section — collapsible section at the end of the report showing what % of the transcript made it into quotes (X% in report · Y% moderator · Z% omitted), with expandable omitted content per session
- Transcript page fix — pages now render correctly when PII redaction is off (was failing with assertion error)
0.6.8 — 3 Feb 2026
- Multi-participant session support — sessions with multiple interviewees get globally-numbered participant codes (p1–p11 across sessions); report header shows correct participant count
- Sessions table — restructured from per-participant rows to per-session rows with a Speakers column showing all speaker codes (m1, p1, p2, o1) per session
- Transcript page format — heading shows
Session N: m1 Name, p5 Name, o1; segment labels show raw codes for consistency with the anonymisation boundary - Session duration — now derived from transcript timestamps for VTT-only sessions (previously showed "—")
- Moderator identification (Phase 1) — per-session speaker codes (
[m1]/[p1]) in transcript files, moderator entries inpeople.yaml,.segment-moderatorCSS class for muted moderator styling
0.6.7 — 2 Feb 2026
- Search enhancements — clear button (×) inside the search input, yellow highlight markers on matching text, match count shown in view-switcher label ("7 matching quotes"), ToC and Participants hidden during search, CSV export respects search filter
- Pipeline warnings — clean dim-yellow warning lines when LLM stages fail (e.g. credit balance too low), with direct billing URL for Claude/ChatGPT; deduplication and 74-char truncation
- CLI polish — "Bristlenose" in regular weight in the header line, "Report:" label in regular weight in the summary
0.6.6 — 2 Feb 2026
- Cargo/uv-style CLI output — clean
✓checkmark lines with per-stage timing, replacing garbled Rich spinner output; dim header (version · sessions · provider · hardware), LLM token usage with cost estimate, OSC 8 terminal hyperlinks for report path; output capped at 80 columns; all tqdm/HuggingFace progress bars suppressed - Search-as-you-type quote filtering — collapsible magnifying glass icon in the toolbar; filters by quote text, speaker, and tag content; overrides view mode during search; hides empty sections/subsections; 150ms debounce
- Platform-aware session grouping — Teams, Zoom cloud, Zoom local, and Google Meet naming conventions recognised automatically; two-pass grouping (Zoom folders by directory, remaining files by normalised stem); audio extraction skipped when a platform transcript is present
- Man page — full troff man page (
man bristlenose); bundled in the wheel and self-installs to~/.local/share/man/man1/for pip/pipx users on first run; wired into snap, CI version gate, and GitHub Release assets - Page footer — "Bristlenose version X.Y.Z" colophon linking to the GitHub repo on every generated page
0.6.5 — 2 Feb 2026
- Timecode typography — two-tone treatment with blue digits and muted grey brackets;
:visitedcolour fix so clicked timecodes stay blue - Hanging-indent layout — timecodes sit in a left gutter column on both report quotes and transcript pages, making them scannable as a vertical column
- Non-breaking spaces on quote attributions prevent the
— p1from widowing onto a new line - Transcript name propagation — name edits made in the report's participant table now appear on transcript page headings and speaker labels via shared localStorage
0.6.4 — 1 Feb 2026
- Concurrent LLM calls — per-participant stages (speaker identification, topic segmentation, quote extraction) now run up to 3 API calls in parallel via
llm_concurrencyconfig; screen clustering and thematic grouping also run concurrently; ~2.7× speedup on the LLM-bound portion for multi-participant studies
0.6.3 — 1 Feb 2026
- Report header redesign — logo top-left (flipped horizontally), "Bristlenose" logotype with project name, right-aligned document title and session metadata
- View-switcher dropdown — borderless menu to switch between All quotes, Favourite quotes, and Participant data views; replaces old button-bar pattern
- Copy CSV button with clipboard icon — single adaptive button that exports all or favourites based on the current view
- Quote attributions use raw participant IDs (
— p1) in the report for anonymisation; transcript pages continue to show display names - Table of Contents reorganised — Sentiment, Tags, Friction points, and User journeys moved to a dedicated "Analysis" column, separate from Themes
0.6.2 — 1 Feb 2026
- Editable participant names — pencil icon on Name and Role cells in the participant table; inline editing with localStorage persistence; YAML clipboard export for writing back to
people.yaml; reconciliation with baked-in data on re-render - Auto name and role extraction — Stage 5b LLM prompt now extracts participant names and job titles alongside speaker role identification; speaker label metadata harvested from Teams/DOCX/VTT sources; empty
people.yamlfields auto-populated (LLM results take priority over metadata, human edits never overwritten) - Short name suggestion —
short_nameauto-suggested from first token offull_namewith disambiguation for collisions ("Sarah J." vs "Sarah K."); works both in the pipeline and in-browser - Editable section and theme headings — inline editing on section titles, descriptions, theme titles, and theme descriptions with bidirectional Table of Contents sync
0.6.1 — 1 Feb 2026
- Snap packaging for Linux —
snap/snapcraft.yamlrecipe and CI workflow (.github/workflows/snap.yml); builds on every push to main, publishes to edge/stable when Store registration completes - Pre-release snap testing instructions in README for early feedback on amd64 Linux
- Author identity (Martin Storey) added to copyright headers, metadata, and legal files
0.6.0 — 1 Feb 2026
bristlenose doctorcommand — checks FFmpeg, transcription backend, Whisper model cache, API key validity, network, PII dependencies, and disk space- Pre-flight gate on
run,transcribe-only, andanalyze— catches missing dependencies before slow work starts - First-run auto-doctor — runs automatically on first invocation, guides users through setup
- Install-method-aware fix messages — detects snap, Homebrew, or pip and shows tailored install instructions
- API key validation — cheap API call catches expired or revoked keys upfront
0.5.0 — 1 Feb 2026
- Per-participant transcript pages — full transcript for each participant with clickable timecodes and video player; participant IDs in the table link to these pages
- Quote attribution deep-links — clicking
— p1at the end of a quote jumps to the exact segment in the participant's transcript page - Segment anchors on transcript pages for deep linking from quotes and external tools
0.4.1 — 31 Jan 2026
- People file (
people.yaml) — participant registry with computed stats (words, % words, % speaking time) and human-editable fields (name, role, persona, notes); preserved across re-runs - Display names — set
short_nameinpeople.yaml, re-render withbristlenose renderto update quotes and tables - Enriched participant table in reports (ID, Name, Role, Start, Duration, Words, Source) with macOS Finder-style relative dates
- PII redaction now off by default; opt in with
--redact-pii(replaces--no-pii) - Man page updated for new CLI flags and output structure
0.4.0 — 31 Jan 2026
- Dark mode — report follows OS/browser preference automatically via CSS
light-dark()function - Override with
color_scheme = "dark"(or"light") inbristlenose.tomlorBRISTLENOSE_COLOR_SCHEMEenv var - Dark-mode logo variant (placeholder; proper albino bristlenose pleco coming soon)
- Print always uses light mode
- Replaced hard-coded colours in histogram JS with CSS custom properties
0.3.8 — 31 Jan 2026
- Timecode handling audit: verified full pipeline copes with sessions shorter and longer than one hour (mixed
MM:SSandHH:MM:SSin the same file round-trips correctly) - Edge-case tests for timecode formatting at the 1-hour boundary, sub-minute sessions, long sessions (24h+), and format→parse round-trips
0.3.7 — 31 Jan 2026
- Markdown style template (
bristlenose/utils/markdown.py) — single source of truth for all markdown/txt formatting constants and formatter functions - Per-session
.mdtranscripts alongside.txtinraw_transcripts/andcooked_transcripts/ - Participant codes in transcript segments (
[p1]instead of[PARTICIPANT]) for better researcher context when copying quotes - Transcript parser accepts both
MM:SSandHH:MM:SStimecodes
0.3.6 — 31 Jan 2026
- Document full CI/CD pipeline topology, secrets, and cross-repo setup
0.3.5 — 31 Jan 2026
- Automated Homebrew tap updates and GitHub Releases on every tagged release
0.3.4 — 31 Jan 2026
- Participants table: renamed columns (ID→Session, Session date→Date), added Start time column, date format now dd-mm-yyyy
0.3.3 — 31 Jan 2026
- README rewrite: install moved up, new quick start section, changelog with all versions, dev setup leads with git clone
- Links to Anthropic and OpenAI API key pages in install instructions
0.3.2 — 30 Jan 2026
- Fix tag auto-suggest offering tags the quote already has
- Project logo in report header
0.3.1 — 30 Jan 2026
- Single-source version:
__init__.pyis the only place to bump - Updated release process in CONTRIBUTING.md
0.3.0 — 30 Jan 2026
- CI on every push/PR (ruff, mypy, pytest)
- Automated PyPI publishing on tagged releases (OIDC trusted publishing)
0.2.0 — 30 Jan 2026
- Tag system: AI-generated badges (deletable/restorable) + user tags with auto-suggest and keyboard navigation
- Favourite quotes with reorder animation and CSV export (separate AI/User tag columns)
- Inline quote editing with localStorage persistence
- Sentiment histogram (side-by-side AI + user-tag charts)
bristlenose rendercommand for re-rendering without LLM calls- Report JS extracted into 8 standalone modules under
bristlenose/theme/js/ - Atomic CSS design system (
bristlenose/theme/)
0.1.0 — 30 Jan 2026
- 12-stage pipeline: ingest, extract audio, parse subtitles/docx, transcribe (Whisper), identify speakers, merge, PII redaction (Presidio), topic segmentation, quote extraction, screen clustering, thematic grouping, render
- HTML report with clickable timecodes and popout video player
- Quote enrichment: intent, emotion, intensity, journey stage
- Friction points and user journey summaries
- Apple Silicon GPU acceleration (MLX), CUDA support, CPU fallback
- PII redaction with Presidio
- Cross-platform (macOS, Linux, Windows)
- Published to PyPI and Homebrew tap
- AGPL-3.0 licence with CLA