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.mdor 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+diversifyis a row inpinky eval --matrix. On the golden set it matches plainhybridexactly (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 buildrecomputes edges + shape;pinky graph communitieslists them;pinky doctorreports the count.reindexkeeps it fresh.- New
graph_node/communitytables, derived likeedgeand rebuilt from the.mdfiles. The migration only adds them: indexed data andusagetelemetry 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+expandis now a row in the variant matrix (pinky eval --matrix) andpinky eval --expandgates on it. CI runstestdata/golden.jsontwice — 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 givetestdata/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-writehooks 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 turnauth/login_controller.rsinto the words "auth login controller" and hope BM25 found something.brain_context(file)MCP tool — same path, on demand. Opt-in: not in the defaultPINKY_MCP_TOOLSset, 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 scanonly skipped.git,targetandnode_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 ruleindex::reindexalready used.
Added — graph expansion in retrieval
- **
brain_searchandpinky searchnow 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_boostonly ever re-ordered entries that had already matched; this changes recall**. - Budget-neutral: neighbours compete for the existing
limitslots (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
MetaFilteras any other result (diarystays 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
HashMapiteration 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 intobrain.dband re-surfaced by everybrain_searchfrom 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 ascheme://user:pass@hostURL 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 — CLIsave,brain_save,brain_update), so the.mdon disk is clean; and before chunking on index, so a reindex of an already-dirty legacy.mdcannot 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/devor the phrase "thesk-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-runwrites 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_docused to discard the entire frontmatter when any field failed to deserialize, so a single wrong line silently cost the entry itstype,tags,projectand dates. Fields are now probed one by one: only the unusable ones fall back to their default, the warning names them, andpinky lintreports the field instead of a flat "invalid YAML". supersedesaccepts 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 withinvalid type: sequence, expected a stringand take the rest of the frontmatter with it. It is now a JSON array inentry.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(andsupersedes) 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)
edgetable: 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 andsupersedes, and rebuilt on every reindex, save andbacklinks scan— so it never disagrees with the index.- Every edge carries why it exists (
kind) and how much it is trusted (conf):extractedwhen it resolves to exactly one entry,ambiguouswhen 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 doctorreports 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 theusagetelemetry, which is not regenerable from the.mdfiles and feeds the usefulness signal andeval 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-mcpand 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.jsonchange, 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/--stopinspect 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.
- Auto-spawn: the first search starts the daemon; nothing to configure, no
- Lazy embedder (
LazyEmbedder): nothing is loaded until a tool actually needs a vector.model_id()/dim()— the startup index check andbrain_stats— answer without loading. A chat that never queries the brain now costs ~10 MB instead of ~1.2 GB. pinky doctorreports the daemon's state (model, dim, pid, socket).
Changed — model cache
FASTEMBED_CACHE_DIRis pinned centrally to~/.pinky/models. fastembed's default is./.fastembed_cacherelative 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_cacheor the project's) is reused before falling back to the central path, so upgrading re-downloads nothing.doctor --airgapnow 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_HEADLESSenv var /headlessconfig key (1/true/yes/on): silences the push injections of thesession-start,pre-read,pre-writeandpost-bashhooks (they returnOk(())before opening the index). Thebrain_searchMCP tool (pull model) keeps working. Meant for agent orchestration (claude -pper stage = a new session each), wheresession-startfires per stage andpre-readperRead: hundreds of micro-sessions × several reads multiplied thousands of additive tokens without substituting what the agent was going to read anyway.PINKY_HOOK_HITSenv var /hook_hitsconfig key: how many hits eachpre-*/post-bashhook injects. Default3(historical); range[0, 10].0= clean no-op (no injection, no error).PINKY_HOOK_SNIPPETenv var /hook_snippetconfig key: snippet length (chars) per hit. Default240; range[40, 1024]. Smaller = fewer tokens per injection at the cost of less useful context.
Added — MCP server
PINKY_MCP_TOOLSenv var: a comma-list of tool names to expose (defaultsearch,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_searchcompact format (default):title · id:<id>+ snippet — no score, no path. Callers passverbose=trueto restore the historical full line with score + path.brain_searchexcludestype: diaryby default: session logs from thestophook share vocabulary with the real knowledge → BM25 ranks them as competitive noise. Opt back in viainclude_diary=trueor--type diary.brain_searchper-session dedup (dedupe_session, default true): dropsentry_ids already served this session (via hooks or priorbrain_search). Theusagetable already recordssession_id; we now read it back to clean redundant hits.brain_statscompact format (default): justentries+chunks. The model, embedder and per-type breakdown are behindverbose=true, avoiding ~250–600 chars of unused diagnostic per call.
Added — CLI
pinky searchgains--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 searchwhen stdout is a TTY: score pink, type-icon cyan, snippet dim, path muted. RespectsNO_COLOR/CLICOLOR=0. - Per-type glyphs in
pinky searchso the human can spot gotchas at a glance:⚠gotcha,◆pattern,▸decision,·diary,📖guide,•note. pinky doctornow reports the state ofPINKY_HEADLESS,PINKY_HOOK_HITS,PINKY_HOOK_SNIPPET,PINKY_MCP_TOOLSso users can sanity-check why their agent's behaviour changed.
Added — core
MetaFilter.exclude_types(NOT IN) clause in the search engine; letsbrain_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 (default240).- All three new config keys (
headless,hook_hits,hook_snippet) are accepted by~/.pinky/config.toml.
Changed
stophook now respectsPINKY_HEADLESS=1: closes the largest latent leak. The diary is for human sessions (the next morning's breadcrumb); in headless orchestration it was spammingtype: diaryentries into the brain, whichbrain_searchthen served as if knowledge. With the new default inacme-factory(PINKY_HEADLESS=1perclaude -p), the diary no longer pollutes the index.- Per-session dedup in
pre-read/pre-write/post-bash: the hooks no longer re-inject anentry_idalready served to the same session. Thesession_idwas already arriving in the payload (and written tousagefor 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_searchdefault 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-hooksresolved the database as a CWD-relativebrain.db(andsettings.example.jsonforcedPINKY_DB=brain.db), sosession-start/stop/pre-*/post-bashread and wrote a per-project index instead of the central~/.pinky/brain.dbthe CLI and MCP already defaulted to — the central-index migration had missed the hooks. Nowopen_store()usesconfig::default_db_path()(the same resolution as the CLI and the MCP),session-startfalls back to the persisted active project whenPINKY_PROJECTis unset, and the hooks example no longer pinsPINKY_DB.
[0.2.0] — 2026-07-11
Added
- Central per-project index (
~/.pinky/brain.db, schema v9): a single index for every project, partitioned byscope(project:<name>), with a persisted active project. Newpinky projectsubcommands —add,list,use,current,remove, andimport(register a project and index itsdocumentation/into the central DB). The CLI, the MCP, andpinky initall default to this shared index. - Interactive TUI console (
pinkywith 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'sVERSIONpointer (numeric comparison) and, if a newer one exists, runs the official installer (minisign signature + SHA-256 checksum) replacing the binary in its current folder.--checkonly reports;--forcereinstalls anyway. Reusescurl/sh, no new dependencies.- Release notes on the web:
/docs/changelog.html(4 languages) generated from thisCHANGELOG.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 inbrain_save, and resourcespinky://entry/…. - Hooks:
session-startwith real delta + top,pre-*record usage withsession_id,post-bash("you already hit this landmine"),stopextracts 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 syncv2: auto-commit without versioningbrain.db, rebase with auto-resolution of diaries and frontmatter conflicts.pinky bench: reproducible benchmarks (1k→8.7ms · 10k→53ms · 100k→495ms p95); seedocs/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); seedocs/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,linktable);pinky search --linksapplies an opt-in 1-hop boost;pinky lintreports broken wikilinks as a warning (tolerant).
Release infra
- CI for site deployment + signed (minisign) publishing to R2, CDN monitoring, and
scripts/release-local.shfor cutting releases without GitHub Actions.
[0.1.3] — 2026-07-03
Added
pinky inittruly integrates Claude Code: installs theuse-pinky-brain.mdrule in.claude/rules/and wires it via@importinCLAUDE.md(--globalfor~/.claude/); merges hooks into.claude/settings.json(with confirmation;--hooks/--no-hooks); and downloads the embedding model during init so the firstreindexis instant (--no-modelto skip it). New flags:--force,--global,--hooks,--no-hooks,--no-model.
Fixed
pinky-hooks stop: Claude Code'sStopevent doesn't sendauthor/summary(the hook read them and the diary always came out empty/useless). It now readstranscript_pathand extracts the summary from the assistant's last message.- Landing: command examples (hero query, result rows,
pinky searchexample) now change with the selected language (ES/EN/IT/PT).
[0.1.2] — 2026-07-03
Added
- Multi-language CLI (ES/EN/IT/PT):
pinkylocalizes the full--helpand all messages/errors. Language resolves viaPINKY_LANG→~/.config/pinky/config→ system locale (LANG) →es. install.shasks for the CLI language on install (or takes it fromPINKY_LANG) and stores it in~/.config/pinky/config.- Multi-language web documentation (
pinkybrain.dev/docs, ES/EN/IT/PT) generated from the.mdfiles withmake 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, andcontact@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). Theidderives fromstable_id(scope, path)and there is aUNIQUE(scope, path): the same relative path across two scopes (e.g.diary/2026-06-30.mdglobal 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
typeread↔write: normalized (trim + lowercase) both on indexing and filtering, and inbrain_search's scope a bare name is interpreted asproject:<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 (
usageno longer leaves orphan rows that bias pruning). - File size limit on indexing (5 MB): a giant
.mdis skipped with a warning instead of OOM-ing (defense inreindex/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).
fetchderives fromlimit: requesting a highlimitno longer silently truncates recall.chunk_bodymeasures in chars (not bytes): chunking stays consistent with accented ES/IT text.- Invalid YAML frontmatter is no longer silently discarded: it emits a
warn(andpinky lintcatches it).
DX & product
--versionin all three binaries and inpinky doctor.--jsonon every command with stdout output (reindex, eval, dedup, stale, telemetry, save, backlinks); insearchit also disables telemetry (programmatic use).PINKY_DB/PINKY_HASH_EMBEDhonored 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). typevalidation against the canonical set insave/brain_save(normalizes case; rejects typos that would create unfilterable entries).pinky completions {bash,zsh,fish}(autocompletion) and input caps inbrain_search(limitclamp 1..100),scopefilter, enrichedbrain_stats(model + per-type breakdown).brain_savedoesn't leave an orphan.mdif indexing fails; an unparseable JSON-RPC request answersparse 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,
typevalidation,rerank.
Added (core)
- Data integrity (P0): schema versioning (
PRAGMA user_version+metatable); embedding-model guard (Embedder::model_id()+reconcile_model/check_model) preventing vectors from different models from mixing;busy_timeoutfor CLI + MCP + hooks concurrency. - Multi-project (P0): metadata filters (
scope/project/type/tags) in search, exposed viapinky search --project/--type/--tagand inbrain_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) forpre-read/pre-writewithout loading the model;FastEmbedder::new()retries on model-cache lock contention; usage telemetry also frombrain_search. - Product (P2):
pinky init(scaffolding + relocatable MCP registration in.mcp.json);pinky lint(validates frontmatter, exit ≠0 on errors);--jsoninsearch/stats/doctor; hardenedpinky doctor(schema, index model vs. active); input caps in the MCP server;LICENSE(MIT).
Fixed
brain_saveindexed with the server's scope and ignored the argument'sproject(unfilterable); now consistent withpinky save.pinky statsloaded 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).