Enhanciar Features ← back to app

Everything Enhanciar can do

The living catalog of every feature and service in the product — the single source of truth for demos, sales calls and onboarding. If it shipped, it's on this page.

Last updated: 2026-08-08
Process rule: every shipped feature or fix MUST add a row/entry here. Update the feature matrix and append a changelog entry in the same change that ships it.
No features match — try a different search or clear the filters.

Changelog / Fixes

Reverse-chronological. Every ship and every fix lands here, newest first.

2026-10-04

Pricing, changelog, role pages and comparison pages; Slack bot and actions on by default

  • New public pages. /pricing (Solo $19, Team $80 billed yearly, your own AI key explained in plain words, a per-question cost estimate with its assumption stated, FAQ), /changelog (built from this page’s changelog at deploy time), five role pages under /for/ (sales, support, HR & ops, founders, new hires) with Slack-thread mockups, and honest comparisons at /vs/slackbot and /alternatives/opentag-alternatives. All are in the sitemap, and the feature catalog (/features) is served again as the changelog’s source.
  • No more made-up waitlist pop-ups. The “someone just joined” toast now appears only for real joins from the last 7 days, with their real age; with no recent joins it shows nothing. Times are never randomised.
  • Purple removed from the sub-pages. Every static page (docs, about, security, legal, guides, comparisons, benchmarks) now uses the site’s cream/ink theme, fonts and node logo; theme-color is cream on every page, and the landing page’s leftover violet accent, terminal prompt and checkmark are teal/terracotta.
  • Every-team wording. llms.txt, llms-full.txt, /about, /vs/glean and the landing meta keywords now say “company brain for every team” with engineering as one use case; llms files list USD pricing, every page, and a “Please note” section of things that are not true yet (waitlist only, no SOC 2, no Teams, no CRM/HRIS connectors). New /agents.json.
  • Slack bot and actions default ON. slack_bot and actions feature flags now default to on. Actions stay approval-first (approve needs an active plan; MCP can only propose). Slack installs without the app_mentions:read scope just never receive mention events, so nothing errors. Kill switch and ENHANCIAR_FF_FORCE_*=0 still turn either off.
  • Fixes. The benchmarks page linked to a private GitHub repo (404); it now offers the eval script on request. Static pages and the landing footer link to Pricing, For teams and Changelog; /#waitlist on the homepage opens the waitlist form.
2026-10-04

Landing: track waitlist modal opens

  • waitlist_open event — every "Join the waitlist" CTA now fires waitlist_open (GA + dh-track) when the modal opens, so the funnel reads page_view → waitlist_open → waitlist_click → waitlist_join. Before, a visitor who opened the form and left was invisible, so 0 events couldn't distinguish "nobody clicked" from "clicked and bounced".
2026-10-01

New social share image in the live site theme

  • Change: link previews (X, LinkedIn, Slack) showed the old violet “company brain that shows its receipts” card. New 1200×630 /og-image-v2.png in the cream/black theme: “Ask your company anything.”, every-role question cards, logo and enhanciar.in pill. Rendered by brand/social/og_image/render_og.py (Playwright, fonts-loaded check).
  • New filename so platform caches refetch. og:image, twitter:image and JSON-LD image updated in landing.html and all 21 static pages in public/, which also gained og:image:width/height/alt and twitter:image:alt. The old /og-image.png is kept for existing links.
2026-10-01

Removed the Fazier badge from the landing footers

  • Change: took the "Launched on Fazier" badge off PersonalityPage.jsx, App.jsx and the crawler footer in landing.html, at the founder's request.
2026-10-01

Admin: promotion tracker and a dashboard that refreshes itself

  • One registry of everything posted. docs/business/promotion_posts.json, seeded with 266 records from the old log, transcripts, live YouTube/Dev.to/GitHub listings, the sitemap, the outreach log and the scheduled tasks. Deleted, scheduled, pending and blocked items are kept with their status.
  • New tabs on the Promotion page: Platforms, Posts (filter + add/edit) and Dashboard (charts over time, top posts, what's working). The old tabs live under Engagement log, unchanged.
  • Daily refresh (admin/promotion_refresh.py, 9:00 AM scheduled task) appends snapshots to docs/business/promotion_metrics.json. Login-only platforms come in via a manual-metrics JSON written by the browser check.
2026-10-01

Fix: homepage hero no longer blank for seconds after load

  • Fix: the scroll-reveal treated the hero like any other section: it started at opacity 0 with its fadeUp animations paused until an IntersectionObserver fired, then ran a .7s fade plus a further .1–.2s delayed fadeUp. On a busy main thread the observer callback landed late, so visitors saw only the header on a cream page. The hero (the LCP) is now exempt from the reveal gate, like the header, and its fadeUp is removed (it is drawn already visible on the first React frame; the first version kept a .35s fade but the h1 still sat at opacity 0 for ~0.6s while the mount task blocked the main thread). CSS-only change in personality.css; PersonalityPage.jsx and the design file are unchanged.
2026-10-01

Fix: live homepage hero now shows the company-brain headline

  • Fix: the Sep 30 repositioning only edited the old landing (App.jsx). The live page renders PersonalityPage.jsx, so it still said "Know why your code is the way it is." The hero now reads "Ask your company anything." with the eyebrow "Company brain · Cited answers · For every team" and every-role subcopy. The og:image alt text was updated as well. Note: PersonalityPage.jsx is generated from the Claude Design file, so the design file needs the same change, or a regeneration will bring the old headline back.
2026-09-30

Public support page at /support

  • New: /support, needed for the Slack Marketplace listing. Contact email enhanciar@gmail.com with a promised reply within 1 business day, links to docs, security, privacy, terms and refunds, and a plain-English FAQ (connecting Slack/Google, BYO AI key, data deletion, cancelling, where answers come from) with FAQPage JSON-LD. Added to the sitemap and linked from the landing footers (App.jsx, PersonalityPage.jsx, crawler footer in landing.html) and the static page footers.
2026-09-30

SEO: Dashworks, Glean and Guru alternatives pages

2026-09-30

MCP get_page no longer crashes on loosely-shaped arguments

  • Fixed (Sentry ENHANCIAR-BACKEND-15): MCP clients calling get_page without both category and name hit a pydantic ToolError. The tool now accepts path (“concepts/my-page”), slug, page and title aliases, coerces non-string values, finds the category itself when only a slug is given, and returns a clear {error} instead of throwing.
2026-09-30

Homepage repositioned as a company brain for every team

  • Changed: the hero headline, page title, meta description and social (OG/Twitter) tags now lead with “Ask your company anything. Get the answer and where it came from.” instead of the code-only “Know why your code is the way it is”, matching the company-wide product (Slack, email, docs, tickets, code and live data).
2026-09-30

Fazier launch badge in the footer

  • New: a small “Launched on Fazier” badge in the landing footer (both landing variants and the crawler-visible static footer), required for Enhanciar's free Fazier directory listing.
2026-09-29

Alternatives and comparison pages

  • New: /alternatives/unblocked-alternatives, /alternatives/swimm-alternatives and /compare/best-tools-to-understand-a-codebase. Each has a comparison table (what it does, where it answers, PR citations, pricing, self-hosting), an honest “when to pick the other tool” section, setup steps, an FAQ citing real HN threads, Enhanciar's public pricing, and Article + FAQPage JSON-LD.
  • Sourced: competitor facts link to the vendor's own pricing or docs page and are dated September 2026. No claim of citation re-verification.
  • Linked: from all four guides' Related sections and the crawler-visible landing footer; added to the sitemap, llms.txt and llms-full.txt.
2026-09-29

Three more search-intent guides

2026-09-28

CI’s secret scan had been red on every push since at least 2026-09-24 — three false positives, no real key

  • The symptom. The secrets job (gitleaks over full history) failed on three historical findings: two generic-api-key hits in scripts/run_retrieval_eval.py and PLAN_GRAPH_MEMORY_IMPL.md (commit 99751f1), and a private-key hit in devhive/tools/repo_tools.py (a5d0cc1).
  • None is a real credential. The first two are the disposable CI-stub Fernet key — byte-identical to the one ci.yml already sets. The third is the ingest secret-guard’s own matcher constant (the literal PEM header it refuses to ingest). Nothing to rotate.
  • Why the existing suppressions stopped working. The two stub-key fingerprints were already in .gitleaks-baseline.json, but that baseline was written with --redact, so each entry stores Secret: REDACTED; gitleaks compares baseline entries on the whole finding, so they never matched anything. The PEM constant was covered by a path allowlist for enhanciar/tools/repo_tools.py, which stopped covering the historical commit once the package was renamed from devhive/.
  • The fix. A new .gitleaksignore lists the exact fingerprints (commit:path:rule:line), each with a written justification — no path globs, no rule disabled. It also pins a fake api.acme.io curl example in mock data that only gitleaks ≥ 8.22’s curl-auth-header rule flags, so bumping the pinned 8.21.2 does not re-break the build. Verified locally: zero findings on both 8.21.2 and 8.30.1.
2026-09-28

Google read “enhanciar” as a typo, and the homepage said three different things

  • The symptom. Searching “enhanciar” on Google returned results for “enhancer”. Search Console: 6 pages indexed, 5 more “crawled – currently not indexed”, 9 impressions and 0 clicks in 28 days. Crawling was never the problem: robots.txt, the sitemap and the meta tags were already correct.
  • One message instead of three. The homepage <title> said “the company brain that shows its receipts”, the H1 said “Know what breaks before you decide” and every post said “why the code is the way it is”. Title, OG and Twitter titles now read “the company brain that knows why your code is the way it is”. The hero H1 is “Know why your code is the way it is.” in both the live React hero and the crawler-visible fallback, which must match or it reads as cloaking. The animated “breaks” word treatment went with the old headline.
  • The LinkedIn page is now in Organization.sameAs, next to X, GitHub and the MCP registries. sameAs is the list of profiles Google uses to tie the name “Enhanciar” to this site.
  • First search-intent page: /guides/why-is-this-code-like-this. It is a real how-to (blame with -w -C -C, git log -L and -S, commit → PR, review threads, what to do when nothing was written down) with Article and FAQPage JSON-LD. Enhanciar appears once, at the end, as the automated version of the same search. It is linked from the sitemap, the crawler-visible footer and llms.txt.
  • Still to do outside the code: request indexing for the 5 crawled-not-indexed URLs in Search Console, and build links from other sites (dev.to/Hashnode posts, Product Hunt, directories) so Google stops treating the brand name as a misspelling.
2026-09-26

The promotion record was in three places and joined in none of them

  • New admin page: Promotion. Every comment and post made while promoting the product now has one row — date, platform, which account, that post’s engagement, who we commented as, the snippet we actually wrote, and a link. Seeded with the 39 comments and 9 posts from 23–26 Sep. Four tabs: Comments (sort by newest, or by biggest target), Posts (plus a “which format earns impressions” roll-up), Analytics (latest snapshot with day-over-day deltas, full history, chartable metrics) and Impact (daily activity lined up against the GA4 numbers). Filters for time range, platform and a free-text search over snippets and accounts.
  • Why it exists. The record was split across a markdown tracker (prose — good for the why), three browser tabs (LinkedIn page analytics, GA4, the waitlist count) and nothing that joined them. The question that needed all three at once — “we commented on a 2,000-reaction post last Thursday; did anything happen?” — had no surface.
  • Three things the page refuses to overstate. (1) Engagement is the target post’s, never a claim about our reply’s reach — it is the number worth filtering on when picking tomorrow’s targets, and the caption says so under every table. (2) An unmeasured post renders not measured, not 0: LinkedIn has no page-analytics API on a personal-admin page and X’s is paywalled, so impressions are typed in by hand and a blank must not read as a measurement. (3) Impact is labelled correlation, not attribution on the page itself — GA4 reports social referrals as t.co / referral or Direct, so no session can be traced back to a comment.
  • Snapshots merge rather than replace. The GA4 read and the waitlist read happen at different moments, so saving a second partial snapshot for a date that already has one overwrites only the fields actually filled in. A blank never blanks an earlier value.
  • The data is version-controlled even though the panel is not. admin/ is gitignored (local-only tooling), so the page itself never ships — but the log lives in docs/business/promotion_log.json next to PROMOTION_TRACKER.md, sorted newest-first on every write so the git diff of a week’s promotion reads the same as the page. A fresh clone keeps the entire history and only has to re-create the viewer.
  • What it surfaced immediately. 626 impressions across 5 measured posts and 0 clicks — the page raises that as a warning rather than leaving it to be noticed — and a waitlist flat at 4 across every snapshot while 39 comments and 9 posts shipped. Reach is not the bottleneck; the click is.
2026-09-22

Positioning: dropped the team-size framing

  • “5–50 person engineering teams” removed from /vs/glean. The comparison page’s meta description, OG description, lede and the “Built for” row all pinned Enhanciar to teams of roughly five to fifty people, and one bullet opened with “Your team is small”. Nothing in the product depends on team size — ingest, the knowledge graph, citation verification and MCP work the same at any headcount — so the copy now reads “engineering teams of any size” / “Engineering teams, any size”. The rest of the comparison (when Glean is the better fit, the sourced side-by-side table) is unchanged.
2026-09-21

Prod deploys were landing on an orphan service; MCP GET streams were burning instance time

  • Deploy target fixed. The devhive→enhanciar rename (2026-09-20) also changed deploy.yml to deploy main to a new Cloud Run service named enhanciar. Nothing routed to it (firebase.json rewrites and the Cloud Scheduler jobs all point at devhive) and it carried 5 env vars and no secrets. Real prod stayed frozen on the pre-rename image while its env vars had already been renamed to ENHANCIAR_* — old code reads DEVHIVE_*, so the nightly freshness-probe / freshness-sweep / connector-sync jobs 503’d (“Internal worker auth is not configured”) and none of the 2026-09-20 backend fixes were live. main now deploys to devhive again (service name kept on purpose); the orphan service is deleted.
  • MCP GET stream capped. Registry crawlers open the optional server→client SSE listener and never close it; Cloud Run held each one for the full 1800 s request timeout (208 streams, 35 h of open connections in two days — billed instance time). Anonymous GET now returns 405 (the spec’s “no GET stream offered”), authenticated GET is closed after 120 s; real clients reconnect transparently.
2026-09-17

Ask a teammate

  • The brain names who can answer. On an “I don’t know” (no citations, the answer reads as a miss) the stream now carries an ask_teammate chip instead of the generic ingest nudge, when a person resolves. Resolution reads the pages retrieval almost matched: their file_paths against the ingest-time authors index (_authors_index.json), the GitHub commits API for any file that changed since the ingest (installation token, commit author so squash-merges credit the writer, cached an hour and invalidated by the next push), connector pages’ own authors, and finally whoever ingested that source (every job records it). Everything passes through the people registry, so an opted-out person never surfaces; the asker is never suggested to themselves. New module enhanciar/ask_teammate.py.
  • While you type, too. The composer runs a debounced /api/ask/suggest (keyword retrieval + the index, no model, always 200) and shows “might know this — Ask Priya →” in the same slot as the scheduling assistant.
  • Sending is an action. The card shows the exact message and a channel picker; POST /api/ask/teammate files a slack.ask_teammate proposal, approves it (the click is the consent) and posts it as an in-channel @mention — DMs would need new OAuth scopes on every install. The Slack ts is recorded on the ask (users/{ws}/asks plus a flat ask_threads pointer) so the reply can be found. Hard politeness caps: three asks per person per week, none within a day of the last.
  • The reply comes back and gets written down. The Slack events webhook maps a human reply in that thread to the ask, stores it with a permalink (“skip” releases it), and files it into the wiki at once: a people-answers/<slug> page with the reply verbatim, and a “From the team” section appended to the page the question was nearest to, cited to the message and marked re-verified. The chat polls while an ask is waiting and drops the answer in as a turn. The bot thanks them in-thread with where it was filed.
  • Fixed on the way: the authors index was rewritten wholesale per ingest, so the second repo in a workspace erased the first’s authorship. It is keyed by repo and merged now; readers flatten it.
  • Landing page: the chat that notices. The hero fan is recast around the differentiators — Ask a teammate (animated), Impact analysis, Cited answers, Permissions, Actions — with the Sources and MCP cards dropped. A new section after “One brain”, It notices what you’re about to need, replays the composer’s chips on the shared clock: scheduling (free/busy, one-tap Book), live data (connect the database found in your code, then the table), one clarifying question (which database?), and the “might know this” teammate suggestion. The Ask-a-teammate capability block moves above code review.
  • Three pages the searches actually want. The SXO audit showed the homepage cannot rank for “MCP server for company knowledge” (SERP is docs and directories), “Glean alternative” (comparison pages; the word Glean appeared nowhere on the site) or E-E-A-T queries (no author or contact surface). New: /mcp (endpoint, auth, Claude Code / Cursor / Claude Desktop set-up, the twelve tools, what it can never do), /about (founder, principles, contact) and /vs/glean (an honest comparison that says when Glean is the better fit, plus a line on Unblocked). All three use the public-page template, are in the sitemap, the landing footer, the nav of every public page and llms.txt.
  • Landing page: no auth SDK for strangers, analytics after paint. Lighthouse mobile had 640 ms of blocking time and the two biggest culprits had nothing to do with the page: the Firebase Auth SDK plus the apis.google.com iframe loaded on every visit only to check whether the visitor was already signed in (so they could be bounced to /app), and GA4’s 150 KB library booted before first paint. The signed-in check now runs only when the browser carries the dh_authed marker from a previous sign-in — a first-time visitor never downloads Firebase — and gtag is queued at t=0 but fetched on idle or first interaction, so no event is lost. Verified locally: fresh visitor makes zero Firebase/gapi requests; a returning user still gets the redirect.
  • Sentry: unauthenticated MCP calls are not incidents. A directory’s “try it” button called list_pages with no bearer token; the server answered with the intended “Unauthenticated, pass Authorization…” error and Sentry paged anyway. The before_send filter now also drops ToolErrors whose message is one of ours about auth or membership (unauthenticated, invalid or revoked key, not a member, refused). A tool that fails for any other reason still reports.
  • Waitlist joins know which platform sent them. Every link we post carries utm_source/medium/campaign; GA4 already read them for page views, but a join stored only document.referrer, which X, LinkedIn and Slack blank out. The landing now keeps the first utm_* seen in sessionStorage (dh-track), the waitlist modal forwards them with the join, waitlist.join stores them, and the admin Waitlist table shows utm_source/utm_medium per entry — so “12 joins from HN, 3 from X” is a column, not a guess.
  • Brand mark everywhere Google and browsers look. The favicon, touch icon and Organization.logo were still the old blue “E” tile — and the logo URL sat under the robots-blocked /enhanciar/ prefix, so Google could not fetch it and showed the bare domain as the site name. All icons are regenerated from the brain-nodes mark (favicon.svg/.ico, 16/32, apple-touch-icon, new icon-512.png), the logo points at the root, and WebSite.alternateName is set so the site-name algorithm has every signal it asks for.
  • SEO audit fixes (six-specialist pass, 2026-09-20). The sitemap listed four URLs under /enhanciar/ — a prefix robots.txt disallows — plus one that 301s and the internal changelog; it now lists the eight public root URLs only. /benchmarks declared a canonical of the old /enhanciar/benchmarks.html and linked to the changelog and /enhanciar/api-docs; fixed. The <title>/OG title (“The Company Brain for developers and teams”) now matches the rest of the site (“the company brain that shows its receipts”). JSON-LD: Organization.sameAs filled (X, GitHub, Glama, registry); Solo/Team offers carry availability: PreOrder since sign-up is waitlisted; the two Early Access FAQ answers no longer assert a fixed rupee price, an instant unlock or a lifetime discount — they say what the page says (priority invite, credit, 10% first year, refundable). Firebase Hosting now sends the same security headers Cloud Run already did (nosniff, Referrer-Policy, X-Frame-Options, HSTS with preload). The crawler-visible footer links every public page (before, only privacy/terms), and /llms-full.txt ships alongside /llms.txt. Left as findings, not changes: mobile TBT 640 ms from the Firebase Auth iframe + GA4 loading eagerly on the landing; missing “Glean alternative” / “MCP server” / impact-analysis pages; no founder/about page; www. does not resolve.
  • Link previews had no image. Every share of enhanciar.in on X, LinkedIn or Slack rendered a blank card: the og:image/twitter:image pointed at /enhanciar/assets/…, which robots.txt disallows (the whole app prefix is blocked so login pages don’t get indexed), and link-preview bots honour robots.txt for images. The card image now lives at the root (/og-image.png, regenerated 1200×630 with the current line — “the company brain that shows its receipts” — instead of the old “knowledge mesh” copy), and robots.txt explicitly allows /enhanciar/assets/ so older shares resolve too.
  • Glama connector claimed. Glama indexed the registry listing (grade A, 4.4/5, 12 tools). Ownership is proven over HTTP: /.well-known/glama.json is served from Cloud Run alongside the registry proof, which unlocks the logo, health-check history and analytics on the listing.
  • devhive → Enhanciar, in the code. The backend package is now enhanciar/ (was devhive/): every import, test, the Dockerfile, CI and the admin panel follow. Environment variables are ENHANCIAR_*; the package mirrors DEVHIVE_* ↔ ENHANCIAR_* at import so older deploy configs and .env files keep working, and both Cloud Run services were re-pointed to the new names. Logger names, log lines and docs say Enhanciar. Deliberately unchanged: the internal route prefix /devhive/api (the public alias is /enhanciar/api; Slack, GitHub and OAuth callbacks are registered against it), the Cloud Run service names and the GCP project id.
  • Sentry noise, three sources silenced. (1) MCP protocol errors — a registry crawler probing a tool that does not exist (__verifymcp_auth_probe_…__), bad params, stale session ids — are the client’s mistake answered as a clean JSON-RPC error, yet the SDK’s MCP integration reported them as unhandled and paged the minute the server was published; a before_send filter drops ToolError/McpError of that shape while real faults inside a tool body still report. (2) decrypt() logged at ERROR on every read of a field encrypted under a key that no longer exists (one stale row = 300+ events, since the model picker reads all keys on each page load); it now warns once per ciphertext and the UI’s _unreadable state carries the rest. The two stale fields on the affected account were cleared. (3) Ask-a-Teammate returned 502 when the bot was not in the chosen Slack channel; that is the asker’s to fix (the sheet says so) and is now a 409, so 5xx alerts mean an actual outage.
  • Official MCP Registry listing. Enhanciar is published to registry.modelcontextprotocol.io as in.enhanciar/enhanciar (a remote streamable-HTTP server with a required Authorization header) so it surfaces in Glama, PulseMCP and every client that browses the registry. Namespace ownership is proven over HTTP: a signed proof file is served at /.well-known/mcp-registry-auth from Cloud Run — Firebase Hosting drops dotfolders from its upload, so firebase.json now rewrites /.well-known/** to the API, which also un-404s the Apple Pay domain file. The private key never touches the repo; mcp-registry/server.json is the source of truth for the listing.
  • Landing on phones, fixed section by section. Hero fan cards grow to their content instead of clipping the Ask-a-teammate flow; marquee pills stay single-line; the problem section’s scattered pills wrap instead of spilling off a 560px canvas; two-ended rows (label · meta) wrap with a gap instead of colliding — the old selector matched justify-content:space-between without the space React serialises, so it never fired; chip rows wrap whole chips rather than breaking a chip’s words; grids nested inside cards stay two-up; the graph card scales its 800×520 drawing and drops the pixel-placed labels, with its stat rows in normal flow. Follow-up: the marquee pills were still tall ovals with the text at the top on real phones — the browser rewrites the inline animation shorthand (so the selector never matched) and the fluid-width rule caught the track via its font-size: 13px, squeezing it to the viewport; both fixed, pills are single-line and centred. The wiki card’s sidebar + page grid stacks instead of being forced two-up.
  • The login screen speaks the landing page’s language. Cream ground, Inclusive Sans headline with the highlight in teal instead of a violet gradient, mono labels and fields, an ink pill for Create account and an outlined pill for Google, and a tinted right column with the active step outlined in ink. Scoped to .signup-screen (theme-personality.css), so the signed-in app keeps its own theme.
  • The scheduling assistant knows the same people. “Schedule a meet tomorrow with pushkarxoxo” now resolves the name through the people registry (git authors, Slack posters) as well as connected members: they appear as an invite chip and get the calendar invite by email, without an availability check. A day with no time (“tomorrow”, “Friday”) defaults to 10:00 instead of leaving the chip dateless. Fix: the assistant asked the lite model with the user’s personal key rather than the workspace’s, so inside a team workspace it silently returned nothing and only the regex “Add to calendar” chip ever appeared — no time, no attendees. It now resolves the key the way chat does.
  • Behind the ask_teammate flag (default off); needs clarify_chat for the chip UI and a Slack install for the workspace. Tests in tests/test_ask_teammate.py.
2026-09-16

A landing page with a personality

  • The marketing page is rebuilt from a Claude Design file, mechanically. frontend/src/landing/PersonalityPage.jsx is generated from “Redesign 3 v2 – Personality” by a converter (kept in the session scratchpad; the file header says so) rather than hand-ported — attributes map 1:1, inline styles become style objects, <sc-if>/<sc-for> become conditionals and maps, and the runtime’s style-hover/style-active become generated classes in personality.generated.css. Behaviour is one hook, PersonalityState.js, a line-for-line translation of the design’s state class. Cream ground, Inclusive Sans at 120px with tight tracking, a JetBrains Mono body, tinted rounded cards, pill buttons.
  • Everything the old page did that a design file can’t express is wired in PersonalityApp.jsx. All five “Join the waitlist” CTAs open the same modal (free join + paid Early Access Pass, with its GA funnel events); the Solo and Team pricing CTAs join the list while the waitlist is on and start the real Razorpay checkout when it’s off, so sandbox still works end to end; the signed-in redirect to /app, the social-proof toast, and the brand-name override all carry over. Footer and nav placeholders (#docs, #api, legal) resolve to the real routes.
  • Motion is scroll-driven now. The design shipped its own micro-animations (fade-ups, typing dots, fill bars, the card fan-in) but they all fired at load, so anything below the fold had already played. Sections now rise in as they enter, grid children stagger, and each section’s inline keyframes are paused until it’s in view. Cards lift on hover. The hero’s fanned cards dissolve forward on hover — z-index can’t animate, so the card starts translucent and blurred and sharpens as it rises, and after the entrance has played the inline fan-in is retired so leaving the card doesn’t replay it. All of it respects prefers-reduced-motion.
  • Two converter bugs fixed before anyone saw them, and a responsive layer. Elements with white-space: pre had their newlines collapsed, which turned the skills YAML into one long line and blew the features grid out to the right — the converter now preserves whitespace there, and grid children get min-width: 0. The 1440px artboard is fluid: containers cap at 1172px, grids go single-column under 900px, the hero’s lead-and-CTA row stacks, and the fanned cards become a snap-scrolling strip on phones.
  • A new “One brain, then the work” section, right after How it works. Six sources feed a graph that draws itself in (nodes, edges, a counter climbing to 218/412), the wiki page assembles from it, then a question gets a cited answer and a PR gets a grounded review — all on one 16-beat loop. Each source row carries its own short dashed arrow toward the graph (so it always lines up with its name and never crosses a node), and a small arrow in the gap points into whichever of Ask or Review is live. The Ask card shows a waiting line until the wiki is written, so it never sits empty. Four step pills jump the loop to that moment (Ingest replays the graph build). No panel backgrounds under the stage — with them, the arrows and cards read as clutter. On phones the stage stacks and the arrow overlay is hidden.
  • Hero cards no longer snap in front of each other. Hovering a fanned card brings it to the front and lifts it with one plain transform transition, equally smooth in and out. The four stat tiles are gone.
  • Go-live pass. Every click target in the page (FAQ rows, demo chips, wiki-card buttons, step pills, hero cards) is now a real button for keyboard and screen-reader users — focusable, and Enter/Space activates it. On phones the header keeps its Join button (only the text links collapse), the hero CTAs no longer wrap, and footnote-size text is lifted to 12px. Provider tiles name providers, not models (Anthropic · Claude, OpenAI · GPT, Google · Gemini, custom endpoints · Ollama / Groq / OpenRouter / Mistral) so they never go stale. The Early Access Pass price set in the admin panel is fetched once per page and quoted everywhere the page mentions it (FAQ and modal), not just at checkout. Sitemap gains refunds, security, docs, API docs and this page; the Offer structured data and the no-JS fallback describe the current USD plans.
  • The wiki card’s buttons do something. See blast radius, Open in graph and Ask a question swap the card’s body for that view (impact counts and dependency paths, a mini cluster graph, a typed question with a cited answer); clicking the active one returns to the page. Provider tiles are stacked label/value with no repeated model names. The founder-note section and its nav link are gone. The waitlist modal now follows the same language — cream card, ink border and pills, Inclusive Sans heading, mono body, palette dots instead of emoji.
2026-09-10

The Map grows instead of floating

  • The force simulation is gone, and the cloud with it. Nodes were placed by a d3 force layout, which was faithful to the link structure and unreadable: the first thing anyone wants from this view is how many areas are there and how big, and a hairball answers that last. Every cluster now owns an angle on a central ring and grows outward from it as a branching filament — the most-connected pages near the trunk, leaves at the tips. A page is not one step but a short walk of several small segments that sheds side hairs as it goes; one long step per page draws a spoke, a walk draws a filament. Layout is seeded per cluster, never Math.random, so a re-render cannot reshuffle the picture.
  • Colour drift tells a trunk from a tip. Branches are stroked segment by segment, blending from the cluster’s own colour toward the accent with distance from the ring, and thinning as they go. Links are still drawn — they are the true structure — but as a faint bowed wash under the filaments, because they are not the first read. Ambient per-node drift was removed: a dot that wanders off the branch it grew on is a lie about the drawing, so the slow whole-field rotation carries the motion instead.
  • Chrome is monospace and states what it encodes. Node labels, stat cells and caption are all mono; the header is divided cells with uppercase micro-labels (pages / links / clusters) instead of one run-on sentence, and the canvas carries the key on the drawing: one node = one page · size = connections · click to read it. The prose stat line stays for screen readers.
2026-08-30

“Key rejected” about a key that was fine

  • The error blamed the only part the user had right. OpenRouter’s docs print POST https://openrouter.ai/api/v1/chat/completions, and our field asks for a Base URL, so the endpoint is what gets pasted — reasonably. The verifier then requested …/chat/completions/models, got a 404, and _verify_status_for mapped 404 to invalid_key: “the provider does not recognise this key.” The key was valid. Verified against the live endpoint: pasted URL → 404, trimmed URL → 200 and 396 models.
  • Fixed in three places, because one would have been a patch. normalize_base_url() strips a trailing operation path (/chat/completions, /responses, /messages, /completions, /embeddings, /models, and combinations) and runs both when the URL is saved and when a request is built — so URLs already stored wrong start working without anyone re-saving them. A 404 from a custom endpoint is now bad_endpoint, not invalid_key: we build the first-party URLs, so a 404 there really is a bad request, but a custom URL is the user’s and is the likelier thing to be wrong. And the field says what it wants: the root ending in /v1, paste either, we trim it.
  • What Test actually does, since it is fair to ask. It calls the provider’s model list endpoint with the key — never a generation, so it costs nothing — and reports how many models that key can reach. If the model currently selected belongs to that provider, it also checks that the model appears in the list, which is the difference between “Key works” and “Key works, but it cannot reach the selected model.”
2026-08-30

Five things the redesigned pane still got wrong, one of them a real bug

Follow-up: the optimistic paint was not enough on its own — every control that writes now also says it is writing. A spinner appears beside the one control you clicked (tracked by field, not by the pane-wide saving flag, so one click does not spin every switch), and the segmented control is no longer disabled mid-save: disabling the thing you just clicked is what made it feel slow, because the selection had already moved and the pane greyed out anyway.

  • A saved key and a locked model list, both true, neither explained. The key row reads the STORED value to print “Saved ••••”; the model picker reads the DECRYPTED one to decide what is reachable. When decryption fails — a rotated KMS key, ciphertext that no longer parses — those two disagree, and the product showed a green Saved badge above twenty greyed-out models with nothing to connect them. _load_prefs now returns which providers are stored-but-unreadable, the API passes it through, and the row says Unreadable in orange with the fix: paste the key again. Three states, because there are three.
  • “Custom endpoint” named the mechanism, not the thing. Nobody has a provider called Custom Endpoint; they have a Groq key. The row is now “Groq, OpenRouter, Ollama, Mistral…” with “Any OpenAI-compatible provider” under it, and the two fields inside are labelled API key and Base URL instead of sitting there unlabelled and being guessed at.
  • Every control waited for the network before it moved. A toggle sent a PUT, then a GET, then finally re-rendered — so flipping a switch felt like the page had missed the click. Preferences now paint optimistically and roll back if the save fails. The four key fields are deliberately excluded: prefs holds the server’s masked form, and writing a raw key into it would show the key back in its own Saved badge.
  • Two words are not a choice. Standard and Saver said nothing about what they do; the explanation had been reduced to a title tooltip. The consequence of the mode you are on is now written out in full under the control, and it changes when you switch.
  • API keys and the MCP server were one card. Generating a key for your own script and pasting an endpoint into Cursor are separate jobs done on separate days; they read as one long thing with two headings. Two disclosures now — and the MCP one renders its own header from inside the component, because that section is admin-gated and a card that opens onto nothing is worse than no card.
2026-08-30

Models & API, rebuilt from the design instead of from its own history

  • The pane had grown by accretion and read like it. Three sentences of BYOK policy, then the model dropdown, then a sentence under the dropdown, then another about the background model, then four API-key fields each with an uppercase heading and Show / Test / Clear, then an endpoint URL every user was asked for and almost none needed, then a key generator, then an MCP config snippet. Every fact was true and none was findable. Rebuilt against a Claude Design mock: the model states itself once in a block of its own with Change model as the action beside it; the keys are one collapsed row per provider showing the only two things anyone checks — is it saved, is it the active one; the custom base URL lives inside the custom provider’s own row, where it is meaningless without that key; and API keys plus the MCP server fold under one Developer access row.
  • Nothing the mock omitted was dropped. A design is a reference, not an inventory: the MCP endpoint URL, the tool list, the per-client quick setup, the config snippet, the verify walkthrough and Test / Clear on every key are all still here, one disclosure away. The rule the page follows is that clutter is fixed by ordering what a reader needs, not by deleting what a reader needs less often.
  • The honesty caveats moved rather than thinned. “Spends tokens on your own key” was written twice, once under each automation switch; it now sits once in the Automations header, above both. “We never bill you for tokens and never train on your data” moved to the key form’s footer, next to the button that saves the key. The background model is still named in full, still says not reachable yet rather than none.
  • Two layout bugs found by building it. The active-model block had overflow:hidden for its rounded corner, which clipped the model picker’s dropdown in half — a clipping ancestor is invisible until something inside it needs to escape. And ModelPicker gained a triggerLabel so its button can read as an action while the model name is stated once, larger, above it.
2026-08-30

The background model has a name even when no key can reach it

  • “None is available” was never true. Settings named the lite model only when resolve_lite_model returned one — and that returns None whenever no key reaches the provider. So the pane told users no background model existed, when in fact the model was perfectly well known (gemini-3.1-flash-lite for Gemini, gpt-4o-mini for OpenAI, claude-haiku-4-5 for Claude) and only unreachable. Unreachable is not unknown, and unknown is never rendered as none. New lite_model_name() answers “which model” without asking “can I call it”; the API now returns the name plus a reachable flag, and the line reads “…use a cheaper model, gemini-3.1-flash-lite” either way, adding “Not reachable yet — no working key for this provider” only when that is so.
  • One mapping, not two. The custom-endpoint lite tiers (Groq 8B, gpt-oss 20B, DeepSeek free, Mistral Small) were inline inside resolve_lite_model; naming the model needed the same table. Factored into _custom_lite_name and shared, rather than copied — a second copy of a mapping is a second thing that can be wrong about the same question.
  • A card whose padding never applied. The “Unattended work” section wrapped its body in class="b", but the stylesheet only defines .bill-section .body. Text sat flush against the card edge, and three rounds of inline-style patching had failed to fix it — because the bug was a class name, not a spacing value. Read the governing CSS first.
2026-08-11

Six risks had been written down as “unverified” for a while; reading the code settled all six

  • Two repos with the same file path — real, and worse than a gap. A code-graph node’s key is its path relative to its OWN repo root, so src/index.js is one key for both repos and a plain dict.update kept the last writer. The survivor is not a missing node: its file claims one repo while the edges pointing at it came from the other, so impact analysis answers confidently about the wrong codebase. Both loaders — the graph tools and impact analysis, which had separate copies of the merge — now share one rule: first writer keeps the bare key so every existing lookup still resolves, a colliding node is kept under {repo}::{key} rather than dropped, every node carries the repo it came from, and the collision is logged instead of being discovered through a wrong answer.
  • The chats/conversations drift was still live in a second consumer. Fixed in the contradiction ledger days ago; skills._EVIDENCE_CATEGORIES had the identical list with the identical omission, so every Slack and WhatsApp page was invisible to skill evidence — and a skill’s evidence is most often a conversation. Every consumer that filters pages by category is now checked against what the pipeline actually writes.
  • One “tiny” call could still become an unbounded bill. Every caller caps the content it interpolates and no call site capped the result, so a pathological page showed up as cost rather than as an error. There is one ceiling now, in lite_complete — the right place, because truncating upstream loses records while these tasks are classifications whose answers do not improve past a few thousand characters.
  • A project can hold more than one Firestore database. databases/(default) was hardcoded, so anyone who had created a named database got a 404 — reported as an empty collection, silently. The database is read through the same resolver as the project id and api key, for the same reason: a second place that knows where a stored value lives is a second place that can be wrong about it.
  • Freshness turned out to be FINE, and that is worth recording too. It was suspected of repeating the _detected_sources bug because save() replaces the file wholesale. It does — but every writer goes load → mutate → save, so a second repo’s ingest adds its pages instead of erasing the first’s. Now pinned by a test, so it stops being re-suspected.
  • Two intent gates were still written from the sentences one user typed. “Display the products” and “let me view the covers” were not visual questions; “database design” and “shape of the data” were not structural ones. And the URL lifter’s depth bound of two could not reach {"gallery": [{"url": …}]} — one wrapper key more, and a perfectly ordinary way to store pictures. Its key list is vendor-neutral now rather than naming only the store in front of us.
  • One item is deliberately NOT fixed. Chat can still cite a page from a repo you filtered out, because the chat client sends no repo filter at all — there is nothing for retrieval to scope by. That is a missing capability, not a defect, and it needs a selector, an API parameter and repo-aware retrieval. Building it quietly at the end of an audit sweep would have shipped a backend filter with no way to use it.
2026-08-29

Severity was a word inside a message, and a third of the spend was unmetered

  • Every application log line reached Cloud Run as DEFAULT. enhanciar wrote plain text to stdout, so the “WARNING” in WARNING enhanciar.email_send: email send failed was Python’s format string INSIDE the message, not the entry’s severity. Nothing the app ever said about itself could be found by a severity filter — not in Logs Explorer, not by an alert, not by our own errors page, which reported “no errors” while a customer’s confirmation email was failing. Cloud Logging promotes known keys from a JSON line, so {"severity": "ERROR", "message": …} is all it took: _log.error is an error everywhere now.
  • JSON only where something parses it. On Cloud Run (K_SERVICE) or an explicit flag; locally a wall of JSON is strictly worse than the text a person reads. And it re-formats the EXISTING handlers rather than adding one — uvicorn installs its own first, and a second handler prints every line twice, once structured and once not, which is worse than either.
  • The lite model was spending money nobody could see. The composer’s scheduling chip runs one lite call per debounced keystroke; the collection chooser, follow-ups and contradiction checks run one per turn. None were metered, so the Cost page showed query alone. Free on a free tier — and not free at all on gpt-4o-mini, where a user was paying as they typed with no way to see it. Recorded now as kind lite, deliberately separate from query: “what I asked for” and “what the product did on my behalf” are different questions, and blending them hides which one is growing. Pricing needed no work — the catalog resolves it from token counts already.
  • Token counts come from the provider, with a floor. Google reports prompt/candidates, the OpenAI-compatible world reports prompt/completion, and some custom endpoints report nothing at all — in which case meter.record DROPS the call, because both counts are zero. So a provider that says nothing is estimated rather than zeroed; recording zero reports a real cost as free.
  • Caught in review: that estimate started as a second copy of server._estimate_tokens, justified by a circular import that was only half real — server imports llm_model, so the dependency runs one way and the shared home was always available. Two copies of one heuristic are two things that can drift apart about the same question. One now, in llm_model, with server delegating. Suite 2635 passed / 1 skipped.
2026-08-29

A page that promised an email it never sent, and a provider tab that really was hardcoded

  • “Confirmation is on its way to your inbox” — for a message that did not exist. The sending mailbox had expired, SMTP answered 553 Sender address rejected, and _send_blocking swallowed it. Swallowing was RIGHT — a failed email must never break a signup — but it returned nothing, so the caller could not tell sent from not-sent and therefore always claimed sent. It now returns the outcome without ever raising, the API carries emailed, and the modal drops the promise when it is false: the person is still on the list, they are simply not sent hunting through their spam folder for our mistake. An unconfigured SMTP counts as failure, not success — reporting “sent” with no SMTP configured is how a local run convinces you email works.
  • The OpenRouter tab was not hardcoded by design — it was the one provider with no live fetch. Google, OpenAI, Anthropic and any custom endpoint are all asked what they can reach; OpenRouter had a single curated entry and was reachable only by pointing the generic “custom” base URL at it, which nobody would guess from a tab labelled OpenRouter. Its /models endpoint is public, so the list is now fetched: 396 models, 21 free, free-first, every id still carrying the openrouter/ prefix that routes it.
  • The existing tests refused the first version of that fix, correctly. Fetching unconditionally broke “no keys saved → no network”, an invariant worth more than a populated tab for someone who has never used OpenRouter. It now fetches on evidence of actual use: a key, or a selected openrouter/* model.
  • Settings finally names the background model. The composer’s scheduling chip runs on the LITE model — debounced, on every keystroke — and that model is DERIVED from the chat provider, never chosen, so nothing in the product had ever named it. Someone on OpenAI was paying for gpt-4o-mini as they typed with no way to see it. Resolved server-side and displayed, because the mapping lives in resolve_lite_model and a second copy in the client is a second copy that can be wrong.
  • Admin panel: the Errors page reported “no errors” through a total outage. Two defaults did it — pinned to prod, floored at severity>=ERROR — while Cloud Run logs a quota-refused request at WARNING. 89 of the 429s that took both sites down were structurally invisible. Worse, EVERY application log line is severity DEFAULT (enhanciar writes plain text to stdout, so the “WARNING” in a message is Python’s format string, not the entry’s severity) — so no _log.error in the codebase could ever reach that page. Now both services, a WARNING floor, and a text match for app-level failures. Rows 13 → 112.
  • Filters and IST across the whole panel — environment, time range, severity, search and HTTP status, from one shared widget set so pages cannot drift; every table stamped with what window, which environment, as of when. Every timestamp is IST through a single formatter. One real bug fell out: per-day charts bucketed on UTC dates, so a 01:00 IST signup was counted on the previous day.
  • And a bug I caused and had to find: a module-level import enhanciar.email_send in a new test file bound the submodule as a package attribute at COLLECTION time, silently defeating monkeypatch.setitem(sys.modules, …) in an unrelated suite. The baseline was clean, so it was mine. Lazy import. Suite 2623 passed / 1 skipped.
2026-08-10

Everything here was debugged against one workspace, and some of it had quietly learned that workspace as the shape of the world

  • A workspace with two databases could query only one of them. “A Firebase connection exists” was read as “the question is about Firebase” — true only for a workspace that has nothing else connected, which is the only shape this was ever tested on. The SQL branch sat below an unconditional return, so a customer with Postgres invoices and a Firebase side project could not query their invoices from chat AT ALL. Worse, the schema handed to the collection chooser came from SQL introspection, so it picked a TABLE name, read it against Firestore, and got nothing — indistinguishable from an empty collection, and completely silent. The schema is engine-scoped now, and the chooser matching no collection is treated as what it is: a routing signal, not a verdict on the workspace.
  • Image detection was a carve-out for one storage vendor. A literal "firebasestorage" test, next to two general rules that object-store URLs do not match — they carry no file extension, and a hostname is not a /storage/ path segment. So a customer on S3, Azure Blob, plain GCS, Cloudinary or CloudFront got image_cols=[] and no gallery: the exact bug this feature exists to fix, reserved for everyone who is not the test workspace. Now one predicate over the CLASS — extensions, path words, object-store hosts, and signed-download parameters — and the two copies of that rule, which had already begun to disagree, are one.
  • The worked example in the spec prompt is now written in the customer’s own field names. It used to say subtitle_field: "area", meta_field: "duration", y: "bookings". A model copying those onto an invoices table names columns that do not exist, and the resolver drops the whole component — silently. The example is built from the block’s actual columns, so it cannot teach a foreign vocabulary and it demonstrates the “must match exactly” rule by obeying it.
  • Another customer’s brand was in the global system prompt. Two few-shots named the real test workspace and its payment provider. Models imitate examples; on a sparse workspace that redirect could put one customer’s app name into another customer’s chat. Replaced with placeholders that are obviously placeholders. Product copy in the Connectors pane had the same leak.
  • Two correctness bugs fell out of the same review. Expansion carried the parent document’s scalars from row 0 onto every record, so records lifted out of a SECOND wrapper document were labelled with the first one’s values. And scalar-ness was judged from row 0 alone, so a column that is a map in only some documents could carry a structure into a cell as though it were a value.
  • There is a guard now, and it reads the code the way the runtime does. It parses every module and checks the string literals that are actually RUN — docstrings excluded, because a fix is easier to understand next to the bug that motivated it. Comments may name the real workspace; a prompt, a label or a heuristic may not. Suite 2602 passed / 1 skipped.
2026-08-10

I verified the schema block was DELIVERED and never that it contained the answer

  • The structural-question block shipped hours earlier and was called done. What was checked was the mechanism — it builds, only for structural questions, and reaches the prompt. What was never checked was the CONTENT. Two defects were sitting in the output of a feature reported as working, and both are only visible if you read what it produces rather than that it produces something.
  • The gate had no word for “where”. what…stored and how…stored were there; the most natural phrasing of a storage-location question was not. “Where are the images stored, give me the structure” matched only on the trailing word structure — the same sentence without it returned an empty block.
  • And the sampler described a collection by the name of the box its records came in. It recorded top-level keys only, so CustomPopularTreks had exactly ONE field — selectedPopularTreks — because every real field sits inside an array of maps below it. The chat path had understood that shape for days; the sampler kept its own shallower idea of the same data. That is the “one shape, two readers” bug from the audit, in the two readers that most need to agree.
  • So the shape has one reader: new enhanciar/record_shape.py, used by the chat path and the schema sampler both. A collection now reports images:array @ selectedPopularTreks[].Details.images — the field, its type, and where it actually lives, which is the whole question.
  • Two numbers that are not the same number. Expansion turns one document into seven records; it is still ONE document, and _sampled_doc_counts hands that figure to the collection chooser as “~N documents”. The count is taken before reshaping, the fields after.
  • Caught in my own first version: the array prefix was applied to every column, which labelled the parent document’s updatedAt as selectedPopularTreks[].updatedAt. A path that is confidently wrong is worse than no path, so carried-over parent fields are now tracked through expansion and keep their own depth.
  • And the fix would have been invisible to every existing workspace. The schema is sampled once and cached, and the Tables pane offers a re-sample only when there is NO schema — so an account that already had one would have kept its old answer permanently while the code that produces a better one sat in production. The artifact carries a shape version now; a file written by an older sampler is re-read rather than trusted. This is the difference between a fix and a fix that reaches anybody. Suite 2589 passed / 1 skipped.
2026-08-10

The cards arrived; the pictures did not, because the fallback only fired when there were no cards

  • The log said the pipeline had already won: reason=ok · blocks=1 · image_cols=['images'] · q='show me photos from CustomPopularTreks'. Rows expanded, the picture column detected, the block on the wire. And no [autorender] line — because the model HAD emitted a ui block, just one with no image_field. The deterministic gallery only fires when the model emitted nothing, so a nearly-right spec beat it. extractUiSpec reads the FIRST fence, so appending a corrected one could never win.
  • An incomplete spec is now completed rather than rejected. The block already carries image_columns — the server’s own answer to “which column holds pictures” — so a card_grid that omitted image_field takes it from there. No second copy of the detection rule on the client: it consumes the decision, it does not re-derive it.
  • Nothing is invented when nothing was detected. With an empty image_columns the cards render titles and no pictures, exactly as before — and every existing guard still holds, so a cell that is a javascript: string reaches no <img src>.
  • Reproduced in a real browser before shipping, not just in the suite. The stub was changed to emit the failing shape — a card_grid with no image_field beside a block naming its image column — and Playwright counted 3 images on screen. An earlier run of that harness had proved nothing because a broken patch left the stub unmodified; the test only counts once it is failing first. Both halves pinned by test_chat_data_blocks.py. Suite 2577 passed / 1 skipped.
2026-08-10

Two caps in sequence, and the first one decided what the second was allowed to rank

  • Expansion kept the first 15 inner keys. On the live record that cut Details — the map holding the name and the image URLs — before flattening could see it, so the ranked cap added minutes earlier had nothing to rank. The log showed the result exactly: cols=['price','area','maxAltitude','Details.price','rating','highlights',…] image_cols=[]. Positional truncation is a decision about importance made by insertion order.
  • There is one budget decision now, and it happens last, where the VALUES are known. Columns are ranked before the cap binds: pictures first, then something a card can be titled with, then everything else. A rich record no longer loses its photo to a field called additionalPaymentInfo.
  • Ranking goes through the same URL lifter the wire uses. An image field is usually ["https://…"] or {"url": …} and only a bare string by accident — ranking on the raw type kept name and still dropped images. One reader of that shape, used by both.
  • Cards are titled by a column called name/title/label when one exists, else the shortest text column. Insertion order had been titling them “Dharamshala”.
  • And chat can finally read the database’s SHAPE. Tables, ERD and DB Graph all render the sampled schema; chat received wiki pages and live rows and never the structure, so “give me the structure” was answered from what the code implies. A structural question now gets a deterministic block built from the same _db_schema files the panes read — engine-agnostic, so a Postgres schema renders in SQL words and a document store in document words, and sampled counts are labelled sampled. No model call, no live read, nothing added to an ordinary question. Suite 2577 passed / 1 skipped.
2026-08-10

The log answered in one line what two prompt rewrites had guessed at

  • cols=['Details', 'price', 'isCustom'] · image_cols=[] — the diagnostic added minutes earlier settled it. Expansion had lifted the array into rows correctly, but each record keeps its real fields one level deeper inside a Details map. There was never an image COLUMN to detect, only a map that happened to contain one. Two prompt rewrites had been aimed at a model that was behaving reasonably.
  • Nested maps now flatten into columns, recursively. The first version handled exactly the two levels in front of me, which is a fix for one document rather than for databases — documents nest arbitrarily. The walk is recursive to a bounded depth: three covers real shapes like Details.contact.phone, and past that a document is usually holding a sub-TABLE, which flattening would render unreadable. The remainder is kept whole rather than exploded.
  • Two details that decide whether rows stay honest. Map-ness is decided per COLUMN, not per cell — a document simply missing the map must contribute blanks under the flattened names; deciding per cell produced a stray column named after the parent and slid values under the wrong headers. And a leaf keeps its bare key only while unambiguous: price on the parent and price inside the map are different facts, so a collision keeps both full paths.
  • Recovered from my own bad edit: the first attempt at generalising spliced out four live functions. Caught by the suite, reverted to the last commit, and re-applied as a clean insertion rather than patched forward. Suite 2570 passed / 1 skipped.
2026-08-10

Prompt wording is not a mechanism — the gallery renders itself now

  • Two prompt fixes in a row failed to produce a picture. The mechanical chain was verified correct each time: one document expanded into 3 rows, the images column detected from its Firebase Storage URLs, blocks=1 on the wire, and an imperative instruction naming the column and the component. The model described the photos in prose regardless.
  • So the server decides. When the question asks to SEE something, a block carries a detected image column, and the model emitted no ui block of its own, the card_grid is appended deterministically. It references the block by id, so the values are the database’s, never retyped.
  • Narrow on purpose. A model that DOES render is never second-guessed — two galleries are worse than none. A non-visual question never grows one. No image column, no rows, no render. And the title is never a URL column, because a card titled with a URL is worse than no card.
  • The diagnostic that would have shortened this is now in the live-data log: the block’s columns and the detected image columns, beside the reason. Whether detection worked was guessed at twice today; it is a lookup from here on. Suite 2562 passed / 1 skipped.
2026-08-10

“Use ONLY facts from the wiki context” was outranking the rows we had just fetched

  • The photos question fetched everything it needed and still answered in prose. Verified server-side before touching a prompt: the single CustomPopularTreks document expanded into 3 rows, the images column was detected from its Firebase Storage URLs, blocks=1 reached the client, and the model was explicitly told “picture URLs live in: images… USE card_grid”. It ignored it.
  • Because a general rule was quietly outranking the specific one. The generative-UI instruction said “Use ONLY facts from the wiki context” and “OMIT the block entirely for ordinary prose answers”. Live database rows are not wiki pages, so a model following that reads the fetched rows as ineligible material and omits the component — exactly the shape of the “when in doubt, NONE” bug fixed an hour earlier, in a different prompt.
  • The rule now names live rows as eligible, and a question that asks to SEE something is an explicit exception to omitting. The image instruction is imperative rather than advisory, and says what the failure looks like: describing pictures in prose does not answer “show me photos”.
  • Worth stating: this was a prompt bug, not a pipeline bug. Every mechanical step — expansion, detection, transport — was already correct and is pinned by tests, which is what let the diagnosis take one reproduction rather than a guess. Suite 2554 passed / 1 skipped.
2026-08-10

A privacy fix that quietly took the feature with it

  • “What treks do you have running between November and January” started answering NONE. The log said reason=no_sql collections=[] while Tables showed 14 collections and the database was connected — so the chooser was declining, not failing. Reproducing the exact prompt it sees made the cause obvious: the privacy rule added yesterday ended with “When in doubt, NONE”, and on this workspace every trek-named collection is EMPTY while the records live in a sibling. The rule turned the normal case into doubt.
  • The protection is now scoped instead of weighted. Pick a collection whenever the question is about records — and pick the best available even when the obviously-named one is empty, because the records usually live in a sibling. Answer NONE only for questions about how the SYSTEM works, which is the only case the privacy rule was ever for. It also now names what it protects: orders, users, contacts, payments.
  • The “Other” card was not harmless after all. I said it could be ignored; it was being injected into the chat prompt, so the model answered “DATA SOURCES DETECTED BUT NOT CONNECTED: Other” to a question the connected Firebase could have answered. That card is a Razorpay payment client the detector could not classify — there is no connector for it and no query path would use it. Only sources with a real connector (or a DSN, which is self-describing) reach the prompt note now.
  • Both were found by running the product, not by reading it, and both came from work shipped hours earlier. Suite 2550 passed / 1 skipped.
2026-08-09

Audited today’s bugs by shape, and two of the shapes had other instances

  • Deep thinking was silently off for every Claude 5 model — the exact bug found in the Gemini path hours earlier, one provider over. The gate tested for the substrings “sonnet-4”/“opus-4”, so the newest models answered False. Widening it alone would have been WORSE than leaving it: manual budget_tokens is deprecated on 4.6 and rejected with a 400 on 4.7, 4.8 and the 5 family, which take {"type":"adaptive"}. The gate returns a MODE now, not a boolean. A third trap surfaced inside the fix: claude-sonnet-4-20250514 parsed as version 4.20250514 — “newer than 4.6” — and would have been sent the parameter it rejects.
  • Ten more unchecked r.json() calls in DB Graph, ERD and Tables, where a 500’s error body normalises to an empty graph and the pane reports “No schema to graph” for a server error. Same false-empty class already fixed in Wiki and Code Tree; these were missed then.
  • The rest is written down rather than guessed at. docs/planning/BUG_CLASS_AUDIT.md generalises each of today’s nine bugs into the SHAPE that caused it, records where that shape was swept and found safe (claims merge correctly; graph.json is namespace-wide by design), and ranks six unencountered risks by what to test first — freshness records surviving a second repo’s ingest at the top, because it is the same wholesale-replace shape that deleted the detected-sources record.
  • Confidence is stated per finding. “Confirmed” means it was run and the wrong answer observed; “unverified” means the shape is present and the failure plausible but not reproduced. Suite 2542 passed / 1 skipped.
2026-08-09

A second Firebase project was saved, listed in Settings, and used by nothing

  • Two repos, two Firebase projects, one reachable. _firebase_conn returned the FIRST connection and firestore_query had no hint parameter at all — while the SQL path had had connection selection and a “which database?” clarify for months. So project B was unqueryable and project A silently answered questions about it. Firestore now uses the same picker and the same ambiguity sentinel, so the existing clarify chip covers it without a second protocol; two projects and no clue in the question asks which, rather than guessing.
  • Matching had to get sharper than the SQL version. Two projects of one product share a prefix (trekngo-f4e81 vs trekngo-new), so name-token overlap matches both. Project IDs are matched as whole labels and the longest match wins.
  • Collections are scoped to the chosen project, and sampled doc counts with them. Offering project A's collection names for a question about project B produces a read that returns nothing — indistinguishable from an empty collection, which is the failure the whole live-data path exists to avoid.
  • Sampling now covers every project, one _db_schema file each, so Tables, ERD and DB Graph list both databases through the picker they already had.
  • And the question “which repo did this banner come from” uncovered a worse bug: _detected_sources.json is a per-NAMESPACE file that was written per-REPO and wholesale, so ingesting a second repo deleted the first repo's detected databases — its Firebase stopped being offered in Connectors and its collection list vanished from every live query. Records now merge by project identity, keep both repos as witnesses, and union what each repo knew. The connector card says found in <repo>.
  • Code tree gains a repo filter, shown only when there is more than one repo — 405 files in one flat list with no way to tell whose they were. A node with no recorded repo stays visible under every filter: absent provenance is not evidence it belongs elsewhere. Suite 2537 passed / 1 skipped.
2026-08-09

“Connected, but carries no projectId” — about a database chat was reading fine

  • One data shape, two readers, and the second one was wrong. _firebase_conn returns the WHOLE saved connection; the ids live nested under config. The chat path unwrapped that correctly. The schema sampler, written later, read project_id at the top level and therefore always found nothing — while is_firestore_connected kept answering True, because it checks the nested value. Hence the contradiction on screen: a connection named after its own project, reported as having no project.
  • Fixed structurally, not locally. A single firebase_project(uid) resolver now owns the shape and both paths call it. A second place that knows a data shape is a second place that can be wrong about it.
  • Seven of the sampler’s own tests were stubbing the WRONG shape — a flat {"project_id": …} that nothing ever saves. That is precisely how the bug passed its own suite while failing in production. The stubs now use exactly what the one-click connect writes, so the tests exercise the real path.
  • Found by the diagnostic shipped one commit earlier. The previous message for this was “nothing to sample”, which covered four failures and pointed at none of them; the specific one named the step and the cause was obvious within a minute. Suite 2519 passed / 1 skipped.
2026-08-09

“Nothing to sample” was four different failures wearing one sentence

  • A button whose only failure mode is “nothing came back” is unactionable for the user and unfindable for us. The Firestore sampling pass could return empty because the connection carries no projectId, because no collection names are recorded, or because the sidecar that records them is not on this instance — and all three printed the same line. Each step now names itself, in the API response, in the UI, and in the log.
  • The distinction that matters: collection names are recorded when the repo that QUERIES Firestore is ingested. “Never ingested that repo” and “the record did not survive to this server” look identical from the outside and need different fixes, so the message says which, and the diagnostic names the exact file.
  • The “what fills this pane” bullets were the SQL story under a Firestore heading — connection strings and ORM migrations, for a database that has neither. They now say where collection names come from, that reads depend on your Firestore rules, and that counts are sampled from the first page rather than counted. Suite 2512 passed / 1 skipped.
2026-08-09

Every Slack page was excluded from the contradiction ledger by one word

  • The scan read chats; the connector pipeline writes conversations. _KIND_TO_CATEGORY maps a CONVERSATION source to conversations, and the ledger’s category list still named the older chats — so every Slack and WhatsApp page was skipped in silence. That is the ledger’s most human source of disagreement (two people saying different numbers in a thread), removed by a word nobody re-checked when the pipeline was renamed. Both names scan now, so pages already on disk under the old one keep working.
  • The exclusion of code-derived pages is deliberate and stays. entities, concepts and flows are LLM prose ABOUT code, so a disagreement between two of them is a summarisation artefact rather than two humans contradicting each other — the exact false positive that trains a team to ignore a doc-drift bot. A test now pins that, so changing it needs a reason rather than a nudge.
  • Worth stating plainly: a workspace whose only source is code therefore has nothing to scan, and the card correctly finds nothing. The ledger arbitrates docs, tickets, mail and conversations against the repo — the repo is the referee, not one of the players. Suite 2509 passed / 1 skipped.
2026-08-09

“Connect GitHub” under a picker that says “2 connected repos”

  • Four panes told a user to connect things they had already connected. Code Reviews showed “Connect GitHub →” directly beneath “Pick from 2 connected repos”; Tables said “No database connected” while chat was answering questions out of that database in the next tab. The empty-state copy was static and asserted a CONNECTION fact none of these panes had checked.
  • The cause was one API answering a different question than it was asked. /api/db/connections dropped every connection that was not Postgres or MySQL, because its first and only consumer was an introspection picker that needs a DSN. Every pane added since read it as “what does this workspace have connected” — so a Firebase-only workspace answered nothing. Connections are now all returned with an introspectable flag; the picker filters, the empty state does not.
  • Unknown is still not none. The connection hook starts at null and a failed lookup stays there, so a network blip can never render as “no database connected” — the same rule the action-routing note needed.
  • Tables now offers “Sample collections now”, which runs the Firestore sampling pass and shows the reason when it finds nothing. The automatic pass had four different ways to return in silence, so three empty panes had no explanation anywhere; every outcome is now logged, and a failed attempt retries after a minute instead of being written off for the life of the process.
  • Suite 2504 passed / 1 skipped, including two older tests updated rather than deleted: the picker still refuses to offer an engine it cannot open, it just no longer denies that the connection exists.
2026-08-09

Eight pages cited, two actually read

  • Asked what happens when a question spans more pages than the context budget holds, the answer turned out to be a citation bug. Measured: eight matching pages of 8k against the 24,000-char budget — 8 rendered as sources under the answer, 2 actually sent to the model. Six citations for pages it never saw a byte of, under a product whose one promise is that every claim is cited. Sources are now sliced to the pages that genuinely contributed; a partially-included page still counts, because bytes did reach the model.
  • And the model is now told when the budget cut matching pages — without it, it sees a tidy set, has no way to know more matched, and answers as though it read everything. That is how a partial answer acquires a confident tone. One line: how many were left out, and to say the answer may be partial.
  • The array expansion is shape-driven, so it is not a Firestore special case. A Postgres jsonb (or MySQL json) column holding an array of objects is the identical “one cell contains the whole table” problem, and the SQL path now gets the same treatment. It stays deliberately narrow: one-or-two-row results where exactly ONE field is an array of 2+ objects. Two array fields is ambiguous — which one is “the” table? — so nothing is reshaped; an array of scalars is a list, not a table; a many-row result keeps its own row identity. Ordinary relational rows are returned untouched, by identity.
  • Parent scalars ride along. Expanding an items array out of an order row keeps order_id on every produced row — dropping it would silently lose the thing that makes the rows mean anything. Suite 2498 passed / 1 skipped.
2026-08-09

The cap was the wrong fix, and the last blank wait is gone

  • Capping the row cell would have broken the feature it was meant to protect. CustomPopularTreks is one document whose array holds every featured trek — so the working “treks between November and January” answer was reading the whole catalogue out of that one uncapped cell. Truncating at 400 characters cut it mid-array: verified before shipping that Trek 6 had vanished from the block. A cost fix that silently deletes data is not a fix.
  • Expanded instead of truncated. One document holding an array of objects is now lifted into real rows with real columns: the same catalogue goes from ~2,000,000 characters to 1,943, all seven treks intact, and the structured block finally carries rows the UI can render verbatim — cards and photos included — instead of the model retyping them out of a giant string. Deliberately narrow: it fires only on one-or-two-document results where exactly one field is an array of 2+ objects, so ordinary tables are untouched.
  • The response now opens BEFORE the wiki search. Retrieval — up to 24,000 characters from up to 8 pages — ran before the response existed, so the first phase of every question was a motionless “working…”. Deferring the database work had made the second wait legible and left this one exactly as blank as before. Everything from retrieval onward became a coroutine that returns its generator; the wrapper sends “Searching your wiki…” first and then awaits it. All three exits (cached reply, empty brain, model) go through one path.
  • Every check that needs a real HTTP status stays in front of it — 401, 402, 409, missing key. Once the status line is on the wire the status code is spent, so a preparation failure after that point arrives as an error event the chat can render instead of a broken 500.
  • Measured in headless Chromium: “Searching your wiki…” at 173 ms → “Checking your connected sources…” at 1,041 ms → reasoning panel → answer → 3 photos. 34 network chunks over 4,296 ms, one agent bubble throughout. Suite 2488 passed / 1 skipped.
2026-08-09

An architecture question pulled customers’ email addresses, and one document cost $0.24

  • “Explain how the trek booking flow works end to end” read the Orders collection and printed real customers’ email addresses into the answer. Nobody asked to see records. The collection chooser was told how to pick a RELEVANT collection but never told when to pick NONE, and a question about how the system works is answered from code and docs — never from customer data. That rule is now explicit, with “when in doubt, NONE”: a missing table costs a follow-up question, the wrong one exposes people’s personal data.
  • “How many documents are in CustomPopularTreks?” cost 469,658 input tokens — about $0.24 for an answer that is the number 1. That collection is a single document holding an array of every featured trek. The wire-facing block has always capped its cells; the PROSE block the model actually reads never did, so one document stringified into roughly two million characters of prompt. Cells cap at 400 chars, the block at 20k, and anything dropped is stated in the block — a silently shortened table is one the model will describe as if it were complete.
  • “Photos of nov to jan treks” picked two collections that are real and empty, then gave up. winterTrek and Treks hold zero documents; the records are in CustomPopularTreks. The sampling pass that fills Tables and ERD already knew the doc counts and nothing read them back — the chooser now sees EMPTY (0 documents — cannot answer anything) beside each name, which is the cheapest possible way to stop an empty collection being chosen.
  • The log that made all three findable now reads reason=ok collections=['CustomPopularTreks'] blocks=1 instead of the placeholder it printed before. Every one of these was a lookup, not a guess. Suite 2485 passed / 1 skipped.
2026-08-09

The database was read, paid for, and thrown away one line before the prompt

  • Moving live-data work inside the stream put the await AFTER the prompt was built. user_prompt is an f-string interpolated from context_text; assigning the fetched rows into that variable afterwards changes nothing, because the string had already been copied. So every turn queried Firestore, paid for two model calls to choose collections, and then told the model “no relevant pages found”. The answer that came back — “the provided wiki context does not contain photos or scheduling data” — was correct, which is exactly why it was hard to see.
  • data_blocks had the same bug one level up. The route built them from a detail dict before the coroutine that fills that dict had been awaited, so every turn shipped zero blocks. No question could render a card, a table or a photo from live rows, no matter what the model asked for. Both now read after the fetch, through one shared _blocks_from_detail.
  • The diagnostic built to explain live-data misses was explaining nothing. The [live-data] reason= line was logged in the route, which only ever held the placeholder — five hours of production logs all reading reason=deferred collections=[]. It now logs inside the stream, after the await, with the real reason and the block count.
  • Deep thinking was switched on and never requested. _gemini_supports_thinking was a substring test for “2.5”, written when 2.5 was the newest model — so it silently answered false for Gemini 3, the default in the picker. Nothing failed; the capability check had simply aged out. It compares a version now, so 4 and 5 need no edit.
  • Two stacked bubbles while an answer loaded. The status line was stored as a field on a message, so a status event arriving before any text had to CREATE a message to live on — while the loading row rendered the same idea beneath it. A transient progress line is not a message: it has its own state now, and the loading row stands down as soon as the answer’s own bubble exists.
  • Verified in headless Chromium, not in a unit test: 34 network chunks over 3,330 ms, “Checking your connected sources…” at 172 ms, the reasoning panel streaming inside a single bubble (agent rows: 3, before and after), and 3 photos rendered at the end. Suite 2474 passed / 1 skipped.
2026-08-09

Streaming was never broken in the code that streams

  • Every server-side test passed, and the answer still arrived in one lump. The generator was always correct — measured, it yielded its first event in under a millisecond. The failure was two layers out. GZipMiddleware was registered AFTER the middleware whose entire job was to disable it for streams, and Starlette wraps the LAST-registered middleware OUTERMOST — so gzip ran outside the stripper and read the original Accept-Encoding on every request. The strip could never fire.
  • Measured through a real HTTP client, all six chunks of a 1.5-second stream arrived in the same millisecond. zlib holds small writes until its buffer fills, so the whole response landed at once. The chat had a live status line and token-by-token text the entire time and none of it could reach the browser: what you saw was “working…”, then everything.
  • The fix removes the ordering dependency rather than correcting the order. One _StreamAwareGZip middleware decides per path — streams pass through untouched and get X-Accel-Buffering: no, everything else is compressed as before. There is no longer a registration sequence that can silently undo it, and a test fails if a bare GZipMiddleware is ever added back.
  • And the test that was missing all along: a real browser. A dev harness now mounts the real chat component outside the auth gate, served by the real vite proxy against the real middleware, driven by headless Chromium. It reports the wire and the screen together: content-encoding: (none), 31 network chunks spread over 2,695 ms, and the screen going “Checking your connected sources…” (133 ms) → “Writing the answer…” (1,122 ms) → text growing 451→937 chars → 3 photos rendered. Assertions that stop at the Python layer cannot see transport, which is exactly where this lived.
2026-08-09

An answer that cannot do the thing should say what CAN

  • Asked to change trek dates, the chat refused and stopped. Accurate and useless — a button that would have filed exactly that work sat underneath the refusal, and the one thing the person needed to know (that Enhanciar can turn this into a tracked ticket) was the thing it did not say. The system prompt now carries the boundary and the route together: you read, you do not write, every change is proposed and happens only after someone approves it in Actions — so when asked to change something, say so in one sentence, name what it can be filed as, then name where the change is really made if the wiki knows (the admin screen, the file that owns the value).
  • Only trackers that are actually connected get named, because offering a Jira ticket to a team with no Jira is an ad for a setup flow rather than an answer. That needs a lookup, so it is cached per (workspace, user) for a minute — one Firestore round-trip per conversation, not per question.
  • And the lookup failing is its own answer. None is not []: treating “could not ask” as “no tracker is connected” states something false about the user’s setup and sends them to a flow they already finished. Unknown drops the sentence entirely, keeps the boundary, and is deliberately not cached — a transient blip must not hold the degraded answer for the next minute of a conversation.
  • The follow-up suggester was told the matching rule, or the offer would land with no button under it. A refused change request is the one case that is not someone learning something, and the ticket is titled as the change that was wanted rather than as the refusal. Suite 2451 passed / 1 skipped.
2026-08-09

Six defects found by using the product, not by reading it

  • A second repo silently deleted the first repo’s pages. The page-merge path keys on SOURCE KIND, and _norm_source collapses every repository to github so the Graph’s filter chips stay usable. Merging needs the opposite question. Two repos in one workspace that both described an entities/user.md took the same-source overwrite branch, and repo B replaced repo A with no error, no event, and nothing in any log. Merge identity is now repo:<name>, so those two pages combine into attributed blocks — which is what the marker machinery was built for. Found while answering “does anything break with multiple things ingested?”; the answer was yes.
  • And the “Combined from N sources” line under a merged page had been wrong since it was written. _split_frontmatter returns the body still carrying the newline after the closing ---, so the anchored ^# in the note-refresh substitution never matched and the sub silently did nothing. Frontmatter counted every source correctly while the line a reader actually sees stayed frozen at whatever it said after the first merge.
  • Firebase collections existed everywhere except the three panes built to show them. Tables, ERD and DB Graph all read _db_schema/, and only the SQL introspector ever wrote one — so a Firebase-only workspace saw “No entities yet” three times while chat was answering questions off live rows from that exact database. Firestore has no catalog to read, so the schema is now SAMPLED: columns unioned across documents, types inferred, nullability counted by VALUE (the reader flattens documents into shared columns, so counting keys would call every column non-nullable and say nothing). Relations come from Firestore referenceValue pointers and <collection>Id fields whose target is a collection we actually have — never from name similarity, and flagged heuristic so the ERD’s source filter can switch them off. Every count is labelled a sample, because counting a collection properly means walking all of it.
  • An empty collection now still gets a card. Hiding it made “this is empty” and “this does not exist” look identical — the precise confusion behind “why does it say 0 documents?”.
  • “Start extraction” answered with a /tmp path and no way forward. repos.json stores where a clone LANDED, inside a per-instance temp dir; the file is mirrored durably, the directory is not. So on every pod but the one that ingested — and on every pod after a deploy — FK extraction followed a valid-looking path to nothing. New repos_origin.json records where each clone CAME FROM, is mirrored beside repos.json, and a missing working copy is re-cloned instead of reported.
  • “Show me photos” could never have worked. An image field in a real catalog is ["https://…"] or {"url": "…"}, and the wire-safety step flattened both with str() into ['https://…'] — not a URL. The rows held the images and the block discarded them one line before the wire. Cards now take an image_field; only http(s) reaches an <img src>, because that cell is database content and therefore attacker-reachable on a shared workspace. A dead URL removes the element rather than showing a broken-image glyph that reads as our bug.
  • A follow-up chip filed a GitHub issue with no title. The chat wrote Jira’s field names for every tracker — summary + project_key — while GitHub reads title + repo. The proposal was not mislabelled; it genuinely had no title, and would have been filed that way. The field map now lives in one module both producers import. The ticket body also carries the QUESTION, not just the answer: a description consisting entirely of “I cannot update the database directly” tells whoever picks it up nothing about what was wanted.
  • Also fixed: a coroutine leak on the answer-cache hit path — the deferred live-data work was neither awaited nor closed, one RuntimeWarning per cached request. Suite 2439 passed / 1 skipped.
2026-08-09

While it’s loading, show nothing — never something false

  • A connector row rendered “Connect” before its status arrived, then flipped to “Connected” a moment later. That first frame is not a neutral placeholder, it is a claim — and a wrong one, about a source that was already wired up. Status-unknown is now its own state: a skeleton pill the same size as the control it replaces, so nothing jumps when the answer lands. “Coming soon” is a static catalog fact and still shows instantly.
  • The nav badge said “1” while the pane showed no banner, and only a page refresh reconciled them. Two independent GETs of the same endpoint can disagree, and did — the pane fetched suggestions itself with no retry, so an empty first answer stuck until something forced a remount. The pane now reads the value App already owns for the badge. One fetch, one variable: they are structurally incapable of differing, rather than merely expected to agree.
  • A failed fetch stopped impersonating an empty workspace. Wiki, Code Tree and DB Graph all read r.json() with no r.ok check, so a 500’s error body became zero pages and the pane said “No pages yet — ingest a repo” to someone whose wiki is full. Each now separates “nothing here” from “we couldn’t ask”, and says which.
  • Caught by measuring, not by looking: the first skeleton implementation collapsed to ZERO width inside the connector list, because a flex child shrinks by default — it would have rendered nothing at all, which is worse than the false button it replaced. flex: 0 0 auto, then verified every placeholder reports real pixels (78×26 pills, 239/195/150×11 list rows) with the shimmer live. Honours prefers-reduced-motion; announced via aria-busy rather than being purely visual.
2026-08-09

Every namespace artifact now has a durability decision — enforced by a test, not by memory

  • Three outages this month were one shape: something written under wiki/<ns>/ that nobody added to the durable-mirror allowlist, so it lived exactly as long as the pod. A full sweep of the codebase for namespace-root writes found two more still open — and the new guard immediately found a third I had missed.
  • repos.json (now mirrored) — the repo NAME index. Its stored paths are worthless on another pod, which is fine (every consumer guards with isdir and falls through to the GCS raw store) — but losing the index made _augment_context_with_code return nothing on a cold pod, silently dropping source-grounded code answers for BYOK users while the sources sat safely in GCS.
  • _db_schema/ (now mirrored) — the introspected database structure behind the DB Graph and ERD tabs. Read straight off disk with no regeneration path, so both tabs went blank on any pod that had not run the introspection itself: a feature that looks deleted rather than one needing a click.
  • _system/ (now mirrored) — a team’s own code-review guidelines. Written through the wiki API (durable) but invisible to the end-of-ingest sweep, which skips underscore dirs. Whether your guidelines survive should not depend on which code path created them.
  • The structural part: a hand-maintained allowlist rots — that is the whole reason this kept happening. tests/test_storage_durability.py now scans the source for namespace-root writes and fails when one is neither mirrored nor listed in EPHEMERAL_BY_DESIGN with a stated reason. Adding an artifact and forgetting the decision breaks the build at the only moment it is cheap to fix. Suite 2404 passed / 1 skipped.
2026-08-08

A workspace with 85 pages reported itself empty — because one directory existed

  • The symptom was a vanished “connect Firebase” banner; the cause emptied the whole brain. The GitHub webhook’s repo_ever_ingested pulls the _state artifact alone onto a cold pod to answer “was this repo ever ingested?” — creating wiki/<ns>/_state/ and nothing else. _ensure_fresh then asked bool(listdir(dir)), saw a non-empty directory, concluded “pages are present”, and skipped the Firestore restore. Meanwhile namespace_has_pages — which correctly skips underscore-prefixed system dirs — reported an empty brain. The log line said it exactly: “Auto-rehydrated 0 wiki pages + 19 graph artifacts”.
  • No data was ever lost — 85 pages in Firestore, 56 in GCS, the whole time. But on that pod the wiki read empty, chat had nothing to ground on, and the DB banner was suppressed as empty_brain. The per-pod “already rehydrated” marker then made it permanent for that instance.
  • The rule, stated once: whatever decides to RESTORE must ask the same question the READER asks. Both now call empty_brain.namespace_has_pages. And because artifacts-on-disk-with-no-pages is a state no ingest ever produces, a pod that finds itself in it retries the page restore exactly once — bounded, so a genuinely page-less workspace still costs one Firestore stream, and an already-poisoned pod self-heals instead of staying empty until it recycles.
  • Tests: a _state-only directory does not suppress the restore; real pages still skip it; the incoherent state is retried once, not per request. Suite 2400 passed / 1 skipped.
2026-08-08

Chat components render the database’s rows, not the model’s memory of them

  • Ask “good treks for winter” with Firebase connected and the answer can arrive as cards; ask for date-wise trends and it can arrive as a line chart. The chat already had a generative-UI fence (card_grid / table / stat_row / bar_chart) — but it made the model RETYPE the fetched rows into its spec, which is how a chart ends up plotting the model’s memory of a number instead of the number.
  • Now the live-DB rows ride the stream verbatim as a data_blocks event ahead of the text, and the spec just points: {"type":"card_grid","data":1,"title_field":"name","pick":[0,3,7]}. The model contributes column names and row indices — never values — and the client joins spec with rows. pick selects and orders by index, so “show only the winter ones” works without a single retyped cell.
  • Strict and fail-safe: an unknown block id, a field that isn’t one of the block’s columns, or an out-of-range pick renders NOTHING — the prose answer always stands alone, never beside a half-right component. Inline specs (wiki-derived answers with no fetched rows) work exactly as before.
  • New line_chart component — hand-rolled themed SVG (no charting dependency): sorted time/number axis, gridline ticks, dot markers. Cells that aren’t wire-safe are stringified and capped server-side before they cross.
  • Tests: the server fills the structured block on live hits; the prompt teaches reference-not-retype; and the client join is executed through node from pytest — pick selects and orders, every invalid reference refuses, tables project verbatim, date axes sort, inline passes through. Suite 2397 passed / 1 skipped.
2026-08-08

“116 unchanged” now actually means 116 skipped — the incremental gate read the wrong state file

  • Caught live, watching a README-only auto-ingest re-extract all eight domains. The webhook chain itself was flawless — push to webhook to job in 2 seconds — and the event stream printed “0 added, 1 modified, 116 unchanged”… and then zero domains skipped.
  • The diff and the skip gate were reading different state files. Incremental state saves under <state_key>__<branch>.json; the diff loaded exactly that, but the per-domain gate re-loaded with a different key and no branch — a file that does not exist. Empty state read as “first run”, first runs never skip, and every push re-extracted the entire repo on the user’s own key while claiming incrementality two lines up.
  • The gate now literally shares the diff’s variable (had_prior_state) — one state read, one answer, nothing to drift. Two tests pin it: the gate contains no second load_state, and no branchless load_state call exists anywhere in the pipeline.
  • Cost impact is the point: for a busy repo this was the difference between pennies per push and a full ingest per push. Suite 2393 passed / 1 skipped.
  • And then the run died anyway — which found a second bug. Sandbox lacked ENHANCIAR_JOB_EXECUTOR=cloud_tasks (prod has it), so the job ran in-process on a scale-to-zero, CPU-throttled instance. Every manual run had survived only because the open event-stream tab pinned the instance; the first webhook run — with no request to pin anything — was killed by the first scale-in and sat at “running” forever. Sandbox now dispatches via Cloud Tasks like prod (queue, worker URL and SA were already configured; only the selector was missing), and the orphaned job is marked failed with the reason.
2026-08-08

The “Other” database card shows its working — files, connection shape, schema

  • “worker LLM saw database code” told the user nothing they could check — while the Firebase card beside it named package.json, firebase.json and a projectId. The asymmetry read as a detector that guessed. The worker’s database sightings now carry a files field, and the card’s evidence renders whatever the sighting actually holds: the files where the database code lives (checkable), how the app connects, the tables/collections read out of the code, and the env vars involved.
  • A DSN’s password can never reach the banner. The connection hint may quote a DSN that is literally committed in the repo — userinfo is stripped (mysql://REDACTED@host/db) before it can render, the same house rule the regex evidence already follows: name where a credential lives, never print it back.
  • A sighting with no detail now admits it — “a worker reported database code in this domain but named no file — treat as a hint, not a finding” — instead of dressing vagueness up as evidence.
  • Tests: files/hint/schema/env rendering pinned; password excision pinned; the detail-free fallback pinned. Suite 2391 passed / 1 skipped.
2026-08-08

The last two audit footnotes: key ids stay out of stored evidence, arrow components can’t slip the sweep

  • A Razorpay key_id rode into a stored evidence quote — by policy, not by accident. The scanner deliberately leaves public payment identifiers alone (they ship in frontend checkout code by design, and database auto-detect reads them), so redaction passed it through into _claims.json. But the sidecar is durable, mirrored and rendered, and “a key_id is configured at index.js:12” needs no value. The storage gate now excises Razorpay key ids and Stripe publishable keys on the way in — scanner policy unchanged everywhere else.
  • const AllTreks = () => … parses to zero named declarations, so the coverage sweep’s “substantive” filter skipped it — the one file of 42 that got no page on the verified run. An arrow-function component still calls things (hooks, fetch, map) and the AST records those, so calls joins functions/classes/routes/tables as a substantive signal. The exclusions still hold: a barrel file re-exports without calling; a static data array calls nothing.
  • Tests: key-id excision keeps the fact and drops the value (ordinary quotes untouched); an arrow component with only calls is swept while a barrel is not. Suite 2389 passed / 1 skipped.
2026-08-08

Every [[wikilink]] now resolves or stops being a link

  • On a run with 100% file coverage, 54 of 93 wikilinks still dangled. The page-writer invents cross-links to pages that will never exist — [[Razorpay API]], [[Material UI Icons]] — and writes case-variants ([[Edit Camps]]) that the graph’s exact-slug soft-edge match silently fails to resolve. A dead link is milder than a missing document, but it advertises a page that isn’t there, and a case-variant quietly LOSES a real graph edge.
  • Two layers, same shape as the coverage fix: ask nicely, then verify deterministically. The prompts now restrict [[links]] to names from the worker extraction (those become pages) and make external services plain text. And after all pages are written — before the graph reads them — a resolve pass walks every link: a case-variant is canonicalized to its page’s slug (gaining the edge casing was losing), an unresolvable name is rewritten as plain text. The event stream reports both counts.
  • Verified on the real ingested workspace before shipping: 80 unresolvable links unlinked across 30 pages, 0 dangling remain, all 108 resolvable links untouched, idempotent on a second run. Suite 2387 passed / 1 skipped.
2026-08-08

Coverage is measured, not trusted — and confidence is earned, not self-graded

  • The fresh audit showed the bottleneck had moved: the worker could SEE all 66 files and still wrote 40 entities. No prompt wording fixes a model grading its own coverage, so the pipeline stopped asking. After each extraction it now diffs the AST’s substantive files (has functions/classes/routes/tables) against the entities’ file_paths and puts exactly the missed files in front of the worker once more — one follow-up call, listing only what was skipped, with a skipped escape hatch for files that genuinely are trivial. Deterministic on both ends: measured before, measured after, and the event stream reports “sweep recovered 21 of 24 file(s)”. Fail-open — a sweep that errors leaves coverage what it was, never breaks the ingest. Verified on a full offline pipeline run before this shipped.
  • confidence: high was the model grading its own homework — 58 of 62 pages on the fresh run, while its own verifier found 19 unsupported and 150 unverifiable claims. The API now serves claim_confidence, derived at serve time from the claim-verdicts sidecar (worst verdict wins: unsupported→low, partial/unverifiable→medium, all-supported→high), and the wiki page header shows it as a colored pill. Absent — not defaulted — when a page was never checked, so “unverified” can never dress up as “high”. Nothing is written into the page: frontmatter stays the model’s words, the audit stays the system’s.
  • Tests: the sweep chases only substantive uncovered files (barrel files and non-code stay a judgment call), generous path matching prevents duplicate pages, subset batches carry only their own edges; verdict→confidence mapping pinned including the absent-not-defaulted rule. Suite 2383 passed / 1 skipped.
2026-08-08

A failed extraction batch is retried, resumable, and never silently permanent

  • “If I rerun, it picks up the failed ones, right?” The honest answer was no — twice. A batch got one shot with no retry; and worse, the run’s incremental state hashed EVERY file as done, including the failed batch’s — so the next ingest diffed them as unchanged, skipped the domain, and the loss became permanent until someone happened to edit those exact files. A warning on the event stream and then a quiet forever-skip.
  • Three layers now. In-run: a batch whose output has no parseable JSON is retried once with a “output ONLY the JSON” nudge — most parse failures are a truncated or fenced tail, and one fresh call recovers them. On resume: every parseable batch is checkpointed BEFORE the stitch under a batch-suffixed key, so a crash or resume reloads the good batches and re-buys only the failed ones (“3 of 4 batches loaded from checkpoint”). On rerun: files from twice-failed batches are excluded from the saved state, so the next run of the source diffs them as new and re-extracts them — the event stream says so at both ends.
  • A partial stitch is never checkpointed as the domain’s worker output. Resume Path B trusts that file wholesale; checkpointing a partial merge would lock the missing batches out of every future resume. The per-batch checkpoints carry the resume instead.
  • Tests: a cached batch costs zero calls; a garbled batch gets exactly one retry and is checkpointed; a twice-failed batch reports its files and spares its neighbours. Suite 2377 passed / 1 skipped.
2026-08-08

Big domains are chunked across worker calls and stitched — coverage no longer fits in one context

  • Naming every file is not the same as covering it. The roster fix made the AST summary honest, but a worker asked about 120 files in ONE call still has one detail budget on the way in and one ~32K-token budget on the way out — so the tail of a big domain got thin extractions even when listed. The same architectural answer the page-writer already uses (one page per call) now applies a stage earlier: a domain over 30 files is split into path-sorted batches (a directory’s files travel together), each batch gets a worker call with FULL detail for every file, and the JSONs are stitched back into one extraction before the checkpoint.
  • The stitch is invisible downstream. Entities deduped by slug with their file_paths/routes/env_vars unioned (the same component seen from two directories is one entity, and dropping the duplicate’s paths would orphan files from the graph); concepts, flows and relationships deduped by key; the merged JSON is byte-shaped like a single worker’s output, so checkpoints, resume, the integrator and the page-writer needed zero changes.
  • One bad batch loses that batch, never the domain — and it says so on the event stream (“2 of 4 extraction batches produced no usable JSON”) instead of shipping silence. Cost scales with coverage on the user’s own key: a 120-file domain is ~4 calls now, which is the product doing what it says rather than quietly reading a quarter of the repo.
  • Tests: every file in exactly one batch; edges ride with their source’s batch; cross-batch entity dedupe with path union; garbage batch counted and survived; stitched shape identical to a single worker’s. Suite 2374 passed / 1 skipped.
2026-08-08

Truncation stopped being prohibition: every file in a domain now reaches the worker

  • 42% of a real workspace’s files had no wiki page — including its two largest components — and the cause was an 8000-character string cap. The worker prompt tells the model the AST summary lists “EXACTLY the files this domain owns” and forbids describing any other file. But the summary detailed only the first 50 files in directory walk order inside an 8000-char budget — so files past the cap were never listed, and the instruction converted that truncation into a prohibition. The graph, built from the untruncated sidecar, still linked to them: 60 dangling wikilinks pointing at exactly the files the workers were forbidden to describe.
  • The contract is now structural. Detail blocks go to the RICHEST files first (functions + classes + routes + tables), so a 1400-line component can never lose its slot to a barrel file walked earlier. Every code file past the detail budget is still named in a roster the final truncation cannot reach, marked “still THIS domain’s files” with a pointer to read_single_file for content. Budget default 8000 → 32000 chars (≈8K input tokens against a 1M context).
  • Tests: 200 files far over budget — all named; the richest file walked last still gets detail; overflow lands in the roster, not the void. Suite 2369 passed / 1 skipped.
2026-08-08

The verify stage keeps the redaction promise, and an ambiguous basename is refused, never guessed

  • The ingest banner says a detected credential’s value “was never sent to a model or written to the wiki” — and the verify stage broke both halves. Its source read opened the clone directly, bypassing secret_scan, so a repo-committed secret went into the tier-2 critic’s prompt, came back inside an evidence quote, and was stored in _claims.json — a durable artifact mirrored to GCS and rendered in the UI. The tell: the deliberately-public Firebase apiKey did NOT appear in the sidecar while the two real secrets did — the redaction path and the evidence path disagreed on exactly the lines that matter.
  • Two choke points now, belt and braces. SourceIndex.read — the only read feeding the critic — redacts at the read, so the model never sees the value and its quotes are quotes of the same redacted text it was shown (keeping evidence-location consistent). And claims._clean_claim — the only gate into the sidecar — redacts quote, text and why, whatever route a record arrives by, including tier-1 AST evidence and any future caller.
  • The already-stored artifact was purged. The affected workspace’s _claims.json in GCS was rewritten through the same redactor; zero hits on re-check, and the deploy recycles any pod still holding the old local copy. The leaked pair is a test-mode key and is being rotated regardless.
  • Every index.js was one entity, as far as the graph could tell. The file→node index that bridges AST edges to wiki entities also keys by basename — and the rule was “first node to claim a basename wins it”. In a JS repo, where every directory has an index.js, one entity silently absorbed every AST edge touching any OTHER index.js: three false graph edges and wrong file attributions on a real workspace traced to exactly this. An ambiguous basename now simply never enters the index — judged corpus-wide, so a lone cited index.js can’t claim the thirty uncited ones — and every get(path) or get(basename) lookup site stays correct without change: the fallback misses instead of lying. Same refusal rule the claim verifier already reads by.
  • Tests: redaction pinned at both choke points (and that ordinary records pass through untouched); collision, corpus-ambiguity, root-file shadowing and same-node twin paths pinned on the index. Suite 2366 passed / 1 skipped.
2026-08-08

The “connect Firebase” banner survives a restart, and pages stopped being confidently wrong

  • The detected-sources banner lived exactly as long as one pod. The scan that powers “we noticed you use Firebase — connect it” writes _detected_sources.json during the clone, to the pod that ran the ingest — and that file was never in the GCS artifact mirror. The next restart lost it, and the suggestions route read the absent path back as an empty list. Detection was never broken; the file simply evaporated. It is now a durable artifact like graph.json and _claims.json: mirrored at end of ingest, restored on the first read from a cold pod. Existing brains get theirs back on their next ingest.
  • Two routes read that file before rehydrating it. The suggestions endpoint checked “does this brain have pages” on a disk nothing had restored yet — its own helper documents rehydrate-first as the caller’s job — and one-click auto-connect did the same. Both now rehydrate before touching disk.
  • An audited wiki showed every page claiming confidence: high while 38% of claims verified as supported — and one page described environment variables and upload flows for a file that is a static data array. The page-writer prompt let the model pick its own confidence with no rubric and mandated “2-4 paragraphs, go deeper” with no way to decline. Confidence is now earned against a rubric (padded page = low), section lengths follow the evidence, and the fabrication classes the audit caught are banned by name: no invented env vars, no imagined behavior for static data, empty arrays are good answers.
  • The worker’s extraction fields became transcription fields. env_vars, routes, tables_used and external_services may only contain names literally present in the AST facts or source — a hardcoded config object is not an environment variable — and inferences must be marked “Likely” and kept out of those fields. This is the Karpathy wiki rule (“say the wiki has no confident answer instead of synthesizing”) applied at write time, where it belongs.
  • Tests: the durable-artifact list, a root-level restore round-trip, and rehydrate-before-read ordering on both routes are pinned. Suite 2358 passed / 1 skipped.
2026-08-08

Empty panes stopped telling an already-ingested workspace to go and ingest something

  • Impact offered “Ingest a repo” beside 256 analysable files. “Empty” is two different situations wearing one face: nothing ingested at all, versus everything ingested and nothing SELECTED. The pane showed the first while its own picker was full, which reads as “your data is gone”. With targets loaded there is now no ingest button — the title already IS the instruction — and the body says how many things can be analysed.
  • Skills pointed at the one step already done. Skills are extracted FROM ingested material by a separate pass — the “Suggest skills” button in that pane’s own header. Sending an ingested workspace back to Ingest points away from the step it has NOT done. The empty state now runs that extraction directly when there is source material, and only falls back to “ingest a repo” when there genuinely is none.
  • The Connectors badge said 2 while the page showed none — and the cause was yesterday’s fix. Both read the identical list from the identical endpoint, so the only way they can disagree is TIME. Suppressing suggestions when “the latest job failed” made a stable answer depend on transient job state, so two fetches seconds apart got different answers. The rule is now “does this brain have pages”, which does not flicker as jobs come and go — and is the thing the sentence “found in the code you ingested” actually claims.
  • Worth recording as a lesson, not just a fix: the first version was right about the symptom and wrong about the key. A condition that changes underneath two callers will always produce a disagreement somewhere; the question to ask of any suppression rule is not “is it correct now” but “does it still say the same thing in ten seconds”.
  • Tests: a brain with a detection file but no pages offers nothing and says why; the workspace-scoping tests now create a real page, so they exercise the whole path rather than passing through a short-circuit. Suite 2354 passed / 1 skipped; eslint 0 errors; build green.
2026-08-08

“This workspace has no key” is said before the ingest, not after — and a refusal stopped looking like a crash

  • The whole failure rendered as [object Object]. Two different 409s reach the Ingest button and they are shaped differently: the duplicate-ingest guard sends a plain sentence, while the workspace-key failure sends a structured object — {code, message, owner, workspace_name} — precisely so the UI can tell an owner (who can add the key) from a member (who cannot). The client assigned j.detail straight through and handed it to new Error(), which stringifies an object to [object Object]. The real sentence was sitting in .message the whole time, naming the workspace and exactly where to add the key.
  • A refusal is not a failed run. Nothing had started, yet the pane showed FAILED · Ingestion halted at 100% with a full red bar — which reads as “your ingest broke” rather than “we did not start one”. A 409 now leaves the pipeline idle, shows the reason as a toast, and takes an owner straight to Workspaces.
  • Better still, say it before the button is pressed. With workspace_keys on, a TEAM workspace runs on ONE owner-supplied key and a member’s personal key is deliberately never consulted — so an unkeyed team can neither answer nor ingest, and the only way anyone found out was by trying. A banner in the header now names the workspace, and tells a member WHO can fix it rather than sending them to a page that cannot help them. It appears only when the feature is actually on: with the flag off, key resolution is byte-identical to personal keys and the warning would be a lie. Personal workspaces never show it.
  • The Activity poll backs off. Two independent loops were polling /api/jobs every 3 seconds regardless of state — roughly forty requests a minute to watch a page where nothing was happening. The fast rate is only worth paying for while there is progress to show, so it drops to 20s when no job is active.
  • A correction. The first read of this failure blamed the new duplicate-ingest guard. It was not that: [object Object] could only have come from the STRUCTURED detail, and the guard sends a string. The data was fine and there was no duplicate — the workspace simply had no key. Suite 2353 passed / 1 skipped.
2026-08-08

Activity opens on the list of runs, and a run is something you open

  • It opened straight onto whichever run was newest. That reads fine with one run and badly with several — a shared workspace has more than one person ingesting, and “whose progress am I watching?” is not answerable from a detail view that never said which run it picked. The list is the page now; a run is something you open, with ← All runs to come back. Without that link a run was a dead end whose only exit was another tab.
  • Each row carries what decides whether you open it: a status dot (running / failed / done), the source, the kind, when it ran — and who started it, shown ONLY when that is not you. “Started by you” on every row is noise; a teammate’s run is the one fact that changes whether you should start your own.
  • The old 260px “Recent jobs” rail is gone, not kept alongside. A list on the left and the same list on the right is one list too many, and the rail was the worse of the two: one line per row, no room to say who started a run or how far along it is.
  • A connector run stopped being called “*”. Connector jobs store scheme://filter — notion://* for everything, gdrive://<folder> for a subset — and the label was built by splitting on “/” and taking the last segment, so a whole-connector sync rendered as a bare asterisk, which names nothing. It reads “Notion” now, and “Google Drive · <folder>” when a filter is actually set.
  • Verified as a round trip, not just a render: the three states appear with the right dots, the teammate attribution shows on their rows only, clicking opens the detail, and the back link returns to a list still holding three runs. Suite 2347 passed / 1 skipped; eslint 0 errors; build green.
2026-08-08

The Cost page prices calls from the same catalog the model picker uses — instead of a second table of its own

  • Two price tables, disagreeing. The Cost page applied two constants — PRICE_IN 0.00125 / PRICE_OUT 0.005 per 1k tokens — to every call whatever ran it. Replacing that with a per-model table on the page was still wrong, because model_catalog.py has been resolving prices all along: LiteLLM’s maintained catalog (input_cost_per_token, kept current by people whose job it is), falling back to a small curated table, with an explicit free-tier set. The model picker has been showing those numbers the whole time. The page’s copy was the wrong one — its rate for gemini-3-flash-preview had been recalled rather than looked up.
  • That module already names this exact mistake as the reason it stopped hand-typing prices: “every hand-maintained list rots: ours carried Claude Opus 4.1 while Opus 5 was shipping, and a hand-typed price for gemini-3-flash that was simply wrong.” Adding a second hand-typed table one directory away was repeating it.
  • One source now. A new GET /api/pricing delegates to the same price_for() the picker calls, so a figure on the Cost page and a figure in the model dropdown cannot disagree. The Cost page needs a lookup BY ID rather than the picker’s catalog, because it prices historical calls whose models may no longer be in the user’s available list. Nothing in the frontend knows a price any more; a test fails the build if a literal rate reappears there.
  • Unknown is now visibly unknown. An id we have no rate for renders as an em dash, not $0.0000 — those look identical and mean opposite things, and the catalog’s rule is explicit: never fabricate a number for an unrecognised id. A total whose mix contains an unpriced model is marked partial rather than quietly under-reported, and unpriced models are excluded from the effective-rate blend rather than counted as zero, which would drag the rate down.
  • Why gemini-3-flash-preview is in the picker at all — the question that surfaced this. The model list is not hand-written: available_models asks each provider what the user’s own key can reach (/v1beta/models for Google), with a static list as a floor so an outage degrades rather than empties the dropdown. Google’s API lists that preview id even though its public pricing page does not carry it; LiteLLM does, which is where the picker’s $0.50/$3 comes from. So the id is real and callable — the gap was only ever in MY table, not in the product.
2026-08-08

A Resume button for failed runs, teammates can watch a run, and the “found in your code” banner stops appearing after a failure

2026-08-08

Escaping the braces fixed one outage and shipped another — the worker prompt now has none

2026-08-08

Search Console’s “Blocked due to unauthorized request (401)” was the sandbox refusing to hand over robots.txt

2026-08-07

The Ingest button says whether it will cost anything, before you press it

2026-08-07

Activity shows the whole team’s runs, and a repo already being ingested is refused instead of ingested twice

2026-08-07

The branch picker offered “main” for a repository whose default branch is “master”

2026-08-07

The Connectors grid and the GitHub pane stopped disagreeing about whether GitHub is connected

2026-08-07

“Use my own key” for a workspace, and both Install buttons behave the same

2026-08-07

A GitHub installation can belong to a team — by binding, never by mere membership

2026-08-07

The invite email’s “Accept” button now actually accepts

2026-08-07

“Install” stopped silently bouncing you to GitHub, invite mail points at the environment that sent it, and Workspaces got its badge

2026-08-07

Onboarding explains what a workspace is, and lets you make a team on the spot

2026-08-07

A deleted workspace stopped haunting the switcher — and the “Select…” box goes back to saying Personal

2026-08-07

Ingest picks from the repos you have actually connected, and keeps the paste box for the ones you have not

2026-08-07

One “install” button that reconnects first, and a GitHub rail entry that appears only once GitHub is connected

2026-08-06

The feature tour was being suppressed for the account that had never seen it

2026-08-06

The dropdown now looks like a dropdown, the key step says what the key buys, and “Other” got its missing icon back

2026-08-06

Eight provider chips became one dropdown, with the real logos and no invented prices

2026-08-06

Refreshing the browser on the key step skipped onboarding entirely

2026-08-06

Onboarding showed a new user three paid options and hid every free one

2026-08-06

The Map painted its empty state and its loading state on top of each other

2026-08-06

The reset-the-world button can spare a few collections — as a KEEP list, never a delete list

2026-08-06

Two housekeeping fixes that had no changelog entry, and the rule that caught them

2026-08-06

The model already notices credentials the scanner misses. We were throwing that away.

2026-08-06

A key hardcoded in an ordinary file was ingested, sent to a model, and written into the wiki

2026-08-06

The outbound redactor knew our credentials and not the customer’s

2026-08-06

The verifier was answering a question nobody asked, and calling it a verdict

2026-08-06

Map, Files and Skills were empty after an ingest that worked

2026-08-06

The database suggestion says where it read that, and what pressing the button does

2026-08-06

Re-entering Connectors shows the connectors

2026-08-06

An ingest button stays shut while its own run is going

2026-08-06

Starting an ingest takes you to the run

2026-08-06

An ingest printed the token it cloned with

2026-08-05

A push keeps a repo current; it never reads one for the first time

2026-08-05

The two buttons on a repo card say what they read, where it lands, and what it costs

2026-08-05

The dropdown menu escapes the box it was drawn in

2026-08-05

Native <select> retired, pane by pane

2026-08-05

The custom dropdown can be used without a mouse, and gets out of the way on a phone

2026-08-05

Each repo picks the branch that re-ingests it

2026-08-05

A push re-ingests the workspace you installed from, not your personal brain

2026-08-05

The GitHub install page said “read-only”; GitHub’s said “read and write”

2026-08-05

MCP can reach a team brain, because the key says which one

2026-08-05

The Cost per-member table says who, not 0wsEOjKLzhRT4VhKdK2Uf5a3KFR2

2026-08-05

A brain with nothing in it now says so, instead of claiming it searched

2026-08-05

Switching workspace now actually switches the app

2026-08-02

Ingest › Activity now draws the run the way the design draws it

2026-08-02

Every pane is the same width — and that width is a deliberate departure from the design

2026-08-02

A repo ingested from a team workspace now lands in that team’s brain

2026-08-02

A team workspace can run on its owner’s key — and never quietly on yours

2026-08-02

You can now see what a shared workspace costs — before anyone’s key starts paying for it

2026-08-02

A credential you connected inside a team workspace was invisible to that team’s ingest

2026-08-02

Tidying up a connector’s documents is no longer the same button as disconnecting it

2026-08-02

A team workspace is never left without its owner

2026-08-02

A file uploaded into a team workspace never reached the brain — and we are the ones who broke it

2026-08-02

Three kinds of spend were being recorded where nothing reads — your Cost page will go up

2026-08-02

Both verifiers looked for your API key in a folder instead of in your account

2026-08-02

A React component used without importing it is now a lint error, not a crash

2026-08-02

Lint — the third operation, and the one schedule that runs it

2026-08-02

Claim verification, part 5: the other half — is the ANSWER supported by the pages it cites?

2026-08-02

A team workspace’s credentials belong to the workspace, not to whoever is looking at them

2026-08-02

A Test button beside every API key — free, and specific about which of four things went wrong

2026-08-02

The uid/namespace bug is now a test failure instead of a code review

2026-08-02

The token meter now knows who pays, separately from whose wiki it is

2026-08-02

The ERD labelled every relationship N : 1, because both of its 1 : 1 tests read fields that do not exist

2026-08-02

The free half of the verifier was not durable, so a cold pod quietly moved the bill onto your key

2026-08-02

Claim verification, part 4: the verdict shows up next to the sentence it judges — and the launch docs stop promising more than it does

2026-08-02

Claim verification, part 3: an eighth pipeline stage, and a sidecar that must never read as “clean”

2026-08-02

Claim verification, part 2: the paid tier, and the four ways a checker can lie

2026-08-02

Claim verification, part 1: the AST becomes an oracle, and learns when to say “I cannot check this”

2026-08-02

“…and check every claim” is back on the Ingest headline, because it is now true

2026-08-02

Ingest, rebuilt to the design — Activity, and a failed run that reported itself green

2026-08-02

Ingest, rebuilt to the design — Feed files

2026-08-02

Ingest, rebuilt to the design — Add source

2026-08-02

Ingest, rebuilt to the design — the shell

2026-08-02

The last emoji left the interface — including the ones inside chat messages

2026-08-02

Emoji sweep: Onboarding, DB Graph, Billing, GitHub, Settings, Feed, credentials

2026-08-02

Four controls on the Ingest form were connected to nothing

2026-08-02

Impact’s LOW / MEDIUM / HIGH badge was a threshold dressed as a verdict

2026-08-02

Files fed into a team brain were quietly landing in your personal one

2026-08-02

One-click database connect copied personal connection strings into the team brain

2026-08-02

Clearing a connector token in Settings brought the old one back

2026-08-02

“Webhook delivery healthy” was asserting something nothing had ever checked

2026-08-02

Chat printed the model’s markdown as literal characters

2026-08-02

Team workspaces: eight more places where the write went to one document and the read came from another

2026-08-02

Settings said every saved key was “Not set”, because the route behind it had been 500ing for every user since the day it shipped

2026-08-02

The activity feed stopped labelling itself with emoji

2026-08-02

Workforce stops drawing a frame inside the window’s frame

2026-08-02

Three panes finish the design’s smaller list: Skills, Map, ERD

2026-08-02

Workspaces: the invite field was behind the button you had to find first

2026-08-02

The shell: rail width, selected states, and a wordmark that stopped competing with its own logo

2026-08-02

Attach in the chat composer, and a wider reading column

2026-08-02

The sidebar icons were drawn a third heavier than the rest of the app

2026-08-02

The pipeline preview listed five stages, two of which do not exist

2026-08-02

The waiting placeholder said “thinking…” even with Thinking switched off

2026-08-02

The web chat never learned that “hi” is not a question

2026-08-01

Three small things that were in the wrong place, or nowhere

2026-08-01

The ingest percentage now admits it is an estimate

2026-08-01

Chat’s sources were a separate object below the answer

2026-08-01

Four more panes stopped saying “Loading…” and nothing else

2026-08-01

An action card now says what kind of finding it is

2026-08-01

The activity feed ran backwards, and the pipeline strip was the narrowest thing on screen

2026-08-01

The Wiki’s table of contents was decorative

2026-08-01

Two “blocked” design elements had cheaper, honester answers

2026-08-01

“Stage 3 of 7” — counted, not claimed

2026-08-01

Pick which tables (or pages, or tickets) to ingest

2026-08-01

Five fields the design needed and the backend never sent

2026-08-01

A job whose process died kept reporting progress forever

2026-08-01

Hovering an event-stream line made it unreadable

2026-08-01

A connected Database never appeared under “Connected sources”

2026-08-01

A saved database connection stops looking like an empty form

2026-08-01

Connector credentials were saved where the connector never looks

2026-08-01

The Connectors grid answered from memory

2026-08-01

One palette, five real tokens, and DB Graph stops being styled for a dark app

2026-08-01

DB Graph now uses the databases you actually connected

2026-08-01

The chat read like a log file, and the footer sent customers to a build log

2026-08-01

The app promised a trial we never grant and a token cap we no longer have

2026-08-01

User documentation, because this page was never for users

2026-08-01

The chat suggests what to do with an answer, instead of always offering a ticket

2026-08-01

“hi” is not a question the brain failed to answer

2026-08-01

The Slack backfill window moved to the button it governs

2026-08-01

The Slack ingest preview listed raw timestamps instead of threads

2026-08-01

A team workspace never sent its id with any connector or ingest call

2026-08-01

The header counter said “254 today” and never said of what

2026-08-01

The whole app was rendering in the wrong typeface

2026-08-01

Three surfaces that were shipped switched off

2026-08-01

The Slack panel now says what to type in Slack, and whether you have to keep pressing Ingest

2026-08-01

Disconnecting Slack now removes the bot from Slack

2026-08-01

Design pass wave 2 — Tables, Billing, ERD, Workforce, Ingest

2026-08-01

The two states nobody designed: empty and loading

2026-08-01

Design pass, wave 1 — and a new “Who to ask” card

2026-07-31

Token caps are gone — the paywall is a subscription check, not a budget

2026-07-31

Security pass: a cross-tenant page read, SSRF guards, and a billable endpoint nobody could reach

2026-07-31

A chatty Slack channel no longer re-ingests the workspace on every message

2026-07-28

One contact address everywhere, because only one mailbox exists

2026-07-28

Ask the brain in Slack — @mention the bot, get a cited answer in-thread

2026-07-26

Removal notices, and invites you can actually see — in both directions

2026-07-27

Adding someone who already has an account told them nothing at all

2026-07-27

Team invites: the member cap read the wrong plan, in both directions

2026-07-27

Cost table headers say what the numbers actually are

2026-07-27

Every emoji-as-icon is gone, connected sources get real brand marks, and Files / GitHub / WhatsApp join the sidebar

2026-07-27

The nav and Settings rails get the design comp's icon set — and the emoji come out

2026-07-27

Home gets the right-hand rail from the design comp — and three stat tiles that refuse to make numbers up

2026-07-27

CI now deploys Firebase Hosting too — the half of prod that a green deploy didn't cover

2026-07-26

A security page you can actually read, and a model picker that asks your provider instead of guessing

2026-07-26

Freshness phase 2: connector pages go event-stale, staleness gets section-level precision, stale pages can regenerate as a diff you approve — and the Contradiction Ledger arrives

2026-07-25

Freshness fast-follow: connector pages join time decay, and re-verify cards become approvable

2026-07-25

Freshness, part 2: stale pages are ranked lower, marked in the answer, and badged in the UI

2026-07-25

Freshness, part 1: knowledge that knows when it has gone out of date — and the incremental-ingest bug found on the way

2026-07-25

Mobile chat: the clarify session could stick for 24 hours, surviving "New chat"

2026-07-25

Launch-blocker: every new BYOK key 404'd because the default model was retired

2026-07-25

Chat remembers the thread, the chips became clickable — and an adversarial review of both

2026-07-25

Clarify chat — a miss now comes with a one-click way to fix it

2026-07-24

Live database answers now work on any LLM provider — and pick the right DB when several are connected

2026-07-24

Three connector fixes — team-workspace DB suggestions, per-source de-dupe, Slack webhook loop guard

2026-07-24

Sandbox test-plan switcher — test plan-gated features + quotas without paying

2026-07-24

Workspaces page redesign — delete a workspace, real member names, one clear list

2026-07-24

Responsive/CSS audit — mobile marketing pages fixed at 375px, desktop hero hardened

2026-07-24

Static site moves to Firebase Hosting (CDN) — kills the ~22s cold start

2026-07-22

Performance: 5 Firestore reads → 1, one fewer API call on first paint, no double token verify

2026-07-22

Payments visibility + Apple Pay domain verification

2026-07-22

Two bugs the first real payment exposed: double-checkout window, and crediting the list price

2026-07-22

Early Access Pass: ENHANCIAR_EARLY_ACCESS_PRICE_USD test-price override

2026-07-21

Waitlist is now enforced on the backend, not just the SPA; one-branch deploy; cleanup

2026-07-21

Landing CTAs are waitlist-aware — the sandbox stops pushing "Join the waitlist"

2026-07-21

Sandbox unlock page no longer leaks /devhive/ in the URL

2026-07-21

Admin panel: a "Platform keys" page that reads the live Cloud Run secrets

2026-07-21

Early Access Pass — priority, not instant access; and USD-only billing

2026-07-20

Graph memory — Phase 5: the retrieval benchmark is published at /enhanciar/benchmarks.html

2026-07-20

Graph memory — Phase 4: self-improving retrieval (feedback loop + knowledge gaps)

2026-07-20

Graph memory — Phase 3: query auto-routing

2026-07-20

Graph memory — Phase 2: typed relation triples

2026-07-20

Graph memory — Phase 1: retrieval eval harness + recorded baseline

2026-07-19

Action layer — Phase 8: landing page advertises Actions (sandbox only until go-live)

2026-07-19

Action layer — Phase 7: hardening, MCP tools, runbook

2026-07-19

Action layer — Phase 6: Workforce reports file proposed actions

2026-07-19

Action layer: the draft-first executors land (Gmail draft · Notion page · Zendesk internal note)

2026-07-19

Action layer — Phase 5: three more executors (GitHub issue · Slack message · Calendar event)

2026-07-19

Action layer — Phase 4: PRD mode (a spec becomes an epic + its stories)

2026-07-19

Action layer — Phase 3: the approval UI + a chat "Create ticket from this" button

2026-07-19

Action layer — Phase 2: ingest extracts action items (transcripts/PRDs → review queue)

2026-07-19

Action layer — Phase 1: Jira + Linear create-issue executors (approve → real ticket)

2026-07-19

Action layer — Phase 0: proposal queue + executor registry + REST surface (behind the actions flag)

2026-07-19

Sandbox = full test rig: every feature forced on, quotas + rate limit lifted (prod unchanged)

2026-07-19

Feature flags: deployment force-override + fail-closed waitlist default (sandbox can open without opening prod)

2026-07-19

Sandbox: password-gated preview at sandbox.enhanciar.in

2026-07-19

Internal refactor: modularized LLM helpers and job runners

2026-07-19

Connector-sync bug sweep: six incremental-sync correctness fixes

2026-07-19

Removed the demo-data feature entirely

2026-07-19

Three frontend bug fixes: chat stream token loss, mid-stream chat-switch corruption, silent subscription→one-time downgrade

2026-07-19

Hybrid vector search — Phase 1 + 2 (index build + query-time fusion), behind a flag default-off

2026-07-19

Confluence confirmed shipped — status flipped P → S, restored on landing

2026-07-19

Landing page: broadened beyond engineering-only framing

2026-07-19

Landing page: competitor-informed copy pass across every section

2026-07-18

Landing now targets solo developers and teams (not teams only)

2026-07-19

Early Access Pass re-scoped: perks, not a queue-skip

2026-07-18

Rebrand: purge every visible "Enhanciar" → "Enhanciar"

2026-07-18

Waitlist: stop showing the queue number + mock social-proof popups

2026-07-18

Waitlist modal + paid Early Access Pass

2026-07-18

App sealed to the public — invite-only, no sign-in route

2026-07-18

Landing: honest quotes, waitlist funnel analytics, social-proof toast

2026-07-18

Pre-launch waitlist + invite gate

2026-07-16

Ingest panel remembers itself across tab switches

2026-07-16

SEO/GEO overhaul: AI crawlers can finally read the site

2026-07-16

Drive browser: filters get a real loading state

2026-07-15

Drive browser: real filters + honest labels (no more "All · 100")

2026-07-15

Drive browser: see your real file count, pick exactly what to ingest

2026-07-15

Drive ingest: the 100-file cap is gone — pick your pull size

2026-07-13

Drive ingest v2: confirm before ingesting, pick kinds, and Gemini watches your videos

2026-07-13

Drive ingest keeps your folder structure

2026-07-13

Fix: brand mark invisible on gradient chips (chat avatar & co.)

2026-07-13

Scheduling chips: one combined chip + mixed messages answer both halves

2026-07-13

Chat scheduling: Enter now books the meeting (not a wiki question)

2026-07-13

Cost dashboard survives deploys + workspace switcher in the top bar

2026-07-13

Deploys ~3× faster: build in CI with layer cache + uv, ship by image

2026-07-13

Fix: Chat tab crashed ("l is not a function") after a Chrome update

2026-07-13

Ingest preview: a peek, not a dump — plus Drive type filters

2026-07-12

Ingest rail now shows Gmail + Google Drive

2026-07-12

Cloud Run bill ≈₹150/day → ~₹0: request-based billing + Cloud Tasks ingest

2026-07-10

Chat: instant sidebar thread + typed-in smart titles

2026-07-10

OAuth redirects now live on /enhanciar/ in production + final branding cleanup

2026-07-10

Google connect is now bring-your-own-client only (no silent shared fallback)

2026-07-10

Branded URLs: /devhive/ → /enhanciar/ everywhere users see them

2026-07-10

Google setup Step 4: paste all scopes at once

2026-07-10

Calendar connector: dropped the ingest UI — it's live-only now

2026-07-10

Ask your calendar in chat — live, never stale

2026-07-10

Faster app load: code-split the tab panels (−62% initial JS)

2026-07-10

Meeting scheduler in chat: teammate availability + one-click booking

2026-07-09

Lite-model background tasks: smart chat titles + real-time scheduling detection

2026-07-09

Team workspaces, part 2: teammate invites + several people's Google accounts

2026-07-09

Team workspaces, part 1: per-source privacy + shared-brain ingest

2026-07-09

Google Calendar connector — meetings & events in the brain

2026-07-08

Gmail/Drive: working "Ingest →", day/count filters, collapsing setup

2026-07-08

Gmail/Drive "what will be ingested" preview + copy audit (wave 3)

2026-07-08

Copy audit — jargon, contradictions, ghost buttons & stale setup guides (waves 1–2)

2026-07-08

Google connectors — reconnect handling + Internal-first setup guide

2026-07-07

New — image & video capture (vision AI)

2026-07-07

New connector — Confluence

2026-07-07

Audit follow-ups — Drive extraction, error states, dead-code removal

2026-07-06

Pre-launch audit fixes — paywall & revenue integrity

2026-07-06

Connector polish: no more sample data + user-facing copy

2026-07-06

CRITICAL: paywall was inert — `unlimited_quota` defaulted ON

2026-07-06

Launch-readiness: subscription sync-verify + payment-flow fixes

2026-07-06

Security hardening batch (from the review)

2026-07-06

Security: close paywall bypass on /api/overview/regenerate

2026-07-06

Durable per-user LLM usage (admin cost visibility)

2026-07-06

No free tier: value actions gated behind a paid subscription

2026-07-06

Removed the free tier from billing + removed demo data from Connectors

2026-07-06

Gmail + Google Drive connectors, live again — via your own OAuth client (no CASA)

2026-07-06

Billing tab redesigned into a proper overview (+ clear upgrade path)

2026-07-06

Daily connector auto-refresh (opt-in, incremental)

2026-07-06

Retrieval: entity router index, entity-scored search, abstention + recency (no embeddings)

2026-07-06

Onboarding email: removed false “14-day trial” + hidden connectors

2026-07-06

Correction: connector ingest DOES use the LLM · router index for connector docs

2026-07-06

Connectors ingest from the Ingest tab (source panel + Manage-connection link)

2026-07-06

Connector page: progressive disclosure, page previews, direct deep-links

2026-07-06

Prominent “share your pages with the integration” callout (the #1 Notion gotcha)

2026-07-06

Connector page: prominent “Ingest now” + “Remove connection” (deletes its data)

2026-07-06

Hidden Gmail + Google Drive connectors (pending Google verification)

2026-07-06

OAuth connectors: “Connect” actually starts sign-in; no operator button

2026-07-06

Connector page: preview what will be ingested · no dead “View in Wiki”

2026-07-06

Settings is now tabbed with a left-rail index

2026-07-06

Save buttons now reflect saved state (no more “Connect” on a saved key)

2026-07-06

Per-connector document list — “here are your N Notion pages”

2026-07-06

Connector keys live on the connector page · cleaner connected state

2026-07-05

Connector setup guides — clearer steps, fixed buttons, current Notion flow

2026-07-05

Land on Home after login · per-run ingestion model · Individual vs Bulk (Gemini Batch) mode

2026-07-05

Secret-safe ingest + live Firestore proven on a real repo (trekngo)

2026-07-05

Hybrid DB extraction — workers read the schema out of your code, so the brain can query Firestore live

2026-07-04

Live-data query tool — the brain queries your connected database on demand

2026-07-04

Generative UI — chat answers can render components (cards, tables, charts)

2026-07-04

Auto-detect databases in ingested code → connector suggestions + nav badge

2026-07-04

Ingest UX: overall progress bar, Ingestion History, roomier Map

2026-07-04

Ingestion cost controls (Gemini spend was 98% of the cloud bill)

2026-07-04

Root-cause fix: eliminate duplicate extraction at the source (disjoint domain file-scoping)

2026-07-04

Fix duplicate wiki pages: cross-domain entity dedup by file path

2026-07-04

Ingestion reliability: fix truncated scout/worker output + robust JSON parsing + content-gen integrator

2026-07-04

Free Ollama Cloud models for ingestion (bigger free budget than Groq)

2026-07-04

Free Groq models selectable for ingestion (no more burning paid Gemini tokens)

2026-07-03

Map source filter: merged nodes light up under every source they came from

2026-07-03

Brain-like multi-source merge: one wiki page enriched by every source (no more silent overwrite)

2026-07-03

Fix — Files tab: human-readable file details instead of empty "—" cards

2026-07-03

Fix — Map node click: rich info for every node, no more "couldn't load" error

2026-07-03

Map is now Galaxy-only with cluster legend, node info drawer, micro-animations

2026-07-03

Galaxy view: Obsidian-style light-theme knowledge map (new default)

2026-07-03

AI Workforce v1 (flagged): charter agents + run ledger + Ask the council, and the last emoji leave the chrome

2026-07-03

Constellation Map: the knowledge graph as a night sky, plus chat suggested prompts

2026-07-03

App redesign phase 1: grouped sidebar, Home tab, real deep links, brand illustrations

2026-07-03

Routing & redirect fixes: connectors land back where you left, no more base-path 404s

2026-07-02

Website connector rebuilt Firecrawl-style: real markdown, sitemap crawling, JS rendering

2026-06-28

Stop re-cloning needlessly: SHA-skip on re-ingest + no-clone Overview rebuild

2026-06-28

The brain now knows WHAT a project is — auto product Overview + CPU-throttling fix

2026-06-28

Chat history persistence — conversations survive refresh + a real thread list

2026-06-28

Blast-radius graph: fix overlapping node labels

2026-06-28

Visual blast-radius in chat + live connector read-through (Jira/Linear/Slack)

2026-06-28

Impact: pick the target from a dropdown (+ chat already answers blast-radius)

2026-06-28

Ingest activity feed: human-readable progress + more robust domain-plan parsing

2026-06-28

Fix: re-sync a GitHub App installation after account delete + recreate

2026-06-28

Large-repo ingest: chunked, media-skipping clone with a per-chunk timeout

2026-06-27

Graph: filter by ingestion source + cross-source "knows why" links between discussions and code

2026-06-27

Fix: Impact / blast-radius silently went blank after every deploy (code-graph artifacts weren't durable)

2026-06-27

Pick a connected repo/PR instead of typing it + a blast-radius graph in Impact

2026-06-27

Login right panel — broadened beyond "repo" + scoped microanimations

2026-06-27

AI Code Review section — animated preview mockups replace the static images

2026-06-27

Ingest "Add source" redesigned — dense 3-column layout (no more dead whitespace)

2026-06-27

Fix: Connectors grid showed "Connect" for already-connected GitHub/Slack/Google

2026-06-27

Connector setup guides + links; connectors no longer "coming soon"

2026-06-27

Ingest tab restructure + Connectors sync

2026-06-27

Unified ⚡ Ingest tab (Ingest + Feed + Monitor merged)

2026-06-27

Impact analysis, real Slack ingest, docs-on-merge + repositioning to "the team brain that knows why"

2026-06-25

Critical fix: bare-slug repo ingest no longer silently produces an empty brain

2026-06-25

Code Review = feature, not a separate tier + light section imagery

2026-06-25

AI Code Review → Bito parity: PR summary, one-click fixes, secret scanning

2026-06-25

Distinct routes: / (landing), /login, /app

2026-06-25

Fix: landing→app flash for signed-in users

2026-06-24

AI Code Review — landing story + in-app discovery + Higgsfield visuals

2026-06-24

Launch asset: 30s SaaS launch video — real product UI, in code (light theme)

2026-06-24

Code Reviews tab — submit a PR for AI review from the app (BYOK UI)

2026-06-24

AI Code Review on Pull Requests (BYOK review engine)

2026-06-23

Launch asset (v8, final): 2-minute claymation promo film — recurring cast + last-frame chaining

2026-06-23

Launch asset: 2-minute claymation promo film "Monday Gets a Brain"

2026-06-18

Monitor: job picker — see every parallel job, not just one

2026-06-18

Fix: ingested repo now appears in the wiki (GitHub chip)

2026-06-18

Chat can show real source code + demo switched to nanoid

2026-06-18

Billing crash fix + Tree click fix + removable token quota

2026-06-18

Connected knowledge graph + chat-able repo + readable graph

2026-06-18

Wiki: filter by connector + readable page names

2026-06-18

Monitor counter no longer freezes at 1/N + ingest survives bad LLM categories

2026-06-18

Fixed tab layout shift + bigger top-right toasts

2026-06-18

Demo-data mode — backend (feature flag, sample dataset, ingest endpoint)

2026-06-18

New Connectors tab + chat/header polish + WhatsApp sample-data mode

2026-06-18

WhatsApp connector — QR link, chat picker, sync into the brain

2026-06-10

Enrichment hardened — compulsory LLM digestion + chunked map-reduce

2026-06-10

LLM enrichment for non-code ingestion

2026-06-10

Landing page refresh — 11-connector copy, Skills & Process Map section, features catalog link

2026-06-10

Skills tab — compiled-skills library, process map & chat-triggered skill creation

2026-06-10

Workspaces tab — management panel & active-workspace switcher

2026-06-10

Settings — connector credential forms (Notion / Linear / Jira / Zendesk / Databases)

2026-06-10

Feed tab — drag-and-drop "feed your brain" UI

2026-06-10

Auto-compiled skills after every ingest

2026-06-10

Team workspaces & query-time ACL enforcement v1

2026-06-10

Company Brain expansion build