Changelog

All notable changes to Pinky Brain. Format based on Keep a Changelog; versioning follows SemVer.

[Unreleased]

Nothing yet.

[0.3.0] — 2026-08-13

The graph release. Until now a brain was a bag of documents and a query had to hit the right one head-on. The links between entries — [[wikilinks]], the // Brain: breadcrumbs left in your code, supersedes — are now indexed, traversed at search time and measured: search reaches entries the query alone could not, the hooks answer from the file in front of you, and pinky graph report says what shape your knowledge has. Schema v9 → v12, migrated without losing indexed data or the usage telemetry that is not regenerable from the .md files. It also ships two things a brain should have had from day one: a shared embeddings daemon (three open chats went from 3.7 GB to 1.23 GB of RSS) and secrets redaction on write — a knowledge base is the worst possible place for a credential, because it exists to resurface things.

Changed

  • A diary no longer represents a community. Diaries mention everything that happened that day, so they collect links and top any centrality ranking — true for the topology, useless as an answer to "what is this cluster about". On a real 203-entry brain the two biggest clusters were both represented by a date; now they are represented by the knowledge in them. Diaries keep their edges and their centrality: they are only skipped when picking who speaks for a group, and a community made only of diaries still gets one (a date beats a faceless cluster).

Added — the graph shapes what you get back

  • Diversification by community: past 2 hits from the same cluster, the rest of the window is demoted (not dropped). One dense corner of the brain stops answering the whole question; a cluster that genuinely owns the answer still wins. Same budget — it rearranges the window, never grows it.
  • pinky graph report: the shape of a brain in prose — clusters with their hubs, the load-bearing entries, what is connected to nothing, and the links that point nowhere. --out GRAPH_REPORT.md or stdout.
  • Core knowledge at session start: the hook now names the few entries the rest of the brain hangs off (highest betweenness). Different from "most used" — that is what the agent reached for lately, this is what the knowledge is built around. Silent when no shape has been computed.
  • hybrid+expand+diversify is a row in pinky eval --matrix. On the golden set it matches plain hybrid exactly (hit@5 1.000 · MRR 0.981 · nDCG 0.986): the graph-aware ranking costs nothing on the cases that corpus can express.

Both ranking steps are off with PINKY_GRAPH_EXPAND=0.

Added — the shape of the graph (schema v12)

  • Communities (Louvain): entries clustered by edge density alone — no embeddings, no similarity threshold, no model. On a real 130-entry brain: 39 communities labelled from the members' own tags (acme-store, brain, loop-hygiene, rate-limit…), plus 31 entries connected to nothing.
  • Centrality (exact Brandes betweenness): the documents most paths run through — each community's "hub", the handful everything else hangs off. Normalized 0..1 so it means the same in a brain of 50 and one of 5 000; above 5 000 nodes it falls back to degree and says so instead of quietly changing meaning.
  • pinky graph build recomputes edges + shape; pinky graph communities lists them; pinky doctor reports the count. reindex keeps it fresh.
  • New graph_node / community tables, derived like edge and rebuilt from the .md files. The migration only adds them: indexed data and usage telemetry survive.

Determinism over optimality. Louvain is order-dependent and usually shuffles; here nodes are visited in a fixed order, there is no RNG, and community ids are re-assigned by (size desc, first member) — so a rebuild over an unchanged graph is byte-identical and community 0 is always the biggest cluster. These ids end up in reports and ranking decisions someone will diff, which is worth more than the last 1% of modularity.

Added — the gate for the graph expansion

  • hybrid+expand is now a row in the variant matrix (pinky eval --matrix) and pinky eval --expand gates on it. CI runs testdata/golden.json twice — plain and expanded — against the same thresholds: the expansion takes result slots, so it has to earn them. A drop there means the hop constants got too generous.
  • testdata/golden-graph.json: cases whose query matches one entry head-on while the relevant document is a different one, reachable only by following an edge. Plus two fixtures (guide-edge-tls-rotation, decision-queue-broker) that give testdata/ a real [[wikilink]] graph.
  • crates/pinky-core/tests/graph_recall.rs: the gate that measures those cases — 3/3 unreachable without the expansion, 3/3 reached with it, same number of results either way, and provenance on every entry the graph put on screen.

Why the recall gate is a test and not a pinky eval run

A golden set cannot express "unreachable" over a ten-document corpus, and measuring it revealed that: golden-graph.json scores ~1.0 against testdata/ with the expansion off. Not a bug in the feature — every engine returns everything it has, so the "far" document is a normal candidate sitting at position 3. Recall only becomes measurable once the corpus outgrows the fetch window and the ranking has a tail, so the test builds one (the real fixtures plus generated filler) instead of committing 80 noise files and moving every other baseline. Measured on that corpus, all three cases go from absent to present, and the displaced hit is always the weakest one.

Added — the graph answers about code files (no query, no model)

  • pre-read/pre-write hooks now start from the file itself. They resolve the // Brain: <slug> breadcrumbs inside the file being read or written, and only fall back to the old keyword guess when it has none. A breadcrumb is knowledge someone wrote down about that file; the lexical path could only turn auth/login_controller.rs into the words "auth login controller" and hope BM25 found something.
  • brain_context(file) MCP tool — same path, on demand. Opt-in: not in the default PINKY_MCP_TOOLS set, so it costs no tool-list tokens.
  • pinky graph of <slug> lists an entry's edges with kind, confidence and the evidence behind each; pinky graph of --file <path> shows what the brain knows about a code file. Absolute paths (what a hook or editor knows) match the relative paths the scan stored.
  • None of it touches the embeddings model: it answers with the shared daemon asleep and in airgapped installs.

Fixed

  • Git worktrees no longer multiply the graph. backlinks scan only skipped .git, target and node_modules, so .claude/worktrees/* — a full copy of the repo per branch — was scanned once per worktree. One real repo: 782 breadcrumbs where there were 12, and a single relation showing up as 60 edges. Hidden directories are now skipped wholesale, the same rule index::reindex already used.

Added — graph expansion in retrieval

  • **brain_search and pinky search now reach entries the query alone could not. An entry one explained hop from a strong hit — linked by a [[wikilink]], named from the same code file, or the replacement of a superseded hit — can now enter the results. link_boost only ever re-ordered entries that had already matched; this changes recall**.
  • Budget-neutral: neighbours compete for the existing limit slots (at most 2), so a response never grows by a single token.
  • Every expanded hit says why it is there: ↳ via [[slug]] (wikilink) in the MCP and CLI output, "via" in --json. A caller that cannot tell "matched your query" from "one hop away" will cite the second as the first.
  • Ambiguous edges never invite an entry, and neighbours pass the same MetaFilter as any other result (diary stays out by default) — checked against the filter before they can consume one of the two slots.
  • Off with PINKY_GRAPH_EXPAND=0.

Fixed

  • A superseded entry's replacement is now reachable. The penalty demoted the outdated entry to a quarter of its score and left it at that: the entry that replaced it stayed invisible unless it happened to match on its own. It is now pulled in from any retrieved candidate, inheriting the score the query actually produced before the penalty — the penalty means "do not show the outdated entry", not "this was irrelevant".
  • Ties in the ranking are no longer resolved at random. Equally-scored entries came out in HashMap iteration order, so the same query over an unchanged index could answer in a different order on every run. They now break by entry id — invisible in a demo, but it made the eval harness noisy and any A/B of a retrieval change unreadable.

Added — secrets are redacted on write (#57)

  • A credential no longer survives a save. The rule tells agents to save as soon as they discover something; when the discovery is "the deploy failed with the wrong token", the token used to be written verbatim into the .md, indexed into brain.db and re-surfaced by every brain_search from then on. A knowledge base is the worst place for a secret — it exists to resurface things.
  • New pinky_core::redact: sk-ant-…, sk-…, sk_live_…, ghp_…/gho_…/ ghs_…/github_pat_…, AKIA…/ASIA…, xox[bpasr]-…, AIza…, JWTs, PEM private key blocks and the password of a scheme://user:pass@host URL become …REDACTED — keeping the prefix, so the note still says which credential it was about, and keeping the URL's user and host.
  • Applied at two seams: save::validated() (every write path — CLI save, brain_save, brain_update), so the .md on disk is clean; and before chunking on index, so a reindex of an already-dirty legacy .md cannot carry the secret into the index.
  • No entropy heuristics: every rule is a prefix plus a shape, so a sha256, a UUID, a commit hash, postgres://localhost/dev or the phrase "the sk- prefix" come out byte-identical. Corrupting real knowledge to catch a hypothetical secret would be the worse failure.
  • pinky redact [folder] [--dry-run] backfills a corpus written before this existed. --dry-run writes nothing and exits ≠0 when it finds something (CI gate); a real run archives each original under .archive/ first, so it is reversible.

Fixed — one bad frontmatter field no longer costs the whole block (#63)

  • A malformed field is now isolated. parse_doc used to discard the entire frontmatter when any field failed to deserialize, so a single wrong line silently cost the entry its type, tags, project and dates. Fields are now probed one by one: only the unusable ones fall back to their default, the warning names them, and pinky lint reports the field instead of a flat "invalid YAML".
  • supersedes accepts a list (schema v11). supersedes:\n - a.md\n - b.md — what a real brain ends up containing when one note consolidates several older ones — used to fail with invalid type: sequence, expected a string and take the rest of the frontmatter with it. It is now a JSON array in entry.supersedes, resolves to one graph edge per replaced path, and every one of them is demoted in the ranking. Existing indexes migrate in place (no reindex needed); a single path still renders as a scalar.
  • tags (and supersedes) also accept a bare scalar: tags: postgres.

Changed — the golden set grew from 16 to 35 cases (#58)

  • The relevance gate now runs against 35 golden cases over the 8 testdata/ documents (was 16): every document carries the literal query, the paraphrase with no shared vocabulary, and — for the long runbook — one query per section, which is what actually exercises heading-aware chunking. The hard cases are the ones that disambiguate neighbouring topics ("retries" lives in both the retry pattern and the runbook's queue section).
  • Both CI gates stay green, with more margin than before: hash embedder hit@5 1.000 / MRR 0.981 (gate 0.9 / 0.85), real model 1.000 / 1.000 (gate 0.95 / 0.9). Now that ties break deterministically, those numbers are stable run to run instead of flapping.

Added — resolved knowledge graph (schema v10)

  • edge table: the graph the index always had the raw material for, now resolved once instead of at every query. Built from [[wikilinks]], the // Brain: <slug> code breadcrumbs and supersedes, and rebuilt on every reindex, save and backlinks scan — so it never disagrees with the index.
  • Every edge carries why it exists (kind) and how much it is trusted (conf): extracted when it resolves to exactly one entry, ambiguous when several entries share the basename. That last case used to be resolved silently as "the first one wins"; it is now recorded, and callers can use it to re-rank but never to pull a new entry into a result set.
  • Wikilinks that resolve to nothing are counted as dangling instead of disappearing (28 real entries in a project: 22 edges and 4 dangling links).
  • A code file naming more than 5 entries is treated as an index, not as evidence that they are related: no clique, no flattened graph.
  • pinky doctor reports the graph (edge count by kind, or "not built yet").
  • Resolution is deterministic: candidates are sorted, so a collision always resolves the same way and a rebuild over unchanged input is a no-op.

This is the foundation for graph-based retrieval; nothing traverses it yet, so search behaves exactly as before. See docs/TECH-DESIGN-knowledge-graph.md.

Changed — migrations

  • A schema bump that only ADDS a derived table no longer wipes the index. The migration used to drop every derived table on any version change; v9 → v10 only creates edge, so indexed data survives — and with it the usage telemetry, which is not regenerable from the .md files and feeds the usefulness signal and eval mine. Migrations from below v9 still rebuild.

Added — shared embeddings daemon

  • pinky embed-daemon: the embeddings model now lives in **one process per machine** instead of one per chat. pinky-mcp and the CLI talk to it over a unix socket (~/.pinky/embed.sock, PINKY_EMBED_SOCKET) and hold no model themselves.
    • Auto-spawn: the first search starts the daemon; nothing to configure, no .mcp.json change, no launchd/brew service.
    • Single instance: the daemon binds the socket before loading the model, so racing clients that lose the bind exit without allocating anything.
    • Idle exit after PINKY_EMBED_IDLE_SECS (default 900) — the memory comes back when you stop searching. --status / --stop inspect and release it.
    • Fallback: if the daemon is unreachable, the caller loads the model in-process as before, so search never breaks because of IPC.
  • Lazy embedder (LazyEmbedder): nothing is loaded until a tool actually needs a vector. model_id()/dim() — the startup index check and brain_stats — answer without loading. A chat that never queries the brain now costs ~10 MB instead of ~1.2 GB.
  • pinky doctor reports the daemon's state (model, dim, pid, socket).

Changed — model cache

  • FASTEMBED_CACHE_DIR is pinned centrally to ~/.pinky/models. fastembed's default is ./.fastembed_cache relative to the cwd, and MCP servers run with the cwd set to each project: that produced one 465 MB copy of the model per repository. An existing cache (~/.fastembed_cache or the project's) is reused before falling back to the central path, so upgrading re-downloads nothing. doctor --airgap now checks the resolved cache instead of a hardcoded .fastembed_cache.

Why the daemon

Measured with 0.2.2 on macOS: a single pinky-mcp sat at 1.24 GB RSS right after initialize, before any tool call (470 MB of fp32 ONNX weights plus ORT's optimized graph and the protobuf parse fragmentation, all private heap — nothing shareable between processes). One process per open chat meant ~5 GB with 4 chats. Measured after this change, three chats running a real brain_search: 3 × 11 MB + one 1.2 GB daemon = 1.23 GB total (was 3.7 GB), and 0 MB while no chat searches.

[0.2.2] — 2026-07-17

The token-aware release. Adds the missing knobs to push-inject context only when it has value, and to keep the agent's brain_search responses lean. All opt-in to keep it off in interactive single sessions.

Added — hooks

  • PINKY_HEADLESS env var / headless config key (1/true/yes/on): silences the push injections of the session-start, pre-read, pre-write and post-bash hooks (they return Ok(()) before opening the index). The brain_search MCP tool (pull model) keeps working. Meant for agent orchestration (claude -p per stage = a new session each), where session-start fires per stage and pre-read per Read: hundreds of micro-sessions × several reads multiplied thousands of additive tokens without substituting what the agent was going to read anyway.
  • PINKY_HOOK_HITS env var / hook_hits config key: how many hits each pre-*/post-bash hook injects. Default 3 (historical); range [0, 10]. 0 = clean no-op (no injection, no error).
  • PINKY_HOOK_SNIPPET env var / hook_snippet config key: snippet length (chars) per hit. Default 240; range [40, 1024]. Smaller = fewer tokens per injection at the cost of less useful context.

Added — MCP server

  • PINKY_MCP_TOOLS env var: a comma-list of tool names to expose (default search,save,stats); * restores all 7. Saves ~1.6k tok/turn of system-prompt schema for read-only / deep agents that never curate. Curation tools (brain_update, brain_delete, brain_feedback, brain_similar) become opt-in.
  • brain_search compact format (default): title · id:<id> + snippet — no score, no path. Callers pass verbose=true to restore the historical full line with score + path.
  • brain_search excludes type: diary by default: session logs from the stop hook share vocabulary with the real knowledge → BM25 ranks them as competitive noise. Opt back in via include_diary=true or --type diary.
  • brain_search per-session dedup (dedupe_session, default true): drops entry_ids already served this session (via hooks or prior brain_search). The usage table already records session_id; we now read it back to clean redundant hits.
  • brain_stats compact format (default): just entries + chunks. The model, embedder and per-type breakdown are behind verbose=true, avoiding ~250–600 chars of unused diagnostic per call.

Added — CLI

  • pinky search gains --exclude-types (comma list; e.g. diary,note), --compact (token-aware format, mirroring the MCP default), and --no-color (force-disable ANSI).
  • ANSI colours in pinky search when stdout is a TTY: score pink, type-icon cyan, snippet dim, path muted. Respects NO_COLOR / CLICOLOR=0.
  • Per-type glyphs in pinky search so the human can spot gotchas at a glance: gotcha, pattern, decision, · diary, 📖 guide, note.
  • pinky doctor now reports the state of PINKY_HEADLESS, PINKY_HOOK_HITS, PINKY_HOOK_SNIPPET, PINKY_MCP_TOOLS so users can sanity-check why their agent's behaviour changed.

Added — core

  • MetaFilter.exclude_types (NOT IN) clause in the search engine; lets brain_search (and CLI) default-exclude a set of entry types cleanly.
  • SearchOptions.snippet_len (already shipped in v0.1.5 but now plumbed through both callers): a per-call snippet length in chars (default 240).
  • All three new config keys (headless, hook_hits, hook_snippet) are accepted by ~/.pinky/config.toml.

Changed

  • stop hook now respects PINKY_HEADLESS=1: closes the largest latent leak. The diary is for human sessions (the next morning's breadcrumb); in headless orchestration it was spamming type: diary entries into the brain, which brain_search then served as if knowledge. With the new default in acme-factory (PINKY_HEADLESS=1 per claude -p), the diary no longer pollutes the index.
  • Per-session dedup in pre-read/pre-write/post-bash: the hooks no longer re-inject an entry_id already served to the same session. The session_id was already arriving in the payload (and written to usage for telemetry); now it's also read back to drop redundant hits. Two Reads that derive queries matching the same gotcha (e.g. auth/login.rs + auth/logout.rs) won't re-inject it. On by default; no env var.
  • brain_search default limit 10 → 3; MAX_LIMIT 100 → 20 (an interactive agent asking for >20 hits is worse off with the extra context than without).

Site

  • New "Token-aware by default" section in the landing (6 benefit cards covering the new env knobs).
  • Nav gains a "Token-aware" link.
  • Feature card "Agent-native" updated to reflect the 3-by-default toolset.
  • Benefit card updated from "7 tools" → "Configurable toolset".
  • Access card MCP now shows the 3 default tools with checkmarks and the PINKY_MCP_TOOLS=* escape hatch.

[0.2.1] — 2026-07-13

Fixed

  • Hooks now write to the central index. pinky-hooks resolved the database as a CWD-relative brain.db (and settings.example.json forced PINKY_DB=brain.db), so session-start / stop / pre-* / post-bash read and wrote a per-project index instead of the central ~/.pinky/brain.db the CLI and MCP already defaulted to — the central-index migration had missed the hooks. Now open_store() uses config::default_db_path() (the same resolution as the CLI and the MCP), session-start falls back to the persisted active project when PINKY_PROJECT is unset, and the hooks example no longer pins PINKY_DB.

[0.2.0] — 2026-07-11

Added

  • Central per-project index (~/.pinky/brain.db, schema v9): a single index for every project, partitioned by scope (project:<name>), with a persisted active project. New pinky project subcommands — add, list, use, current, remove, and import (register a project and index its documentation/ into the central DB). The CLI, the MCP, and pinky init all default to this shared index.
  • Interactive TUI console (pinky with no subcommand): live search, a preview pane, help and modals, with localized chrome (ES/EN/IT/PT).

Changed

  • Repository migrated to English + public-release cleanup.
  • Dependency bump: crossbeam-epoch 0.9.18 → 0.9.20 (RUSTSEC-2026-0204).

[0.1.5] — 2026-07-05

Added

  • pinky update [--check] [--force] — self-update: compares the local version against the CDN's VERSION pointer (numeric comparison) and, if a newer one exists, runs the official installer (minisign signature + SHA-256 checksum) replacing the binary in its current folder. --check only reports; --force reinstalls anyway. Reuses curl/sh, no new dependencies.
  • Release notes on the web: /docs/changelog.html (4 languages) generated from this CHANGELOG.md, plus a version badge on the landing linking there.

Site

  • Landing with a "smart memory" section (temporal knowledge, implicit usefulness signal, Obsidian graph, sync, 7 MCP tools, CI-gated quality), a measured-metrics strip (binary size, RAM, p95, nDCG), and a Docs tab.
  • Truthfulness fixes: the landing showed 3 MCP tools (there are 7) and the model as ~100 MB (measured: ~450 MB, optional).

[0.1.4] — 2026-07-04

Three batches of the evolution plan (P5–P8, PRs #16–#47). Index schema: v8 (destructive migration of derived tables; reindex after updating — the .md files are untouched).

Retrieval quality

  • Eval v2: variant matrix (lexical/hybrid/±rerank/±decay), hit@k + MRR + nDCG metrics, and pinky eval --mine (proposes golden cases from real telemetry). CI gates: 0.9/0.85 with hash-embed, 0.95/0.9 with the model.
  • Multi-column FTS (title/tags/text with BM25 weights) + native snippet().
  • Heading-aware chunking: the embedding carries «title > heading» context; it also fixes the cumulative-RRF bug (max-passage per entry).
  • Snowball stemming ES/EN/IT/PT in a shadow column.
  • Staleness decay with per-type half-life (gotcha 90d, diary 30d, decision/pattern 365d, guide exempt).
  • supersedes + search --as-of: temporal facts; "what did we know then".

Agent loop

  • MCP: brain_feedback, brain_update, brain_delete (archives to .archive/), brain_similar, anti-duplicate in brain_save, and resources pinky://entry/….
  • Hooks: session-start with real delta + top, pre-* record usage with session_id, post-bash ("you already hit this landmine"), stop extracts decisions/commits from the transcript and correlates implicit usefulness (what the agent actually re-read → useful=1).
  • pinky stats --value: value report (real usage, confirmed usefulness, noise).
  • pinky new (per-type templates), pinky review (batch triage), dedup v2 (centroid + lexical MinHash).

Product & operations

  • pinky snapshot/restore: verifiable backup (tar.gz + SHA-256 manifest, verify-then-write, destination outside the working tree).
  • pinky sync v2: auto-commit without versioning brain.db, rebase with auto-resolution of diaries and frontmatter conflicts.
  • pinky bench: reproducible benchmarks (1k→8.7ms · 10k→53ms · 100k→495ms p95); see docs/BENCHMARKS.md.
  • pinky doctor --airgap: verifies 100% offline operation (fails if anything would need the network).
  • docs/VERSIONING.md + downgrade rejection: an old binary over a newer-schema index now fails clearly instead of operating silently.
  • Central config ~/.pinky/config.toml (thin layer); pinky init --demo.
  • Full i18n (ES/EN/IT/PT) also in MCP and hooks; shared catalog.

Security & supply chain

  • Opt-in index encryption (encrypted-index, SQLCipher + PINKY_INDEX_KEY); see docs/SECURITY.md.
  • cargo-deny (deny.toml) in CI, auditable binaries (cargo auditable), CycloneDX SBOM per release, actions pinned by SHA, publish = false.
  • Reproducible builds: rust-toolchain.toml (1.96.1) + remap-path-prefix + a CI job that builds twice and compares sha256.

Interop

  • Obsidian: [[wikilinks]] are parsed on indexing (schema v8, link table); pinky search --links applies an opt-in 1-hop boost; pinky lint reports broken wikilinks as a warning (tolerant).

Release infra

  • CI for site deployment + signed (minisign) publishing to R2, CDN monitoring, and scripts/release-local.sh for cutting releases without GitHub Actions.

[0.1.3] — 2026-07-03

Added

  • pinky init truly integrates Claude Code: installs the use-pinky-brain.md rule in .claude/rules/ and wires it via @import in CLAUDE.md (--global for ~/.claude/); merges hooks into .claude/settings.json (with confirmation; --hooks/--no-hooks); and downloads the embedding model during init so the first reindex is instant (--no-model to skip it). New flags: --force, --global, --hooks, --no-hooks, --no-model.

Fixed

  • pinky-hooks stop: Claude Code's Stop event doesn't send author/summary (the hook read them and the diary always came out empty/useless). It now reads transcript_path and extracts the summary from the assistant's last message.
  • Landing: command examples (hero query, result rows, pinky search example) now change with the selected language (ES/EN/IT/PT).

[0.1.2] — 2026-07-03

Added

  • Multi-language CLI (ES/EN/IT/PT): pinky localizes the full --help and all messages/errors. Language resolves via PINKY_LANG~/.config/pinky/config → system locale (LANG) → es.
  • install.sh asks for the CLI language on install (or takes it from PINKY_LANG) and stores it in ~/.config/pinky/config.
  • Multi-language web documentation (pinkybrain.dev/docs, ES/EN/IT/PT) generated from the .md files with make docs.

[0.1.1] — 2026-07-03

Web/branding only — the binaries' code did not change from 0.1.0.

  • Public landing at pinkybrain.dev (Cloudflare Pages), multi-language (ES/EN/IT/PT) with a switcher, a custom logo (chunk grid), a measured token-metrics band, a benefits section, and contact@pinkybrain.dev.

[0.1.0] — 2026-07-03

First release. Local knowledge engine (Phases 0–3 + P0–P4). See docs/PROD-ROADMAP.md for the per-phase detail.

Data integrity (P4)

  • Entry identity by (scope, path) (schema v2). The id derives from stable_id(scope, path) and there is a UNIQUE(scope, path): the same relative path across two scopes (e.g. diary/2026-06-30.md global and per-project) no longer collides or overwrites in the PK. Automatic v1→v2 migration (rebuilds derived tables) with a visible notice asking to reindex (the brain never goes silently "empty"), and a version stamp that only moves forward (an old binary can't downgrade a newer index).
  • Consistent type read↔write: normalized (trim + lowercase) both on indexing and filtering, and in brain_search's scope a bare name is interpreted as project:<name>. Absolute cap on search candidates.
  • Incremental reindex by (scope, path): the hash no longer crosses scopes (it used to skip or overwrite homonymous entries from another scope).
  • Telemetry cleanup on deleting/rewriting an entry (usage no longer leaves orphan rows that bias pruning).
  • File size limit on indexing (5 MB): a giant .md is skipped with a warning instead of OOM-ing (defense in reindex/watch/sync).

Retrieval

  • No-search-terms guard: a query made only of punctuation/symbols now returns empty instead of noise (the zero vector used to bring back arbitrary chunks with score > 0).
  • fetch derives from limit: requesting a high limit no longer silently truncates recall.
  • chunk_body measures in chars (not bytes): chunking stays consistent with accented ES/IT text.
  • Invalid YAML frontmatter is no longer silently discarded: it emits a warn (and pinky lint catches it).

DX & product

  • --version in all three binaries and in pinky doctor.
  • --json on every command with stdout output (reindex, eval, dedup, stale, telemetry, save, backlinks); in search it also disables telemetry (programmatic use).
  • PINKY_DB/PINKY_HASH_EMBED honored by the CLI (it used to ignore them): the CLI and the agent see the same base. Precedence: flag > env > default.
  • Read-only commands fail clearly if the index doesn't exist (they used to create an empty one and report "0 entries").
  • Actionable first-run errors (model failed to load → suggests --hash-embed).
  • type validation against the canonical set in save/brain_save (normalizes case; rejects typos that would create unfilterable entries).
  • pinky completions {bash,zsh,fish} (autocompletion) and input caps in brain_search (limit clamp 1..100), scope filter, enriched brain_stats (model + per-type breakdown).
  • brain_save doesn't leave an orphan .md if indexing fails; an unparseable JSON-RPC request answers parse error (-32700) instead of being ignored.

Hooks

  • Time budget (800 ms) in pre-read/pre-write: they degrade to no-op instead of blocking a Claude Code tool.
  • Never create a phantom DB: if there's no index, the hook is a no-op (it used to litter every repo).
  • Atomic diary append (O_APPEND): concurrent stops no longer overwrite each other.
  • Versioned registration example in hooks/settings.example.json.

Quality, CI & packaging

  • CI runs the eval as a relevance gate (fails the build on a ranking regression), with the golden set expanded to 13 cases (ES + IT, paraphrases) and an OS matrix (Linux + macOS).
  • Release with --locked (reproducible) + per-artifact SHA256 checksums.
  • install.sh (curl | sh): detects OS/arch, verifies the checksum, and installs all 3 binaries.
  • Product documentation: conversion-oriented README, docs/INSTALL.md, docs/CONFIGURATION.md.
  • New coverage: schema migration, multi-scope reindex, type validation, rerank.

Added (core)

  • Data integrity (P0): schema versioning (PRAGMA user_version + meta table); embedding-model guard (Embedder::model_id() + reconcile_model/check_model) preventing vectors from different models from mixing; busy_timeout for CLI + MCP + hooks concurrency.
  • Multi-project (P0): metadata filters (scope/project/type/tags) in search, exposed via pinky search --project/--type/--tag and in brain_search.
  • Operability (P1): tracing-based logging to stderr (PINKY_LOG); CI on GitHub Actions (fmt + clippy -D warnings + tests + real E2E).
  • Fast hooks (P1): search::lexical (BM25) for pre-read/pre-write without loading the model; FastEmbedder::new() retries on model-cache lock contention; usage telemetry also from brain_search.
  • Product (P2): pinky init (scaffolding + relocatable MCP registration in .mcp.json); pinky lint (validates frontmatter, exit ≠0 on errors); --json in search/stats/doctor; hardened pinky doctor (schema, index model vs. active); input caps in the MCP server; LICENSE (MIT).

Fixed

  • brain_save indexed with the server's scope and ignored the argument's project (unfilterable); now consistent with pinky save.
  • pinky stats loaded the embedding model without needing it.
  • Root-path containment in save (defense in depth).

Base (initial core)

Hybrid SQLite index (FTS5 + sqlite-vec, RRF), pinky CLI, MCP server, Claude Code hooks, and maintenance (dedup, staleness, backlinks, rollups, eval).