Skip to content

Latest commit

 

History

History
480 lines (466 loc) · 398 KB

File metadata and controls

480 lines (466 loc) · 398 KB

Key files — per-file index (gbrain repo)

On-demand reference. CLAUDE.md (the always-loaded orientation file) routes here via its Reference map. Read a file's entry before editing that file.

Entries describe CURRENT behavior + load-bearing invariants only. Release history lives in CHANGELOG.md + git log / git blame, NOT here. Do not append per-release **vX.Y.Z:** narration — CI enforces this (scripts/check-key-files-current-state.sh).

  • src/core/operations.ts — Contract-first operation definitions (the foundation). Exports upload validators validateUploadPath, validatePageSlug, validateFilename, plus matchesSlugAllowList(slug, prefixes) (glob matcher: <prefix>/* matches recursive children; bare <prefix> matches exact only). OperationContext.remote is a REQUIRED field flagging untrusted callers; OperationContext.allowedSlugPrefixes is the trusted-workspace allow-list set by the dream cycle; OperationContext.auth?: AuthInfo is threaded through HTTP dispatch for scope enforcement in serve-http.ts before the op runs. put_page enforces: when viaSubagent and allowedSlugPrefixes is set, slug must match the allow-list; else the legacy wiki/agents/<id>/... namespace check applies. Auto-link skipped only when remote=true && !trustedWorkspace. Every Operation carries scope?: 'read' | 'write' | 'admin' + localOnly?: boolean; sync_brain, file_upload, file_list, file_url are admin + localOnly (rejected over HTTP). Four trust-boundary call sites (put_page allowlist, file_upload trust-narrowing, submit_job protected-name guard, auto-link skip) use FAIL-CLOSED semantics: ctx.remote === false for trusted-only sites, ctx.remote !== false for "untrust unless explicit-false" — anything not strictly false is treated as remote (closes the HTTP MCP shell-job RCE where a read+write OAuth token could submit shell jobs). sourceScopeOpts(ctx) encodes the source-scoped read precedence ladder — federated array (ctx.auth.allowedSources) wins over scalar (ctx.sourceId/ctx.auth.sourceId) over nothing; every read-side op handler routes through it so a source-bound OAuth client can't see neighboring sources via search/query/list_pages/get_page/find_experts/query's image path. put_page's inline disk write-through is the shared writePageThrough helper (src/core/write-through.ts), ATOMIC via temp-sibling + rename so a crash or concurrent gbrain sync can't read a half-written .md; same helper backs gbrain brainstorm/lsd --save. Link provenance surface (#1941): add_link (gbrain link/link-add) + remove_link (gbrain unlink/link-rm) expose link_source/link_type; add_link rejects the reconciliation-managed built-ins via MANAGED_LINK_SOURCES (markdown/frontmatter/mentions/wikilink-resolved) and defaults omitted provenance to 'manual' (the engine's own default stays 'markdown' for internal callers); list_link_sources (gbrain link-sources, read) lists provenances via sourceScopeOpts. CLI aliases register through cliHints.aliases (collision-guarded in src/cli.ts).
  • src/core/engine.ts — Pluggable engine interface (BrainEngine). clampSearchLimit(limit, default, cap) takes an explicit cap so per-operation caps can be tighter than MAX_SEARCH_LIMIT. Exports LinkBatchInput/TimelineBatchInput for the bulk-insert API (addLinksBatch/addTimelineEntriesBatch). readonly kind: 'postgres' | 'pglite' discriminator lets src/core/migrate.ts and others branch without instanceof + dynamic imports. Methods: batchLoadEmotionalInputs(slugs?) (CTE-shaped read with per-table aggregates so page × N tags × M takes never produces N×M rows), setEmotionalWeightBatch(rows) (UPDATE FROM unnest($1::text[],$2::text[],$3::real[]) composite-keyed on (slug, source_id)), getRecentSalience(opts), findAnomalies(opts). PageFilters has sort?: 'updated_desc'|'updated_asc'|'created_desc'|'slug' + PAGE_SORT_SQL whitelist consumed by both engines. listAllPageRefs(): Promise<Array<{slug, source_id}>> ordered by (source_id, slug) — cheap cross-source enumeration replacing the getAllSlugs()→getPage(slug) N+1 (which silently defaulted to source_id='default'); parity across postgres-engine.ts + pglite-engine.ts; Pinned by test/e2e/multi-source-bug-class.test.ts. SearchOpts+PageFilters add sourceIds?: string[] (federated read axis; both engines apply WHERE source_id = ANY($N::text[]) when set, preserve scalar sourceId fast path when unset); traverseGraph(slug, depth, opts?) and traversePaths(slug, opts?) accept opts.sourceId/opts.sourceIds. traverseGraph opts has frontierCap?: number (per-iteration recursive-CTE cap, approx per-BFS-layer); return type Promise<GraphNode[]> for MCP wire stability; export TraverseGraphOpts; Postgres uses parenthesized LIMIT N ORDER BY (slug, id) inside the recursive term, PGLite mirrors with positional params; Pinned by test/regressions/v0_36_frontier_cap.test.ts. Phantom-redirect methods: refreshPageBody(slug, sourceId, compiled_truth, timeline, content_hash) narrow-UPDATEs three columns + updated_at, skipping soft-deleted rows (content_hash refresh required so gbrain sync sees the canonical as unchanged after fence merge); migrateFactsToCanonical(phantomSlug, canonicalSlug, sourceId) UPDATEs entity_slug+source_markdown_slug on every active fact row keyed on the phantom, preserving embedding/validUntil/kind/status/source_session/confidence; parity at test/phantom-redirect-engine-parity.test.ts. getAdjacencyBoosts(pageIds): Promise<Map<number, AdjacencyRow>> powers the per-query graph-signals stage — one SQL query returning inbound-link counts among top-K plus a cross-source count (links from differing source_id); COALESCE(p.source_id,'default') null safety, HAVING >= 1, cross-source CASE-WHEN excludes the target's own source; parity SQL across both engines; SearchResult gains optional base_score, backlink_boost, salience_boost, recency_boost, exact_match_boost, graph_adjacency_boost, graph_cross_source_boost, session_demote_factor, reranker_delta + internal staging fields; Pinned by test/e2e/graph-signals-engine.test.ts. Two REQUIRED methods: deletePages(slugs, {sourceId}): Promise<string[]> (single-batch primitive returning slugs actually deleted) and resolveSlugsByPaths(paths, {sourceId}): Promise<Map<path,slug>> (batch path→slug lookup); sourceId REQUIRED on both at the type level (asymmetric with single-row deletePage which keeps optional/'default'); both short-circuit on empty input and throw when > DELETE_BATCH_SIZE. Embedding-signature stale-detection quartet: countStaleChunks(opts?) gains optional signature?: string widening the stale predicate from embedding IS NULL to ALSO include chunks whose JOINed page embedding_signature IS NOT NULL AND <> $signature (NULL signature is GRANDFATHERED, never counted; omit signature for the legacy NULL-only count); sumStaleChunkChars(opts?: {sourceId?, signature?}): Promise<number> = SUM(LENGTH(chunk_text)) over stale chunks (same predicate + embed_skip filter + optional sourceId scope), used by gbrain sync --all cost preview via estimateCostFromChars; setPageEmbeddingSignature(slug, {sourceId?, signature}) stamps pages.embedding_signature after a page's chunks (re)embed, idempotent no-op when page absent; invalidateStaleSignatureEmbeddings({signature, sourceId?}): Promise<number> NULLs embedding+embedded_at on every chunk whose page signature is set AND differs, returning the count, called BEFORE listStaleChunks so signature-drift pages flow through the NULL-embedding keyset cursor unchanged (NULL never invalidated). Widens findOrphanPages(opts?: {sourceId?, sourceIds?}) (candidate-side scoping only; inbound links counted from any source). Pinned by test/sum-stale-chunk-chars.test.ts, test/embedding-signature-stale.test.ts, test/e2e/engine-parity.test.ts. Free-text alias layer: resolveAliases(aliasNorms, opts?): Promise<Map<string, Array<{slug, source_id}>>> (READ; maps each normalized alias to declaring (slug, source_id) pairs, source-scoped) and setPageAliases(slug, sourceId, aliasNorms) (WRITE; replaces the full alias set, delete-then-insert, empty clears, idempotent on the unique triple), called by the importFromContent ingest projection and the reindex --aliases backfill; parity across both engines, Pinned by test/search/page-aliases-engine.test.ts. searchVector in both engines injects the shared buildBestPerPagePoolCte per-page max-pool so a page surfaces on its strongest chunk. executeRawDirect(sql, params?, opts?) is the lock-hot-path sibling of executeRaw: same single-statement contract, but routes to the direct session-mode pool when dual-pool is active (Postgres/Supabase port 5432) so a long-held lock heartbeat survives the transaction pooler's per-transaction connection recycling; PGLite delegates straight to executeRaw (no pooler). Both engines implement it; the Minion lock path (claim/renewLock) is the consumer.
  • src/core/engine-constants.ts — single source of truth for engine batch-sizing constants. Exports DELETE_BATCH_SIZE = 500 consumed by both engines' deletePages + resolveSlugsByPaths and by the sync delete + rename loops. Lives outside engine.ts (the interface module) to avoid circular-import worry — bounded per-statement work for predictable lock hold time + write amplification.
  • src/core/background-work.ts (#1762/#1745/#1775) — process background-work registry: the single owner of "drain every fire-and-forget DB-write sink before the CLI disconnects." registerBackgroundWorkDrainer({name, order, drain(timeoutMs), abort?}) + drainAllBackgroundWorkForCliExit({timeoutMs}) over a Map<name, BackgroundWorkDrainer> (idempotent registration by name; __registerDrainerForTest returns an unregister handle). Drains in explicit (order, name) order — facts FIRST (order 0) so its abort-path DB logIngest runs against the freshest live engine — and AWAITS abort() only when drain() reports unfinished>0. Best-effort per drainer: one sink's failure never blocks the others or the disconnect. FOUR sinks register at module import (rule-of-four): facts/queue.ts (order 0; abort=shutdown() cancels a hung facts:absorb Haiku via internalAbort), last-retrieved.ts (order 1), search/hybrid.ts (order 2; awaitPendingSearchCacheWrites bounded via Promise.race), eval-capture.ts (order 3; captureEvalCandidate self-tracks its promise via awaitPendingEvalCaptures). Both CLI-exit paths (src/cli.ts op-dispatch finally + handleCliOnly finally) call it before engine.disconnect() — closing the PGLite busy-loop where db.close() raced an in-flight job and pinned the single-writer lock (#1762). CLI-EXIT-ONLY: the facts shutdown() abort is permanent process state, never call in a long-lived gbrain serve. Companion changes: src/core/ai/gateway.ts withDefaultTimeout(caller, ms) bounds every outbound AI call (chat 300s, embed+multimodal 60s; env GBRAIN_AI_{CHAT,EMBED,MULTIMODAL}_TIMEOUT_MS; composed with caller signals via AbortSignal.any) and the op-dispatch + handleCliOnly force-exit timers process.exit(process.exitCode ?? 0) so a hung disconnect can't mask an errored op as success; src/core/postgres-engine.ts reconnect() module-mode branch re-establishes via idempotent db.connect() + connectionManager.setReadPool refresh instead of db.disconnect() (no null window for concurrent ops; fail-loud on real connect failure — #1745); src/core/search/hybrid.ts embedQueryBounded + a shared QueryEmbedDeadline (6s, floored 2s per embed via MIN_QUERY_EMBED_BUDGET_MS; env GBRAIN_QUERY_EMBED_TIMEOUT_MS) bounds the cache-lookup AND inner query embeds so a stalled provider falls back to keyword instead of hanging past the 10s force-exit (#1775). Incorporates + hardens PR #1763 (@ElliotDrel). Pinned by test/core/background-work.test.ts, test/search/query-embed-deadline.test.ts, test/eval-capture-drain.test.ts, test/e2e/postgres-reconnect-singleton.test.ts, test/e2e/pglite-cli-exit.serial.test.ts, test/fix-wave-structural.test.ts.
  • src/core/search/graph-signals.ts — per-query graph-signals helper. applyGraphSignals(results, engine, opts) runs as the 4th post-fusion stage (after backlink/salience/recency). Three boosts: ADJACENCY_BOOST=1.05 (page linked from 2+ OTHER top-K results — local hub for THIS query), CROSS_SOURCE_BOOST=1.10 (page linked from 2+ DIFFERENT sources — corroborated across team brains, dormant in single-source brains), SESSION_DEMOTE=0.95 (3+ results from same chat session — keep the highest-scoring at full score, demote the rest). All three inherit the floor-ratio gate preventing weak pages from being boosted past strong ones via popularity. computeScoreDistribution(results) emits min/p25/p50/p75/p95/max + reorder_band_width. sessionPrefix(slug) extracts the chat-session anchor (chat/2026-05-15-...). Pure pairedBootstrapPValue(deltas, resamples, rng) exported for eval gates. Test seam via adjacencyFn DI. Fail-open: any error logs via logGraphSignalsFailure (JSONL audit via audit-writer) and returns the input array unchanged. Pinned by test/search/graph-signals.test.ts (incl. the IRON-RULE floor-gate regression).
  • src/core/search/hybrid.ts extension — runPostFusionStages has a 4th stage (graphSignalsEnabled, onGraphMeta, onScoreDistribution). base_score stamped at function entry idempotently (captured ONCE before any boost stage mutates score). Each post-fusion stage stamps its multiplier: applyBacklinkBoostbacklink_boost, applySalienceBoostsalience_boost, applyRecencyBoostrecency_boost. applyReranker (earlier in the pipeline) stamps reranker_delta as a rank delta (positive = improved). applyExactMatchBoost in src/core/search/intent-weights.ts stamps exact_match_boost when fired. Per-stage attribution powers gbrain search --explain — every boost surface carries its own field so formatResultsExplain reads them all without coupling to internal stage ordering.
  • src/core/search/explain-formatter.ts — renders SearchResult[] as a multi-line per-result breakdown for gbrain search --explain. Reads every boost-stamping field. Handles the "no boosts applied" empty path. 4-decimal precision with trailing-zero strip. Pinned by test/search/explain-formatter.test.ts.
  • src/core/search/mode.ts extension — graph_signals: boolean knob in ModeBundle (defaults: conservative=false, balanced=true, tokenmax=true). KNOBS_HASH_VERSION appends a gs= parts entry per the cache-key contamination convention so a graph-on cache write can't be served to a graph-off lookup. SearchKeyOverrides + SearchPerCallOpts + loadOverridesFromConfig + SEARCH_MODE_CONFIG_KEYS + resolveSearchMode + attributeKnob all carry the field. Opt-out: gbrain config set search.graph_signals false. Mid-deploy query_cache rows from before the upgrade hash differently — natural row segregation, clears within cache.ttl_seconds (3600s default).
  • src/core/audit/audit-writer.ts — shared JSONL audit primitive consolidating the hand-rolled audit modules. Exports createAuditWriter({kind, recordSchema}) returning {log, readRecent} plus shared helpers computeIsoWeekFilename(kind, now?) and resolveAuditDir() (honors GBRAIN_AUDIT_DIR). ISO-week file rotation; best-effort writes (stderr warn on failure, never throws); read-path scans current-week + previous-week files for boundary spans. Refactored onto it for parity: src/core/rerank-audit.ts, src/core/audit-slug-fallback.ts, src/core/minions/handlers/shell-audit.ts, src/core/minions/handlers/supervisor-audit.ts, src/core/facts/phantom-audit.ts (each module's public API preserved bit-for-bit). The graph-signals-failures audit (logGraphSignalsFailure) uses the same primitive. One hand-rolled audit remains at src/core/skillpack/audit.ts. Pinned by test/audit/audit-writer.test.ts.
  • src/core/cli-options.ts extension — CliOptions gains explain: boolean. parseGlobalFlags recognizes --explain anywhere in argv (stripped before command dispatch). src/cli.ts formatResult for search + query cases routes to formatResultsExplain from src/core/search/explain-formatter.ts when CliOptions.explain is set; falls through to the existing JSON / human formatters otherwise.
  • src/commands/search.ts:gbrain search stats extension — graph_signals section (enabled/source/failures_count/failures_by_reason). JSON envelope adds a graph_signals sibling property; _meta.metric_glossary adds graph_signals.enabled + graph_signals.failures_by_reason. Human output prints the section after the existing block. Reads search.graph_signals config first, falls back to the mode default. Pinned by test/search/search-stats-graph-signals.test.ts.
  • src/commands/doctor.ts extension — graph_signals_coverage check wired into both runDoctor (local) and doctorReportRemote (HTTP/JSON thin-client path). Reads search.graph_signals config first, falls back to mode default; silent ok when disabled. Computes inbound link coverage on the page set; warns at <10% with gbrain extract all fix hint; ok at ≥30% ("fire on most queries") and 10-29% ("fire occasionally"), each with the percentage embedded. Pinned by cases in test/doctor.test.ts.
  • src/core/engine-factory.ts — Engine factory with dynamic imports ('pglite' | 'postgres').
  • src/core/pglite-engine.ts — PGLite (embedded Postgres 17.5 via WASM) implementation, all BrainEngine methods. listLinkSources({sourceId?, sourceIds?}) returns distinct link_source provenances + counts (ORDER BY count DESC, link_source ASC NULLS LAST; scalar + federated scoped; parity with postgres-engine.ts) powering gbrain link-sources. addLinksBatch/addTimelineEntriesBatch/addTakesBatch pass the whole batch as one JSONB document via jsonb_to_recordset(($1::jsonb)->'rows') (bound through executeRawJsonb with a { rows } wrapper; rows built by the shared src/core/batch-rows.ts helpers, NUL-stripped), and are batchRetry-wrapped. connect() wraps PGlite.create() in a try/catch that emits an actionable error (macOS 26.3 WASM bug #223, points at gbrain doctor); the lock is released on failure so the next process can retry cleanly. searchKeyword/searchKeywordChunks multiply ts_rank by the source-factor CASE at chunk grain; searchVector is a two-stage CTE — inner CTE keeps ORDER BY cc.embedding <=> vec so HNSW stays usable, outer SELECT re-ranks by raw_score * source_factor, inner LIMIT scales with offset to preserve pagination. initSchema() calls applyForwardReferenceBootstrap() BEFORE replaying SCHEMA_SQL — probes for forward-referenced state the embedded blob needs (pages.source_id, links.link_source, links.origin_page_id, content_chunks.symbol_name, content_chunks.language, sources FK target, plus files.source_id, files.page_id, oauth_clients.source_id, oauth_clients.federated_read, sources.archived, sources.archived_at, sources.archive_expires_at) and adds only what's missing; threads the DDL connection from initSchema so probes run inside the advisory-lock scope; no-op on fresh installs and modern brains (closes the upgrade-wedge bug class #239/#243/#266/#357/#366/#374/#375/#378/#395/#396/#1018/#974/#820). getBrainScore returns 100/100 with full breakdown (35/25/15/15/10) when pageCount === 0 (vacuous truth — empty brain has no coverage problem); Pinned by test/brain-score-breakdown.test.ts empty-brain assertion + test/doctor-report-remote.serial.test.ts. disconnect() uses snapshot+early-null (snapshot _db/_lock, null instance fields BEFORE any await so a concurrent connect() can't see a partial mid-close state) wrapped in try/finally guaranteeing lock-release even if db.close() throws; KEEPS close-then-release order (release-then-close was rejected: it would widen the window where a sibling process connects to a still-closing brain); Pinned by test/pglite-engine-disconnect.serial.test.ts. Exports classifyPgliteInitError(message): 'bunfs' | 'macos-26-3' | 'unknown' + buildPgliteInitErrorMessage(verdict, original) routing the catch-block hint by failure shape (bunfs matches literal $$bunfs OR ENOENT[\s\S]*pglite\.data co-occurrence, surfaces a paste-ready bun upgrade + Node fallback; macos-26-3 keeps the #223 link; unknown falls through); Pinned by test/pglite-init-classifier.test.ts. Implements deletePages(slugs, {sourceId}) + resolveSlugsByPaths(paths, {sourceId}) via slug = ANY($1::text[]) array-param binding, caller-chunking primitive throwing when input exceeds DELETE_BATCH_SIZE, deletePages returns RETURNING slug rows so callers filter pagesAffected to confirmed deletes. Implements the embedding-signature stale-detection quartet — sumStaleChunkChars({sourceId?, signature?}), setPageEmbeddingSignature(slug, {sourceId?, signature}), invalidateStaleSignatureEmbeddings({signature, sourceId?}), widened countStaleChunks({sourceId?, signature?}) (the signature opt widens via JOIN pages p ... WHERE cc.embedding IS NULL OR (p.embedding_signature IS NOT NULL AND p.embedding_signature <> $signature), NULL grandfathered); parity SQL with postgres-engine.ts. PGLite-specific DDL (pgvector, pg_trgm, triggers).
  • src/core/postgres-engine.ts — Postgres + pgvector implementation (Supabase / self-hosted). addLinksBatch/addTimelineEntriesBatch/addTakesBatch pass the batch as one JSONB document — INSERT ... SELECT FROM jsonb_to_recordset(($1::jsonb)->'rows') AS v(...) JOIN pages ... bound through executeRawJsonb({ rows }) — which encodes arbitrary free text safely (the old unnest(${arr}::text[]) array-literal path crashed Postgres with "malformed array literal" on calendar/Zoom context, gbrain#1861) and sidesteps the 65535-parameter cap; takes declares native recordset column types (page_id int, weight real, active boolean, …) so no per-element casts; all three are batchRetry-wrapped. searchKeyword/searchVector scope statement_timeout via sql.begin + SET LOCAL so the GUC dies with the transaction instead of leaking across the pooled postgres.js connection. getEmbeddingsByChunkIds uses tryParseEmbedding so one corrupt row skips+warns instead of killing the query. searchKeyword/searchKeywordChunks/searchVector apply source-aware ranking by inlining the source-factor CASE and NOT (col LIKE …) hard-exclude from src/core/search/sql-ranking.ts; searchVector is a two-stage CTE (HNSW-safe inner ORDER BY, source-boost re-rank in outer SELECT) carrying p.source_id inner→outer. _savedConfig retains the connect config; reconnect() tears down + recreates the pool from saved config (called by supervisor watchdog after 3 consecutive health-check failures, and by batchRetry on a retryable connection error). Concurrent callers share one in-flight _reconnectPromise (they await the single reconnect rather than racing a half-rebuilt pool); ownership re-samples through the atomic db.connect() token on the connect leg. reconnect(ctx?) accepts the triggering error and records a pool-recovery audit event (reap_detected/reconnect_other/reconnect_succeeded/reconnect_failed) for the pool_reap_health doctor check. executeRaw is a single-statement passthrough — no per-call retry (unsound for non-idempotent statements; recovery is supervisor-driven). connect() applies resolveSessionTimeouts() from db.ts as connection-time startup parameters (statement_timeout, idle_in_transaction_session_timeout) so orphan pgbouncer backends can't hold locks for hours. countStaleChunks()+listStaleChunks() server-side-filter on embedding IS NULL for embed --stale (eliminates ~76 MB/call client-side pull); upsertChunks() resets both embedding AND embedded_at to NULL when chunk_text changes without a new embedding. initSchema() calls applyForwardReferenceBootstrap() BEFORE replaying SCHEMA_SQL on the same probe set as PGLite (extended for column-only forward-reference cases: files.source_id, files.page_id, oauth_clients.source_id, oauth_clients.federated_read, sources.archived/archived_at/archive_expires_at); the entire probe path runs on the DDL connection threaded from initSchema (closing a concurrent-bootstrap race for Supabase pooler users); closes #1018/#974/#820. disconnect() is idempotent — _connectionStyle tracks whether the engine owns its pool (worker engines) or shares the module-level singleton; second call on an instance-pool engine is a no-op rather than clobbering the singleton; and a module-style engine only calls db.disconnect() when it owns the singleton (_ownsModuleSingleton, set from the db.connect() creation token), so a borrower probe engine's teardown leaves the cycle owner's connection intact. Pinned by test/e2e/postgres-engine-disconnect-idempotency.test.ts + test/postgres-engine-singleton-ownership.test.ts. getBrainScore empty-brain parity with PGLite — 100/100 with breakdown 35/25/15/15/10 when pageCount === 0 (both engines must agree to keep doctor-report-remote.serial.test.ts deterministic). Implements deletePages(slugs, {sourceId}): Promise<string[]> via DELETE FROM pages WHERE slug = ANY($1::text[]) AND source_id = $2 RETURNING slug (single round-trip; caller chunks); resolveSlugsByPaths does SELECT slug, source_path FROM pages WHERE source_path = ANY($1::text[]) AND source_id = $2; FK cascades through content_chunks/links/tags/raw_data/timeline_entries/page_versions, files.page_id+links.origin_page_id go SET NULL; throws when input exceeds DELETE_BATCH_SIZE (from src/core/engine-constants.ts); both short-circuit on empty input. Implements the embedding-signature stale-detection quartet (sumStaleChunkChars, setPageEmbeddingSignature, invalidateStaleSignatureEmbeddings, widened countStaleChunks, all accept optional signature extending "stale" to model/dims-swap drift via the pages.embedding_signature JOIN, NULL grandfathered; the embedding IS NULL server-side filter is preserved as the no-signature fast path); Pinned by test/e2e/engine-parity.test.ts.
  • src/core/cjk.ts — Single source of truth for CJK detection. Exports CJK_RANGES_REGEX, CJK_SLUG_CHARS (character-class fragment for embedding inside other regexes), CJK_SENTENCE_DELIMITERS (。!?), CJK_CLAUSE_DELIMITERS (;:,、), CJK_DENSITY_THRESHOLD = 0.30, hasCJK(s), countCJKAwareWords(s) (30% density threshold — English docs with one Japanese term stay whitespace-tokenized; Chinese-dominant docs get char-counted), and escapeLikePattern(s) (escapes %, _, \\ for ILIKE ... ESCAPE '\\'). BMP-only ranges (Han / Hiragana / Katakana / Hangul Syllables). Consumers: expansion.ts, sync.ts:slugifySegment, operations.ts:validatePageSlug + validateFilename, chunkers/recursive.ts:countWords + DELIMITERS, pglite-engine.ts:searchKeyword + searchKeywordChunks.
  • src/core/audit-slug-fallback.ts — Weekly ISO-week-rotated audit JSONL at ~/.gbrain/audit/slug-fallback-YYYY-Www.jsonl. logSlugFallback(slug, sourcePath) fires when importFromFile falls back to a frontmatter slug because slugifyPath returned empty (emoji / Thai / Arabic / non-CJK exotic-script filenames). readRecentSlugFallbacks(days) reads the last N days for gbrain doctor's slug_fallback_audit check. Honors GBRAIN_AUDIT_DIR via the shared resolveAuditDir(). Separate surface from sync-failures.jsonl — that file carries bookmark-gating semantics that info events shouldn't trigger.
  • src/core/embedding-pricing.tsEMBEDDING_PRICING map keyed provider:model for the post-upgrade reindex cost estimate. Sibling to anthropic-pricing.ts. Entries: OpenAI text-embedding-3-large ($0.13/1M), 3-small ($0.02/1M), ada-002 ($0.10/1M), Voyage 3-large ($0.18/1M), 3 ($0.06/1M). lookupEmbeddingPrice(modelString) returns a tagged union (known with price + unknown with provider name); estimateCostFromChars(charCount, pricePerMTok) uses 3.5 chars/token. Unknown providers degrade to "estimate unavailable" instead of fabricating numbers.
  • src/core/post-upgrade-reembed.ts — Pure functions backing the gbrain upgrade chunker-bump cost prompt. computeReembedEstimate(engine, model) queries real SQL (COUNT(*) + COALESCE(SUM(LENGTH(compiled_truth)) + SUM(LENGTH(timeline)), 0)) on pages WHERE chunker_version < MARKDOWN_CHUNKER_VERSION. formatReembedPrompt(est, graceSeconds) is the stderr-line formatter. runPostUpgradeReembedPrompt(engine, model, opts) orchestrates the 10-second Ctrl-C window; TTY-only wait (non-TTY auto-proceeds for CI / cron); GBRAIN_NO_REEMBED=1 bails with a doctor-warning marker; GBRAIN_REEMBED_GRACE_SECONDS=0 skips the wait.
  • src/commands/reindex.tsgbrain reindex --markdown [--limit N] [--dry-run] [--json] [--no-embed] [--repo PATH]. Walks pages WHERE page_kind = 'markdown' AND chunker_version < MARKDOWN_CHUNKER_VERSION in 100-row batches ordered by id. Rows with non-null source_path re-import via importFromFile; rows without fall back to importFromContent. Both paths pass forceRechunk: true to bypass importFromContent's content_hash short-circuit — without it the chunker version bump never reaches pages whose source content hasn't changed, AND the stripFactsFence privacy strip never applies to pre-strip chunks. Idempotent — partial-completion re-runs pick up via id-ordered batches. Wired into src/commands/upgrade.ts:runPostUpgrade after apply-migrations. The DB-only fallback (no source file on disk) does NOT pass body-only compiled_truth to importFromContent (that path re-parses with EMPTY frontmatter and OVERWRITES the page's real frontmatter/title/timeline); it getPage+getTags, reconstructs FULL markdown via serializeMarkdown(frontmatter, compiled_truth, timeline, {type, title, tags}), and re-imports THAT so re-chunking a DB-only page preserves everything while bumping chunker_version. Pinned by test/reindex-preserve-tags.test.ts.
  • src/commands/reindex-code.tsgbrain reindex --code [--source ID] [--dry-run] [--yes] [--json] [--force] [--no-embed]. Walks pages WHERE type = 'code' in 100-row batches, replays through importCodeFile for chunk + embed + content_hash folding. Idempotent unless --force bypasses the content_hash early-return. Cost-preview model field reads getEmbeddingModelName() from the gateway so preview reflects what the gateway will actually embed with. An informational stderr nudge inside runReindexCode (so dry-run + execute both surface it): when the configured embedding model isn't code-tuned (allowlist {'voyage-code-3'}, case-insensitive bare match), prints a recommendation to switch to voyage:voyage-code-3; suppress with GBRAIN_NO_CODE_MODEL_NUDGE=1, --no-embed, or --json. Pure shouldNudgeCodeModel(bareName) returns a tagged NudgeDecision union (takes the bare model name, emits qualified voyage:voyage-code-3 for the paste-ready gbrain config set line). When --yes is absent and the caller is non-TTY or passed --json, the cost gate refuses (exit 2, no spend) via the pure exported buildCostRefusal({json, previewMsg, preview, costUsd, model}): {stdout?, stderr?} — JSON envelope only when --json is explicit, otherwise a human refusal on stderr (the spend guardrail is independent of the output format). Pinned by test/ai/voyage-code-3-recipe.test.ts, test/reindex-code-nudge.serial.test.ts, test/reindex-code-model-source.serial.test.ts (IRON-RULE regression for the cost-preview fix), test/reindex-cost-refusal.test.ts.
  • src/commands/sync.ts:resolveSlugByPathOrSourcePath — Resolves a slug by pages.source_path first (returns the stored slug for frontmatter-fallback pages whose path doesn't derive a slug), then falls back to resolveSlugForPath(path). Threaded into all 4 delete/rename call sites (performSync's un-syncable cleanup at ~:531, deletes at ~:603, rename oldSlug at ~:622). Without this, emoji-only / Thai / Arabic filenames whose slug came from frontmatter would orphan on delete/rename (the delete path would compute the wrong path-derived slug). Best-effort query — pre-migration brains fall through to the legacy path.
  • src/core/sources-ops.ts — Multi-source registration + clone-lifecycle ops (addSource, recloneIfMissing, defaultCloneDir, isOwnedClone, unownedHint). Reclone-ownership invariant (must-never-violate): gbrain may only delete/re-clone a clone it created, NEVER a user working tree. recloneIfMissing deletes local_path, so it gates on isOwnedClone(src) and throws a SourceOpError('unmanaged_path', ...) BEFORE any filesystem op when ownership is unprovable — fail-closed. Ownership is proven by config.managed_clone === true (written by addSource's --url path, covering default-location and --clone-dir clones) OR local_path === defaultCloneDir(id) (back-compat for pre-marker clones, via exact normalized-path equality, symlink-free). A row with remote_url + an unowned local_path (a user-registered working tree, e.g. sources add --path) is refused untouched; re-add with --url to regain auto-reclone. The reclone is EXDEV-safe: clone into a SIBLING temp of local_path (not the shared clones/.tmp, which may sit on a different mount than a --clone-dir target), then swap (move old aside → move new in → drop old) so local_path is never left missing-and-unrecoverable; on swap failure the original is restored, and if restore fails the error names the aside path so it's never reflexively deleted. A TOCTOU re-check re-confirms ownership immediately before the destructive move and rejects a symlink leaf swapped in after the entry check (symlink_escape). unownedHint(src, state) is the shared recovery message used by both the core error and the gbrain sync --source CLI error; gbrain sources restore special-cases unmanaged_path to print "DB row restored; gbrain syncs this path read-only" instead of the misleading "try sync to recover" guidance. SourceOpErrorCode includes unmanaged_path. Pinned by test/sources-ops.test.ts, test/sources-resync-recovery.test.ts.
  • src/core/utils.ts — Shared SQL utilities extracted from postgres-engine.ts. Exports parseEmbedding(value) (throws on unknown input, used by migration + ingest paths where data integrity matters) and tryParseEmbedding(value) (returns null + warns once per process, used by search/rescore paths where availability matters more than strictness). isUndefinedColumnError(err) predicate — pattern-matches Postgres SQLSTATE 42703 / "column ... does not exist" with engine-driver shape variation tolerated; replaces bare catch {} blocks in oauth-provider.ts so genuine errors (lock timeout, network blip, permission denied) propagate while column-missing falls through to the legacy fallback. validateSourceId(id) throws on anything outside ^[a-z0-9_-]+$, used by the per-source disk-layout fix in patterns.ts/synthesize.ts before any join(brainDir, '.sources', source_id, slug+'.md') so source_id can't traverse out of brainDir. rowToPage populates the required Page.source_id from the SELECT projection (scripts/check-source-id-projection.sh enforces every projection feeding rowToPage includes the column).
  • src/core/db.ts — Connection management, schema initialization. resolveSessionTimeouts() returns statement_timeout + idle_in_transaction_session_timeout (defaults 5min each, env-overridable via GBRAIN_STATEMENT_TIMEOUT/GBRAIN_IDLE_TX_TIMEOUT/GBRAIN_CLIENT_CHECK_INTERVAL). Both connect() (module singleton) and PostgresEngine.connect() (worker pool) consume the result via postgres.js's connection option, sending GUCs as startup parameters that survive PgBouncer transaction mode (setSessionDefaults kept as a back-compat no-op shim). connect() returns Promise<boolean>true iff THIS call created the module singleton, false if it joined an existing one; the decision is atomic (no await between the if (sql) null-check and the synchronous sql = postgres(...) assignment), so two concurrent module connects can't both claim creation. PostgresEngine stores the return as its _ownsModuleSingleton token and only the creating engine may db.disconnect() the singleton — a borrower probe engine (lint/doctor config-lift) no-ops its disconnect, so its teardown can't null the connection the long-lived cycle owner is still using (the dream-cycle "connect() has not been called" failure). The module sql is only ever nulled by db.disconnect() (postgres.js auto-reconnects its own internal pool and never touches our reference). disconnect() snapshots + nulls sql before awaiting s.end() so a concurrent connect can't join a pool that's already closing.
  • src/commands/migrate-engine.ts — Bidirectional engine migration (gbrain migrate --to supabase/pglite).
  • src/core/import-file.ts — importFromFile + importFromContent (chunk + embed + tags). importFromContent and importCodeFile stamp pages.embedding_signature via setPageEmbeddingSignature(slug, {sourceId, signature: currentEmbeddingSignature()}) when the import actually embedded (not --no-embed) so a model/dims swap is detectable as stale; importCodeFile only stamps when every chunk was freshly embedded this call (needsEmbedIndexes.length === chunks.length), mixed reuse-by-hash pages stay unstamped (reindex --code --force / embed --stale handle those). importFromContent's tag reconciliation is ADD-ONLY: it only addTag (idempotent, ON CONFLICT DO NOTHING). The tags table has no provenance column and frontmatter tags are stripped from stored pages.frontmatter (markdown.ts:118), so a frontmatter-origin tag can't be distinguished from a DB-enrichment tag (auto-tag / dream synthesize / signal-detector) at re-import — deletion is unsafe (would wipe enrichment under gbrain reindex --markdown). Accepted trade-off: removing a tag from frontmatter no longer removes it from the DB on next sync (needs a tag_source provenance column). Pinned by test/reindex-preserve-tags.test.ts + test/import-file.test.ts.
  • src/core/sync.ts — Pure sync functions (manifest parsing, filtering, slug conversion). Exported pruneDir(name: string): boolean is the single source of truth for descent-time directory exclusion across walkers — blocks node_modules (no leading dot, so naive walkers slipped through and inflated MISSING_OPEN counts via vendor packages), dot-prefix dirs, ops/, and *.raw sidecars; isSyncable applies it per path segment, and walkMarkdownFiles in src/commands/extract.ts + listTextFiles in src/core/cycle/transcript-discovery.ts consult it BEFORE recursing to save the IO of walking thousands of vendor files (closes #923 + #202). manageGitignore worktree discriminator matches the gitdir path segment (/modules/<name> = submodule, /worktrees/<name> = worktree, per Git's documented layout) so Conductor worktrees (first-class repos) get .gitignore management for storage-tiering (closes #889). The sync-failure ledger (failure store, error classifier, the shared bookmark gate, and the doctor severity rule) lives in src/core/sync-failure-ledger.ts; sync.ts re-exports classifyErrorCode, summarizeFailuresByCode, loadSyncFailures, unacknowledgedSyncFailures, acknowledgeSyncFailures, recordSyncFailures, decideSyncFailureSeverity, applySyncFailureGate, and the SyncFailure type for backward-compatible imports — see its entry below.
  • src/core/sync-failure-ledger.ts — the bounded auto-skip sync failure ledger (issue #1939; formerly inline "Bug 9" in sync.ts). A LEAF module (imports only fs/path/crypto/config) so sync.ts can re-export it without a circular dependency. State lives in ~/.gbrain/sync-failures.jsonl, one JSON object per line, keyed by (source_id, path) with a per-key attempts count and a 3-state machine: open (fresh/blocking) → acknowledged (human resolved via gbrain sync --skip-failed) or auto_skipped (chronic). classifyErrorCode(errorMsg) regex classifier with 12 codes (SLUG_MISMATCH, YAML_PARSE, YAML_DUPLICATE_KEY, MISSING_OPEN, MISSING_CLOSE, NESTED_QUOTES, EMPTY_FRONTMATTER, NULL_BYTES, INVALID_UTF8, STATEMENT_TIMEOUT, FILE_TOO_LARGE, SYMLINK_NOT_ALLOWED) plus UNKNOWN (also recognizes PAGE_JUNK_PATTERN from the content-sanity gate); summarizeFailuresByCode(failures) returns sorted [{code, count}]; MISSING_OPEN/MISSING_CLOSE/EMPTY_FRONTMATTER regexes match the markdown.ts validator strings, FILE_TOO_LARGE covers import-file.ts:199, 352, 401, SYMLINK_NOT_ALLOWED covers :347. All mutations run under withLedgerLock (cross-process file lock) with an atomic rename write. The auto-skip threshold resolves via resolveAutoSkipThreshold() from GBRAIN_SYNC_AUTOSKIP_AFTER (default DEFAULT_AUTOSKIP_AFTER = 3; 0 disables the valve = pure fail-closed). Two pure decision functions are the unit-test surface: decideGateAction({fileFailures, sentinels, attemptsByPath, threshold, skipFailed}) returns hard_block | block | advance | advance_then_autoskip (sentinels like <head> ALWAYS hard-block, even with --skip-failed, so a history rewrite can't auto-skip; any FRESH failure with attempts < threshold blocks fail-closed; only when ALL failures are chronic does it advance_then_autoskip), and decideSyncFailureSeverity({entries, nowMs, failHours}) returns the sync_failures doctor status (ok when zero unresolved; fail when ≥10 OPEN-blocking or the oldest OPEN failure has blocked the bookmark past failHours; otherwise warnauto_skipped-only rows stay WARN-visible regardless of count because the bookmark already advanced). applySyncFailureGate(input) is the one orchestrator BOTH sync paths (incremental + full/runImport) call: it records/clears ledger rows, runs decideGateAction, then executes effects in the crash-safe order (advance the bookmark FIRST via the injected advance() callback, THEN auto-skip the chronic set) so a crash can never mark a file skipped while leaving sync wedged. isSkippablePath rejects <…> sentinels. Pinned by test/sync-failure-ledger.serial.test.ts + test/sync-failures.test.ts.
  • src/core/storage.ts — Pluggable storage interface (S3, Supabase Storage, local).
  • src/core/storage-config.ts — Storage tiering: loadStorageConfig reads gbrain.yml, normalizes deprecated keys (git_tracked/supabase_only) to canonical (db_tracked/db_only) with once-per-process deprecation warning, and runs normalizeAndValidateStorageConfig (auto-fixes missing trailing /, throws StorageConfigError on tier overlap). Path-segment matcher: media/x/ does NOT match media/xerox/foo. Uses a dedicated parser for the gbrain.yml shape rather than gray-matter (broken on delimiter-less YAML).
  • src/core/disk-walk.tswalkBrainRepo(repoPath) returns Map<slug, {size, mtimeMs}> from one recursive readdirSync. Skips dot-dirs, node_modules, non-.md files. Used by gbrain storage status to replace per-page existsSync + statSync (~400K syscalls on 200K-page brains → tens).
  • src/core/git-head.ts — local git HEAD freshness probe for gbrain doctor. isSourceUnchangedSinceSync(localPath, lastCommit, opts?) returns true iff localPath is a git repo whose current HEAD matches lastCommit; when opts.requireCleanWorkingTree is true also requires a clean working tree (mirrors gbrain sync's force-walk gate at sync.ts:1075 so doctor and sync agree on "is there work to do?"). requireCleanWorkingTree is boolean | 'ignore-untracked' — in 'ignore-untracked' mode the clean probe runs git status --porcelain --untracked-files=no so a quiet repo with stray untracked dirs (?? companies/, ?? media/) is still "unchanged" (sync's incremental path keys off the commit diff and never imports untracked files); GitCleanProbe gains an ignoreUntracked? second arg. Two probe seams (_setGitHeadProbeForTests, _setGitCleanProbeForTests) keep unit tests R2-compliant (no mock.module). Uses execFileSync with array args so shell metachars in local_path cannot escape to a shell (the regression test runs real execFileSync against '/nonexistent/$(touch <sentinel>)/repo' and asserts the sentinel is never created). Fail-open on every error (missing path, not a git repo, git not installed, timeout, NULL inputs, dirty-probe errored → false) preserving the caller's prior time-based behavior. The chunker-version-match check lives in the caller (doctor.ts) because it depends on engine state (sources.chunker_version vs CHUNKER_VERSION from src/core/chunkers/code.ts). Pinned by test/core/git-head.test.ts (incl. the shell-injection regression guard).
  • src/core/source-health.ts — per-source health metrics for gbrain sources status + doctor's federation_health. Commit-relative staleness: newestCommitMs(localPath) = HEAD committer time via git log -1 --format=%ct (fail-open null; NO working-tree mtime parsing — committed content only, robust against the porcelain-mtime bug farm); pure lagFromContentMs(contentMs|null, lastSyncMs|null, nowMs) = remote/column comparator (null lastSync → null; negative wall-clock → skew passthrough; contentMs <= lastSync → 0; else/null-content → wall-clock). computeAllSourceMetrics(engine, sources, {probeContent?}): LOCAL (probeContent:true, gbrain sources status) → isSourceUnchangedSinceSync(..., {requireCleanWorkingTree:'ignore-untracked'}) ? 0 : wall-clock (live commit-hash catches HEAD moving to an old-dated commit a timestamp compare would miss); REMOTE (default, federation_health on the HTTP MCP path) → lagFromContentMs(row.newest_content_at, ...), NO git subprocess (trust boundary). commitTimeMs(localPath, sha) is the newestCommitMs sibling pinned to an arbitrary commit (committer time via git show -s --format=%ct <sha>, fail-open null, execFileSync array args) — the resumable sync stamps newest_content_at against its pinned target commit, not whatever HEAD raced to. Pinned by test/source-health.test.ts.
  • src/core/git-remote.ts — SSRF-hardened git invocations for remote-source cloneRepo and pullRepo. Exports two distinct flag constants because git's argv grammar treats them differently: GIT_SSRF_FLAGS (3 -c config flags — protocol.allow=user, protocol.file.allow=never, http.allowRedirects=false) is global config, spread BEFORE the subcommand verb; GIT_SSRF_SUBCOMMAND_FLAGS = ['--no-recurse-submodules'] is subcommand-scoped, spread AFTER the verb (a combined array would spread --no-recurse-submodules before the verb where real git rejects it exit 129). cloneRepo argv: git <GIT_SSRF_FLAGS> clone <GIT_SSRF_SUBCOMMAND_FLAGS> --depth=1 [--branch X] -- <url> <dir>. pullRepo argv: git <GIT_SSRF_FLAGS> -C <dir> pull <GIT_SSRF_SUBCOMMAND_FLAGS> --ff-only. Pinned by test/git-remote.test.ts position-anchored regression guard (argv.indexOf('--no-recurse-submodules') > argv.indexOf(verb)).
  • src/commands/storage.tsgbrain storage status [--repo P] [--json]. Split into pure data (getStorageStatus) + JSON formatter + human formatter (ASCII-only) matching the orphans.ts pattern. PageCountsByTier and DiskUsageByTier are distinct nominal types so swaps fail at compile time.
  • gbrain.yml (brain repo root) — Optional storage tiering config. Top-level storage: section with db_tracked: and db_only: array-valued keys. gbrain sync auto-manages .gitignore for db_only paths on successful sync (skips on dry-run, blocked-by-failures, submodule context, or GBRAIN_NO_GITIGNORE=1). gbrain export --restore-only [--repo P] [--type T] [--slug-prefix S] repopulates missing db_only files from the database.
  • src/core/supabase-admin.ts — Supabase admin API (project discovery, pgvector check).
  • src/core/file-resolver.ts — File resolution with fallback chain (local -> .redirect.yaml -> .redirect -> .supabase).
  • src/core/chunkers/ — 3-tier chunking (recursive, semantic, LLM-guided). code.ts is a tree-sitter-based semantic chunker for 30 languages (plus SQL via DerekStride/tree-sitter-sql) with embedded-asset WASMs (src/assets/wasm/), @dqbd/tiktoken cl100k_base tokenizer, small-sibling merging. CHUNKER_VERSION is folded into importCodeFile's content_hash so chunker shape changes force clean re-chunks across releases. extractSymbolName has an inline SQL branch (extractSqlSymbolName) diving through DerekStride's statement wrapper into the inner DDL child (create_table/create_function/create_view/create_index/create_procedure/create_type/create_schema/create_database/create_trigger/alter_table/alter_view) and extracting the target identifier via the name field with identifier-shaped fallback; DML kinds (select/insert/update/delete/merge/with) deliberately return null so chunks emit unnamed (code-def is a DDL signal). normalizeSymbolType has parallel SQL branches mapping create_table → 'table', create_view → 'view', etc. src/commands/code-def.ts:DEF_TYPES is extended with 'table' | 'view' | 'index' | 'procedure' | 'schema' | 'database' | 'trigger' so the new chunks surface in gbrain code-def <name> queries.
  • src/core/errors.tsStructuredAgentError + buildError + serializeError. Every agent-facing surface (code-def, code-refs, usage errors) uses this envelope; matches the CycleReport.PhaseResult.error shape.
  • src/assets/wasm/ — 37 tree-sitter grammar WASMs + tree-sitter runtime. Committed to the repo so bun --compile embeds them deterministically via import path from ... with { type: 'file' }. The CI guard scripts/check-wasm-embedded.sh fails the build if the compiled binary ever silently falls through to recursive chunks. tree-sitter-sql.wasm (DerekStride/tree-sitter-sql @ c2e1e08db1ea20dc23bdb8d228a81a8756e9c450, built with tree-sitter-cli@v0.26.3 --abi 14) adds SQL coverage at 11 MB — larger than peers because the grammar covers PostgreSQL + MySQL + SQLite + T-SQL basics (40 MB generated parser.c); the compiled binary grows ~6%.
  • src/commands/code-def.ts + src/commands/code-refs.ts — symbol definition + references lookup. Query content_chunks.symbol_name or chunk_text ILIKE with page_kind='code' filter. Auto-JSON when stdout is not a TTY (gh-CLI convention). Bypass the standard searchKeyword DISTINCT ON (slug) collapse so multiple call-sites from the same file surface. The JSON envelope (CLI + the code_def/code_refs MCP ops) carries status + ready from src/core/code-graph-readiness.ts so a count:0 result is distinguishable as not_built (no code indexed) vs ready (genuinely no match); human output prints a one-line hint when not ready.
  • src/core/code-graph-readiness.ts — typed readiness signal shared by the four code-* surfaces (code-def/code-refs/code-callers/code-callees). resolveCodeReadiness(engine, {kind:'symbol'|'edge', count, sourceId?, allSources?}) returns {status:'not_built'|'indexing'|'ready'|'unknown', ready, has_code, pending_edges}. count>0 short-circuits to ready with no query; on empty it runs EXISTS probes against content_chunks JOIN pages (page_kind='code') — no page_kind index needed, and the pending probe rides the partial idx_content_chunks_edges_backfill. kind:'symbol' (code-def/refs) is 2-state + brain-wide because symbol metadata is set at chunk time; kind:'edge' (code-callers/callees) is 3-state + source-scoped, with the pending predicate mirroring the resolver (edges_backfilled_at IS NULL OR < EDGE_EXTRACTOR_VERSION_TS from src/core/chunkers/symbol-resolver.ts) so a resolver-version bump never falsely reports ready. Probe scope matches each command's result-query deleted_at posture (def/refs don't filter deleted_at, so neither do the probes). Any DB error returns status:'unknown' (fail-open; never breaks the command). readinessHint(r) renders the human one-liner. Wired into code-def.ts/code-refs.ts (brain-wide), code-callers.ts/code-callees.ts (resolved sourceId/allSources), and all four code_* MCP op handlers in src/core/operations.ts. Pinned by test/code-graph-readiness.test.ts + readiness-envelope cases in test/e2e/code-intel-mcp-ops-pglite.test.ts.
  • src/core/search/ — Hybrid search: vector + keyword + RRF + multi-query expansion + dedup. searchKeyword/searchKeywordChunks/searchVector apply source-aware ranking at the SQL layer (curated content like originals/, concepts/, writing/ outranks bulk content like <fork>/chat/, daily/, media/x/). searchVector uses a two-stage CTE so source-boost re-ranking doesn't kill the HNSW index. Hard-exclude prefixes (test/, archive/, attachments/, .raw/ by default) filter at retrieval, not post-rank. Both gates honor detail !== 'high' so temporal queries surface chat pages normally.
  • src/core/search/intent.ts — Query intent classifier (entity/temporal/event/general → auto-selects detail level).
  • src/core/search/eval.ts — Retrieval eval harness: P@k, R@k, MRR, nDCG@k metrics + runEval() orchestrator.
  • src/core/search/source-boost.ts — Source-type boost map keyed by slug prefix. DEFAULT_SOURCE_BOOSTS (originals/ 1.5, concepts/ 1.3, writing/ 1.4, people/companies/deals/ 1.2, daily/ 0.8, media/x/ 0.7, /chat/ 0.5, archive/ 0.5, extracts/ 0.3) and DEFAULT_HARD_EXCLUDES (test/, attachments/, .raw/). archive/ is DEMOTED (findable, ranked below curated), not hard-excluded — archive holds high-signal history users expect to retrieve; the demote is a prior at the SQL/fusion layer and the cross-encoder reranker can still promote a strongly-matching archive page. parseSourceBoostEnv/parseHardExcludesEnv parse comma-separated prefix:factor pairs from GBRAIN_SOURCE_BOOST/GBRAIN_SEARCH_EXCLUDE. resolveBoostMap and resolveHardExcludes merge defaults + env + caller SearchOpts.exclude_slug_prefixes/include_slug_prefixes. The surviving exclude policy is auditable via the hidden_by_search_policy doctor check (src/commands/doctor.ts, local + remote paths) which counts chunked pages withheld per active exclude prefix, reusing resolveHardExcludes + buildVisibilityClause + the exported escapeLikePattern.
  • src/core/search/sql-ranking.ts — Pure SQL string builders. buildSourceFactorCase(slugColumn, boostMap, detail) emits a CASE with longest-prefix-match wins (returns literal '1.0' when detail === 'high' for temporal-bypass parity with COMPILED_TRUTH_BOOST). buildHardExcludeClause(slugColumn, prefixes) emits NOT (col LIKE 'p1%' OR col LIKE 'p2%') — OR-chain wrapped in NOT, NOT NOT LIKE ALL/ANY (those quantifiers don't express set-exclusion). LIKE meta-character escape covers all three of %, _, AND \ (backslash is Postgres LIKE's default escape char). Single-quote doubling on SQL string literals so injection-style inputs are inert text. buildBestPerPagePoolCte(...) is the shared per-page max-pool CTE both engines' searchVector inject — instead of returning the single best chunk per page from an inner ORDER BY embedding <=> vec LIMIT N (which let a page lose to a neighbor on ONE weak chunk while its strong chunk sat just below the inner cut), the CTE pools the BEST chunk score per (source_id, slug) composite key so a page surfaces on its strongest evidence; composite key (not bare slug) keeps multi-source brains correct; single source of truth so the two engines can't drift.
  • src/core/search/title-match.ts — pure, zero-I/O title-phrase matcher shared by the production title boost AND NamedThingBench (no drift). isTitlePhraseMatch(query, title) returns true when the normalized query is a contiguous token run inside the title with >= MIN_CONTENT_TOKENS=2 non-stopword tokens, OR an exact full-title match (covers deliberate 1-word chosen names like "Mingtang"). Token-boundary matching (never raw substring, so "art" doesn't match "Bartholomew"); small conservative English stopword set excluded from the content-token floor (guards against promoting generic pages on stopword-y queries); NFKC normalize so CJK / width variants converge. Exports tokenizeTitle + __test__ internals.
  • src/core/search/alias-normalize.ts — ONE normalizer shared by the WRITE path (ingest projects frontmatter aliases: into page_aliases) and the READ path (search matches query against page_aliases), so stored aliases can't silently fail to match queries via divergent normalization (same single-source posture as cjk.ts). normalizeAlias(raw) does NFKC + lowercase + whitespace-collapse + trim + strip one layer of wrapping quotes/brackets; returns '' for empty (callers MUST skip empty aliases). normalizeAliasList(value) coerces a frontmatter scalar / array / comma-list / garbage into a deduped list of normalized non-empty aliases — used by both the ingest projection and the reindex --aliases backfill.
  • src/core/search/evidence.ts — the agent-facing why-it-matched contract (closes the root behavior where an agent read one blended score, decided "no strong match, safe to create", and wrote a duplicate over a fully-developed page). classifyEvidence(r) names the strongest signal (precedence: alias_hit > exact_title_match > high_vector_match (base ≥ HIGH_MATCH_FLOOR=0.85) > keyword_exact (base ≥ SOLID_MATCH_FLOOR=0.6) > weak_semantic). createSafetyFor(evidence) derives the don't-duplicate hint (exists/probable/unknown) the agent keys off INSTEAD of a raw threshold (a blended RRF/cosine score is not a calibrated probability). stampEvidence(results) stamps evidence + create_safety in place once at pipeline end (after the alias hop, before slice); idempotent.
  • src/core/search/mode.ts extension — title_boost: number | undefined knob in ModeBundle (default 1.25 for all three modes; multiplier for the post-fusion title-phrase boost). Override chain: per-call SearchOptssearch.title_boost config (clamped [1.0, 5.0]) → bundle. KNOBS_HASH_VERSION appends a tib= parts entry so a title-boost-on cache write can't be served to a title-boost-off lookup. SEARCH_MODE_CONFIG_KEYS gains search.title_boost.
  • src/commands/search-diagnose.tsgbrain search diagnose "<query>" --target <slug> [--json] [--source <id>]: Phase-0 retrieval diagnostic. Traces WHERE a target page surfaces (or fails to) across keyword / vector (per-page max-pool) / alias / hybrid layers and names the layer responsible for an incident, so an operator can pin whether the fix is max-pool/innerLimit (vector) vs title/alias. The verdict names the layer that DOES surface the target (or "none"). Pinned by test/search/search-diagnose.test.ts.
  • src/commands/reindex-aliases.tsgbrain reindex --aliases [--limit N] [--dry-run] [--json] [--source <id>]: backfills the free-text alias layer for EXISTING pages whose frontmatter aliases: predate the alias table (the import-time projection covers new + changed pages). Reads each page's frontmatter aliases:, writes via engine.setPageAliases. Idempotent + convergent (setPageAliases replaces a page's alias set) so no op-checkpoint needed; walks listAllPageRefs (cheap cross-source enumeration), --source narrows. Pinned by test/search/reindex-aliases.test.ts.
  • src/eval/retrieval-quality/harness.ts + src/commands/eval-retrieval-quality.ts + test/fixtures/retrieval-quality/namedthing.jsonl — NamedThingBench, the retrieval-quality eval that makes the named-thing-miss incident impossible to reintroduce silently. Seven query families, each a distinct failure class: title-substring (the direct regression), generic-to-named (tourist label → named thing), alias-synonym (declared alias / romanization → canonical), multi-chunk-dilution (one strong chunk among many weak — stresses max-pool), short-vs-rich, graph-relationship (guardrail), hard-negative (precision guard, must NOT return a page). gbrain eval retrieval-quality <fixture.jsonl> runs it with hard gates (e.g. title-substring Hit@1 ≥ 0.95, alias Hit@1 ≥ 0.98, multi-chunk-dilution Hit@3 = 1.0). Pure: caller injects a SearchFn (CLI uses hybridSearch, tests stub) so it's engine-agnostic. Metric glossary entries (hit@1/hit@3) added to src/core/eval/metric-glossary.ts. Pinned by test/eval-retrieval-quality.test.ts + test/retrieval-quality-harness.test.ts.
  • docs/architecture/RETRIEVAL.md + docs/architecture/RETRIEVAL_MAXPOOL_INCIDENT.md — retrieval-pipeline architecture reference + the named-thing-miss incident write-up (root cause, the five-layer fix, the eval that pins it).
  • src/core/types.ts extension + src/core/operations.ts:search + src/core/import-file.ts + src/cli.ts + src/core/search/telemetry.ts — the wiring layer for the retrieval cathedral. SearchResult gains evidence, create_safety, title_match_boost, alias_hit (all optional; evidence/create_safety reference the union types in evidence.ts). The search MCP op uses a cheap-hybrid path by default and accepts a per-call mode (conservative|balanced|tokenmax) honored ONLY for trusted/local callers (resolvePerCallMode(ctx, ...) — remote callers use the configured mode so a remote provider can't force tokenmax spend); every search path stamps evidence fail-soft. importFromContent projects frontmatter aliases: into page_aliases via normalizeAliasList + engine.setPageAliases so new + changed pages register aliases at ingest. src/cli.ts adds the gbrain search diagnose dispatch (lazy import) and reconciles the search CLI path with the cheap-hybrid op. src/core/search/telemetry.ts extends the rollup with the rank-1 base_score drift signal (sum/count + 3 coarse buckets, aggregate not per-query), surfaced via gbrain search stats, backed by migration v111's search_telemetry columns. Tests: test/cli-search-dispatch.test.ts, test/search/per-call-mode.test.ts, test/search/telemetry-rank1.test.ts, test/search/title-boost-stage.test.ts, test/search/alias-hop.test.ts, test/search/evidence.test.ts, test/search/searchvector-maxpool.test.ts, test/search/pre-migration-failopen.test.ts.
  • src/commands/eval.tsgbrain eval command: single-run table + A/B config comparison. Sub-subcommand dispatch on args[0] routes gbrain eval export + gbrain eval prune + gbrain eval replay into session-capture handlers; bare gbrain eval --qrels … fall-through preserves the legacy IR-metrics flow. gbrain eval cross-modal is in the dispatch (the user-facing path is the cli.ts no-DB branch — src/commands/eval.ts:cross-modal only fires when callers re-enter with an existing engine).
  • src/commands/eval-cross-modal.ts — multi-model quality gate. Three different-provider frontier models score the OUTPUT against the TASK on a 5-dim list. Verdict pass (exit 0) / fail (exit 1) / inconclusive (exit 2; <2/3 model successes). Reuses src/core/ai/gateway.ts:chat() so config/auth/aliasing comes from the gateway recipe registry — no parallel provider stack. Self-configures the gateway (configureGateway(loadConfig() + process.env)) since the cli.ts dispatch bypasses connectEngine(). Default cycles 3 in TTY, 1 in non-TTY (partial cost guardrail) via the shared resolveCycleDefault(explicit, isTty) in src/core/eval/cycle-default.ts; the cost-estimate banner appends cycleDefaultSuffix(...) (for 1 cycle(s) (non-interactive default; --cycles N for more)) when the value is the silent non-TTY fallback, so the 1-vs-3 difference isn't hidden. Receipts land at gbrainPath('eval-receipts')/<slug>-<sha8-of-output>.json. --batch <jsonl> [--limit N] [--concurrent N] [--max-usd FLOAT] [--yes] fans out cross-modal scoring across a LongMemEval-shape JSONL; mutually exclusive with --task (fail-fast usage error if both set); filters kind: "by_type_summary" rows; pre-flight cost estimate refuses if > --max-usd without --yes (default cap 5.00 USD). Semaphore-bounded fan-out via inline runWithLimit<T>(items, limit, fn) (exported for unit tests): max N questions in-flight × 3 model slots = ceiling of 3N parallel API calls (default --concurrent 3 → 9). Per-question receipts land in a per-batch tempdir and are deleted at end of run; the summary receipt inlines per-question verdicts as JSON, not file paths. Exit precedence (batch-level policy, NOT inherited from aggregate.ts): ERROR > FAIL > INCONCLUSIVE > PASS. DI seam: runEvalCrossModal(args, opts?: {runEval?: typeof runEval}) mirrors runEvalLongMemEval(args, {client?}); tests pass opts.runEval to bypass real LLM calls AND the gateway availability check. Pinned by test/eval-cross-modal-batch.test.ts.
  • src/core/eval/cycle-default.ts — single source of truth for the eval cycle-count default. Exports DEFAULT_CYCLES_TTY = 3, DEFAULT_CYCLES_NONTTY = 1, resolveCycleDefault(explicit, isTty): {cycles, usedNonTtyDefault}, and cycleDefaultSuffix(r) (returns (non-interactive default; --cycles N for more) only when the non-TTY default was applied, else ''). Consumed by eval-cross-modal.ts, eval-takes-quality.ts (run + regress), and takes-quality-eval/runner.ts (core uses only the constant — library stays TTY-agnostic; the CLI owns the TTY=3 upgrade + banner annotation). eval-suspected-contradictions.ts applies the same transparency to its $5/$1 budget default via a budgetUsdExplicit flag (the budget is overwritten in-place so explicitness can't be inferred post-hoc). Not shared with resolveWorkersWithClamp (different domain, no engine, no dedup). Pinned by test/eval/cycle-default.test.ts, test/eval-suspected-contradictions-budget-default.test.ts.
  • src/core/cross-modal-eval/json-repair.tsparseModelJSON(raw) named export with a 4-strategy fallback chain (direct parse → fence-strip → trailing-comma + single-quote + embedded-newline repair → regex nuclear option). Adversarial input throws rather than fabricating scores — the aggregator treats a throw as "this model contributed nothing this cycle" so the gate stays correct at >=2/3 successes.
  • src/core/cross-modal-eval/aggregate.ts — pure verdict logic. Pass criterion: (successes >= 2) AND (every dim mean >= 7) AND (every dim min across models >= 5). Inconclusive when <2/3 models returned parseable scores (regression guard for the v1 Object.values({}).every(...) === true empty-array PASS bug).
  • src/core/cross-modal-eval/runner.ts — orchestrator. Each cycle runs Promise.allSettled([gwChat(slotA), gwChat(slotB), gwChat(slotC)]) (bare allSettled, no rate-leases for the CLI path). Stops early on PASS or INCONCLUSIVE; runs up to 3 cycles. Default slots: openai:gpt-4o / anthropic:claude-opus-4-7 / google:gemini-1.5-pro. estimateCost() exports a small per-model pricing table (drifts; refresh alongside model-family bumps).
  • src/core/cross-modal-eval/receipt-name.ts — receipt filename binds (slug, SKILL.md sha-8). findReceiptForSkill(skillPath, receiptDir) returns 'found' | 'stale' | 'missing'. Skillify-check surfaces the status as informational; the audit does NOT fail on missing/stale receipts.
  • src/core/cross-modal-eval/receipt-write.ts — wraps fs.writeFileSync with mkdirSync({recursive:true}) ahead of every write (gbrainPath() does NOT auto-mkdir).
  • src/commands/eval-export.ts — streams eval_candidates rows as NDJSON to stdout with schema_version: 1 prefix on every line. EPIPE-safe, progress heartbeats on stderr, stable id-desc tiebreaker so --since windows never dupe/miss rows.
  • src/commands/eval-prune.ts — explicit retention cleanup. Requires --older-than DUR. --dry-run reports would-delete count.
  • src/commands/eval-replay.ts — contributor-facing replay tool. Reads NDJSON from gbrain eval export, re-runs each captured query / search op against the current brain, computes set-Jaccard@k between captured + current retrieved_slugs, top-1 stability rate, and latency Δ. Stable JSON shape (schema_version: 1) for CI gating; human mode prints a regression table. Pure Bun, zero new deps. The dev-loop half of BrainBench-Real. See docs/eval-bench.md. parseNdjson skips lines where _kind === 'baseline_metadata' so gbrain bench publish baselines parse cleanly without the metadata header polluting row counts. Exports replayCore(engine, opts): Promise<{summary, results}> + ReplaySummary type so gbrain eval gate calls replay in-process (NOT subprocess — avoids gbrain-version-drift for source-tree CI). CLI runEvalReplay wraps replayCore.
  • src/core/bench/baseline-file.ts + src/core/bench/qrels-file.ts + src/core/bench/correctness-gate.ts + src/commands/bench-publish.ts + src/commands/eval-gate.ts — the eval-loop wave. gbrain bench publish --from <captured.ndjson> --to <X.baseline.ndjson> writes a baseline (stamps stable query_hash per row; metadata header carries _kind: 'baseline_metadata' + thresholds + source_hash + baseline_mean_latency_ms; deterministic sort by (tool_name, query_hash); strict: empty=fail, dupes=fail with paste-ready hint, --to exists=refuse without --force). gbrain eval gate [--baseline X] [--qrels Y] is the two-gate dispatcher (regression gate via in-process replayCore, correctness gate via bare hybridSearch for determinism, both must pass when both flags set, exit 0 PASS / 1 FAIL / 2 USAGE). Source-id-aware: bench publish dedup key is (tool_name, source_ids, query_hash); qrels compare keys are ${source_id}::${slug} everywhere (closes the multi-source bug class at the file-shape layer). Latency math: (baseline + delta) / baseline <= multiplier. Fail-closed: ANY in-process throw flips verdict to fail with named breach in breaches[] — never silently exit 0. .qrels.json preserves the 12-row test/fixtures/eval-baselines/qrels-search.json fixture (slug-only relevant_slugs + first_relevant_slug auto-promote to source_id='default') AND supports the federated shape (explicit relevant: [{source_id, slug}] + expected_top1). correctness-gate.ts runs each qrels query via bare hybridSearch; per-query throw recorded as errored: true and flagged as gate failure. Audit JSONL at ~/.gbrain/audit/bench-publish-YYYY-Www.jsonl. Pinned by test/bench/baseline-file.test.ts, test/bench/qrels-file.test.ts, test/bench/correctness-gate.test.ts, test/bench-publish.test.ts, test/eval-gate.test.ts, test/eval-replay-metadata-skip.test.ts, test/cycle/nightly-probe-adapters.test.ts, test/autopilot-nightly-probe-wiring.test.ts, test/e2e/eval-loop.test.ts.
  • src/core/cycle/nightly-probe-adapters.ts — bridges the autopilot's object-shape NightlyProbeDeps to the argv-shape runEvalLongMemEval + runEvalCrossModal CLI functions. Cross-modal adapter argv MUST include --output summaryPath (without it the summary lands at the default receipt path and the adapter reads nothing from summaryPath). In-process invocation (NOT subprocess) — avoids gbrain-version-drift for source-tree CI. Pinned by test/cycle/nightly-probe-adapters.test.ts (incl. argv-shape regression for the --output requirement).
  • src/commands/autopilot.ts extension — tick body invokes runNightlyQualityProbe when cfg.autopilot.nightly_quality_probe.enabled === true (default OFF — opt-in to protect API spend). NO scheduler-side rate-limit check — runNightlyQualityProbe's internal shouldRunNightly (reading the audit JSONL) is the single source of truth. Probe call wrapped in try/catch that logs via logError and does NOT bump consecutiveErrors (probe failure is informational, never crashes the loop). Default max_usd cap = 5. Pinned by test/autopilot-nightly-probe-wiring.test.ts.
  • test/eval-replay-gate.test.ts + test/fixtures/eval-baselines/qrels-search.json — hermetic retrieval qrels gate running in the standard PR unit-shard CI matrix (.github/workflows/test.yml, NOT the fixed-file E2E workflow). Uses the canonical PGLite block (test-isolation R3+R4) and the basis-vector embedding pattern from test/e2e/search-quality.test.ts:23-28 for fully hermetic retrieval. The qrels fixture (12 queries) uses PLACEHOLDER names only (alice-example, widget-co-example, etc. — privacy rule) and embeds each query at a deterministic basis dimension so retrieval is reproducible. Each query lists relevant_slugs[] + first_relevant_slug; the test computes top1_match_rate (top-1 == first_relevant) and recall@10 (fraction of relevant_slugs in top-10), asserting both meet floors (defaults >= 0.80 and >= 0.85). Env-overridable floors GBRAIN_REPLAY_GATE_TOP1_FLOOR / GBRAIN_REPLAY_GATE_RECALL_FLOOR (via withEnv() per R1). Refresh discipline: when ranking changes intentionally move expected slugs, edit qrels-search.json directly with a Why: line in the commit body or the gate degrades to rubber-stamp. Pinned by test/eval-replay-gate.test.ts (incl. a privacy-grep regression guard against real-name reintroduction).
  • src/core/cycle/nightly-quality-probe.ts + src/core/audit-quality-probe.ts + test/fixtures/longmemeval-nightly.jsonl + test/nightly-quality-probe.test.ts — opt-in nightly cross-modal quality probe. The phase runs gbrain eval longmemeval --by-type against the committed 10-question placeholder fixture, pipes output through gbrain eval cross-modal --batch --max-usd 5 --yes, and writes one event per run to ~/.gbrain/audit/quality-probe-YYYY-Www.jsonl (ISO-week-rotated, mirrors audit-slug-fallback.ts; honors GBRAIN_AUDIT_DIR). Default DISABLED — opt-in via gbrain config set autopilot.nightly_quality_probe.enabled true (prevents surprise API spend). 24h rate limit (pure shouldRunNightly(now, recentEvents, windowMs?)) skips with audit row outcome: rate_limited. Embedding-key short-circuit: longmemeval needs gateway.embedQuery(), so the phase exits early with outcome: no_embedding_key + stderr warn when no provider configured. Full DI surface via NightlyProbeDeps (isEnabled, hasEmbeddingProvider, resolveMaxUsd, resolveRepoRoot, runLongMemEval, runCrossModalBatch, now) so the unit test stubs every external effect. Cost ceiling: $5/run × 30 nights ≈ $150/month worst-case; expected ~$10.50/month. New nightly_quality_probe_health doctor check (src/commands/doctor.ts, right after slug_fallback_audit) reads last 7 days: SKIPPED when flag off (with enable command); OK when enabled + all PASS; WARN on any FAIL / ERROR / BUDGET_EXCEEDED with per-outcome counts. Pinned by test/nightly-quality-probe.test.ts.
  • src/commands/eval-trajectory.ts + src/commands/founder-scorecard.ts + src/core/trajectory.ts — temporal trajectory + founder scorecard. gbrain eval trajectory <entity> shows the chronological typed-claim history (mrr/arr/team_size/etc) with regressions auto-flagged inline; gbrain founder scorecard <entity> rolls up claim_accuracy / consistency / growth_trajectory / red_flags into one JSON. Pure-function math in trajectory.ts: detectRegressions(points, threshold) walks consecutive metric-value pairs per metric (10% drop default, env override GBRAIN_TRAJECTORY_REGRESSION_THRESHOLD); computeDriftScore(points) returns 1 - mean(cosine(emb[i], emb[i-1])) over existing embeddings (null when <3 embedded points). Backed by BrainEngine.findTrajectory(opts) — both Postgres and PGLite, single SQL query, deterministic ORDER BY valid_from ASC, id ASC. Source-scoped via the sourceId scalar / sourceIds array dual pattern; visibility-filtered for remote callers. MCP op find_trajectory (read scope, NOT localOnly) registered after find_experts. Migration v67 adds optional typed-claim columns (claim_metric, claim_value, claim_unit, claim_period) + a partial index on (entity_slug, claim_metric, valid_from) WHERE claim_metric IS NOT NULL. Fence widens from 10 to 14 cells when any row has typed data; renderer stays at 10 cells when none do (no churn diff on existing fences). Metric labels normalize to lowercase snake_case via normalizeMetricLabel (15-entry seed map). The consolidate cycle phase does semantic upsert keyed on (page_id, claim, since_date) (fixes the duplicate-takes bug where re-running the cycle after extract_facts cleared consolidated_at appended duplicates via MAX(row_num)+1) and writes chronological valid_until on each cluster's older facts. The extract_facts cycle phase batch-embeds via gateway.embed() before insert AND threads pages.effective_date as the pageEffectiveDate fallback for valid_from (precedence: fence-row > pageEffectiveDate > now()). The contradiction probe MUST NOT write valid_until — grep guard at test/eval-contradictions/no-valid-until-write.test.ts. Haiku extraction lives in src/core/facts/extract.ts (not the extract-facts.ts cycle phase); pageEffectiveDate is OPTIONAL because fence-write.ts callers have no Page object. Migration v89 adds a nullable event_type TEXT column on facts so the substrate carries event-shaped rows (event_type='meeting' / 'job_change' / 'location_change') alongside metric rows. TrajectoryPoint.event_type: string | null projected by both engines. TrajectoryOpts.kind?: 'metric' | 'event' | 'all' filter (default 'all'); founder-scorecard + eval-trajectory pass kind: 'metric' explicitly. Back-compat pinned by test/regressions/v0_40_2_0-trajectory-backcompat.test.ts (byte-identical computeFounderScorecard + computeTrajectoryStats with and without event rows); engine parity in test/engine-parity-event-type.test.ts.
  • src/core/trajectory-format.ts — shared formatTrajectoryBlock(points, entitySlug, opts) consumed by both gbrain think (production) and the LongMemEval harness (benchmark). Groups by (metric ?? event_type), per-metric cap 20, total cap 100, knowledge_update intent annotates value-change rows with (superseded prior). Emits a <trajectory entity="..."> XML envelope — INJECTION_PATTERNS in src/core/think/sanitize.ts escapes </trajectory>, <trajectory ...> open tags, and attribute injection so adversarial fact text can't break out. Pinned by test/trajectory-format.test.ts.
  • src/core/think/intent.ts + src/core/think/entity-extract.ts — pure classifyIntent(question) returns 'temporal' | 'knowledge_update' | 'other' (regex-first, no LLM, 'other' fast path short-circuits with zero SQL). extractCandidateEntities(question, retrievedSlugs) pulls high-precision candidates from retrieved entity-prefix slugs (people/, companies/, organizations/) and medium-precision noun phrases. Stop-word boundaries + leading-verb stripper handle "When did I last meet Marco" → marco. Both consumed by runThink and the LongMemEval harness so the two paths cannot drift. Pinned by test/think-intent.test.ts and test/think-entity-extract.test.ts.
  • src/commands/eval-suspected-contradictions.ts + src/core/eval-contradictions/{judge,runner,types,date-filter,cost-tracker,cache,severity-classify,cross-source,trends,calibration,judge-errors,auto-supersession,fixture-redact}.tsgbrain eval suspected-contradictions [run|trend|review]. Probe samples top-K retrieval pairs per query (cross-slug + intra-page chunk-vs-take), date pre-filters (3-rule layered — same-paragraph-dual-date overrides separation rule), LLM judge (query-conditioned; UTF-8-safe truncation; confidence-floor double-enforcement; resolution_kind output drives paste-ready commands), persistent cache keyed on (chunk_a_hash, chunk_b_hash, model_id, prompt_version, truncation_policy) (prompt edits cleanly invalidate prior verdicts), Wilson 95% CI calibration on the headline percentage with small_sample_note when n<30, judge_errors as first-class typed counters (parse_fail/refusal/timeout/http_5xx/unknown — avoids bias from silent skip), trend writes to eval_contradictions_runs, source-tier breakdown reuses DEFAULT_SOURCE_BOOSTS prefix logic, deterministic sampling (combined_score DESC + lex tiebreaker for stable cache hit-rate). Hermetic via judgeFn + searchFn DI in the runner; never touches the real gateway in tests. Engine surface: BrainEngine.listActiveTakesForPages (batched), writeContradictionsRun + loadContradictionsTrend, getContradictionCacheEntry + putContradictionCacheEntry + sweepContradictionCache. Schema migrations v51 + v52. MCP op find_contradictions (read scope, NOT localOnly, NOT in subagent allowlist — user-initiated only). Doctor check surfaces high-severity findings with paste-ready resolution commands; synthesize phase pre-fetches the latest probe's top-5-by-severity findings and threads them into buildSynthesisPrompt as an informational block. Architecture doc: docs/contradictions.md.
  • src/core/think/index.tsrunThink builds its internal LLMClient via a small adapter wrapping gateway.chat() from src/core/ai/gateway.ts (not new Anthropic() directly) so stdio MCP launches (Claude Desktop, Cursor) that don't inherit shell env still find a key set via gbrain config set anthropic_api_key (the gateway reads ~/.gbrain/config.json AND env). Test seam: opts.client?: ThinkLLMClient injection works (test/think-pipeline.serial.test.ts, test/think-gateway-adapter.test.ts); opts.stubResponse short-circuits before any LLM call. When neither key nor client is available, the "no LLM available" stub fires with NO_ANTHROPIC_API_KEY. Trajectory injection (default ON): runThink orchestrates classifyIntent(question)extractCandidateEntities(question, retrievedSlugs)findTrajectory (5s Promise.race timeout per candidate, concurrency cap 3) → formatTrajectoryBlock. buildThinkUserMessage (in src/core/think/prompt.ts) has a trajectory?: ThinkTrajectoryBlockOpts slot honoring BOTH prompt orderings (calibration mode: retrieval → calibration → trajectory → question; default mode: question → retrieval → trajectory → instruction). The MCP think op handler extracts sourceScopeOpts(ctx) to scalar sourceId / allowedSources / remote on RunThinkOpts so federated-read OAuth clients can't see trajectory rows outside their source scope. Config key think.trajectory_enabled (default true). Any error in the trajectory path degrades to "no block injected" + TRAJECTORY_INJECTION_FAILED warning — the think call never crashes from trajectory. Production path skips fallback_slugify resolutions (avoid querying invented slugs); the LongMemEval harness accepts them. Pinned by test/think-trajectory-injection.test.ts. Debug: GBRAIN_THINK_DEBUG=1 gbrain think "..." prints the spliced prompt to stderr.
  • src/core/operations.ts extension (orphans fix) — findOrphanPages (both engines) filters p.deleted_at IS NULL on the candidate side AND adds JOIN pages src ON src.id = l.from_page_id WHERE src.deleted_at IS NULL to the EXISTS subquery on the link-source side, so soft-deleted pages don't appear as orphans AND links from soft-deleted source pages don't suppress live pages from orphan results. Pinned by test/orphans.test.ts's soft-delete cases.
  • src/commands/eval-longmemeval.ts + src/eval/longmemeval/{harness,adapter,sanitize}.tsgbrain eval longmemeval <dataset.jsonl> runs the public LongMemEval benchmark against gbrain's hybrid retrieval. One in-memory PGLite per run via createBenchmarkBrain + withBenchmarkBrain (NO EphemeralBrain class). Between questions, TRUNCATE over runtime-enumerated pg_tables (schema-migration-safe); infrastructure tables (sources, config, gbrain_cycle_locks, subagent_rate_leases) preserved. cli.ts pre-dispatch bypass so eval longmemeval skips connectEngine() — the user's ~/.gbrain brain is never opened. --expansion defaults OFF (deterministic, no per-query Haiku); pass --expansion to opt in. Default model via resolveModel() 6-tier chain with models.eval.longmemeval config key. Sanitization parity: harness.ts reuses INJECTION_PATTERNS from src/core/think/sanitize.ts so adding a pattern covers takes AND benchmarks. Retrieved chat content wrapped in <chat_session id="..." date="...">; the answer-gen system prompt declares content UNTRUSTED. LLM injection seam: runEvalLongMemEval(args, {client?: ThinkLLMClient}) lets tests stub the client without an API key. p50 25.9ms / p99 30.3ms warm reset+import+search on Apple Silicon (test/eval-longmemeval.test.ts perf gate). Hand the JSONL to LongMemEval's evaluate_qa.py to score (not bundled — needs OpenAI gpt-4o). Per-question JSONL row carries question: string (additive; evaluate_qa.py ignores unknown fields) so gbrain eval cross-modal --batch has the task text without joining; also question_type: string and recall_hit?: boolean so a --resume-from run rebuilds cumulative recallByType from the file alone. --by-type flag emits a {schema_version:1, kind:"by_type_summary", recall_by_type:{...}, aggregate:{...}} line as the FINAL line; resume-replace strips any prior summary at the tail so 5 resumed runs produce 1 summary. Empty-bucket guard: aggregate.rate is null (not NaN) when no questions had ground truth. Optional --by-type-floor F (0..1) exits non-zero with a stderr line per breached question_type (default informational). Pure buildByTypeSummary(buckets) + emitByTypeSummary(path, summary) + seedRecallByTypeFromFile(path, bucket) exported for unit tests. Inline Haiku extractor + trajectory routing (methodology change): src/eval/longmemeval/extract.ts runs extractAndInsertClaims() over each haystack session before retrieval, populating the benchmark brain's facts table inline at import. Single Haiku call per session with content-hash cache (cuts a 3-iteration run from $1.50 to $0.50 when sessions repeat). Per-question alias map (fresh per question, never leaks) collapses "Marco" + "Marco Smith" + "marco" to one canonical slug via first-mention-wins. Fail-open on every error path (malformed JSON, Haiku throw, insert collision, empty array → inserted: 0). getCacheStats() writes empirical hit rate to stderr. src/eval/longmemeval/intent.ts prefers the dataset's question_type label before falling back to the SHARED regex set from src/core/think/intent.ts — single source of truth means think and longmemeval cannot drift. runOneQuestion routes temporal/knowledge_update intents through shared extractCandidateEntitiesfindTrajectory → splice into the answer-gen prompt before the retrieved-sessions block. --no-trajectory bypasses BOTH extractor and intent routing (baseline default-on vs no-trajectory across 3 seeds with paired-bootstrap CI). JSON envelope adds 5 per-question fields when trajectory routing is on: intent, trajectory_points, entity_resolved, resolution_source, methodology_note. The methodology_note writes to stderr at run completion (extractor=haiku-preprocess-full-haystack-v1) — honest disclosure that the published number is "gbrain + Haiku-preprocess pipeline" vs "gbrain alone", NOT directly comparable to baseline LongMemEval scores without that note. Pinned by test/longmemeval-extract.test.ts, test/longmemeval-intent.test.ts, test/longmemeval-trajectory-routing.test.ts (end-to-end through runEvalLongMemEval with both clients stubbed).
  • docs/eval-bench.md — contributor guide for using captured data to benchmark retrieval changes before merging. Linked from CONTRIBUTING.md under "Running real-world eval benchmarks (touching retrieval code)".
  • src/core/eval-capture.ts — op-layer capture wrapper called from src/core/operations.ts query + search handlers (catches MCP + CLI + subagent tool-bridge from one site). Fire-and-forget; failures route to engine.logEvalCaptureFailure so gbrain doctor sees drops cross-process. Capture is off by default — isEvalCaptureEnabled resolution: explicit config.eval.capture (true/false) wins, else process.env.GBRAIN_CONTRIBUTOR_MODE === '1', else off. Contributors set export GBRAIN_CONTRIBUTOR_MODE=1. PII scrubber gate is independent and defaults to true regardless of CONTRIBUTOR_MODE.
  • src/core/eval-capture-scrub.ts — zero-deps PII scrubber: emails, phones, SSN, Luhn-verified credit cards, JWT-shaped tokens, bearer tokens.
  • src/core/search/hybrid.ts — Cathedral II Promise<SearchResult[]> return shape. onMeta?: (m: HybridSearchMeta) => void callback so op-layer capture records what hybridSearch actually did; existing callers leave it undefined. HybridSearchOpts.types?: PageType[] (on SearchOpts) threads a multi-type filter into per-engine searchKeyword + searchVector + searchKeywordChunks as AND p.type = ANY($N::text[]) (primary consumer gbrain whoknows, filters to ['person','company']); AND-applies alongside the single-value type filter. hybridSearch resolves the embedding column at the boundary via resolveColumn(loadRegistry(cfg), opts.embedding_column, cfg) from src/core/search/embedding-column.ts, threads the ResolvedColumn descriptor (not a raw string) into per-engine searchVector, and uses isCacheSafe(resolved, cfg) for the cache-skip decision so a repointed embedding builtin doesn't leak across vector spaces. cosineReScore calls engine.getEmbeddingsByChunkIds(ids, resolved.name) so rerank uses vectors from the active column, not the hardcoded OpenAI embedding. The query MCP op accepts embedding_column for per-call A/B; search (keyword-only) rejects it. Two post-fusion stages + evidence stamp: applyTitleBoost(results, query, titleBoost, floorThreshold) multiplies a result's score by the resolved title_boost when isTitlePhraseMatch fires, stamps title_match_boost, inherits the floor-ratio gate so a title match can't shove a much-stronger page below it; applyAliasHop(engine, results, query, opts) normalizes the query, calls engine.resolveAliases, and on exact normalized-alias match surfaces that page at top-of-organic + epsilon with alias_hit=true; stampEvidence(...) runs LAST (after the alias hop, before slice) on every path — keyword-only, no-embed, and full hybrid — so MCP callers and --explain read the same evidence + create_safety contract. title_boost resolved from the mode bundle and threaded in.
  • docs/eval-capture.md — stable NDJSON schema reference for gbrain-evals consumers.
  • test/public-exports.test.ts — runtime contract test (R2). Imports each of the 17 public subpaths via package name and pins a canary symbol per module. Paired with scripts/check-exports-count.sh.
  • src/core/embedding.ts — OpenAI text-embedding-3-large, batch, retry, backoff. BATCH_SIZE=100 (per-recipe pre-split + recursive halving + adaptive shrink-on-miss live in the gateway; the outer paginator is for progress-callback granularity, not batch protection). estimateEmbeddingCostUsd(tokens) prices against the currently-configured model's rate via currentEmbeddingPricePerMTok() (resolves the per-1M-token rate via lookupEmbeddingPrice(gatewayGetModel()) from embedding-pricing.ts, falling back to the OpenAI 3-large rate 0.13 only when the gateway is unconfigured or the model is unknown to the pricing table). EMBEDDING_COST_PER_1K_TOKENS retained for back-compat with direct importers/tests. currentEmbeddingSignature(): string returns the embedding-provenance signature <provider:model>:<dims> (e.g. openai:text-embedding-3-large:1536) stamped onto pages.embedding_signature at every embed-write site; DELIBERATELY excludes the chunker version (tracked separately via pages.chunker_version) — this signature is strictly the EMBEDDING space, so a model OR dimension swap makes the stored signature differ from current and a page becomes stale. Same unconfigured-gateway fallback as the cost helpers. willEmbedSynchronously({v2Enabled, serialFlag, noEmbed}): SyncEmbedMode is the single source of truth for whether gbrain sync --all embeds at sync time ('inline') or defers to per-source embed-backfill minion jobs ('deferred') — mirrors sync.ts's effectiveNoEmbed resolution exactly (v2Enabled && !serialFlag && !noEmbed → deferred) so cost gate and embed decision can't drift. shouldBlockSync(costUsd, floorUsd, mode): boolean is the pure cost-gate decision: blocks ONLY when mode === 'inline' && costUsd > floorUsd — deferred mode never blocks (the backfill's $X/source/24h cap is the real money gate). Pinned by test/sync-cost-preview.test.ts + test/embedding-signature-stale.test.ts.
  • src/core/ai/dims.ts — per-provider providerOptions resolver for embed-time dimension passthrough; the single source of truth for "which provider needs which knob to produce vector(N)". Exports dimsProviderOptions(implementation, modelId, dims) (called by embed() in gateway.ts), VOYAGE_OUTPUT_DIMENSION_MODELS (private const — the 7 hosted Voyage models that accept output_dimension: voyage-4-large, voyage-4, voyage-4-lite, voyage-3-large, voyage-3.5, voyage-3.5-lite, voyage-code-3 — nano deliberately excluded), VOYAGE_VALID_OUTPUT_DIMS = [256, 512, 1024, 2048] as const, supportsVoyageOutputDimension(modelId), isValidVoyageOutputDim(dims). Voyage path uses the SDK-supported dimensions field ({ openaiCompatible: { dimensions: N } }), NOT Voyage's output_dimension wire-key — the voyageCompatFetch shim in gateway.ts:541 translates dimensions → output_dimension before the HTTP body is built (the AI SDK's openai-compatible adapter doesn't recognize the wire-key, so sending it from here would be silently dropped and Voyage would return its default 1024-dim). Runtime guard: when a Voyage flexible-dim model is configured with dims outside VOYAGE_VALID_OUTPUT_DIMS, throws AIConfigError with a paste-ready gbrain config set embedding_dimensions <256|512|1024|2048> hint at the embed boundary (most common trigger: embedding_model: voyage:voyage-4-large without embedding_dimensions, falling back to DEFAULT_EMBEDDING_DIMENSIONS=1536, an OpenAI default not a Voyage one).
  • src/core/ai/types.ts — provider/recipe types. EmbeddingTouchpoint has optional chars_per_token (default 4, matching OpenAI tiktoken on English) and safety_factor (default 0.8, budget-utilization ceiling), both consulted only when max_batch_tokens is also set; Voyage declares chars_per_token=1 + safety_factor=0.5 to handle dense payloads (CJK/JSON/base64). Pre-split budget = max_batch_tokens × safety_factor / chars_per_token. EmbeddingTouchpoint.multimodal_models?: string[] model-level allow-list for recipes mixing text-only + multimodal models under one touchpoint (Voyage's 12 models share supports_multimodal: true but only voyage-multimodal-3 accepts /multimodalembeddings); when omitted, recipe-level supports_multimodal is sufficient. AIGatewayConfig.embedding_multimodal_model?: string lets embedMultimodal() route to a different model than embedding_model (OpenAI text + Voyage images without flipping the primary pipeline). Recipe.default_headers?: Record<string, string> (static) and Recipe.resolveDefaultHeaders?(env) (env-templated) seam for per-recipe headers riding alongside auth on every openai-compat touchpoint; mutually exclusive (declaring both throws AIConfigError at gateway-configure time); keys conflicting with the resolved auth header (Authorization, the resolver's custom header) rejected at applyResolveAuth call time so defaults can't shadow auth. Used by OpenRouter for the HTTP-Referer + X-OpenRouter-Title + X-Title attribution triple.
  • src/core/ai/gateway.ts — unified seam for every AI call. embedQuery(text, opts?) and isAvailable(touchpoint, modelOverride?) accept a model override so the resolved-column path embeds via the column's provider (Voyage / ZeroEntropy / OpenAI) instead of the global default; the hybrid path passes {embeddingModel: resolved.provider, dimensions: resolved.dimensions} and the gateway resolves the matching recipe via instantiateEmbedding(). isAvailable('embedding', 'voyage:voyage-3-large') checks the override's recipe (not the default) so hybrid skips vector search only when the active column's provider is actually down. zeroEntropyCompatFetch shim (sibling to voyageCompatFetch) handles ZE's non-OpenAI-compatible wire shape — rewrites the request URL /embeddings → /models/embed, injects input_type (default 'document'; 'query' when threaded via providerOptions.openaiCompatible.input_type) and explicit encoding_format: 'float', and rewrites the response {results: [{embedding}], usage: {total_bytes, total_tokens}}{data: [{embedding, index}], usage: {prompt_tokens, total_tokens}} so the SDK's openai-compatible Zod schema validates. Layer 1 (Content-Length) + Layer 2 (per-embedding) OOM caps via tagged ZeroEntropyResponseTooLargeError (kept separate from VoyageResponseTooLargeError because test/voyage-response-cap.test.ts does structural source-text greps pinning the Voyage name). Wired in instantiateEmbedding() via the recipe.id === 'zeroentropyai' branch. gateway.rerank() native HTTP path (no AI-SDK reranking abstraction): resolves the configured reranker via getRerankerModel(), posts to ${recipe.base_url}/models/rerank with bearer auth, returns RerankResult[] sorted by relevance. RerankError.reason classifier: auth | rate_limit | network | timeout | payload_too_large | unknown. 5s default timeout (search hot path). Pre-flight payload guard rejects bodies over recipe.touchpoints.reranker.max_payload_bytes with reason: 'payload_too_large'. _rerankTransport test seam mirrors _embedTransport. embedQuery(text) threads inputType: 'query' through dimsProviderOptions() (4-arg). getRerankerModel() accessor + isAvailable('reranker') branch; configureGateway + reconfigureGatewayWithEngine thread reranker_model; applyResolveAuth + defaultResolveAuth widen touchpoint param to include 'reranker'. embedMultimodalOpenAICompat() routes recipes with implementation: 'openai-compatible' (LiteLLM, Anyscale, vLLM, Gemini multimodal via proxy) through the standard /embeddings endpoint with content arrays carrying image_url entries; the Voyage /multimodalembeddings path is unchanged (gateway selects by recipe implementation tag). Runtime dimension validation throws AIConfigError (with model id + observed + expected) before the vector reaches storage when the provider returns a width that doesn't match the recipe's default_dims or the brain's embedding_dimensions. Pinned by test/openai-compat-multimodal.test.ts. Module-scoped _embedTransport defaults to AI SDK embedMany, with __setEmbedTransportForTests(fn) test seam so tests drive embed() with a stubbed transport. splitByTokenBudget and isTokenLimitError exported @internal (pure functions reused by the test file). Module-level _shrinkState: Map<recipeId, {factor, consecutiveSuccesses}> halves the recipe's effective safety_factor on token-limit miss (floor 0.05) and heals back ×1.5 after SHRINK_HEAL_AFTER=10 consecutive successes. configureGateway() walks every registered recipe at construction and emits a once-per-process stderr warning for any embedding touchpoint missing max_batch_tokens (excluding the canonical OpenAI fast-path). resetGateway() clears _shrinkState, the warned-set, and restores the real transport. embedMultimodal() reads cfg.embedding_multimodal_model first (falls back to cfg.embedding_model); after the recipe-level supports_multimodal fast-fail, validates the resolved model against touchpoint.multimodal_models when declared (closes the Voyage-text-only-into-multimodal-endpoint footgun before any HTTP call). getMultimodalModel() accessor mirrors getEmbeddingModel / getChatModel. Exported VoyageResponseTooLargeError tagged class: voyageCompatFetch's two OOM-defense caps (Layer 1 Content-Length at :595, Layer 2 per-embedding base64 at :619) throw it; the inbound response-rewriter's try/catch (which swallows parse failures so misshaped responses fall through to the SDK parser) checks instanceof VoyageResponseTooLargeError and rethrows so the cap is actually effective (regression assertion in test/voyage-response-cap.test.ts pins the instanceof ⇒ throw err line). AI SDK v6 toolLoop compat (gbrain skillopt rollouts AND production background subagent jobs both route through chat() / toolLoop): in chat(), tool defs wrap the raw JSON Schema with the SDK's jsonSchema() helper (inputSchema: jsonSchema(t.inputSchema)) — v6's asSchema() treats a bare {jsonSchema: ...} object as a thunk and throws "schema is not a function"; new exported pure toModelMessages(messages: ChatMessage[]): unknown[] converts gbrain's provider-neutral ChatMessage[] into v6 ModelMessage[] — tool results (pushed by toolLoop as role:'user' with bare-value tool-result blocks) become a dedicated role:'tool' message with structured output:{type:'json'|'text'|'error-text', value} parts; null output preserved as {type:'json', value:null} (not dropped); text/tool-call blocks pass through with v6 field names (toolCallId/toolName/input); applied at the generateText call (messages: toModelMessages(opts.messages)). The converter is the load-bearing fix for the production subagent path, not just skillopt. Pinned by test/gateway-model-messages.test.ts. Companion fix in src/core/skillopt/rollout.ts: the inline paramsToSchema dropped items on array params; it now uses the shared paramDefToSchema from src/mcp/tool-defs.ts (single source of truth, recursive on items/enum/default).
  • src/core/ai/recipes/zeroentropyai.ts — ZeroEntropy openai-compatible recipe declaring BOTH embedding (zembed-1, 7 Matryoshka dims: 2560/1280/640/320/160/80/40) AND reranker (zerank-2 flagship + zerank-1 + zerank-1-small, 5MB payload cap) touchpoints. implementation: 'openai-compatible' (pinned by regression in test/ai/zeroentropy-recipe.test.ts). base_url_default: 'https://api.zeroentropy.dev/v1' already ends with /v1, so the zeroEntropyCompatFetch URL rewrite /embeddings → /models/embed produces …/v1/models/embed (NOT …/v1/v1/… — pinned by regression). chars_per_token: 1 + safety_factor: 0.5 match Voyage's dense-content hedge.
  • src/core/ai/recipes/llama-server-reranker.ts — sibling of llama-server (the embedding recipe) for llama.cpp in --reranking mode. Distinct recipe rather than dual-touchpoint extension because --reranking and --embeddings are mutually exclusive at server-launch time, so the two backends need independent base URLs (default 8081 here vs 8080 there). Declares reranker touchpoint with models: [] (user-provided id matching the --alias the user launched with), path: '/rerank' (leaf-only; consumes RerankerTouchpoint.path override; gateway concatenates with base_url_default which ends in /v1, producing …/v1/rerank), default_timeout_ms: 30_000 (consumed by src/core/search/mode.ts's reranker timeout chain — CPU-only first-call warmup headroom; the 5s mode-bundle default would fail-open as timeout), cost_per_1m_tokens_usd: 0 (recognized by FREE_LOCAL_RERANK_PROVIDERS in src/core/budget/budget-tracker.ts so --max-cost callers don't hard-fail on local rerank). Setup hint emphasizes --alias because llama-server's /v1/models defaults model id to the gguf file path without it. Covers Qwen3-Reranker via llama.cpp AND self-hosted ZE weights via llama.cpp — same recipe, different --model at launch. Pinned by test/ai/recipe-llama-server-reranker.test.ts. Voyage / Cohere / vLLM rerankers stay out of scope (different wire shapes). Same wave adds: path?: string + default_timeout_ms?: number on RerankerTouchpoint in src/core/ai/types.ts; consumed by the URL build at src/core/ai/gateway.ts:rerank() and by mode-resolution at src/core/search/mode.ts:resolveSearchMode (precedence: per-call > config-key > recipe touchpoint default > mode bundle); LLAMA_SERVER_RERANKER_BASE_URL env passthrough in src/cli.ts:buildGatewayConfig; FREE_LOCAL_RERANK_PROVIDERS set in src/core/budget/budget-tracker.ts:lookupPricing (rerank-kind-only zero-pricing for the local provider prefix); doctor-fix at src/commands/models.ts:probeRerankerConfig reads search.reranker.model via loadSearchModeConfig + resolveSearchMode (closes file-plane / DB-plane divergence where doctor said "not configured" while live search was actively reranking — the field-plane getRerankerModel() read nothing writes); probeRerankerReachability reads the recipe's default_timeout_ms so CPU-only cold-start doesn't false-fail.
  • src/core/ai/recipes/openrouter.ts — OpenRouter openai-compatible recipe: single key, many providers via openrouter:<provider>/<model> strings. base_url_default: 'https://openrouter.ai/api/v1'. Embedding touchpoint: openai/text-embedding-3-small at 1536 dims with Matryoshka dims_options: [512, 768, 1024, 1536]; max_batch_tokens: 300_000 = OpenAI's aggregate-per-request token cap (NOT per-input). Chat touchpoint declares 8 curated entry points (gpt-5.2, gpt-5.2-chat, gpt-5.5, claude-haiku-4.5, claude-sonnet-4.6, claude-opus-4.7, gemini-3-flash-preview, deepseek-chat) but openai-compat tier accepts any model ID; deliberately no max_context_tokens because OR's catalog spans 128K to 1M+. supports_subagent_loop: false is INFORMATIONAL — the real gate is isAnthropicProvider() in src/core/model-config.ts which hard-pins gbrain's subagent infra to Anthropic-direct. Declares resolveDefaultHeaders(env) returning OR's three attribution headers: HTTP-Referer (required for OR app-attribution), X-OpenRouter-Title (preferred), X-Title (back-compat alias); defaults to https://gbrain.ai / gbrain; forks override via OPENROUTER_REFERER / OPENROUTER_TITLE env vars. Smoke-tested by test/ai/recipe-openrouter.test.ts (incl. the shape-test regression guard: every model in the chat list matches ^[a-z0-9-]+\/[a-z0-9._-]+$).
  • src/core/rerank-audit.ts — failure-only JSONL audit at ~/.gbrain/audit/rerank-failures-YYYY-Www.jsonl (ISO-week rotation, mirrors src/core/audit-slug-fallback.ts). Exports logRerankFailure({reason, model, query_hash, doc_count, error_summary}) + readRecentRerankFailures(days). Deliberately no logRerankSuccess: writing once per tokenmax search is hot-path I/O churn AND success events leak query volume + timing into a local audit file. gbrain doctor's reranker_health check reads search.reranker.enabled first so "no events in window" is interpreted correctly (disabled → ok; enabled → ok). Query text SHA-256-prefix-hashed (8 hex chars) for privacy. GBRAIN_AUDIT_DIR env override honored via the shared resolveAuditDir().
  • src/core/search/embedding-column.ts — single source of truth for "which content_chunks.* column does this query rank against?" Pure functions, no engine I/O: loadRegistry(cfg) walks the embedding_columns config (DB plane, JSON map keyed by column name with {provider, dimensions, type} entries), seeds the OpenAI embedding builtin when unset, validates everything before it lands (column-name regex, type ∈ vector | halfvec, dims in [1, 8192], provider format) using Object.create(null) + Object.hasOwn so a key like constructor rejects instead of resolving to Object.prototype.constructor. resolveColumn(registry, override?, cfg) is the boundary call: returns a frozen ResolvedColumn descriptor ({name, provider, dimensions, type}) honoring per-call override → search_embedding_column config → 'embedding' default; throws UnknownEmbeddingColumnError with the list of registered names on miss. isCacheSafe(resolved, cfg) compares the full embedding SPACE (provider + dimensions + name) against cfg's default so a repointed embedding builtin doesn't serve OpenAI-shaped cache rows. validateResolvedColumn(descriptor) re-validates hand-rolled descriptors that bypass the registry (internal-SDK passthrough) so the SQL-injection escape hatch through the descriptor field is closed. Consumed by hybridSearch, gateway.embedQuery(text, {embeddingModel, dimensions}), cosineReScore, and the query MCP op (per-call embedding_column param). Pinned by test/search/embedding-column.test.ts (prototype-pollution, descriptor passthrough, env-only Postgres install, empty-brain coverage gate, cache-space comparison).
  • src/core/search/rerank.ts — the call-site abstraction. applyReranker(query, results, opts) slots between dedupResults() and enforceTokenBudget() in src/core/search/hybrid.ts. Slices opts.topNIn (default 30) by current RRF order, sends to gateway.rerank(), reorders by relevanceScore desc, appends the un-reranked tail unchanged (recall protection). Fail-open on every RerankError.reason: any error logs via logRerankFailure and returns the input array unchanged. Stamps rerank_score onto reordered items so downstream telemetry sees the new ordering signal. topNOut: null is the explicit "don't truncate" signal — semantically distinct from undefined ("fall through to mode bundle"). Test seam: opts.rerankerFn stubs gateway.rerank without the network.
  • src/core/search/return-policy.ts (default OFF) — intent-aware adaptive return-sizing. Pure, dependency-light module that trims the final ranked candidate set to an intent-driven cap instead of returning the full top-K. entity intent gets a tight cap; temporal/event/general get a recall-preserving cap. A minKeep failsafe (≥1) guarantees a human never gets a silent blank when candidates exist. WHY a cap, not a score-cliff detector: PrecisionMemBench instrumentation (gbrain-evals) measured the rank1→rank2 RRF gap is ~identical whether rank-1 is correct (0.602) or wrong (0.569) — mechanical decay, not a separatrix; rank-1 is right in 94% of single-answer cases, so "return a tight set" is the whole win and cliff-cutting just adds noise. Exports AdaptiveReturnConfig, DEFAULT_ADAPTIVE_RETURN (frozen: enabled=false, entityMax=2, otherMax=6, minKeep=1), AdaptiveReturnDecision ({applied, intent, cap, kept, total}), AdaptiveReturnInput (boolean | Partial<AdaptiveReturnConfig> | undefined), adaptiveReturnFromConfig(cfg), resolveAdaptiveReturn(perCall, fromConfig) (defaults → config → per-call merge), adaptiveReturnEnabled(...) (cache-skip gate check), applyAdaptiveReturn(results, intent, cfg) (the trim). Config knobs (DB or file plane): search.adaptive_return (master switch), search.adaptive_return_entity_max, search.adaptive_return_other_max, search.adaptive_return_min_keep (each clamped ≥1). Wired into hybridSearch AFTER applyReranker, BEFORE the limit slice, and ONLY on the first page (offset===0) — paginating a confidence-gated set is incoherent, so paginated calls fall through to the fixed limit. Stamps the decision onto HybridSearchMeta.adaptive_return for gbrain search --explain. hybridSearchCached SKIPS the cache when the gate is on (a trimmed set must not be served to a gate-off lookup and vice versa). SearchOpts.adaptiveReturn + HybridSearchMeta.adaptive_return declared in src/core/types.ts. Agent-facing: the query op (src/core/operations.ts) exposes an adaptive_return boolean param whose description instructs the agent WHEN to set it (single-answer → on; breadth/exploration → off; pass limit:1 for a hard single-answer cap), threaded into hybridSearchCached — end users never touch the config knob; their agent decides per query (same pattern as salience/recency). Pinned by test/search/return-policy.test.ts (mechanism) + test/search/query-op-adaptive-return.test.ts (agent surface: param exists + description teaches both directions + the never-empty contract).
  • src/core/search/autocut.ts (default ON in reranked modes) — Weaviate-style autocut: score-discontinuity result-sizing on the cross-encoder rerank separatrix. applyAutocut(results, scoreOf, cfg) normalizes the reranker scores, finds the largest consecutive gap, and cuts there when it clears jumpRatio (default 0.20); robust to unsorted provider output (cuts on a sorted copy, keeps items in INPUT order via a score threshold), guards top<=0/non-finite, never returns empty, and no-ops when <2 results carry a finite rerank_score (covers the reranker fail-open path). WHY rerank_score and NOT RRF/cosine: gbrain measured (see return-policy.ts) that the RRF rank1→rank2 gap is ~flat whether rank-1 is right or wrong — not a separatrix; the cross-encoder score IS. So autocut runs ONLY where the reranker ran (the floor reaches balanced+tokenmax; conservative is a documented no-op). Exports AutocutConfig, DEFAULT_AUTOCUT (frozen: enabled=true, jumpRatio=0.20, minKeep=1), AutocutDecision ({applied, signal:'rerank'|'none', cut, kept, total, gapRatio}), AutocutInput, autocutFromConfig, resolveAutocut, applyAutocut. Cache-key integration (clean path, not the adaptive-return cache-skip hack): enable+sensitivity flow through ModeBundleResolvedSearchKnobsknobsHash exactly like graph_signals. mode.ts adds autocut/autocut_jump (conservative false, balanced/tokenmax true@0.20) AND sets reranker_top_n_in = searchLimit for reranked modes (so the reranker scores the full returned set; there is no un-scored tail for autocut to wrongly drop — closes the load-bearing recall finding). KNOBS_HASH_VERSION is 8 (title_boost claimed 7; autocut appends 8 — one-time global cache cold-miss on upgrade). Wired into hybridSearch AFTER adaptive-return, BEFORE the limit slice, first page only; emits HybridSearchMeta.autocut. BOTH the cache-miss finalMeta and cache-HIT cachedMeta rebuilds carry autocut+adaptive_return+mode+embedding_column. Preserves alias-hop exact matches: applyAutocut takes an optional preserve predicate; hybrid passes r => r.alias_hit === true so a canonical page injected by applyAliasHop after reranking (no rerank_score) is never cut. Agent surface: query op autocut boolean (ceiling override — false forces full top-K); SearchOpts.autocut; --explain shows per-result rerank_score, formatAutocutSummary renders the decision when search meta is threaded; gbrain search modes attribution; metric glossary autocut.signal/autocut.gap_ratio. Config: search.autocut, search.autocut_jump. Default-ON backed by an in-repo eval gate — test/search/autocut-eval.test.ts (also bun run eval:autocut) measures precision-lift-without-recall-regression over labeled qrels fixtures with modeled cross-encoder distributions (no API key, no sibling repo; runs in CI): mean precision 0.33→0.94, recall 1.00→0.95, ZERO recall regression on enumeration queries. Env-overridable floors. Pinned by test/search/autocut.test.ts (pure-fn), test/search/query-op-autocut.test.ts (agent surface), test/search/autocut-integration.serial.test.ts (IRON-RULE behavioral via rerankerFn DI seam: cliff trims, flat doesn't, no-reranker no-ops, autocut:false ceiling, composes with adaptive-return), test/search/autocut-eval.test.ts (the precision/recall gate), and the v=8 knobsHash assertions in test/search-mode.test.ts.
  • src/core/ai/recipes/voyage.ts — Voyage AI openai-compatible recipe. Declares chars_per_token=1 + safety_factor=0.5 so the gateway pre-splits Voyage batches at a 60K-character budget (50% of 120K-token cap with the dense-tokenizer ratio), avoiding the backfill loop where tiktoken-grounded budgeting undercounted Voyage's actual token usage. Declares multimodal_models: ['voyage-multimodal-3'] so the gateway rejects text-only Voyage models pointed at the multimodal endpoint with a clear AIConfigError instead of waiting for Voyage's HTTP 400. Recipe docstring at :7-16 names the seven hosted flexible-dim models that accept output_dimension (voyage-4-large, voyage-4, voyage-4-lite, voyage-3-large, voyage-3.5, voyage-3.5-lite, voyage-code-3) and notes voyage-4-nano is the open-weight variant fixed at 1024-dim that does NOT accept the parameter (negative regression assertion in test/ai/gateway.test.ts: dimsProviderOptions returns undefined for voyage-4-nano). voyage-code-3 is the recommended embedding model for gstack per-worktree code brains (Topology 3 in docs/architecture/topologies.md); discoverability surfaces: decision-tree branch in docs/integrations/embedding-providers.md, Topology 3 "Recommended embedding model" subsection, runtime nudge from gbrain reindex --code against non-code-tuned models. Recipe-shape regression pinned by test/ai/voyage-code-3-recipe.test.ts.
  • src/core/ai/recipes/anthropic.ts — Anthropic recipe (chat + expansion touchpoints). Canonical id is claude-sonnet-4-6 (no date suffix); a reverse alias claude-sonnet-4-6-20250929 → claude-sonnet-4-6 keeps stale user configs working (rescues facts.extraction_model and models.dream.synthesize). Recipe-shape regression pinned by test/anthropic-model-ids.test.ts.
  • src/core/model-pricing.ts — single source of truth for paid-cloud CHAT/completion model pricing (USD per 1M tokens, input | output). CANONICAL_PRICING is a provider:model-keyed table (Anthropic Opus 4.8/4.7/4.6 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5 both dateless + dated, plus OpenAI / Google / Together / DeepSeek panel models). canonicalLookup(modelId) resolves bare (claude-opus-4-8), colon (anthropic:claude-opus-4-8), and slash (anthropic/...) forms — bare ids default to the anthropic: provider; nested OpenRouter ids (openrouter:anthropic/...) intentionally MISS so OpenRouter markup isn't repriced as the inner vendor. Every other chat-pricing table is a DERIVED view of this one (NOT a hand-copied duplicate), so cross-table price drift is structurally impossible. Embeddings live separately in embedding-pricing.ts (different unit). Pinned by test/model-pricing.test.ts whose drift guard asserts each derived view equals canonical and that the cross-modal panel models are all present.
  • src/core/anthropic-pricing.ts — bare-keyed Anthropic VIEW of model-pricing.ts (the anthropic: canonical entries with the prefix stripped). Kept distinct because many callers look up by bare Claude id and because estimateMaxCostUsd(modelId, inTokens, maxOutTokens) carries the null-on-miss contract the dream-cycle budget gate depends on (non-Anthropic ids return null → caller warns BUDGET_METER_NO_PRICING once and runs unbounded). estimateMaxCostUsd routes bare/colon/slash ids through splitProviderModelId. Do NOT hand-edit prices here — the map is derived from canonical, so it cannot drift. ANTHROPIC_PRICING is consumed by budget/budget-tracker.ts, minions/batch-projection.ts, and cycle/budget-meter.ts.
  • src/core/takes-quality-eval/pricing.ts — fail-closed budget pricing for eval takes-quality run --budget-usd N. MODEL_PRICING is a curated provider:model allowlist (default panel + likely overrides) whose VALUES are derived from model-pricing.ts via canonicalLookup; an allowlisted id missing from canonical throws at module load. Schema is {input_per_1m, output_per_1m}. A model NOT on the allowlist aborts the run with an actionable error rather than guessing (distinct from cross-modal-eval/runner.ts, which silently estimates zero on unknown models — both now source numbers from canonical).
  • src/core/budget/budget-tracker.ts — keystone primitive for the brainstorm cost-cathedral wave. One typed error (BudgetExhausted with reason: 'cost' | 'runtime' | 'no_pricing'), one schema-stable audit JSONL at ~/.gbrain/audit/budget-YYYY-Www.jsonl. Contracts: record() throws when cumulative spend exceeds cap (the cap is a real ceiling, not a suggestion); reserve() hard-fails with reason: 'no_pricing' when maxCostUsd is set AND the model is missing from pricing maps (warn-once preserved when cap is unset); extractUsageFromError(err, fallback) returns err.usage when the SDK provides it, else the pessimistic fallback (caller passes maxOutputTokens, not the optimistic pre-call estimate). onExhausted(cb) fires once synchronously BEFORE the throw propagates so callers can persist checkpoints. Replaces three parallel copies (inline brainstorm class, cycle/budget-meter, eval-contradictions). Adapts the old BudgetMeter (public shape preserved + schema_version: 1 stamped on every dream-budget audit line). Pinned by 18 unit cases.
  • src/core/audit-week-file.ts — single source of truth for ISO-week audit JSONL filename math. Exports isoWeek(d), isoWeekFilename(prefix, now?), resolveAuditDir() (honors GBRAIN_AUDIT_DIR). Year-boundary correctness pinned by tests at 2020-W53 (the 53-week year), 2025-W01 rolling in from 2024-12-30 (Monday), 2026-W01. Four call sites migrated: src/core/minions/handlers/shell-audit.ts, src/core/facts/phantom-audit.ts, src/core/audit-slug-fallback.ts, src/core/cycle/budget-meter.ts. Each keeps its compute<X>AuditFilename thin wrapper for back-compat with existing tests.
  • src/core/ai/gateway.ts:withBudgetTracker — gateway-layer enforcement via AsyncLocalStorage<BudgetTracker>. withBudgetTracker(tracker, fn) installs the tracker on the module-internal store; every gateway.chat / embed / rerank call inside the scope auto-composes (reserve before, record in try/finally). Outside-scope calls are budget no-ops. Nested scopes restore the outer tracker on exit. getCurrentBudgetTracker() is the test seam. The chat path uses the pessimistic fallback on error paths; the embed path estimates input tokens from char count × recipe's chars_per_token because the AI SDK doesn't surface per-batch embed token usage; the rerank path estimates char count of query+docs. Pinned by 6 unit cases.
  • src/core/diarize/payload-fitter.ts — generic fit-arbitrarily-large-items-into-per-call-token-budget utility. 'batch' strategy is deterministic token-budgeted chunking with no LLM calls. 'summarize' strategy embed-clusters into ceil(items/4) groups via cheap deterministic nearest-neighbor on cosine, Haiku-summarizes each cluster via Promise.allSettled at parallelism=4. Each Haiku call composes the active BudgetTracker via the AsyncLocalStorage. Quality gate: when success_ratio < min_success_ratio (default 0.75), result is flagged degraded: true — the fitter preserves the successful subset; the caller decides whether to surface a partial result or abort.
  • src/core/brainstorm/checkpoint.ts — crash-resilient checkpoint for gbrain brainstorm and gbrain lsd. Persists FULL idea bodies (~50KB/run) so resume MERGES pre-crash ideas with post-resume ideas before the judge runs (a resume that produces only second-run output is silent partial output). run_id = sha256(question + profile + sort(close_slugs) + sort(far_slugs)).slice(0,16) — NO embedding bits, stable across embedding-model swaps. Atomic write via .tmp + rename. ONE resume flag (--resume <run_id> covers both failed AND never-attempted crosses); --list-runs prints run_ids mtime-newest-first; --force-resume bypasses the 7-day staleness gate. Cycle purge phase (gbrain dream --phase purge) GCs checkpoints older than 7 days via gcStaleCheckpoints(7). Pinned by test/e2e/brainstorm-resume.test.ts (20 unit + 3 E2E cases incl. the merge contract).
  • src/core/remediation-checkpoint.tsdoctor --remediate checkpoint at ~/.gbrain/remediation/<plan_hash>.json. plan_hash = sha256(JSON.stringify(sorted recommendation ids)).slice(0,16). Schema-versioned, atomic .tmp + rename. gbrain doctor --remediate --resume <plan_hash> (no arg picks newest matching) loads it and skips completed steps. Mismatched plan_hash refuses with a paste-ready message. Cleared on clean completion. Pinned by 13 unit cases.
  • src/core/model-config.ts — Model-string resolution (the seam every internal LLM call walks through). Four-tier system (ModelTier = 'utility' | 'reasoning' | 'deep' | 'subagent') with TIER_DEFAULTS (utility→haiku-4-5, reasoning→sonnet-4-6, deep→opus-4-7, subagent→sonnet-4-6) and tier?: ModelTier on ResolveModelOpts. 8-step resolution chain: cliFlag → deprecated key → config key → models.defaultmodels.tier.<tier> → env var → TIER_DEFAULTS[tier] → caller fallback. isAnthropicProvider(modelString) checks provider:model prefix OR claude- bare-id pattern (routes through splitProviderModelId from src/core/model-id.ts so slash-form ids like anthropic/claude-sonnet-4-6 classify correctly). enforceSubagentAnthropic() is the layer-2 runtime guard: when tier === 'subagent' resolves non-Anthropic, it emits a once-per-(source, model) stderr warn AND falls back to TIER_DEFAULTS.subagent (the Anthropic Messages API tool-loop can't run on OpenAI/Gemini). _resetDeprecationWarningsForTest() also clears _subagentTierWarningsEmitted. Pinned by test/model-config.serial.test.ts.
  • src/core/ai/model-resolver.ts — Recipe-touchpoint validator. assertTouchpoint(recipe, touchpoint, modelId, extendedModels?) takes an optional 4th extendedModels: ReadonlySet<string>: when the modelId is in that set the native-recipe allowlist throw is bypassed (user explicitly opted in via config, so provider rejection surfaces as model_not_found at HTTP call time and gbrain models doctor catches it earlier). Default code paths with hardcoded model strings MUST NOT pass extendedModels — source typos still fail fast (the fail-fast contract for chat + expand + embed stays intact).
  • src/core/ai/gateway.ts extension — module-scoped _extendedModels: Map<providerId, Set<modelId>> registry feeds assertTouchpoint's 4th-arg path. reconfigureGatewayWithEngine(engine) (async, called from cli.ts after engine.connect(), before every command except CLI_ONLY no-DB commands) re-resolves expansion + chat defaults through resolveModel() so models.tier.* and models.default overrides apply to both. DEFAULT_CHAT_MODEL is anthropic:claude-sonnet-4-6. __setChatTransportForTests seam mirrors __setEmbedTransportForTests so tests drive chat() with a stubbed transport.
  • src/core/minions/queue.ts extension — MinionQueue.add() rejects subagent jobs whose data.model resolves via isAnthropicProvider() to a non-Anthropic provider. Lazy-imports model-config.ts to avoid pulling engine types into queue's eager-load surface. Layer 1 of the three-layer subagent provider enforcement (layers 2+3: model-config.ts:enforceSubagentAnthropic runtime fallback + src/commands/doctor.ts subagent_provider check). Pinned by test/agent-cli.test.ts.
  • src/commands/models.tsgbrain models [--json] read-only routing dashboard: prints tier defaults (utility/reasoning/deep/subagent), the resolved value for each (re-walking the resolution chain), every per-task override (11 PER_TASK_KEYS: models.dream.synthesize, models.dream.patterns, models.drift, models.auto_think, models.think, models.subagent, facts.extraction_model, models.eval.longmemeval, models.expansion, models.chat, models.dream.synthesize_verdict), the alias map, and a source-of-truth column (default / config: <key> / env: <VAR>). gbrain models doctor [--skip=<provider>] [--json] fires a 1-token gateway.chat() probe against each configured chat + expansion model and classifies failures into {model_not_found, auth, rate_limit, network, unknown}. Wired into cli.ts dispatch + CLI_ONLY set. A zero-token embedding_config probe runs FIRST, before any chat/expansion probes spend money: probeEmbeddingConfig() reads getEmbeddingModel() + getEmbeddingDimensions() and (for Voyage flexible-dim models) checks isValidVoyageOutputDim(dims) against VOYAGE_VALID_OUTPUT_DIMS. ProbeStatus variant 'config' + optional fix?: string on ProbeResult surface a paste-ready gbrain config set ... line in human + JSON output; touchpoint label 'embedding_config' joins 'chat' and 'expansion'.
  • src/core/init-embed-check.ts — embedding-key validation at gbrain init. runInitEmbedCheck(opts) runs a config-only diagnoseEmbedding (catches a missing key for ANY provider) plus a best-effort liveTestEmbed (1-token gateway.embed(['probe'], {inputType:'query', abortSignal}), 5s AbortController timeout, never throws — catches an invalid/expired key). Loud warning to stderr; init still exits 0 (--no-embedding is the deferred-setup escape; --skip-embed-check / GBRAIN_INIT_SKIP_EMBED_CHECK=1 skip the check). Builds the effective env (process.env + file-plane openai/anthropic/zeroentropy_api_key from loadConfigFileOnly() + opts.apiKey) and configures the gateway via buildGatewayConfig before diagnose/probe, so the check sees the same keys AND provider base URLs runtime will (no false "missing key" for config.json-keyed users; the probe hits the right endpoint). Init-specific warning text names --no-embedding / --skip-embed-check, not the sync-flavored --no-embed. Wired into initPGLite + initPostgres in src/commands/init.ts, with the result added to the --json envelope as embedding_check {ok, reason?, live_ok?}. Pinned by test/init-embed-check.test.ts (hermetic via the gateway embed-transport seam + withEnv).
  • src/core/ai/build-gateway-config.tsbuildGatewayConfig(c: GBrainConfig): AIGatewayConfig, extracted from src/cli.ts (which re-exports it for back-compat). Lets core modules (init-embed-check.ts) reuse it without importing the CLI entrypoint. Single owner of folding file-plane API keys (openai/anthropic/zeroentropy) into the gateway env and threading local-server *_BASE_URL env vars into base_urls; process.env wins. Pinned by test/ai/build-gateway-config.test.ts.
  • src/commands/doctor.ts extension — subagent_provider check (layer 3 of 3). Warns when models.tier.subagent is explicitly set non-Anthropic (message names the bad value + paste-ready fix gbrain config set models.tier.subagent anthropic:claude-sonnet-4-6); also warns when models.default would sneak subagent into a non-Anthropic provider via tier inheritance. OK when subagent tier resolves to Anthropic. Tests in test/doctor.test.ts.
  • src/core/skill-trigger-index.ts — Shared loader that unions per-skill SKILL.md frontmatter triggers: with curated RESOLVER.md / AGENTS.md rows from skillsDir AND the parent dir (preserves the OpenClaw workspace-root layout). UNION semantics: explicit RESOLVER.md rows ADD to frontmatter triggers (don't replace). Dedup keyed on (skillPath, trigger.trim().toLowerCase()). Three consumers fold through this primitive — checkResolvable, runRoutingEvalCli, mounts-cache.composeResolvers — so fixing frontmatter reaches all of them. Exports loadSkillTriggerIndex(skillsDir): SkillTriggerEntry[], entriesToResolverContent(entries): string (synthesizes a markdown-table resolver string for runRoutingEval's string-content API), findPrimaryResolverPath(skillsDir): string | null, the FRONTMATTER_SECTION constant, and _resetWarnedSkillsForTests. Skip rules: non-directory entries, _*/.* prefixes, conventions/+migrations/ subdirs, skills with no SKILL.md (deprecated install/ graceful-skipped), no triggers: array, or malformed YAML (warn-once + skip). Reuses parseSkillFrontmatter from src/core/skill-frontmatter.ts (regex-based, not full YAML). Pinned by test/skill-trigger-index.test.ts (18 hermetic cases). CI gate bun run check:resolver (= bun src/cli.ts check-resolvable --strict --skills-dir skills/) wired into bun run verify.
  • src/core/skill-catalog.ts — host-repo skill catalog backing the MCP list_skills / get_skill ops. Lets a thin MCP client (Codex desktop, Claude Code, Claude Cowork, Perplexity) DISCOVER + FOLLOW the agent repo's fat-markdown skills over gbrain serve — a skill is prose, so "using" one = fetching its body then calling the gbrain MCP tools the server already exposes. Read-scope, NOT localOnly (defensible only via the full mitigation stack): (1) publish gateassertPublishEnabled(ctx, publishSkills); remote callers require mcp.publish_skills === true, default-OFF so an upgrade never silently grants existing read tokens host-skill read; local callers (ctx.remote === false) always pass. (2) path confinementassertSkillNameShape rejects separators/../null/space before any FS access; the client name is a manifest LOOKUP KEY (via loadOrDeriveManifest), never a raw path segment; confineManifestPath does realpath + relative-containment + SKILL.md-regular-file check on EVERY entry (defeats poisoned manifest.json path, symlink/.. escape). (3) frontmatter allowlistGetSkillResult.frontmatter projects a safe subset; private writes_to + sources dropped. (4) prose-only + 256KB cap (MAX_SKILL_MD_BYTES, env GBRAIN_MAX_SKILL_MD_BYTES), size-checked twice (statSync + UTF-8 byte length). (5) no install_path serve for remote — remote callers use autoDetectSkillsDir (no install-path tier) so a hosted gbrain with no agent repo returns storage_error; local callers use autoDetectSkillsDirReadOnly. (6) MCP rate-limiter caps call rate. Config reads honor BOTH planes: readMcpPublishSkills / readMcpSkillsDir prefer the DB plane (engine.getConfig) over the file plane (ctx.config.mcp). Tool-honesty: crossReferenceTools(declared, ctx) splits a skill's declared tools: into usable_tools vs unavailable_tools; buildSkillCatalog's instructions envelope (SKILL_CATALOG_INSTRUCTIONS) carries the "these are prose, follow-then-call-tools" protocol. Skills are host-filesystem repo-global — sourceScopeOpts(ctx) / ctx.brainId deliberately do NOT apply. buildSkillCatalog is resilient (one malformed/escaping skill is skipped, never throws). Config keys in src/core/config.ts: GBrainConfig.mcp?: { publish_skills?, skills_dir? } + KNOWN_CONFIG_KEYS entries mcp.publish_skills/mcp.publish_skills_prompted/mcp.skills_dir + mcp. prefix in KNOWN_CONFIG_KEY_PREFIXES. src/commands/init.ts writes config.mcp = { publish_skills: true, ... } for new installs (existing config wins on re-init). src/commands/upgrade.ts:runPostUpgrade adds a one-time consent prompt (gated by mcp.publish_skills_prompted; existing installs stay OFF until owner opts in). Two ops register in src/core/operations.ts (list_skills with optional section filter + cliHints:{name:'skills'}; get_skill taking name + cliHints:{name:'skill', positional:['name']}) and dynamically import this module to avoid the import cycle (skill-catalog statically imports the operations array). Descriptions in src/core/operations-descriptions.ts (LIST_SKILLS_DESCRIPTION, GET_SKILL_DESCRIPTION, SKILL_CATALOG_INSTRUCTIONS, SKILL_CLIENT_GUIDANCE), pinned by test/operations-descriptions.test.ts. CLI: gbrain skills / gbrain skill <name>. Pinned by test/skill-catalog.test.ts, test/skill-catalog-security.test.ts (path-confinement / poisoned-manifest / symlink-escape), test/skill-catalog-transports.test.ts (publish-gate + remote-vs-local) over test/fixtures/skill-catalog/.
  • src/core/check-resolvable.ts — Resolver validation: reachability, MECE overlap, DRY checks, structured fix objects. CROSS_CUTTING_PATTERNS.conventions is an array (notability gate accepts conventions/quality.md and _brain-filing-rules.md). extractDelegationTargets() parses > **Convention:**, > **Filing rule:**, and inline backtick references. DRY suppression is proximity-based via DRY_PROXIMITY_LINES = 40. parseResolverEntries accepts BOTH the markdown table AND a compact list format (- **skill-name**: trigger1 | trigger2 | trigger3 or - skill-name: trigger1 | trigger2); shapes can mix in one file, folded by the multi-resolver merge. Skill name MUST be kebab-lowercase (regex [a-z][a-z0-9-]+) so prose bullets like - **Note**:/- **Convention**:/- **TODO**: don't false-match as skill rows. skillPath is ALWAYS derived as skills/<name>/SKILL.md: an optional → \skills/path`(or ASCII->) suffix is stripped from the trigger but NOT honored as the path — two consumers (routing-eval.ts:skillSlugFromPath, the manifest lookup) assume the convention; use the table format for non-conventional paths. Multi-trigger rows fan out to one entry per trigger sharing the same skillPath; checkResolvablededupes so the reachability count counts each skill once. Pinned bytest/check-resolvable.test.ts(11 cases: bold+plain forms, Unicode+ASCII suffix strip, ellipsis filter, empty pipe segments, mixed shapes, prose-bullet rejection) +test/check-resolvable-openclaw-compact.test.ts(8 cases overtest/fixtures/openclaw-compact-resolver/andtest/fixtures/openclaw-mixed-merge/). Tutorial: docs/guides/scaling-skills.md` (three-tier scaling: ~300-skill agent to ~4K tokens/turn from ~25K).
  • src/core/repo-root.ts — Shared findRepoRoot(startDir?): walks up from startDir (default process.cwd()) looking for skills/RESOLVER.md. Zero-dependency, imported by doctor.ts and check-resolvable.ts; parameterized startDir makes tests hermetic. Read-path / write-path split: autoDetectSkillsDir (shared, read+write-safe) has tier-0 $GBRAIN_SKILLS_DIR operator override ahead of the 4-tier chain. autoDetectSkillsDirReadOnly wraps it with a tier-5 install-path fallback that walks up from fileURLToPath(import.meta.url) and gates on isGbrainRepoRoot so unrelated repos can't false-positive. Read-path callers (doctor, check-resolvable, routing-eval) use the read-only variant; write-path callers (skillpack install, skillify scaffold, post-install-advisory) stay on the shared function so install-from-~ can't retarget the bundled gbrain skills/ instead of the user's workspace. SkillsDirSource variants 'env_explicit', 'install_path'; AUTO_DETECT_HINT_READ_ONLY documents the extra tier. The --fix safety gate in doctor.ts + check-resolvable.ts refuses auto-repair when detected.source === 'install_path'.
  • src/commands/check-resolvable.ts — Standalone CLI wrapper over checkResolvable(). Exports parseFlags, resolveSkillsDir, DEFERRED, runCheckResolvable. Exit rule: 1 on any issue (warnings OR errors), stricter than doctor's ok flag. Stable JSON envelope {ok, skillsDir, report, autoFix, deferred, error, message} — same shape on success and error. --fix runs autoFixDryViolations BEFORE checkResolvable (same ordering as doctor). scripts/skillify-check.ts subprocess-calls gbrain check-resolvable --json (cached per process) and fails loud on binary-missing. AGENTS.md workspaces resolve natively (see src/core/resolver-filenames.ts). DEFERRED[] is empty. Resolver lookup is the multi-file merge in src/core/check-resolvable.ts — entries collected from every RESOLVER.md/AGENTS.md across the skills dir AND its parent, deduped by skillPath (first occurrence wins). Uses autoDetectSkillsDirReadOnly so cd ~ && gbrain check-resolvable finds bundled skills via the install-path fallback; --fix carries the same install-path safety gate (refuses to write when detected.source === 'install_path').
  • src/core/resolver-filenames.ts — central list of accepted routing filenames (RESOLVER.md, AGENTS.md). Shared by findRepoRoot, check-resolvable, and skillpack install so every code path walks the same fallback chain.
  • src/commands/skillify.ts + src/core/skillify/{generator,templates}.tsgbrain skillify scaffold <name> creates all stubs for a new skill: SKILL.md, script, tests, routing-eval.jsonl, resolver entry, filing-rules pointer. gbrain skillify check <script> runs the 10-step checklist (LLM evals, routing evals, check-resolvable gate, filing audit) against a candidate skill before it lands.
  • src/commands/skillify-check.tsgbrain skillpack-check agent-readable health report. Exit 0/1/2 for CI gating; JSON for debugging. Wraps check-resolvable --json, doctor --json, and migration ledger into one payload. Required item 12 (brain_first_compliance) calls analyzeSkillBrainFirst() on the candidate SKILL.md; exits 1 when the verdict is missing_brain_first (external-lookup pattern present, no callout, no brain_first: exempt). The scaffold path in src/core/skillify/templates.ts pre-inserts the canonical Convention callout into new SKILL.md files so freshly-scaffolded skills pass item 12.
  • src/commands/book-mirror.tsgbrain book-mirror --chapters-dir <path> --slug <slug> [flags]. Submits N read-only subagent jobs (one per chapter; allowed_tools: ['get_page', 'search']), waits for all via waitForCompletion, reads each child's job.result, assembles two-column markdown CLI-side, writes a single operator-trust put_page to media/books/<slug>-personalized.md. Trust narrowing happens at the tool-allowlist layer (subagents can't call put_page) so untrusted EPUB content can't prompt-inject any people page. Cost-estimate prompt before launching; refuses to spend in non-TTY without --yes. Per-chapter idempotency keys (book-mirror:<slug>:ch-<N>) for retry-friendly re-runs. Partial-failure: assembles completed chapters + a ## Failed chapters section. Pinned by test/book-mirror.test.ts (9 cases).
  • src/commands/skillpack.ts + src/core/skillpack/{bundle,scaffold,reference,migrate-fence,scrub-legacy,harvest,harvest-lint,copy,apply-hunks,diff-text,installer}.ts — managed-block install model retired; install/uninstall removed (exit non-zero with a hint to the replacement). Surface: scaffold (one-time additive copy via copyArtifacts in copy.ts; refuses to overwrite; partial-state fills missing paired sources declared in SKILL.md frontmatter sources:), reference (read-only diff lens + --apply-clean-hunks two-way auto-apply via pure-JS unified-diff parser/applier in apply-hunks.ts + diff-text.ts), migrate-fence (one-shot strip of legacy fence; cumulative-slugs receipt → row-parsing fallback; preserves rows verbatim as user-owned routing), scrub-legacy-fence-rows (opt-in row cleanup with skill-present + non-empty-triggers gate), harvest (host→gbrain inverse with symlink-reject + canonical-path containment via a validateUploadPath-style gate + default-on privacy linter in harvest-lint.ts against ~/.gbrain/harvest-private-patterns.txt plus built-in a built-in fork-name pattern + email + Slack-channel patterns; rollback on match). Paired-source declarations live in each SKILL.md's frontmatter sources: array (validated by loadSkillSources in bundle.ts). autoDetectSkillsDir (in src/core/repo-root.ts) has a cwd_walk_up tier ahead of ~/.openclaw/workspace ($OPENCLAW_WORKSPACE precedence preserved). gbrain skillpack check --strict exits non-zero on drift (CI gate); top-level gbrain skillpack-check keeps exit-1-on-issues for cron. Companion editorial skill skills/skillpack-harvest/SKILL.md drives the genericization checklist. Doc: docs/guides/skillpacks-as-scaffolding.md. Test coverage across test/skillpack-{copy,scaffold,reference,reference-apply,apply-hunks,migrate-fence,scrub-legacy,harvest,harvest-lint,frontmatter-sources}.test.ts + 9-case E2E in test/e2e/skillpack-flow.test.ts. installer.ts + test/skillpack-install.test.ts survive — gbrain skillpack diff still uses diffSkill from there.
  • src/core/skillpack/{manifest-v1,tarball,state,remote-source,trust-prompt,bootstrap-display,scaffold-third-party,registry-schema,registry-client,rubric,doctor,init-scaffold,pack-publish,endorse,audit}.ts + examples/skillpack-reference/ + docs/skillpack-anatomy.md + scripts/build-skillpack-anatomy.ts — third-party skillpack ecosystem. gbrain skillpack scaffold <owner/repo|https-url|./tgz|./local-dir> resolves the spec via classifySpec, fetches through SSRF-hardened git-remote.ts (git) or extracts the tarball into ~/.gbrain/skillpack-cache/<host>/<owner>/<repo>/<sha>/, validates skillpack.json (api_version gbrain-skillpack-v1), checks gbrain_min_version, surfaces a TOFU first-install identity-confirm prompt (author + source + pinned commit + tarball SHA + tier; non-TTY requires --trust), records the pin in machine-owned ~/.gbrain/skillpack-state.json (schema gbrain-skillpack-state-v1, atomic .tmp + rename, isAlreadyTrusted skips re-prompt on author+pin match), runs through enumerateScaffoldEntriescopyArtifacts (one-time additive, refuses to overwrite), then DISPLAYS runbooks/bootstrap.md WITHOUT executing (deliberately does not auto-execute). Registry catalog at garrytan/gbrain-skillpack-registry split into registry.json (PR-able, gbrain-registry-v1) + endorsements.json (Garry-only overlay, gbrain-endorsements-v1); effectiveTier merges. registry-client.ts fetches both via If-None-Match etag with 1h soft-TTL + stale-fallback (origins fresh_fetch | cache_warm | cache_soft_stale | cache_hard_stale); hard-fail only on no-cache + no-network. CLI: gbrain skillpack {search,info,registry,doctor,init,pack,endorse}. Doctor walks SKILLPACK_RUBRIC_V1 (10 binary dimensions: 5 required CORE — manifest_valid, skills_have_skill_md, routing_evals_present ≥5 intents, skills_have_unique_triggers MECE, changelog_present_and_current — and 5 quality BADGES — unit_tests_present, e2e_tests_present, llm_eval_present ≥3 cases, bootstrap_runbook_present, license_present); tier eligibility: endorsed needs all 10, community needs core + ≥3 badges, experimental needs core only, blocked when any core fails. --quick ~5s structural sweep; --fix --yes auto-scaffolds auto_fixable: true dimensions and refuses to overwrite files whose mtime is newer than skillpack.json. gbrain skillpack init <name> lands 11 files (skillpack.json, SKILL.md, routing-eval.jsonl, test/example.test.ts, e2e/example.e2e.test.ts, evals/example.judge.json, runbooks/{bootstrap,uninstall,upgrade-template}.md, CHANGELOG, README, LICENSE); freshly-init'd scores 10/10; --minimal skips test/e2e/evals. gbrain skillpack pack packs a deterministic tarball via GNU tar (--sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner + GZIP=-n + TZ=UTC); refuses on tier_eligibility === 'blocked'. Extract caps (5000 files / 100MB total / 1MB per file / 255-char paths / 100:1 compression ratio); rejects symlinks/hardlinks/devices/FIFOs. gbrain skillpack endorse <name> [--tier ...] [--push] [--dry-run] runs in a clone of the registry repo: validates the pack in registry.json, mutates endorsements.json via pure applyEndorsement, stable-key-orders the write, commits endorse: <name> -> <tier>, optionally pushes. JSONL audit at ~/.gbrain/audit/skillpack-YYYY-Www.jsonl (ISO-week rotated, honors GBRAIN_AUDIT_DIR). examples/skillpack-reference/ is a 10/10 reference pack pinned by test/e2e/skillpack-third-party.test.ts. docs/skillpack-anatomy.md auto-generated via scripts/build-skillpack-anatomy.ts (--check for CI drift). CLI dispatch in src/commands/skillpack.ts disambiguates third-party (contains /, ://, .tgz) from bundled-skill kebab; kebab routes bundled-first, registry-fallback. Tests: test/skillpack-{manifest-v1,tarball,state,remote-source,trust-prompt,registry-schema,registry-client,rubric,doctor,init-scaffold,pack-publish,endorse,audit,scaffold-third-party}.test.ts + test/e2e/skillpack-third-party.test.ts. Spec at docs/designs/SKILLPACK_REGISTRY_V1_SPEC.md.
  • src/core/archive-crawler-config.ts — safety gate for the archive-crawler skill. Refuses to run unless archive-crawler.scan_paths: is explicitly set in the brain repo's gbrain.yml. Mirrors the storage-config.ts parsing pattern (sibling file, separate concern from storage tiering). loadArchiveCrawlerConfig(repoPath) throws ArchiveCrawlerConfigError(missing_section | empty_scan_paths | invalid_path | parse_error). normalizeAndValidateArchiveCrawlerConfig rejects relative paths and .. traversal; ~ is expanded; trailing-slash normalized for unambiguous prefix matching. isPathAllowed(candidate, config) is the runtime per-file gate (scan_paths prefix-match with directory-boundary correctness; deny_paths overrides). Pinned by test/archive-crawler-config.test.ts (19 cases).
  • test/helpers/cli-pty-runner.ts — generic real-PTY harness (~470 lines) using pure Bun.spawn({terminal:}) (Bun 1.3.10+; engines.bun pin in package.json). Generic primitives only, no plan-mode orchestrators. Exports launchPty, resolveBinary, stripAnsi, parseNumberedOptions, optionsSignature, isNumberedOptionListVisible, isTrustDialogVisible. Self-tests in test/cli-pty-runner.test.ts (24 cases).
  • src/core/skill-manifest.ts — parser for skill-manifest.json records. Used by skillpack installer to detect drift between the shipped bundle and the user's local edits, so updates merge instead of overwriting.
  • src/commands/routing-eval.ts + src/core/routing-eval.tsgbrain routing-eval catches user phrasings that route to the wrong skill. Reads skills/<name>/routing-eval.jsonl fixtures ({intent, expected_skill, ambiguous_with?}). Structural layer runs in check-resolvable by default (zero API cost). --llm is a placeholder for a future LLM tie-break layer; today it emits a stderr notice and runs structural only. Uses autoDetectSkillsDirReadOnly and the same multi-file resolver merge as check-resolvable, so on OpenClaw layouts (skills/RESOLVER.md + ../AGENTS.md) all three commands see the same trigger index. RESOLVER.md rows carry the full frontmatter triggers: arrays so the structural matcher sees realistic phrasings; ambiguous-fixture annotations cover deliberate skill chains like enrich → article-enrichment.
  • src/core/filing-audit.ts + skills/_brain-filing-rules.json — Check 6 of check-resolvable. Parses writes_pages: / writes_to: frontmatter on skills and audits their filing claims against the filing-rules JSON (error severity). Internal parseFrontmatter is a thin wrapper over the shared src/core/skill-frontmatter.ts parser so both filing-audit and skill-brain-first read the same shape (tools?, triggers?, brain_first?: 'exempt', typed brain_first_typo) from one source of truth.
  • src/core/skill-frontmatter.ts — shared content-based SKILL.md frontmatter parser. Recognizes the brain_first: 'exempt' declarative opt-out and surfaces near-miss declarations (brain-first, BrainFirst, quoted values, unknown values) as a typed brain_first_typo field so doctor can emit a paste-ready hint rather than fail silently. Single canonical form: snake_case brain_first: exempt, lowercase, unquoted.
  • src/core/skill-brain-first.ts — pure analyzer. analyzeSkillBrainFirst(skillPath, content): SkillBrainFirstResult walks the compliance ladder for every SKILL.md: (1) absent external-lookup pattern → no_external; (2) brain_first: exempt frontmatter → exempt_frontmatter; (3) canonical > **Convention:** see [conventions/brain-first.md](...) callout → compliant_callout; (4) explicit ## Phase 1: Brain heading → compliant_phase; (5) first gbrain search/query/get_page reference precedes first external pattern in the BODY (frontmatter stripped) → compliant_position; (6) else missing_brain_first warn. External pattern set: word-boundary regex over web_search, web_fetch, exa, perplexity, happenstance, crustdata, captain_api, firecrawl. Position scan is BODY-ONLY so a tools: [web_search] frontmatter declaration doesn't false-flag the skill. The 40-name FORMERLY_HARDCODED_EXEMPT list is preserved so doctor can emit a "this used to be auto-exempt, declare brain_first: exempt if still appropriate" hint. Consumed by 3 surfaces: doctor check, skillify-check item 12, dry-fix MISSING_RULE_PATTERNS.
  • src/core/skill-fix-gates.ts — shared safety primitives extracted from dry-fix.ts. getWorkingTreeStatus(file) 3-state ('clean' | 'dirty' | 'not_a_repo'); isInsideCodeFence(content, offset); findAfterH1Paragraph(content) (canonical insertion offset for the auto-inserted Convention callout). Both REPLACE expanders (DRY violations) and the INSERT expander (MISSING_RULE_PATTERNS) consume from here so the install-path refusal and dirty-tree gates apply uniformly. src/core/dry-fix.ts re-exports for back-compat with existing call sites.
  • src/core/audit-skill-brain-first.ts — snapshot+diff JSONL audit at ~/.gbrain/audit/skill-brain-first-YYYY-Www.jsonl (ISO-week rotated, honors GBRAIN_AUDIT_DIR via shared resolveAuditDir()). recordBrainFirstRun(results) reads the previous snapshot at ~/.gbrain/audit/skill-brain-first-snapshot.json, diffs against current results, writes transition events (detected | resolved | fixed) one line per change, then atomically overwrites the snapshot via .tmp + rename. Transition-only writes — a stable brain produces 0 audit lines per doctor run. readRecentBrainFirstEvents(days) is the readback path for the future skill_brain_first_trend doctor check. Snapshot file is last-writer-wins under concurrent doctor runs; subsequent runs reconcile.
  • src/core/dry-fix.tsgbrain doctor --fix engine. autoFixDryViolations(fixes, {dryRun}) rewrites inlined rules to > **Convention:** see [path](path). callouts via three shape-aware expanders (bullet / blockquote / paragraph). Five guards: working-tree-dirty (getWorkingTreeStatus() 3-state 'clean' | 'dirty' | 'not_a_repo'), no-git-backup, inside-code-fence, already-delegated (40-line proximity, consistent with detector), ambiguous-multi-match, block-is-callout. execFileSync array args (no shell, no injection surface). EOF newline preserved. Safety primitives are in src/core/skill-fix-gates.ts (back-compat re-exports preserved). MISSING_RULE_PATTERNS INSERT pattern type lives alongside REPLACE patterns — same auto-fix entry point + git-safety gates, but places a canonical callout at a target offset (after-h1-paragraph only). First INSERT pattern is brain_first, auto-inserting > **Convention:** see [conventions/brain-first.md](../conventions/brain-first.md) for the lookup chain (search → query → get_page → external). on any flagged SKILL.md whose verdict is missing_brain_first. Idempotent — re-runs detect the existing callout and skip.
  • src/core/backoff.ts — Adaptive load-aware throttling: CPU/memory checks, exponential backoff, active hours multiplier.
  • src/core/retry.ts — canonical retry primitive for transient connection errors. Exports withRetry<T>(fn, opts) execution wrapper + BULK_RETRY_OPTS constant ({maxRetries:3, delayMs:1000, delayMaxMs:10000, jitter:'decorrelated'}, tuned for Supabase Supavisor's 5-10s circuit-breaker recovery) + BATCH_AUDIT_SITES typed const (closed enum of every audit-emission site) + resolveBulkRetryOpts(env) (reads GBRAIN_BULK_MAX_RETRIES/GBRAIN_BULK_RETRY_BASE_MS/GBRAIN_BULK_RETRY_MAX_MS with >=0 validation, throws on bad input with a paste-ready hint) + abortableSleep(ms, signal?) + RetryAbortError (tagged error for clean shutdown) + computeNextDelay() (pure-fn for 3 jitter modes: 'none', 'full', 'decorrelated'). The execution wrapper is consumed by postgres-engine.ts + pglite-engine.ts batch primitives (addLinksBatch / addTimelineEntriesBatch / upsertChunks) so every caller inherits retry as part of the data-primitive's contract. CI guard scripts/check-no-double-retry.sh fails the build on withRetry(...engine.batch...) patterns (prevents 3×3=9 retry amplification); scripts/check-batch-audit-site.sh validates every string-literal auditSite: '...' against the closed BATCH_AUDIT_SITES enum. Decorrelated jitter (AWS-style: uniform(base, prevDelay*3) capped at delayMaxMs) — 'full' jitter allowed near-zero retries that re-hit the recovering breaker. WithRetryOpts has an optional reconnect?: () => Promise<void> callback awaited in the catch branch AFTER isRetryableConnError classification but BEFORE the inter-attempt sleep — lets engine-level callers rebuild a dead pool/singleton between attempts. PostgresEngine.batchRetry injects () => this.reconnect() (the race-safe _reconnecting guard kicks in). Fail-loud: a reconnect throw PROPAGATES as the new error, replacing the symptomatic "No database connection". onRetry callbacks are awaited (sync arrows work identically; async callbacks correctly delay the sleep). Pinned by test/core/retry.test.ts (37 cases), test/core/retry-stress.slow.test.ts (5 cases, 100 batches × 30% blip rate, asserts zero row loss), test/core/retry-reconnect.test.ts (5 cases), test/e2e/db-singleton-shared-recovery.test.ts (3 DB-gated cases).
  • src/core/process-watchdog.ts (#1633) — out-of-band hard-deadline killer for gbrain sync. A spinning sync (synchronous catastrophic-regex / ReDoS in pack link-inference) STARVES the main event loop, so the existing SIGTERM handler (process-cleanup.ts), --timeout setTimeout, and abort-flag checks can't fire — the process becomes unkillable-by-SIGTERM and, under cron, orphans pile up for 24h+ (the reported incident). installProcessWatchdog({deadlineMs, graceMs?, label?, heartbeatMs?, onWarn?}): WatchdogHandle spawns a Bun worker_threads Worker via new Worker(code, {eval: true, workerData}) — its own OS thread + event loop fires even while main is in an unyielding sync loop. At deadlineMs it process.kill(process.pid, 'SIGTERM') (clean-shutdown chance if responsive); at deadlineMs+graceMs process.kill(process.pid, 'SIGKILL') (uncatchable — guaranteed death under starvation). Signaling SELF has NO PID-reuse footgun (current PID never reused while alive — the reason the detached-child-watches-parent design was rejected). eval: true bakes the worker body into the bun build --compile binary with no separate-file embedding. Empirically validated on Bun 1.3.13 (worker timer + SIGKILL killed a while(true){}-starved process). handle.dispose() (clean-exit finally) worker.terminate()s it; unref()'d so it never keeps the process alive. Pure watchdogDecision(elapsedMs, deadlineMs, graceMs) → 'wait'|'sigterm'|'sigkill' extracted for unit tests. Optional heartbeatMs emits periodic [<label>] parent alive Ns, hard-kill in ~Ms lines (visible in cron logs even under starvation — the diagnosis surface). Fallback: if new Worker throws, degrades to an in-process timer with a loud warning that it can't fire under starvation. Reusable beyond sync (autopilot/cycle are follow-up adopters). Pinned by test/process-watchdog.test.ts (pure decision matrix + handle contract) + test/process-watchdog.serial.test.ts (Bun-pinned spawn integration: a starved harness process IS killed ~deadline+grace, a no-watchdog control does NOT self-exit, clean dispose never kills). Wired into src/cli.ts sync dispatch BEFORE connectEngine (so a connect-phase hang is bounded too); deadline resolved by resolveSyncHardDeadline in sync.ts (precedence: --no-hard-deadline > --hard-deadline > --timeout(non---all) > GBRAIN_SYNC_MAX_RUNTIME_SECONDS env > non-TTY default 3600s > none).
  • src/core/audit/batch-retry-audit.ts — JSONL audit primitive for batch-retry events, built on audit-writer.ts. Schema: {ts, site, batch_size, attempt, outcome: 'success' | 'exhausted', delay_ms, error_message_summary, error_code?}. Privacy: NEVER logs slugs / page IDs / content (mirrors shell-audit.ts). logBatchRetry fires per successful retry recovery; logBatchExhausted fires when retries exhaust and rows are lost. readRecentBatchRetryEvents(hours=24) returns {events, corrupted_lines, files_scanned, files_unreadable} — corruption + permission errors surface to doctor, not silently swallowed. pruneOldBatchRetryAuditFiles(daysToKeep=30) deletes old files, called from gbrain dream --phase purge. File: ~/.gbrain/audit/batch-retry-YYYY-Www.jsonl (honors GBRAIN_AUDIT_DIR). summarizeError routes error messages through the shared redactConnectionInfo helper from src/core/audit/redact-connection-info.ts BEFORE truncation so DSNs / hostnames / credentials / IPv4 octets can't leak into operator-shared JSONL dumps. Pinned by test/audit/batch-retry-audit.test.ts (12 cases) + test/audit/batch-retry-redaction.test.ts (3 privacy regressions).
  • src/core/audit/lock-renewal-audit.ts — JSONL audit primitive for per-job lock-renewal faults. Sibling of batch-retry-audit.ts, built on audit-writer.ts. Four outcomes: failure (single renewLock throw, counter incremented), success_after_failure (recovery; emits the recovery count), gave_up (time-based deadline exceeded; abort fired), executeJob_rejected (the second unhandledRejection vector — the stored executeJob(...).finally(...) promise itself rejected, e.g. failJob threw during the same DB outage). Schema: {ts, job_id, job_name, attempt?, outcome, error_message_summary?, error_code?}. Privacy: NEVER logs lock_token or job.data; error summaries route through redactConnectionInfo BEFORE truncation. Defense-in-depth: every audit call inside the lock-renewal tick's catch block is wrapped in its own inner try/catch so a misbehaving audit-writer can't re-introduce the unhandledRejection bug class. readRecentLockRenewalEvents(hours=24) walks current + previous ISO week with corrupted-line tolerance. pruneOldLockRenewalAuditFiles(daysToKeep=30) is ready for future dream-cycle purge wiring. File: ~/.gbrain/audit/lock-renewal-YYYY-Www.jsonl. Pinned by test/audit/lock-renewal-audit.test.ts (11 cases).
  • src/core/audit/redact-connection-info.ts — Shared pure helper. redactConnectionInfo(text: string): string strips Postgres connection info before any audit JSONL write: postgres:///postgresql:// URLs, host=foo, user=foo, password=foo, pwd=foo, IPv4 octets — each match becomes <REDACTED:kind>. Negative-lookbehind/lookahead [\w.@-] on the IPv4 pattern defeats version-string false positives (v3.1.4.0, tree-sitter@0.26.3.1) while still matching real IPs in PG errors ((192.168.1.42)). Order-sensitive pattern set: URL forms first so substrings inside URLs don't get double-redacted. Idempotent, pure (no I/O), hot-path-safe (regex compiled at module load). Wired into BOTH lock-renewal-audit.ts AND batch-retry-audit.ts. Known limitations: bare-quoted hostnames (at "db.example.com") and usernames (for user "postgres.foo") are NOT caught — the highest-value leak (the IP in those shapes) IS caught. Pinned by test/audit/redact-connection-info.test.ts (15 cases: all 5 patterns + Supabase fixture + ENOTFOUND fixture + version-string false-positive defense).
  • src/core/minions/lock-renewal-tick.ts — Pure extracted function from MinionWorker.launchJob's setInterval body; the structural fix that closes the production unhandledRejection crash class. Exports runLockRenewalTick(deps, state) → Promise<TickResult> + resolveLockRenewalKnobs(env, lockDuration) → LockRenewalKnobs. Three env knobs, all parsed as positive integers with stderr-warn-once-per-process on bad input + default fallback: GBRAIN_LOCK_RENEWAL_MAX_FAILURES (default 3, audit-labeling only — abort triggering is time-based), GBRAIN_LOCK_RENEWAL_CALL_TIMEOUT_MS (default lockDuration/3, bounds the hung-renewLock vector via Promise.race), GBRAIN_LOCK_RENEWAL_SAFETY_MARGIN_MS (default lockDuration/6, ensures release before stall-detector reclaim). The tick checks state.cancelled() at three guard points (entry, after await resolves, after await throws) so a long renewLock that resolves after the job ended bails cleanly. Result type is a tagged union {kind: 'ok' | 'cancelled' | 'lock_lost' | 'should_abort', reason?}; all abort.abort() / clearInterval side effects live in the worker's closure scope, not the pure function. Audit calls wrapped in inner try/catch (defense-in-depth). LockRenewalDeps has an optional reconnect? callback; the renewal-tick's catch branch (after retryable-error classification + the time-based deadline check, before the inter-tick sleep) calls it ONCE, wrapped in its own try/catch. Recovers the mid-process-disconnect case (nulled _sql) where postgres.js auto-reconnect can't help because the engine's pool reference is gone. Deliberately NOT a background withRetry (a 12s background retry could refresh a lock AFTER the worker already decided lock-renewal-failed and another worker reclaimed it → two holders); the reconnect is bounded by the same callTimeoutMs race + abort signal as renewLock itself. Pinned by test/worker-lock-renewal.test.ts (18 hermetic state-machine cases — no fs, no PGLite, no real setInterval).
  • src/core/minions/worker-exit-codes.ts — single source of truth for reserved worker process exit codes, shared by the worker (sets them) and supervisor/CLI (classify them). Exports WORKER_EXIT_RSS_WATCHDOG = 12. The RSS watchdog drain must be self-identifying: a code-0 exit is indistinguishable from a healthy queue-drain, so the supervisor's code===0 → clean_exit classifier never counted it and a respawn loop stayed invisible. A distinct code makes the drain likely_cause=rss_watchdog. Code 12 is deliberately outside {0 clean, 1 runtime_error} and the 128+N signal range.
  • src/core/minions/rss-default.ts — cgroup-aware auto-sized default for the worker RSS watchdog cap. resolveDefaultMaxRssMb(opts?) / describeDefaultMaxRss(opts?) (provenance for the startup log) / readCgroupMemLimitBytes(readFile?). Replaces a flat 2048MB default at every spawn site (jobs work, jobs supervisor, autopilot, MinionSupervisor). Formula clamp(round(0.5 × basisMB), 4096, 16384) where basis = min(cgroupLimit, totalmem). LOAD-BEARING NUANCE: plain os.totalmem() reports HOST RAM, so in a 4GB cgroup on a 126GB host it would pick 16GB, the watchdog would never fire, and the kernel OOM-killer would SIGKILL at 4GB. The cap MUST sit below the real ceiling so the graceful drain (distinct exit code, loud log) beats the kernel's silent kill; the 4096 floor applies only when it stays below the basis. Reads cgroup v2 /sys/fs/cgroup/memory.max (literal max = unlimited) then v1 /sys/fs/cgroup/memory/memory.limit_in_bytes. Explicit --max-rss (including 0 to disable) always wins. Pinned by test/rss-default.test.ts.
  • src/core/cycle/extract-atoms-drain.ts — pure single-hold bounded drain for the silent lens-phase backlog. runExtractAtomsDrain(deps, opts) over injected deps (withLock, runBatch, countRemaining, now, optional onBatch) loops bounded batches under ONE continuous lock hold, rediscovering eligibility each batch (idempotent NOT-EXISTS-on-source_hash, so content mutated by a concurrent process simply doesn't match — no cross-window stale cursor), until the backlog is empty OR the time window elapses. Returns {phase, status, extracted, skipped, remaining, batches, stopped}. Backs gbrain dream --phase extract_atoms --drain. Takes the SAME cycleLockIdFor(sourceId) the routine cycle takes (a concurrent autopilot tick genuinely defers with cycle_already_running); NO release/reacquire-between-windows primitive. The shared wiring helper runExtractAtomsDrainForSource(engine, {sourceId, windowSeconds, brainDir?, maxBatches?, onBatch?}) owns the lock+batch+count+defer wiring (dynamic imports of db-lock/cycle/extract-atoms keep the pure loop cheap to unit-test) and is the ONE drain path for three callers — gbrain dream --drain (which calls it), the extract-atoms-drain Minion handler, and autopilot auto-drain — so lock id / window / defer-on-busy can't drift. sourceId: undefined → legacy gbrain-cycle lock + 'default' extraction; a real id → gbrain-cycle:<id>. LockUnavailableError propagates to the caller (each reports the busy case its own way). Pinned by test/extract-atoms-drain.test.ts.
  • scripts/check-worker-lock-renewal-shape.sh — CI guard wired into bun run verify. Two invariants on src/core/minions/worker.ts: (1) the bug pattern lockTimer = setInterval(async ...) must NOT appear (narrowed via lockTimer = prefix so unrelated setInterval(async) calls — like the stall detector — don't false-fire), (2) runLockRenewalTick must remain referenced so the pure-function test seam survives refactors. Bug-pattern-specific by design — a future refactor to setTimeout-recursion or AbortController-based scheduling passes as long as the bug pattern stays absent. POSIX ERE + [[:space:]] for BSD-grep portability. Honors GBRAIN_LOCK_RENEWAL_SHAPE_TARGET env override for fixture-based meta-tests. Pinned by test/scripts/check-worker-lock-renewal-shape.test.ts (5 cases).
  • src/core/doctor-cause-rank.ts — pure cause-ranking for gbrain doctor. rankIssues(checks) returns non-ok checks ordered fail-before-warn then root-before-symptom then name (deterministic). ROOT_CAUSE_CHECKS / SYMPTOM_CHECKS are ORDERING ONLY — tier membership asserts no causality. downstream_of is set ONLY from a small map of KNOWN grounded edges (queue_health / supervisorworker_oom_loop, since they read the same aborted: watchdog / rss_watchdog source) AND only when the named root is itself failing — never a root×symptom cartesian (co-occurrence never implies causality). fix prefers details.fix_hint else the message. CAUSE_GRAPH_NAMES + allKnownCheckNames() back a drift guard asserting every graphed name is a real check. Consumed by computeDoctorReport (top_issues field, additive, schema_version stays 2) + the "Top issues (ranked by cause)" header in outputResults. Pinned by test/doctor-cause-rank.test.ts.
  • src/commands/doctor.ts extension — computeWorkerOomLoopCheck(engine) is the single authoritative OOM-loop signal, unioning supervised summarizeCrashes(readRecentSupervisorEvents(24)).by_cause.rss_watchdog (cross-week read via readRecentSupervisorEvents so a Monday window can't lose Sunday) + bare-worker minion_jobs error_text='aborted: watchdog' count (Postgres-only; the same source queue_health subcheck 3 reads). Cap comes from the latest rss_watchdog_loop breaker alert's max_rss_mb, else resolveDefaultMaxRssMb() fallback. fail at breaker-tripped or oomKills≥5, warn at ≥1, null otherwise. computePoolReapHealthCheck(engine) is the Postgres-only pool_reap_health check reading readRecentPoolRecoveries(1) — fail when reconnect failures>0 (reconnect throwing is the actionable signal), warn at ≥10 reaps/hr (pooler thrash), null otherwise. Both registered in buildChecks after the supervisor block. The supervisor causeStr carries rss=N (see worker_oom_loop) and queue_health's watchdog message cross-references worker_oom_loop. DoctorReport.top_issues + the cause-ranked render header. worker_oom_loop + pool_reap_health registered under ops in doctor-categories.ts. Pinned by test/doctor-worker-oom-loop.test.ts, test/doctor-pool-reap-health.test.ts.
  • src/commands/doctor.ts extension — supervisor_singleton check (#1849), a SEPARATE check from supervisor (same split precedent as the niceness check) so a singleton-divergence warn can't clobber the crash/liveness precedence. Runs only when a started supervisor event was seen in the last 24h and a live engine is available. Reads the queue-scoped DB lock row (gbrain_cycle_locks WHERE id = supervisorLockId(queue)) and compares the lock holder (holder_host:holder_pid) against the local pidfile holder via the pure classifySupervisorSingleton. mismatch → warn (a second supervisor may be running with a different --max-rss; message names both holders, the effective cap from the started event's max_rss_mb, and the fix gbrain jobs supervisor stop); single → ok (names holder + cap); no_lock → no check emitted. Best-effort try/catch (silent skip on brains without the lock table). Registered under ops in doctor-categories.ts as supervisor_singleton. Pinned by test/supervisor-db-lock.test.ts + test/doctor.test.ts.
  • src/core/audit/pool-recovery-audit.ts — reap/reconnect audit on the shared audit-writer cathedral. Events: reap_detected (CONNECTION_ENDED), reconnect_other (network/auth/health-check), reconnect_succeeded, reconnect_failed. readRecentPoolRecoveries(hours=1) returns {reaps, recoveries, failures, others, events}. Error summaries route through redactConnectionInfo before truncation (DSN/host/IP safe). Emitted ONLY from PostgresEngine.reconnect(ctx?) (the rare reap-retry path, near-zero hot-path cost); reconnect() classifies the threaded error via isConnectionEndedError (in retry-matcher.ts) so only true pooler reaps are labeled reap_detected. The retry callback in retry.ts threads the triggering error as (ctx?: {error?}) => Promise<void>. Pinned by test/audit/pool-recovery-audit.test.ts.
  • src/commands/autopilot.ts extension — per-source extract_atoms auto-drain. Postgres-only block after the freshness fan-out: gated on autopilot.auto_drain.enabled (default true) AND !packDeclaresPhase(engine,'extract_atoms') (the silent-backlog condition) AND per-source countExtractAtomsBacklog > threshold (default 25) AND a daily cap floor(max_usd_per_day / ~$0.30). Enumerates loadAllSources. Submits the PROTECTED extract-atoms-drain job ({allowProtectedSubmit:true}) with a UTC-day time-sloted idempotency key autopilot-extract-atoms-drain:<src.id>:<utcDay> (a static key would block the source after the first job completed). src/core/minions/protected-names.ts adds extract-atoms-drain; src/commands/jobs.ts registers the handler (thin wrapper over runExtractAtomsDrainForSource, LockUnavailableError{deferred:true}); src/core/config.ts adds the autopilot.auto_drain.* config keys + the autopilot. key prefix. Pinned by test/extract-atoms-drain-handler.test.ts, test/autopilot-auto-drain-wiring.test.ts.
  • src/commands/doctor.ts:checkBatchRetryHealthbatch_retry_health check surfacing Supavisor circuit-breaker incidents. Wired into both runDoctor (local) and doctorReportRemote (thin-client). Reads last 24h. States: ok (zero exhausted in 24h OR <3 from a single site), warn (>=3 same-site OR >=5 cross-site), fail (>=20 sustained breaker). Surfaces bad GBRAIN_BULK_* env at doctor startup. Corrupt-JSONL tolerant. Paste-ready fix hints in every warn/fail message. Also reads readRecentDbDisconnects(24) and appends Disconnect-call audit: N call(s) in 24h (most recent caller: <frame>). to ALL three message paths so connection-incident signal is greppable from one gbrain doctor --json call (module-import wrapped in try/catch so older brains without the audit file degrade silently). Pinned by test/doctor-batch-retry.test.ts (10 cases).
  • src/core/audit/db-disconnect-audit.ts — JSONL audit for every call to db.disconnect() and PostgresEngine.disconnect(). Built on audit-writer.ts. Schema: {ts, engine_kind: 'postgres'|'pglite'|'unknown', connection_style: 'module'|'instance'|'unknown', caller_stack, command, pid}. caller_stack captured via new Error().stack truncated to ~20 frames so operators identify the offending caller without inflating JSONL. Privacy: stack frames carry file paths but NO SQL content / row data / user strings. File: ~/.gbrain/audit/db-disconnect-YYYY-Www.jsonl (honors GBRAIN_AUDIT_DIR). readRecentDbDisconnects(hours=24) walks current + previous ISO week and returns {count, most_recent_caller, files_scanned}. Wired into src/core/db.ts:disconnect and src/core/postgres-engine.ts:disconnect, logging BEFORE the early-return branches so even no-op disconnects on never-connected engines are recorded (that case may itself be a caller-side bug). Pinned by test/db-disconnect-audit.test.ts (6 cases: round-trip, stack truncation, sort order, empty-dir nulls, stable feature name, EROFS best-effort).
  • src/core/facts/queue.ts:FactsQueue.drainPending — method drainPending({timeout?: number}): Promise<{drained, unfinished}>. Semantically distinct from shutdown() (which calls this.internalAbort.abort() and would abort the very facts:absorb worker trying to log its post-completion event). Drain lets in-flight finish; only the wait is bounded. Default timeout 1000ms so commands that don't enqueue facts pay one fast 0ms check before exit. src/cli.ts op-dispatch finally block awaits getFactsQueue().drainPending({timeout: 1000}) BEFORE engine.disconnect(). Lazy-import keeps the facts-queue module off the hot path for ops that never touch it. Closes the trailing 'No database connection' line after gbrain capture (post-page-write facts:absorb outlived the CLI process). Pinned by test/facts-queue-drain-pending.test.ts (4 cases: empty fast-path, in-flight settled without abort, unfinished count on timeout, default timeout = 1000ms).
  • scripts/check-no-double-retry.sh + scripts/check-batch-audit-site.sh — CI lint guards wired into bun run verify. The former greps src/ for withRetry(...engine.{addLinksBatch|addTimelineEntriesBatch|upsertChunks}) patterns and fails the build on hit (prevents 3×3=9 retry amplification on incomplete reverts). The latter extracts every string-literal auditSite: '...' from src/ and validates each appears in the BATCH_AUDIT_SITES const in src/core/retry.ts (typo guard — prevents fragmented doctor output).
  • src/core/fail-improve.ts — Deterministic-first, LLM-fallback loop with JSONL failure logging and auto-test generation.
  • src/core/transcription.ts — Audio transcription: Groq Whisper (default), OpenAI fallback, ffmpeg segmentation for >25MB.
  • src/core/enrichment-service.ts — Global enrichment service: entity slug generation, tier auto-escalation, batch throttling.
  • src/commands/enrich.ts + src/core/enrich/thin.ts + src/core/cycle/enrich-thin.tsgbrain enrich --thin: batch-develops stub (thin) pages via brain-internal grounded synthesis. gbrain's model tooling sees only brain-internal context (search / get_page / facts / backlinks), not the web, so enrich consolidates what the brain ALREADY knows about an entity (scattered across meetings, other pages, deals, facts) into one cited page via ONE gateway.chat call per page; web research stays the agent-driven enrich SKILL's job. runEnrichCore(engine, opts, signal) (strict per-source; multi-source iteration is the caller's job) drives enrichOne per candidate: withRefreshingLock('enrich:<src>:<slug>')getPage → deterministic retrieve (hybridSearch + getBacklinks + facts + raw_data, source-scoped, sanitized via INJECTION_PATTERNS) → assessGrounding gate (skip < MIN_CONTEXT_CHARS, no LLM) → buildEnrichPrompt (grounded dossier, [Source: slug] citations, SKIP sentinel) → synth → put_page handler (remote:false, auto-link + write-through) stamping enriched_at + enriched_by:'cli:enrich'. Candidate selection is the SQL-native engine.listEnrichCandidates(opts) (src/core/engine.ts interface + EnrichCandidate/EnrichCandidatesOpts/ENRICH_ORDER_SQL in src/core/types.ts + pg/pglite impls): thin-filter + per-page source-correct inbound count (to_page_id = p.id, mentions excluded) + enriched_at recency guard + whitelisted ORDER BY + LIMIT, lightweight projection (NO bodies). Resume via src/core/op-checkpoint.ts (local enrichFingerprint); budget via BudgetTracker + withBudgetTracker (best-effort under --workers > 1runSlidingPool aborts new claims on BUDGET_EXHAUSTED but does NOT cancel in-flight gateway.chat; pin --workers 1 for a hard ceiling). sanitizeContext (thin.ts) neutralizes the <context>…</context> data-envelope delimiters (injection escape, mirrors the </trajectory> convention); the --background multi-source fan-out idempotency key carries the run fingerprint via exported backgroundIdempotencyKey(sid, args) (a bare enrich:${sid} would return stale completed jobs); runEnrichCore flags budget_exhausted post-hoc when tracker.totalSpent > tracker.cap even when the gateway swallowed the final-call throw (via read-only BudgetTracker.cap getter); body() flushes the checkpoint on BudgetExhausted before it propagates so resume doesn't re-charge. The opt-in enrich_thin cycle phase (default OFF via cycle.enrich_thin.enabled) trickles max_pages_per_tick (default 3) per source with per-source cost cap enforced as min(per_source_cap, brain_wide_remaining) + brain-wide total + walltime caps. Wired into cycle.ts (CyclePhase/ALL_PHASES between conversation_facts_backfill and skillopt/embed; PHASE_SCOPE='source'; NEEDS_LOCK; dispatch), cli.ts (CLI_ONLY + CLI_ONLY_SELF_HELP + THIN_CLIENT_REFUSED_COMMANDS + dispatch), jobs.ts (Minion enrich handler, strict per-source, NOT in PROTECTED_JOB_NAMES). DI seam opts.synthesizeFn keeps tests hermetic (no API key, no mock.module). Pinned by test/enrich/thin.test.ts, test/enrich/idempotency.test.ts, test/enrich-cycle-phase.test.ts, test/e2e/enrich-pglite.test.ts (grew-cited, skip, ordering, multi-source, recency, resume, budget abort + checkpoint flush, final-call overage, lock-skip, provenance), test/e2e/engine-parity.test.ts (listEnrichCandidates pg↔pglite parity).
  • src/core/data-research.ts — Recipe validation, field extraction (MRR/ARR regex), dedup, tracker parsing, HTML stripping.
  • src/commands/embed.tsgbrain embed [--stale|--all] [--slugs ...]. --stale starts with engine.countStaleChunks() (single SELECT count(*) WHERE embedding IS NULL, ~50 bytes wire) so a fully-embedded brain short-circuits with no further reads. When stale chunks exist, engine.listStaleChunks() returns just the chunks needing embeddings (slug + chunk_index + chunk_text + metadata, no vector(1536) payload); caller groups by slug, embeds, re-upserts via upsertChunks. All console.log/console.error call sites use slog/serr from src/core/console-prefix.ts so when runEmbedCore runs inside a per-source withSourcePrefix scope (installed by the gbrain sync --all worker pool) every line carries the [<source-id>] prefix; standalone callers see identical output because slog/serr fall through to bare console fns outside the wrap. Every embed-write path stamps pages.embedding_signature via engine.setPageEmbeddingSignature(slug, {sourceId, signature: currentEmbeddingSignature()}) so a later model/dims swap is detectable as stale. The per-slug path (embedPage, used by gbrain embed <slug> AND sync's post-import embed step) and the full-re-embed path (embedAll) stamp unconditionally per page. The stale path (embedAllStale) first calls invalidateStaleSignatureEmbeddings on a live run so signature-drifted pages flow through the NULL cursor, then stamps each page — but ONLY when EVERY chunk was stale this pass (a partially-stale page keeps preserved chunks of unknown provenance, so it stays unstamped rather than falsely marked current; embed --all fully re-embeds + stamps those). dry-run never mutates: it counts signature-drift via the widened countStaleChunks({signature}) predicate without NULLing anything.
  • src/core/conversation-parser/ — 14-pattern built-in chat-format registry + opt-in LLM polish/fallback. Modules: types.ts (PatternEntry + ParseResult + DateContext + CaptureMap + TimezonePolicy), builtins.ts (14 hand-vetted patterns sourced from public format docs — iMessage/Slack, Telegram bracket + text-export, bold-paren-time, bold-name-no-time, Discord classic + export, WhatsApp ISO + US, Signal, Matrix/Element, IRC classic + weechat, Teams export; module-load validation runs every test_positive[] + test_negative[] sample at startup so a typo in any built-in regex makes gbrain refuse to start; DEFAULT_SPEAKER_CLEAN exported as a module-level default), parse.ts (orchestrator with pattern-priority scoring across the first 10 lines + date derivation chain explicit > frontmatter.date > effective_date > '1970-01-01' + multi-line continuation + timezone warning), llm-base.ts (shared runLlmCall<T> with content-hash cache in-process + DB-persistent via migration v97 + 4-strategy JSON repair + Anthropic-key probe; polish and fallback are thin wrappers), llm-polish.ts (opt-IN; headroom guard skips when tracker within $0.10 of cap; pure applyPolish for merge/drop/edit ops), llm-fallback.ts (opt-IN; NO regex inference + NO persistence), eval.ts (scoreFixture + aggregateScores + parseFixtureJsonl for the fixture-corpus CI gate), nightly-probe.ts (DI-stubbed; mode-gated default tokenmax=ON, conservative/balanced opt-in; adversarial false-positive detection). Pattern bold-name-no-time (regex /^\*\*(?!\[)(.+?):\*\*\s*(.*)$/, index 3 after bold-paren-time) parses **Speaker:** text with NO per-line timestamp (Circleback/Granola/Zoom), anchoring every message at T00:00:00Z of the frontmatter date (line order preserves sequence, same no-time convention as irc-classic); the (?!\[) lookahead rejects telegram-bracket **[18:37] Name:**; non-shadow is the colon-INSIDE-bold regex (NOT declaration order — parse.ts scores every candidate independently, order is only the tie-break). Because **Label:** text is a common prose idiom, the pattern sets optional PatternEntry.score_full_body: true so parse.ts recomputes the winner's acceptance score over the FULL body before the SCORING_MIN_ACCEPTANCE floor, keeping a bold-label notes page at no_match. Pattern bold-paren-time parses **Speaker** (HH:MM): text and (HH:MM:SS) (date_source: frontmatter). Fallback gates: SCORING_HEAD_TRIGGER_THRESHOLD = 0.3 triggers a full-body re-score when the head pass scores below that; SCORING_MIN_ACCEPTANCE = 0.05 blocks essay false-positives. Exported scorePatternFull(body, entry); private getNonBlankLines(body, headCap?) + scoreFromLines(lines, entry) DRY the quick_reject+regex loop. CLI surfaces at src/commands/eval-conversation-parser.ts (gbrain eval conversation-parser <fixture.jsonl> exit 0/1/2, wired into bun run verify via check:conversation-parser) and src/commands/conversation-parser.ts (scan <slug> debug, list-builtins, validate <file>). Doctor checks: conversation_format_coverage, progressive_batch_audit_health, conversation_parser_probe_health. Pinned by test/conversation-parser/{parse,llm-base,llm-fallback,llm-polish,nightly-probe}.test.ts + the 27-case baseline at test/extract-conversation-facts.test.ts (back-compat invariant). Migration v97 (conversation_parser_llm_cache_table). Fixtures at test/fixtures/conversation-formats/{imessage,telegram-bracket,whatsapp-iso,whatsapp-us,signal-export,irc-classic,irc-weechat,matrix-element,teams-export,all,adversarial,bold-name-no-time}.jsonl with scripts/check-fixture-privacy.sh banning real-name leaks.
  • src/core/progressive-batch/ — shared ramp-up + cost-cap + verification primitive (trial 10 → ramp 100 → ramp 500 → full, with verification at each stage), with verifier+policy injection (callers describe HOW TO MEASURE SUCCESS, not WHEN TO WAIT FOR CTRL-C). Modules: types.ts (Stage, StageVerdict, AbortReason, discriminated Verifier union OutputCountVerifier | IdempotentMutationVerifier | NoopVerifier, Policy, StageReport), orchestrator.ts (runProgressiveBatch(items, verifier, policy, runner) — reads getCurrentBudgetTracker() ahead of Policy.maxCostUsd fail-closed; null both ways triggers abort_cost_cap reason='no_budget_safety_net'), audit.ts (ISO-week JSONL at ~/.gbrain/audit/progressive-batch-YYYY-Www.jsonl via the shared audit-writer primitive), stage-report.ts (ASCII formatter for the default Policy.onStageReport). Env knobs: GBRAIN_PROGRESSIVE_BATCH_DISABLED=1, GBRAIN_PROGRESSIVE_BATCH_AUTO=1 (skip Ctrl-C grace), GBRAIN_PROGRESSIVE_BATCH_STAGES=10,100,500. Sites that "jump straight to full" stay that way by default; ramp is opt-in per-site via Policy.interactiveAbortMs > 0. Pinned by test/progressive-batch/orchestrator.test.ts (35 cases, every verdict path).
  • src/commands/extract-conversation-facts.ts + src/core/cycle/conversation-facts-backfill.ts — bulk fact extraction for long-form conversation pages. Walks conversation/meeting/slack/email pages, splits into time-windowed segments (30-min gap or 30-msg cap), prepends a topical/temporal header, and runs through extractFactsFromTurn() so anchor-rich facts surface in gbrain search. Invariants: strict per-source core (runExtractConversationFactsCore({sourceId, ...}) always takes one sourceId; CLI + cycle phase each do their own multi-source iteration because PHASE_SCOPE='source' is taxonomy-only); two-phase enumeration (paginated listPages({type, sourceId, limit:10}); per-page body cap MAX_PAGE_BODY_BYTES=25MB with pages_skipped_too_large counter surfaced in doctor); page-global row_num accumulator (facts unique index is (source_id, source_markdown_slug, row_num) per migration v51 — per-segment row_num would collide); page-level TERMINAL audit row to facts table after all segments commit (source='cli:extract-conversation-facts:terminal'; doctor's NOT EXISTS matches the terminal row so partial-extraction pages stay in backlog); optional opts.budgetTracker? (when present, used as-is — nested withBudgetTracker REPLACES; when absent, core auto-wraps with BudgetTracker({maxCostUsd})); body read covers compiled_truth + timeline; honors facts.extraction_enabled kill-switch with --override-disabled escape; --types LIST allowlist (conversation,meeting,slack,email) with CLI default reading cycle.conversation_facts_backfill.types; fingerprint on sourceId only; string-encoded op-checkpoint entries "<sourceId>|<slug>|<endIso>" for resume (durable audit is the facts terminal row); --background via maybeBackground (Minion handler extract-conversation-facts re-creates BudgetTracker from data.max_cost_usd; on BudgetExhausted mid-job catches + persists + marks completed with result.budget_exhausted=true). The companion cycle phase conversation_facts_backfill (default OFF) iterates listSources(engine), creates ONE brain-wide tracker per tick + wraps the loop in withBudgetTracker + passes the tracker into every per-source call. Two-layer cost AND walltime caps: per-source (max_cost_usd=$1, max_walltime_min=20) AND brain-wide (max_total_cost_usd=$5, max_total_walltime_min=30). Pinned by test/extract-conversation-facts.test.ts (27 cases). Migration v94 adds partial index idx_facts_extract_conversation_session ON facts(source_id, source_session) WHERE source LIKE 'cli:extract-conversation-facts%' (transaction:false + invalid-index pre-drop on Postgres; plain CREATE INDEX on PGLite). src/commands/doctor.ts:computeConversationFactsBacklogCheck is 3-state (SKIPPED when disabled; OK when caught up; WARN when >10 pages lack the terminal row, with paste-ready gbrain doctor --remediate step). src/commands/sources.ts:runAudit adds facts_backfill_estimate: {pages, est_segments, est_cost_usd, types}. Schema-pack gbrain-base.yaml promotes conversation (temporal, extractable) + atom (annotation, NOT extractable) into the base seed; backstop uses hardcoded ELIGIBLE_TYPES in src/core/facts/eligibility.ts:51 not pack extractable. ALL_PAGE_TYPES in src/core/types.ts extended with the two new types.
  • src/core/link-extraction.ts — shared library for the graph layer. extractEntityRefs (canonical) matches [Name](people/slug) markdown links and Obsidian [[people/slug|Name]] wikilinks; extractPageLinks, inferLinkType heuristics (attended/works_at/invested_in/founded/advises/source/mentions), parseTimelineEntries, isAutoLinkEnabled. DIR_PATTERN covers people, companies, deals, topics, concepts, projects, entities, tech, finance, personal, openclaw. Used by extract.ts, operations.ts auto-link post-hook, and backlinks.ts. Opt-in global-basename wikilink resolution (issue #972, default off): WIKILINK_GENERIC_RE catches bare [[name]] wikilinks outside DIR_PATTERN (third pass 2c in extractEntityRefs); EntityRef.needsResolution: true tags refs from this pass (the ref's slug is the wikilink TARGET, name the optional display alias). SlugResolver gains optional resolveBasenameMatches(name): Promise<string[]> (multi-match by design — emits one edge per matching page). The single shared basename matcher is buildBasenameIndex(slugs) + queryBasenameIndex(index, name) + normalizeBasename (keys raw/lower/slugified tail, stable-sorted shorter-first then lexical), used by makeResolver, the FS resolveBasenameMatchesFromSlugs, AND the doctor check so they cannot drift. makeResolver(engine, {mode, sourceId}) builds the index lazily via engine.getAllSlugs({sourceId}) — source-scoped so a bare [[name]] never resolves to a same-tail page in a different source. extractPageLinks gains opts.globalBasename (routes needsResolution refs through resolveBasenameMatches keyed on ref.slug, emits candidates tagged linkType: 'wikilink_basename' + linkSource: 'wikilink-resolved', skips self-loops) and opts.skipFrontmatter (replaces the old nullResolver ternary). All three surfaces (FS extract, DB extract, put_page auto-link) tag provenance with link_source='wikilink-resolved'; put_page includes it in its reconcilable-edge set so stale basename edges are removed when the wikilink or the flag goes away. Exports WIKILINK_BASENAME_LINK_TYPE + isGlobalBasenameEnabled(engine) (resolution order: env GBRAIN_LINK_RESOLUTION_GLOBAL_BASENAME → DB config link_resolution.global_basename → default false). gbrain doctor's link_resolution_opportunity check surfaces a paste-ready enable hint when ≥5 bare wikilinks would resolve AND ≥20% match. Migration v113 widens links_link_source_check to admit 'wikilink-resolved'; v114 (#1941) then opens it to any kebab-case provenance (^[a-z][a-z0-9]*(-[a-z0-9]+)*$, ≤64 chars) so external derivers register their own tag (e.g. citation-graph) without a migration. LINK_EXTRACTOR_VERSION_TS also lives here (bump like CHUNKER_VERSION to invalidate prior extract-stale stamps). Pinned by test/link-extraction.test.ts, test/extract-fs.test.ts, test/doctor.test.ts, test/e2e/global-basename-pglite.test.ts.
  • src/commands/extract.tsgbrain extract links|timeline|all [--source fs|db] [--source-id <id>]: batch link/timeline extraction. fs walks markdown files, db walks pages from the engine (mutation-immune snapshot iteration; use for live brains with no local checkout). No in-memory dedup pre-load — candidates buffered 100 at a time and flushed via addLinksBatch / addTimelineEntriesBatch; ON CONFLICT DO NOTHING enforces uniqueness at the DB layer, created counter returns real rows inserted. ExtractOpts.slugs?: string[] enables incremental extract via extractForSlugs() (single combined links+timeline pass); the cycle path threads sync's pagesAffected through. walkMarkdownFiles(brainDir) still runs to build allSlugs for link resolution. --source-id <id> scopes extraction to one source on federated brains (resolved via resolveSourceWithTier() before any SQL; failures hint gbrain sources list). gbrain extract --stale [--source-id <id>] [--catch-up] [--dry-run] [--json] branch (extractStaleFromDB) — incremental DB-source link+timeline sweep over pages whose pages.links_extracted_at watermark is stale. Stale predicate (shared by both engines + the doctor check): links_extracted_at IS NULL OR links_extracted_at < LINK_EXTRACTOR_VERSION_TS::timestamptz OR updated_at > links_extracted_at (the updated_at arm catches MCP put_page / sync --no-extract edited-since-extract). Three new BrainEngine methods (parity in postgres-engine.ts + pglite-engine.ts + bootstrap probes): countStalePagesForExtraction(opts?), listStalePagesForExtraction({batchSize, afterPageId?, sourceId?, versionTs?}) (returns page CONTENT to avoid N+1 getPage; rowToStalePage in utils.ts maps the row, StalePageRow in types.ts), markPagesExtractedBatch(refs, defaultExtractedAt) (3-array unnest slug[],source_id[],ts[]; each ref may carry its own extractedAt). STALE_BATCH_SIZE default 25 (GBRAIN_EXTRACT_STALE_BATCH; small because page bodies are unbounded — the LIMIT is the only fetch-time memory bound); STALE_TIME_BUDGET_MS 30min wall-clock (--catch-up removes the cap). Non-swallowing flush: link/timeline flush throws propagate and abort the batch; stamp LAST so a crash leaves pages unstamped and they re-extract idempotently (addLinksBatch ON CONFLICT DO NOTHING + timeline dedup). Race fix: extractStaleFromDB stamps with each row's READ updated_at (not now()), so a concurrent edit during the sweep keeps the page stale and it re-extracts next run rather than marked fresh-with-old-content. Source-correct stamping at DB-extract sites via stampExtracted (best-effort, never throws); extractLinksFromDB only stamps the combined watermark when subcommand === 'all' (a links-only run must not hide timeline staleness). LINK_EXTRACTOR_VERSION_TS lives in src/core/link-extraction.ts (bump like CHUNKER_VERSION to invalidate all prior stamps). Migration v112 (pages_links_extracted_at) adds nullable TIMESTAMPTZ + composite (source_id, links_extracted_at) index (CONCURRENTLY + invalid-remnant pre-drop on Postgres, plain on PGLite), NO backfill so the real backlog surfaces on first gbrain doctor. Schema parity in schema.sql + pglite-schema.ts + schema-embedded.ts + REQUIRED_BOOTSTRAP_COVERAGE. src/commands/doctor.ts:checkLinksExtractionLag (the links_extraction_lag check, also in doctorReportRemote) warn-only by default (>GBRAIN_EXTRACTION_LAG_WARN_PCT, default 20%; shared EXTRACTION_LAG_WARN_PCT_DEFAULT + EXTRACTION_LAG_MIN_PAGES=100 + exported _resolveEnvNumber), hard-fails only when GBRAIN_EXTRACTION_LAG_FAIL_PCT is set; vacuous-skips <100 pages (no --source); pre-v112 brains graceful-skip via isUndefinedColumnError; strictly a SQL COUNT (safe on remote/thin-client). src/commands/sync.ts gains --no-extract (threaded through single-source + --all + syncOneSource), stamps links_extracted_at for pagesAffected at the inline-extract call site, and maybeExtractionNudge prints a one-line stderr nudge after a synced | first_sync | up_to_date sync that leaves a backlog (shouldNudgeAfterSync pure predicate; GBRAIN_SYNC_NO_EXTRACT_NUDGE suppresses). src/core/retry.ts adds 'extract.stale' to BATCH_AUDIT_SITES; src/core/doctor-categories.ts adds links_extraction_lag to BRAIN_CHECK_NAMES. Pinned by test/extract-stale.test.ts (incl. edited-after-stamp regression + crash-contract), test/sync-inline-extract-stamps.serial.test.ts, test/sync-nudge-status-gate.test.ts, test/doctor-links-extraction-lag.test.ts, engine-parity (Postgres↔PGLite) for the 3 methods + v112 round-trip. The stale SELECT in both engines projects a deterministic full-µs UTC string to_char(updated_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS updated_at_iso (carried on StalePageRow.updated_at_iso, populated by rowToStalePage in utils.ts with an ISO-only fallback — never String(Date), which ::timestamptz misparses); extractStaleFromDB stamps that exact-precision value, not a JS Date (which truncates to milliseconds), so on Postgres links_extracted_at equals the row's updated_at to the microsecond and links_extraction_lag clears — a ms-truncated stamp stays strictly below the µs updated_at and leaves every page perpetually stale, which extract --stale could never satisfy. to_char (not raw ::text, which is DateStyle-fragile) keeps the projection deterministic. The markPagesExtractedBatch SQL is unchanged, so callers passing an explicit (e.g. backdated) extractedAt still control the stamp and the edited-since arm is exact. A deterministic PGLite regression in test/extract-stale.test.ts injects a µs updated_at, runs --stale, and asserts the lag is 0 and stays 0.
  • src/core/extract/receipt-writer.ts + src/core/extract/rollup-writer.ts + src/commands/extract-status.ts + src/commands/extract-explain.ts + src/commands/extract-benchmark.ts + src/core/schema-pack/scaffold-extractable.ts — unified extract operator surface. Every shipped extractor (deterministic facts.conversation in src/commands/extract-conversation-facts.ts + three LLM-backed cycle phases at src/core/cycle/{extract-atoms,synthesize-concepts,propose-takes,extract-facts}.ts) writes ONE receipt page per run (writeReceipt) + UPSERTs a row to extract_rollup_7d (upsertExtractRollup). Receipt slug extracts/{date}/{kind}/{source_id}/{run_id_short}/round-{N}.md; frontmatter stamps BOTH type: extract_receipt AND dream_generated: true (belt+suspenders against extraction-loop guard drift). extract_receipt joins ALL_PAGE_TYPES in src/core/types.ts; extracts/ prefix gets a 0.3x source-boost demote in src/core/search/source-boost.ts. Migration v104 adds extract_rollup_7d (kind, source_id, day, cost_usd, halt_count, eval_pass_count, eval_fail_count, round_completed_count, rollup_write_failures, updated_at) with PK (kind, source_id, day) + idx_extract_rollup_7d_day. Rollup writes best-effort with process-scoped error-dedup so transient DB failures bump rollup_write_failures instead of crashing the cycle. extract_health doctor check reads last 7 days, warns at halt-rate > 10% AND when rollup_write_failures > 0; pre-v104 brains report ok. CLI: gbrain extract status [--source-id ID] [--kind X] [--verbose] [--json] (7-day rollup, sorted halt_rate desc + cost desc, top-5 + "more rows" hint, stable schema_version: 1); gbrain extract --explain <kind> (resolution chain pack-declared vs built-in cycle phase, prompt_template + fixture_corpus paths with /(missing), last 7d rollup); gbrain extract benchmark --pack X --kind Y (loads pack fixture corpus through strict path validation — rejects absolute paths, .. traversal, null bytes, AND symlinks resolving outside pack root; ships as a stub-reporter). src/core/schema-pack/manifest-v1.ts widens extractable from z.boolean() to z.union([z.boolean(), ExtractableSpecSchema]) (carries prompt_template, fixture_corpus, eval_dimensions, benchmark_min_recall, plus reserved verifier_path — parses but refuses at runtime); extractableSpecsFromPack + getExtractableSpec + refuseVerifierPathInV042 in src/core/schema-pack/extractable.ts; gbrain schema scaffold-extractable <type> --pack <pack> declares the type extractable, generates 5 placeholder fixtures + a prompt template stub under packs/<pack>/{fixtures,prompts}/extract/, refuses to overwrite without --force. Pinned by test/extractable-spec-widening.test.ts (22), test/extract/receipt-writer.test.ts (12, canonical PGLite block R3+R4), test/extract/benchmark.test.ts (17), test/extract/status.test.ts (15), test/schema-pack/scaffold-extractable.test.ts (15, privacy guards), test/doctor-extract-health.test.ts (8).
  • src/commands/import.tsgbrain import <path> [--source-id <id>]: page import with the path-set checkpoint. --source-id <id> routes pages to the named source (resolved via resolveSourceWithTier() at the boundary; consistent across import, extract, graph-query, sources current). Pinned by test/import-source-id.test.ts.
  • src/commands/graph-query.tsgbrain graph-query <slug> [--type T] [--depth N] [--direction in|out|both] [--include-foreign]: typed-edge relationship traversal (renders indented tree). Foreign-edge footer always present (X foreign edges (use --include-foreign to traverse)) so cross-source edges never disappear silently; --include-foreign widens the SQL filter to walk them. Pinned by test/graph-query.test.ts.
  • src/commands/sources.tsgbrain sources {list,add,remove,archive,restore,archived,purge,current,status,audit}. current [--json] calls resolveSourceWithTier() and prints source_id, tier (flag | env | dotfile | local_path | brain_default | seed_default), and optional detail (decision table in skills/conventions/brain-routing.md). status [--json] — read-only per-source dashboard (last sync, staleness, page count, embedding coverage, unacked failures); thin wrapper around buildSyncStatusReport + printSyncStatusReport from src/commands/sync.ts; --json emits stable {schema_version: 1, sources, ...} on stdout; filters input to local_path IS NOT NULL AND archived IS NOT TRUE. audit <id> [--json] — read-only dry-run disk scan for size distribution + would-blocks + junk-pattern hits WITHOUT touching the DB; walks sources.local_path, reads each markdown file, runs assessContent() from src/core/content-sanity.ts, aggregates by verdict (ok | warn_oversize | hard_block_junk_pattern). The live runStatus health table gains a BACKFILL column between EMBED and FAILS (active(N) beats queued(N) beats idle, from SourceMetrics.backfill_active / backfill_queued in src/core/source-health.ts) so operators see deferred embed-backfill minion work after sync --all exits 0; jobCountsBySource in source-health.ts widens its minion_jobs SQL with two COUNT(*) FILTER (WHERE name = 'embed-backfill' AND ...) aggregates (best-effort, all-0 on pre-minions brains). Pinned by test/content-sanity.test.ts, test/import-file-content-sanity.test.ts, test/source-health.test.ts.
  • src/commands/reindex-frontmatter.tsgbrain reindex-frontmatter. Query path wrapped in the standard withEngine(...) lifecycle so engine.connect() runs before the first SQL call. Pinned by test/reindex-frontmatter-connect.test.ts.
  • src/core/source-resolver.ts — 6-tier source resolution. resolveSourceWithTier(engine, explicit, cwd) returns { source_id, tier: SourceTier, detail? } alongside resolveSourceId() (unchanged). SOURCE_TIER_NAMES = ['flag', 'env', 'dotfile', 'local_path', 'sole_non_default', 'brain_default', 'seed_default'] (7 entries; order matches priority). Tier sole_non_default slots between local_path and brain_default: when NO sources.default config is set AND exactly one registered source has local_path AND isn't 'default', auto-route to it; archived sources excluded (try/catch for pre-v34 brains); private pickSoleNonDefaultSource(engine) shared by both resolver entry points so they cannot drift. Exported formatSoleNonDefaultNudge(sourceId): string | null builds the user-facing stderr nudge (null when GBRAIN_NO_SOLE_NON_DEFAULT_NUDGE=1). src/commands/sync.ts:1497-1519 calls resolveSourceWithTier unconditionally so the tier fires; src/commands/import.ts:96-128 mirrors with the tier-gated nudge. Consumed by gbrain sources current, import --source-id, extract --source-id, and the source_routing_health doctor check. Pinned by test/source-resolver-with-tier.test.ts (withEnv() per test-isolation lint), test/source-resolver-sole-non-default.test.ts (14 cases), test/sync-sole-non-default-routing.test.ts (3 PGLite cases driving real runSync).
  • src/core/sync.ts extension — isSyncable factored through private classifySync(path, opts): SyncableReason | null; exported companion unsyncableReason(path, opts) returns the same tagged reason or null when syncable. SYNC_SKIP_FILES is a named export (the four canonical metafile basenames schema.md, index.md, log.md, README.md). SyncableReason union: 'metafile' | 'strategy' | 'pruned-dir' | 'include-glob-miss' | 'exclude-glob-hit'. src/commands/sync.ts:772 cleanup loop guards on unsyncableReason(path) === 'metafile' so previously-indexed metafile pages survive every re-sync. Does NOT cover manifest.deleted (the upstream filter already strips metafiles). Pinned by test/sync-isSyncable-shape.test.ts (15 cases, duality contract) + test/sync-metafile-skip.serial.test.ts (3 PGLite cases incl. the renamed .md → .txt negative).
  • src/core/import-file.ts extension — identity-based dedup pre-check at :427-490. Calls engine.findDuplicatePage?.(sourceId, {hash, frontmatterId}) (optional ? so test doubles compile). Posture: SKIP when frontmatter.id matches (true external duplicate from overlapping ingest roots), WARN-ALWAYS on content_hash collision with different/missing frontmatter.id (templates and daily logs may legitimately share text), FAIL CLOSED on lookup error, bypass via --force-rechunk. Soft-deleted pages excluded at the engine layer so tombstones don't block legitimate re-imports under new slugs. Pinned by test/import-dedup-frontmatter-id.test.ts (11 cases).
  • src/core/engine.ts extension — two interface members: (1) optional findDuplicatePage?(sourceId, {hash, frontmatterId?}): Promise<{slug, id} | null> (identity precedence is content_hash OR frontmatter->>'id', both with deleted_at IS NULL); (2) resolveSlugs(partial, opts?) extended with {sourceId?, sourceIds?} so the MCP fuzzy get_page path scopes by source (field names match sourceScopeOpts(ctx) output so handlers spread directly; back-compatible — no opts gives prior behavior). Plus a stable tiebreaker ORDER BY score DESC, page_id ASC, chunk_id ASC in searchVector in both engines: on a score tie (basis-vector eval fixtures) older page_id wins, closing the planner-non-determinism class where a new index on pages could flip ranking on tied scores.
  • src/core/migrate.ts v95 — pages_dedup_partial_index adds CREATE INDEX pages_dedup_idx ON pages (source_id, content_hash) WHERE deleted_at IS NULL. Postgres uses CREATE INDEX CONCURRENTLY with transaction: false + pre-drops any invalid remnant; PGLite uses plain CREATE INDEX. Powers findDuplicatePage hot path (O(log n) instead of O(n)).
  • src/cli.ts extension — dream dispatch at lines 1063-1080 binds the caught engine-connect error and emits [dream] WARNING: could not connect to DB (...) to stderr before falling through to filesystem-only phases; the runDream(null, ...) no-DB fallback is preserved. Pinned by test/cli-dream-engine-warn.test.ts (2 subprocess cases against good + bad DATABASE_URL).
  • src/commands/autopilot.ts extension — federated-brain co-existence + launchd hygiene. (1) LOCK_PATH resolves via gbrainPath('autopilot.lock') so it honors GBRAIN_HOME (two brains can run autopilot simultaneously without lock-stealing); lock file stores PID, startup checks kill -0 <pid> before refusing to start (stale lock from a crashed process no longer blocks). (2) exported classifyReconnectError(err) returns 'recoverable' | 'unrecoverable'; unrecoverable causes process.exit(0) so launchd backs off instead of looping config.database_url undefined. (3) exported pure generateLaunchdPlist(wrapperPath, home) sets ThrottleInterval=300 so launchd respects the exit-0 backoff. Pinned by test/autopilot-lock-path.test.ts + test/autopilot-reconnect-classifier.test.ts.
  • src/core/oauth-provider.ts + src/commands/serve-http.ts extension — custom /token middleware that runs BEFORE the MCP SDK's clientAuth. The SDK does plaintext compare against the request's client_secret; gbrain stores SHA-256 hashes only, so every confidential-client /token request would fail. The middleware detects confidential auth via Authorization: Basic header OR client_secret_post form body (both shapes per RFC 6749 §2.3.1), verifies via verifyClient(client_id, presented_secret) (SHA-256 hash compare), and falls through to the SDK for public PKCE clients (which the SDK's clientAuth still accepts via NULL-client_secret_hash normalization). Pinned by test/oauth-confidential-client.test.ts (both client_secret_basic and client_secret_post).
  • src/core/sync.ts:pruneDir extension — pruneDir(name, parentDir?) extended with optional parentDir. When provided, additionally rejects directories containing .git as a FILE — the git submodule gitfile pattern (regular repos have .git as a DIRECTORY; submodules as a file pointing into the parent's .git/modules/). Sync + extract walkers thread parentDir so the gitfile-as-FILE check fires per descend step. Best-effort: statSync failures fall through and treat as a normal dir. Closes the phantom-import bug class where syncing a worktree-with-submodules walked into submodule trees. Pinned by test/sync-walker-submodule.test.ts.
  • src/core/minions/handlers/subagent.ts extension — terminal-state short-circuit on resume. When a stored message thread already ends in stop_reason: 'end_turn', the handler returns { ok: true } immediately instead of issuing another messages.create call (re-prompting past end_turn would get a 400 and dead-letter an already-successful job). Pinned by test/subagent-handler.test.ts.
  • src/commands/doctor.ts extension — three checks wired into runDoctor() and the JSON envelope, all warn-only with paste-ready fix hints. (1) checkSourceRoutingHealth(engine) scans up to 200 pages on federated brains and flags pages whose source_id doesn't match what resolveSourceWithTier() would have picked for their source_path; single-source brains short-circuit to ok; the 200-page cap is total across the brain so doctor stays under 5s. (2) checkOauthConfidentialHealth(engine) probes registered confidential clients for /token reachability. (3) checkAutopilotLockScope() (pure, no engine) compares the resolved lock path to $GBRAIN_HOME; warns when set but the lock lives elsewhere, with a PID-safe inspection hint (kill -0 <pid> before deletion). Pinned by test/doctor-v0_37_7_checks.test.ts.
  • skills/conventions/brain-routing.md — agent-facing convention skill documenting the canonical 6-tier source resolution chain (flag → env → dotfile → local_path → brain_default → seed_default) with paste-ready decision tables. Linked from CLAUDE.md's "Two organizational axes" section and from gbrain sources current's hint output.
  • src/commands/doctor.ts extension — buildChecks(engine, args, dbSource): Promise<Check[]> exported as a test seam. runDoctor is a thin wrapper: buildChecks → computeDoctorReport → render + process.exit. All 10 process.exit sites stay in the wrapper; the two early-return paths (no engine, connection failure) return partial check lists instead of inline exits (observable output identical). Pinned by test/doctor-behavioral.test.ts (13 cases: pure aggregation math over computeDoctorReport, orchestrator cases for --fast skip set + --json flag + no-engine partial path + snapshot of load-bearing check names) and test/doctor-cli-smoke.serial.test.ts (1 subprocess case spawning bun run src/cli.ts doctor --json against a fresh PGLite tempdir, asserting schema_version=2 envelope, status enum, non-empty checks array — the render-path coverage buildChecks-only tests miss; quarantined .serial because PGLite write-locks don't play with parallel runners).
  • src/core/cycle.ts extension — runPhaseLint + runPhaseBacklinks carry the export keyword so behavioral tests can drive them directly (internal helpers exposed for test-only consumption; downstream code should NOT depend on them). Pinned by test/cycle-legacy-phases.test.ts (11 cases across both phases: clean run → status='ok', partial fix → status='warn' with dryRun in details, dry-run path doesn't write, throw-from-lib → status='fail' with the wrapper's try/catch envelope populated). Future phase wrappers (sync, extract, embed, orphans, extract_facts, resolve_symbol_edges, recompute_emotional_weight) land as additional describes in the same file.
  • test/operations-trust-boundary.test.ts + scripts/check-operations-filter-bypass.sh — operations trust-boundary contract coverage. Pure assertions over all 74 ops (every op has a scope annotation; every mutating op has a non-read scope; localOnly: true ops are excluded from operations.filter(op => !op.localOnly); the seven historically-sensitive localOnly ops snapshot-pinned by name) plus targeted handler-invocation regressions for the two historically-broken HTTP-callable classes: submit_job with name='shell' + ctx.remote=true MUST reject (the HTTP MCP shell-job RCE class), and search_by_image with image_path + ctx.remote=true MUST reject (the P0 image-leak class). file_upload and sync_brain omitted from handler-invocation tests because they're localOnly: true (that path would test an impossible production scenario). The shell guard greps src/ for any module importing the operations value outside the canonical filter site at src/commands/serve-http.ts (three import shapes: destructured, aliased, namespace), with an explicit 10-entry allow-list + a literal-string check that serve-http.ts still contains operations.filter(op => !op.localOnly). Wired into bun run verify.
  • src/core/content-sanity.ts — pure assessor for the content-sanity defense. assessContent(content, opts): SanityVerdict returns one of ok | warn_oversize | hard_block_junk_pattern | soft_block_oversize with {reason, bytes, matched_pattern_name?}. Six built-in junk patterns (Cloudflare challenge dumps, CAPTCHAs, 403 dumps, bare error-page titles) compiled at module load; operator literal substrings via loadOperatorLiterals() from src/core/content-sanity-literals.ts. ContentSanityBlockError tagged class is the typed throw shape every wrapper site (gbrain import, put_page MCP op, gbrain sync, /ingest webhook) catches via the existing exception flow. The bytes-parity contract pins Buffer.byteLength(content, 'utf8') against the embedder's actual byte count so a 499K-byte page can't be soft-blocked on assessment then overflow on embed. Knob resolution chain env > file (~/.gbrain/config.json) > DB > defaults. Four knobs: content_sanity.bytes_warn (50_000), content_sanity.bytes_block (500_000), content_sanity.junk_patterns_enabled (true), content_sanity.disabled (false; GBRAIN_NO_SANITY=1 is the loud-stderr kill-switch). New assessContentSanity(opts): SanityAssessment returns the three-tier disposition (shouldQuarantine / shouldFlag + reason/detail) consumed by importFromContent and gbrain quarantine scan; adds the fuzzy prose-vs-markup ratio pass (markup chars / total above max_markup_ratio; code pages exempt; gated by prose_check_enabled) on top of the byte + junk-pattern passes. Three more knobs: content_sanity.junk_disposition (quarantine default | reject; no env override — a destructive flip belongs in explicit config), content_sanity.max_markup_ratio (0.85, env GBRAIN_MAX_MARKUP_RATIO, clamped (0,1]), content_sanity.prose_check_enabled (true). Pinned by test/content-sanity.test.ts.
  • src/core/content-sanity-literals.ts — operator literal-substring loader. Reads ~/.gbrain/junk-substrings.txt, one literal per non-comment non-blank line; optional # name=<id> header pairs an identifier with the following literal so audit JSONL groups by site (linkedin_auth_wall, reddit_blocked, etc.). Fail-soft on ENOENT (missing file = empty array). Loaded on every ingest. Deliberately literal substrings (NOT regex) to defeat ReDoS. Pinned by test/content-sanity-literals.test.ts.
  • src/core/embed-skip.ts — 5-site shared predicate for the soft-block embed-skip filter. Exports shouldSkipEmbedding(frontmatter): boolean (JS predicate for callers holding the page in memory), EMBED_SKIP_SQL_FRAGMENT (parameterized SQL clause shared by Postgres + PGLite via executeRaw), and buildEmbedSkipMarker(reason: string) (writes frontmatter.embed_skip = {at: ISO_TIMESTAMP, reason} so the JSONB shape stays uniform). The 5 sites: embed.ts --stale, embed.ts --all, the embed-stale Minion helper, plus both engines' listStaleChunks + countStaleChunks. Single source of truth so the filter cannot drift. Pinned by test/embed-skip.test.ts (cross-site invariant + JSONB shape).
  • src/core/audit/content-sanity-audit.ts — ISO-week JSONL audit at ~/.gbrain/audit/content-sanity-YYYY-Www.jsonl built on the audit-writer.ts primitive. Records every hard-block, soft-block, warn-trip, and quarantine/flag event with {kind, source_id, slug, bytes, matched_pattern_name?, reason, ts}. Doctor reads the last 7 days, aggregates by (matched_pattern_name, source_id) so operators see which scraper is the problem. Honors GBRAIN_AUDIT_DIR for shared-filesystem multi-host setups. Pinned by test/audit/content-sanity-audit.test.ts.
  • src/commands/doctor.ts extension — three checks wired into runDoctor() and the JSON envelope: oversized_pages (warns on pages exceeding content_sanity.bytes_warn), scraper_junk_pages (warns on live DB pages matching any junk pattern that escaped ingest), and content_sanity_audit_recent (reads the last 7 days of audit events, aggregates by pattern+source). Default scans the 1000 most-recent pages; --content-audit opts into a full scan. All three warn-only with paste-ready fix hints (junk → gbrain sources audit <id> + git rm source-of-truth, oversize → split or accept).
  • src/commands/lint.ts extension — lint rules huge-page (flags pages exceeding content_sanity.bytes_warn) and scraper-junk (flags pages matching any junk pattern). Both reuse assessContent() from src/core/content-sanity.ts so lint, doctor, and ingest share one assessor. lint.ts lifts DB config when ~/.gbrain/ is reachable; falls back to file/env on CI. Pinned by test/lint-content-sanity.test.ts.
  • src/commands/embed.ts extension — applies the embed-skip filter at all 5 stale-chunk sites: runEmbedCore --stale, runEmbedCore --all, the embed-stale Minion helper, plus both engines' listStaleChunks + countStaleChunks via EMBED_SKIP_SQL_FRAGMENT. A soft-blocked page is queryable by title/slug but its chunks never enter the embed sweep. The shared helper from src/core/embed-skip.ts is the regression guard — no per-site ad-hoc filter allowed. Pinned by test/embed-skip.test.ts.
  • src/core/import-file.ts extension — importFromContent is the narrow waist every ingest path passes through (gbrain import, gbrain sync, put_page MCP, /ingest webhook). It runs a three-tier content-quality disposition via assessContentSanity from src/core/content-sanity.ts BEFORE chunking: (1) high-confidence junk (built-in Cloudflare/CAPTCHA interstitial patterns + operator literals) → QUARANTINE (stamps the quarantine frontmatter marker, writes ZERO chunks, hides the page from search) OR REJECT (throw → sync-failure) when content_sanity.junk_disposition is reject; (2) fuzzy markup-heavy (prose-vs-markup ratio above content_sanity.max_markup_ratio, warn-tier byte window, code pages exempt) → content_flag:markup_heavy marker (page stays fully searchable, marker rides search results + get_page to warn the agent); (3) oversize → embed_skip soft-block via buildEmbedSkipMarker() PLUS a content_flag:oversized marker, AND deletes any pre-existing chunks in the same transaction so search can't surface stale chunks. Gate-owned markers (quarantine, content_flag) are STRIPPED from untrusted (remote MCP, ctx.remote !== false) frontmatter so a write-scoped client can't hide pages or forge the warning channel; markers are excluded from content_hash so a flagged page doesn't re-embed every sync. gbrain import honors errors > 0 for non-zero exit. classifyErrorCode in src/core/sync.ts recognizes the PAGE_JUNK_PATTERN code so sync-failures.jsonl grouping bins these. extractEntityRefs (canonical; matches both [Name](people/slug) markdown links and Obsidian [[people/slug|Name]] wikilinks), extractPageLinks, inferLinkType heuristics (attended/works_at/invested_in/founded/advises/source/mentions), parseTimelineEntries, isAutoLinkEnabled config helper. DIR_PATTERN covers people, companies, deals, topics, concepts, projects, entities, tech, finance, personal, openclaw. Used by extract.ts, operations.ts auto-link post-hook, and backlinks.ts. Pinned by test/import-file-content-sanity.test.ts.
  • src/core/quarantine.ts — the two frontmatter markers the content-quality gate writes, sibling of src/core/embed-skip.ts (same marker-as-JSONB-object pattern, same JSONB ? existence check that works on Postgres AND PGLite; no schema migration — both are frontmatter JSONB keys). quarantine (key QUARANTINE_KEY) HIDES: set ONLY for high-confidence junk, writes zero chunks, excluded from search via quarantineFilterFragment(pageAlias) / QUARANTINE_FILTER_FRAGMENT (the p-aliased constant), the single source of truth buildVisibilityClause calls so the search filter and marker key can't drift. content_flag (key CONTENT_FLAG_KEY) WARNS, does NOT hide: set for fuzzy markup-heavy / oversize, page stays searchable, marker is READ INTO search/get_page output — deliberately NO SQL filter fragment. Three distinct markers, three reasons (never overloaded): embed_skip = oversized-but-clean, quarantine = junk hidden, content_flag = odd-examine-still-here; a page can carry more than one (oversize → embed_skip + content_flag:oversized) and each clears independently. Exports buildQuarantineMarker / isQuarantined / filterOutQuarantined, buildContentFlagMarker / getContentFlag / hasContentFlag, plus the two key constants. Pinned by test/quarantine.test.ts.
  • src/core/content-sanity.ts extension — new assessContentSanity(opts): SanityAssessment returns the three-tier disposition (shouldQuarantine / shouldFlag + reason/detail) consumed by importFromContent and gbrain quarantine scan. Adds the fuzzy prose-vs-markup ratio pass (markup chars / total chars above max_markup_ratio, default 0.85; code pages exempt; gated by prose_check_enabled, default true) on top of the byte + junk-pattern passes. Three config knobs: content_sanity.junk_disposition (quarantine default | reject; no env override), content_sanity.max_markup_ratio (0.85, env GBRAIN_MAX_MARKUP_RATIO, clamped (0,1]), content_sanity.prose_check_enabled (true). Same env > file > DB > defaults resolution chain. Pinned by test/content-sanity.test.ts.
  • src/commands/quarantine.tsgbrain quarantine <list|clear|scan> operator surface for the content-quality gate. list [--json] [--include-flagged] paginates listPages and reports quarantined (HIDDEN) pages, optionally also content_flag (FLAGGED, searchable) pages. clear <slug> [--force] [--no-embed] [--json] drops both markers and re-imports through the normal pipeline so the page re-chunks + re-embeds and becomes searchable; the gate re-runs on import so genuinely-junk pages re-quarantine (exit 1) unless --force sets GBRAIN_NO_SANITY=1 for that one import. scan [--limit N] [--apply] [--no-embed] [--json] re-assesses already-ingested pages so junk predating the gate gets marked (unchanged content short-circuits normal sync, so it never re-assesses otherwise); dry-run uses the SAME effective content_sanity config thresholds --apply will use, idempotent (skips already-marked pages), --apply re-imports with forceRechunk to set markers + (for quarantine) drop chunks. Dispatched in cli.ts. Pinned by test/quarantine-cli.test.ts.
  • src/core/search/hybrid.ts + src/core/search/sql-ranking.ts + src/core/operations.ts + src/core/types.ts extensions — agent-warning channel. SearchResult.content_flag?: {reason, detail} (new optional field in types.ts) is stamped post-fusion by stampContentFlags (the stampEvidence precedent) in hybridSearch AND in the keyword-only search MCP op so both retrieval paths surface the marker. get_page returns a top-level content_flag parallel field via getContentFlag(page.frontmatter). buildVisibilityClause (sql-ranking.ts) ANDs in QUARANTINE_FILTER_FRAGMENT so quarantined pages are excluded from all six search call sites (alongside soft-delete + archived-source filters). Pinned by test/sql-ranking.test.ts + test/e2e/quarantine-search-exclusion.test.ts.
  • src/commands/doctor.ts extension — two checks wired into runDoctor() + the JSON envelope: quarantined_pages (counts pages carrying the quarantine marker via engine.executeRaw JSONB ? existence, works on PGLite + Postgres; warn-only with a gbrain quarantine list hint) and flagged_pages (counts content_flag pages — searchable but odd; warn-only). Both skip gracefully (status ok, "Skipped") on engines/brains where the probe errors. Pinned by test/doctor.test.ts.
  • src/commands/lint.ts + src/commands/sources.ts extensions — gbrain lint gains a markup-heavy rule (flags pages whose prose-vs-markup ratio exceeds content_sanity.max_markup_ratio, reusing assessContentSanity so lint/gate/scan share one assessor); pinned by test/lint-content-sanity.test.ts. gbrain sources audit <id> becomes disposition-aware: its dry-run disk scan reports would-quarantine / would-reject / would-flag counts driven by the effective content_sanity.junk_disposition + markup config, so an operator previews the gate's verdict before sync. The content-sanity-audit JSONL (src/core/audit/content-sanity-audit.ts) records the new quarantine/flag dispositions.
  • src/core/zombie-reap.ts — idempotent installSigchldHandler() so JS-spawned children get reaped via Bun's internal waitpid(). Bun (like Node) only auto-reaps when a SIGCHLD listener is registered; without it, every child the worker spawns (shell jobs, embed batches, sub-agents) becomes a zombie on exit and holds connection slots. Called once at module load from src/cli.ts (Windows platform guard — SIGCHLD doesn't exist on Windows). Cross-file leak guard via _uninstallSigchldHandlerForTests(). Layer 1 of the three-layer zombie defense; Layer 2 is tini-as-PID-1 wrapping the worker subtree (via src/core/minions/spawn-helpers.ts); Layer 3 is the container's own tini for hard Bun crashes.
  • src/core/minions/ — Minions job queue: BullMQ-inspired, Postgres-native (queue, worker, backoff, types, protected-names, quiet-hours, stagger, handlers/shell).
  • src/core/minions/queue.ts — MinionQueue class (submit, claim, complete, fail, stall detection, parent-child, depth/child-cap, per-job timeouts, cascade-kill, attachments, idempotency keys, child_done inbox, removeOnComplete/Fail). add() takes a 4th trusted arg (separate from opts to prevent spread leakage); protected names in PROTECTED_JOB_NAMES require {allowProtectedSubmit: true} and the check runs trim-normalized (whitespace-bypass safe). add() plumbs max_stalled through with a [1, 100] clamp; omitted values let the schema DEFAULT (5) kick in. handleWallClockTimeouts(lockDurationMs) is Layer 3 kill shot for jobs where FOR UPDATE SKIP LOCKED stall detection and the timeout sweep both fail to evict (wedged worker holding a row lock via a pending transaction). The maxWaiting coalesce path uses pg_advisory_xact_lock keyed on (name, queue) to serialize concurrent submits for the same key, and filters on queue in addition to name so cross-queue same-name jobs don't suppress each other. claim and renewLock issue their UPDATE via engine.executeRawDirect (not executeRaw) so the lock heartbeat runs on the direct session-mode pool that the transaction pooler won't recycle mid-hold; on PGLite this is identical to executeRaw. The two terminal dead-letter paths (handleWallClockTimeouts wall-clock kill and the stall dead-letter CTE in the stall sweep) BOTH increment attempts_made so a long job killed there reads as an honest attempt instead of attempts 0 / started N; the stall path also bumps stalled_counter, surfaced by gbrain jobs get as Attempts: M/N (started: X, stalled: S/MaxS). At submit, add() stamps a default timeout_ms via defaultTimeoutMsFor(jobName) (from handler-timeouts.ts) when the caller passed none, so long handlers aren't wall-clock-killed mid-progress by the short null-default; an explicit opts.timeout_ms always wins. Guarded by test/queue-lock-retry.test.ts (claim never falls back to executeRaw), test/postgres-execute-raw-direct.test.ts (routing decision matrix), and test/minions.test.ts (attempt accounting + default-timeout stamping).
  • src/core/minions/worker.ts — MinionWorker class (handler registry, lock renewal, graceful shutdown, timeout safety net). Aborted jobs call failJob with reason (timeout/cancel/lock-lost/shutdown); shutdownAbort (instance field) fires on SIGTERM/SIGINT and propagates to ctx.shutdownSignal (shell handler listens; non-shell handlers don't). Per-job timeout fires abort.abort(new Error('timeout')) then a 30s grace-then-evict safety net force-evicts the job from inFlight and marks it dead if the handler ignores the abort signal (the moved-out generic abort listener now fires for ANY abort reason). The launchJob lock-renewal block is a thin sync wrapper around the pure runLockRenewalTick from src/core/minions/lock-renewal-tick.ts (NEVER setInterval(async () => await renewLock(...)) — that shape was the unhandledRejection crash class during PgBouncer rotation). Gaps closed: (1) cancelled flag captured in the timer closure stops in-flight IIFEs writing misleading audit events after the job ended; (2) re-entrancy guard tickInFlight + per-call Promise.race timeout (GBRAIN_LOCK_RENEWAL_CALL_TIMEOUT_MS, default lockDuration/3); (3) time-based abort (Date.now() - lastSuccessfulRenewalAt >= lockDuration - GBRAIN_LOCK_RENEWAL_SAFETY_MARGIN_MS) so we release the lock BEFORE another worker can reclaim; (4) explicit .catch() on the stored executeJob(...).finally(...) promise closes the second unhandledRejection vector; (5) exported INFRASTRUCTURE_ABORT_REASONS = new Set(['lock-renewal-failed', 'lock-lost']) so executeJob's catch skips failJob for these (PgBouncer blips don't dead-letter healthy jobs; the stall detector reclaims). CI guard scripts/check-worker-lock-renewal-shape.sh (in bun run verify) asserts the bug pattern stays absent AND launchJob keeps calling runLockRenewalTick. Engine-ownership invariant: start() does NOT call engine.disconnect() on shutdown — the CLI handler in src/commands/jobs.ts case 'work' owns engine lifecycle via try/finally with loud error logging. RSS watchdog uses non-file-backed pages on Linux: exported parseRssFromProcStatus(status) (pure parser; field-presence regex so RssAnon: 0 + RssShmem: 512 parses correctly) and getAccurateRss(readStatus?) (reads /proc/self/status for RssAnon + RssShmem, falls back to process.memoryUsage().rss on macOS / restricted containers / kernel <4.5); the default getRss in WorkerOpts is getAccurateRss. checkMemoryLimit tracks peak RSS, fires an 80%-of-cap soft-warn (once per crossing, carrying peak + in-flight job kinds), and on exceed sets _rssWatchdogTriggered=true (exposed via get rssWatchdogTriggered()) so jobs work's finally can process.exit(WORKER_EXIT_RSS_WATCHDOG) after disconnect (drain self-identifying instead of an opaque code-0 exit). The poll loop wraps claim in try/catch: on a retryable conn error it reconnects ONCE and continues to the next tick rather than blind-retrying (a retry after UPDATE...RETURNING committed but the socket died would double-claim). LockRenewalDeps is wired with reconnect when the engine supports it. The self-health DB-liveness probe now runs EVEN under a supervisor (GBRAIN_SUPERVISED=1): the outer guard is if (this.opts.healthCheckInterval > 0) and only the STALL-detection block is wrapped in if (!isSupervisedChild) — so a supervised worker whose own pool dies self-exits unhealthy(db_dead) after dbFailExitAfter probes (the supervisor watches a different connection and can't see this worker's dead pool), while the supervisor's progress watchdog owns forward-progress (#1801). Pinned by test/worker-lock-renewal.test.ts (18), test/audit/lock-renewal-audit.test.ts (11), test/scripts/check-worker-lock-renewal-shape.test.ts (5), test/worker-shutdown-disconnect.test.ts (asserts disconnectSpy).not.toHaveBeenCalled()), test/worker-rss.test.ts (11), test/worker-supervised-db-probe.test.ts (3).
  • src/core/minions/supervisor.ts — MinionSupervisor process manager. Spawns gbrain jobs work as a child, restarts on crash with exponential backoff, periodic health check. consecutiveHealthFailures counter; on 3 consecutive failures emits health_warn with reason: 'db_connection_degraded' and calls engine.reconnect() to swap in a fresh pool, then resets. Worker exit classifier emits likely_cause on worker_exited events: oom_or_external_kill (SIGKILL), graceful_shutdown (SIGTERM), runtime_error (code 1), clean_exit (code 0), unknown. Consumes detectTini() + buildSpawnInvocation() from src/core/minions/spawn-helpers.ts to wrap the worker subtree in tini-as-PID-1 when tini is on PATH (handles native-addon zombie reaping the in-process SIGCHLD reaper can't reach); exposes isTiniDetected read-only accessor. The spawn-and-respawn loop is the shared ChildWorkerSupervisor core: MinionSupervisor composes it via runSuperviseLoop()new ChildWorkerSupervisor({...}) and maps ChildSupervisorEvent back through emit() SupervisorEvent (JSONL audit consumers see byte-compatible output). PID lock, signal handlers, health check, and process.exit on max-crashes stay in MinionSupervisor. code=0 leaves crashCount untouched (so a worker alternating real crashes + watchdog drains still trips max_crashes); cleanRestartBudget (default 10 restarts per 60s) caps the macOS/non-Linux-fallback tight-loop via health_warn { reason: 'clean_restart_budget_exceeded' } + backoff. shutdown() drains via childSupervisor.killChild('SIGTERM') + awaitChildExit(35_000). Progress watchdog (#1801): healthCheck() restarts an alive-but-wedged child via childSupervisor.restartCurrentChild(35_000) when a queue has claimable work, 0 live-lock active jobs, and stale completions across wedgeRestartChecks (default 3) consecutive checks past wedgeRestartMinutes (default 15, 0 disables) + a startupGraceMs window; bounded by wedgeRestartLoopBudget (default 3 / wedgeRestartLoopWindowMs) which switches to a one-shot wedge_restart_loop alert. The wedge query is the exported queryWedgeSignals(engine, queue, handlerNames) — name+queue-scoped, active_healthy = live-lock only (an expired-lock active row does NOT mask the wedge), due-delayed counted. Claimable names are derived at start via a throwaway registerBuiltinHandlers worker (its new quiet opt). Flags --wedge-restart-minutes / --wedge-restart-checks + env GBRAIN_WEDGE_RESTART_MINUTES / GBRAIN_WEDGE_RESTART_CHECKS. The worker argv is built by the exported pure buildWorkerArgs(opts) (appends --nice N when opts.nice_requested is set, alongside --concurrency/--queue/--max-rss); the niceness apply RESULT (nice_requested/nice_effective/nice_error, computed by the CLI in jobs.ts — the supervisor doesn't call setPriority) rides on the started/worker_spawned audit emissions (#1815). Queue-scoped singleton (#1849): the real authority is a DB lock (tryAcquireDbLock from src/core/db-lock.ts) keyed on supervisorLockId(queue) = gbrain-supervisor:<queue> — keyed on the QUEUE ALONE because the lock row lives inside the target database, so the (database) half of the mutex is physical, not part of the key (an earlier revision mixed in a config-derived DB identity, which let two supervisors on the same physical DB via different-but-equivalent URLs compute different ids and both acquire — fixed). Two supervisors with different $HOME/--pid-file against the same (database, queue) no longer both run with conflicting --max-rss; the second exits LOCK_HELD. The pidfile-cleanup process.on('exit') listener is installed BEFORE the DB-lock acquisition so the LOCK_HELD early-exit can't strand the pidfile this process just created. The default pidfile is now brain-scoped (supervisor-<brainId>.pid) so different brains under one HOME don't false-block. The lock refreshes on its own setInterval (TTL 5min, refresh 60s, max 3 failures = 180s < TTL); a refresh that fails past the threshold exits LOCK_LOST (code 4) rather than risk a split-brain. shutdown() releases the lock so a clean restart re-acquires immediately. The started audit now records max_rss_mb so gbrain doctor's supervisor_singleton check can surface the effective cap. Exports supervisorLockId() and the pure classifySupervisorSingleton({lockLive, lockHolderHost, lockHolderPid, localHost, localPid}) → 'no_lock'|'single'|'mismatch' (host+pid compare, bare pid meaningless cross-host) that doctor consumes. Pinned by test/supervisor.test.ts (16 cases), test/supervisor-tini.test.ts, test/supervisor-wedge.test.ts, test/supervisor-build-worker-args.test.ts, and test/supervisor-db-lock.test.ts.
  • src/core/minions/child-worker-supervisor.ts — shared spawn-and-respawn core reused by both MinionSupervisor (standalone gbrain jobs supervisor daemon) and src/commands/autopilot.ts (autopilot daemon), so the two consumers can't drift into parallel-loop bugs. Pure class: NO PID file, NO signal handlers, NO process.exit, NO health check. Lifecycle events fire via injected onEvent: (ChildSupervisorEvent) => void. Exit classifier: code === 0 leaves crashCount UNCHANGED (preserves flap detection across mixed exit sequences); code != 0 follows runDuration > stableRunResetMs ? 1 : ++crashCount. Clean-restart budget: sliding window of code=0 exits; when count exceeds cleanRestartBudget (default 10) inside cleanRestartWindowMs (default 60s), emits health_warn { reason: 'clean_restart_budget_exceeded' } and applies cleanRestartBudgetBackoffMs (default 1s). The exit classifier special-cases WORKER_EXIT_RSS_WATCHDOGlikely_cause='rss_watchdog', and that exit does NOT bump crashCount (routes to its own breaker); a dedicated _watchdogExitTimestamps sliding window trips a loud rss_watchdog_loop health_warn naming the cap when N watchdog exits land inside the window, INDEPENDENT of the stable-run reset (which would otherwise hide a >5-min-run watchdog drain loop). New opts watchdogLoopBudget (3), watchdogLoopWindowMs (600000), watchdogBackoffMs (30000); ChildSupervisorEvent extended. Public read-only accessors childAlive, inBackoff, crashCount; killChild(signal) gates on liveness (exitCode === null && signalCode === null), NOT .killed.killed flips true once a signal is sent, so the old !this._child.killed guard made a follow-up SIGKILL after an ignored SIGTERM a silent no-op (#1801; the bug also lived in the shutdown() drain). restartCurrentChild(graceMs) (#1801 wedge self-heal) captures the CURRENT child ref, SIGTERM→grace→SIGKILLs THAT ref (never the respawn — closes the timer-kills-fresh-worker race), and flags _intentionalRestart so the exit is likelyCause='wedge_restart', leaves crashCount UNTOUCHED (never trips max_crashes; like rss_watchdog), and respawns immediately (backoff ms:0 reason='wedge_restart'). awaitChildExit(timeoutMs) short-circuits when child.exitCode !== null || child.signalCode !== null so fast-SIGTERM responders don't cause a 35s shutdown hang. Test hooks _backoffFloorMs, _now. supervisor-audit.ts adds rss_watchdog as a non-clean cause + its own CrashSummary.by_cause bucket, and wedge_restart to CLEAN_EXIT_CAUSES (a self-heal, not a crash; denylist preserved so future causes route to legacy). Pinned by test/child-worker-supervisor.test.ts (12 cases).
  • src/core/minions/spawn-helpers.ts — pure detectTini() + buildSpawnInvocation() consumed by both supervisor.ts and autopilot.ts (resolves the DRY violation between the two spawn sites and makes tini wrapping testable without mock.module(), rule R2 of scripts/check-test-isolation.sh). detectTini() calls execFileSync('which', ['tini']) with explicit env: process.env so Bun sees runtime PATH mutations. buildSpawnInvocation(tiniPath, cmd, args) returns {cmd, args} with tini prepended when present, or the bare invocation otherwise. Pinned by test/spawn-helpers.test.ts (5) and test/supervisor-tini.test.ts (4).
  • src/core/minions/niceness.ts — OS scheduling-priority (niceness) primitives for the --nice flag. parseNiceValue(raw) whole-string parses + range-validates to POSIX [-20, 19] (rejects "3.5"/"10abc" that parseInt would silently truncate). applyNiceness(nice, setPriority?, getPriority?) calls os.setPriority(0, n) and ALWAYS re-reads os.getPriority(0) afterwards — in both the success and the catch paths — so a denied renice (EPERM) or an RLIMIT_NICE clamp records the real effective value (e.g. 0), not null; returns {applied, requested, effective, error?}. getEffectiveNiceness(pid, getPriority?) reads an arbitrary pid's niceness (null on dead/unreadable). formatNice(n)+10/0/-5. Applied only at the CLI layer (jobs.ts) so worker.ts/supervisor.ts stay embeddable. Pinned by test/niceness.test.ts.
  • src/core/minions/worker-registry.ts — live worker registry backing niceness observability. Each running gbrain jobs work self-registers worker-<pid>.json under gbrainPath('workers') (brain-isolated via GBRAIN_HOME; entries tagged with currentBrainId() so multiple DBs under one home don't cross-report). registerWorker(info) is best-effort (never blocks the worker) and returns a cleanup fn the caller wires to BOTH the shutdown finally AND process.on('exit') (the unhealthy process.exit(1) bypasses the awaited finally). readWorkers(getNice?) enumerates the dir, drops confirmed-dead pids (classifyLiveness: ESRCH = dead/prune, EPERM = alive/keep), applies a pid-reuse start-time guard (ps -o lstart, rejects a pid whose process started >5s after the entry was written), and re-measures each live worker's niceness now. Reports the worker's REAL pid, sidestepping the tini-wrapper-pid problem. Pinned by test/worker-registry.test.ts.
  • src/core/minions/supervisor-pid.tsreadSupervisorPid(pidFile) → {pid, running}: the shared existsSync → readFileSync → parseInt → process.kill(pid,0) PID-file + liveness reader extracted from the three copies in jobs.ts (supervisor status), jobs.ts (stats), and doctor.ts. EPERM from the liveness probe counts as running. Pinned by test/supervisor-pid.test.ts.
  • src/core/minions/handler-timeouts.ts (#1737) — per-handler default wall-clock budgets. HANDLER_DEFAULT_TIMEOUT_MS maps the long handlers (subagent, subagent_aggregator, embed-backfill, autopilot-cycle) to a 30-min default (the value cycle/patterns.ts already passed for subagents, generalized). defaultTimeoutMsFor(jobName) returns that default or null for short handlers (keep the tight null-default wall-clock). MinionQueue.add() stamps this onto minion_jobs.timeout_ms at submit time when the caller passed no timeout_ms, so a long job submitted without one isn't wall-clock-killed mid-progress; an explicit value always wins and already-queued jobs are NOT backfilled. Pinned by test/minions.test.ts.
  • src/core/minions/types.tsMinionJobInput + MinionJobStatus + handler context types. MinionJobInput.max_stalled is optional; omitted values let the schema DEFAULT (5) kick in, provided values are clamped to [1, 100].
  • src/core/minions/protected-names.ts — side-effect-free constant module exporting PROTECTED_JOB_NAMES + isProtectedJobName(). Kept pure so queue core can import without loading handler modules.
  • src/core/minions/handlers/shell.tsshell job handler. Spawns /bin/sh -c cmd (absolute path, PATH-override-safe) or argv[0] argv[1..] (no shell). Env allowlist PATH, HOME, USER, LANG, TZ, NODE_ENV + caller env: overrides + inherit:-resolved keys. UTF-8-safe stdout/stderr tail via string_decoder.StringDecoder. Abort (either ctx.signal or ctx.shutdownSignal) fires SIGTERM → 5s grace → SIGKILL on child. Requires GBRAIN_ALLOW_SHELL_JOBS=1 on worker (gated by registerBuiltinHandlers). ShellJobParams.inherit?: string[] is a free-form list of snake_case config-key names; the worker resolves each via loadConfig() and injects the value under the derived env key (database_urlGBRAIN_DATABASE_URL; else uppercased). Names persist in minion_jobs.data (and the shell-audit JSONL); values never do. The canonical validator validateShellJobParams (sibling shell-validate.ts) runs PRE-ENQUEUE in both submit surfaces — gbrain jobs submit shell (jobs.ts:271) AND the submit_job op for name='shell' (operations.ts:2085); the handler-entry re-validation here is defense-in-depth (closes the bug class where validation ran AFTER queue.add() persisted the row). The validator does NOT police which config keys the agent inherits — same-uid trust model treats the agent as a peer of the worker.
  • src/core/minions/handlers/shell-inherit.ts — three helpers. INHERIT_NAME_RE (/^[a-z][a-z0-9_]*$/) is the snake_case shape guard used by the validator; rejects __proto__, leading-underscore, uppercase, and path-traversal shapes so audit logs stay readable and prototype-pollution lookups can't smuggle through. deriveEnvKey(name) maps config-key → child-env-key (name.toUpperCase() with one override: database_urlGBRAIN_DATABASE_URL because plain DATABASE_URL is ambiguous). resolveInheritValue(cfg, name) is the value lookup; uses Object.hasOwn to defeat prototype-pollution lookups, returns undefined for missing / non-string / empty-string values. No closed enum — agent and worker share a uid, so refusing arbitrary config keys defends nothing in that trust model.
  • src/core/minions/handlers/shell-validate.tsvalidateShellJobParams(data, opts?) shared pre-enqueue validator. Throws UnrecoverableError with paste-ready operator hints on every failure. Three rules: (1) cmd/argv/cwd/env shape, (2) inherit array shape + snake_case regex per element (prototype-pollution defense), (3) fail-fast on missing config value with gbrain config set <key> hint. Optional redact_secrets?: boolean for output-side scrubbing. Deliberately does NOT police WHICH secrets the agent passes — single-uid trust model. Test seam: opts.config drives the validator hermetically without mocking. Re-called at shell.ts handler entry for defense-in-depth (catches rows submitted before the pre-enqueue validator existed).
  • src/core/minions/handlers/shell-redact.ts — opt-in output-side scrubbing for shell-job stdout/stderr. Pure redactSecretsInText(text, secrets): string-mode replaceAll so regex metacharacters in values stay literal. When the caller passes redact_secrets: true (or --redact-secrets), the handler builds a Map of inherit-name → resolved-value and post-processes both tails before throw/return so persisted result.stdout_tail / result.stderr_tail / error_text carry <REDACTED:name>. Only inherit:-resolved values are scrubbed; caller-supplied env: values pass through. Heuristic — defeats echo "$GBRAIN_DATABASE_URL", not adversarial encode-then-print. Default false.
  • src/core/config.ts:ensureGitignore — idempotent retroactive writer of ~/.gbrain/.gitignore (single line *). Called from saveConfig() so every config-writing path lays it down, AND from runPostUpgrade() so existing users pick it up on gbrain upgrade. Never clobbers a user-customized .gitignore (checks file exists + content non-empty before writing). Scope: blocks casual git add ~/.gbrain from inside an enclosing worktree, but does NOT cover already-tracked files, screenshots, backups (Time Machine / iCloud / Dropbox), or git add -f. The doctor check home_dir_in_worktree surfaces what .gitignore can't.
  • src/commands/doctor.ts:home_dir_in_worktree — filesystem check walking up from gbrainPath() toward $HOME looking for a .git directory (main repo) or .git file (linked worktree pointer; Conductor + git-worktrees topology). Walk terminates at $HOME so a .git above the user's home doesn't false-positive. Honors GBRAIN_HOME (appends .gbrain to the override). Warn (not fail) with worktree-root path + paste-ready fix pointing at GBRAIN_HOME override or moving the brain.
  • src/core/minions/handlers/shell-audit.ts — per-submission JSONL audit trail at ~/.gbrain/audit/shell-jobs-YYYY-Www.jsonl (ISO-week rotation; override via GBRAIN_AUDIT_DIR). Best-effort: mkdirSync(recursive) + appendFileSync; failures logged to stderr, submission not blocked. Logs cmd (first 80 chars) or argv (JSON array). Never logs env values.
  • src/core/minions/handlers/supervisor-audit.ts — supervisor lifecycle JSONL audit at ~/.gbrain/audit/supervisor-YYYY-Www.jsonl (ISO-week rotation; shares computeIsoWeekName() with shell-audit.ts). writeSupervisorEvent(emission, supervisorPid) appends one line per event (started, worker_spawned, worker_exited, backoff, health_warn, health_error, max_crashes_exceeded, shutting_down, stopped, worker_spawn_failed). readSupervisorEvents({sinceMs}) is the readback for gbrain doctor. Exports isCrashExit(event), summarizeCrashes(events), CrashSummary type, and CLEAN_EXIT_CAUSES denylist ('clean_exit' | 'graceful_shutdown'). Single regression point — both gbrain doctor (supervisor check at doctor.ts:1011-1043) and gbrain jobs supervisor status (jobs.ts:803-826) import from here so the two surfaces can't drift. isCrashExit classifies a single worker_exited against the denylist: clean/graceful are NON-crashes; everything else (incl. any future likely_cause from child-worker-supervisor.ts) is a crash; audit lines lacking likely_cause fall back to code !== 0. summarizeCrashes returns {total, by_cause: {runtime_error, oom_or_external_kill, unknown, legacy}, clean_exits} — the legacy bucket catches both old fallback entries AND unrecognized future causes (fail-loud, not silent underreport); denylist-over-allowlist is deliberate. Pinned by test/supervisor-audit.test.ts (14 cases) and 4 source-grep wiring assertions in test/doctor.test.ts.
  • src/core/minions/backpressure-audit.ts — sibling of shell-audit.ts for maxWaiting coalesce events. JSONL at ~/.gbrain/audit/backpressure-YYYY-Www.jsonl. One line per coalesce with (queue, name, waiting_count, max_waiting, returned_job_id, ts). Closes the silent-drop vector the maxWaiting guard introduced.
  • src/core/minions/handlers/subagent.ts — LLM-loop handler. Two-phase tool persistence (pending → complete/failed), replay reconciliation for mid-dispatch crashes, dual-signal abort (ctx.signal + ctx.shutdownSignal), Anthropic prompt caching on system + tool defs. makeSubagentHandler({engine, client?, ...}) factory; MessagesClient is an injectable interface the real SDK implements structurally. Throws RateLeaseUnavailableError (renewable) when rate-lease capacity is full. Anthropic 400 prompt is too long responses (status 400 + body matches /prompt is too long|prompt_too_long|context.*length/i) classify as UnrecoverableError so the job goes straight to dead on first attempt instead of stalling three times. Catches both initial-prompt overflow and turn-N tool-loop accumulation that synthesize.ts's chunker can't bound ahead of time.
  • src/core/minions/handlers/subagent-aggregator.tssubagent_aggregator handler. Claims AFTER all children resolve (queue guarantees every terminal child posts a child_done inbox message with outcome). Reads inbox via ctx.readInbox(), builds a deterministic mixed-outcome markdown summary. No LLM call.
  • src/core/minions/handlers/subagent-audit.ts — JSONL audit + heartbeat writer at ~/.gbrain/audit/subagent-jobs-YYYY-Www.jsonl. Events: submission (one per submit) + heartbeat (per turn boundary: llm_call_started | llm_call_completed | tool_called | tool_result | tool_failed). Never logs prompts or tool inputs. readSubagentAuditForJob(jobId, {sinceIso}) is the readback for gbrain agent logs.
  • src/core/minions/rate-leases.ts — lease-based concurrency cap for outbound providers (default key anthropic:messages, max via GBRAIN_ANTHROPIC_MAX_INFLIGHT). Owner-tagged rows with expires_at auto-prune on acquire; pg_advisory_xact_lock guards check-then-insert; CASCADE on owning job deletion. renewLeaseWithBackoff retries 3x (250/500/1000ms).
  • src/core/minions/wait-for-completion.ts — poll-until-terminal helper for CLI callers. TimeoutError does NOT cancel the job; AbortSignal exits without throwing. Default pollMs: 1000 on Postgres, 250 on PGLite inline.
  • src/core/minions/transcript.ts — renders subagent_messages + subagent_tool_executions to markdown. Tool rows splice under their owning assistant tool_use by tool_use_id. UTF-8-safe truncation; unknown block types fall through to fenced JSON.
  • src/core/minions/plugin-loader.tsGBRAIN_PLUGIN_PATH discovery. Absolute paths only, left-wins collision, gbrain.plugin.json with plugin_version: "gbrain-plugin-v1", plugins ship DEFS only (no new tools), allowed_tools: validated at load time against the derived registry.
  • src/core/minions/tools/brain-allowlist.ts — derives the subagent tool registry from src/core/operations.ts (13-name allow-list). By default put_page schema is namespace-wrapped per subagent (^wiki/agents/<subagentId>/.+). When BuildBrainToolsOpts.allowedSlugPrefixes is set, the put_page schema describes the prefix list to the model and the OperationContext is threaded with allowedSlugPrefixes — trust comes from PROTECTED_JOB_NAMES gating subagent submission (MCP cannot reach this field); only cycle.ts (synthesize/patterns) and direct CLI submitters set it. Allow-list includes get_recent_salience + find_anomalies but deliberately NOT get_recent_transcripts (all subagent calls run ctx.remote === true and the trust gate rejects remote callers, so it would always reject; the cycle synthesize phase calls discoverTranscripts directly instead). paramsToInputSchema() consumes paramDefToSchema from src/mcp/tool-defs.ts; required-aggregation at the tool-def level stays here (the shared helper is per-param).
  • src/mcp/tool-defs.tsbuildToolDefs(ops) helper; MCP server + subagent tool registry both call it, byte-for-byte equivalence pinned by test/mcp-tool-defs.test.ts. Exports the recursive paramDefToSchema(p: ParamDef) — single source of truth for ParamDef→JSON Schema mapping shared by three consumers: buildToolDefs (stdio MCP), src/commands/serve-http.ts:837 (HTTP MCP tools/list), and src/core/minions/tools/brain-allowlist.ts:84 (subagent registry). Recursive on items so nested array-of-arrays preserves inner shape on the wire. Key ordering (type, description, enum, default, items) is intentional so JSON.stringify output stays byte-stable. test/mcp-tool-defs.test.ts has a findArrayWithoutItems walker that fails on any type: 'array' lacking items.type.
  • src/core/minions/attachments.ts — Attachment validation (path traversal, null byte, oversize, base64, duplicate detection).
  • src/commands/agent.tsgbrain agent run <prompt> [flags] CLI. Submits subagent (or N children + 1 aggregator) under {allowProtectedSubmit: true}. Single-entry --fanout-manifest short-circuits. Children get on_child_fail: 'continue' + max_stalled: 3. --follow is the default on TTY; streams logs + polls waitForCompletion in parallel. Ctrl-C detaches, does not cancel.
  • src/commands/agent-logs.tsgbrain agent logs <job> [--follow] [--since]. Merges JSONL heartbeat audit + subagent_messages into a chronological timeline. parseSince accepts ISO-8601 or relative (5m, 1h, 2d). Transcript tail renders only for terminal jobs.
  • src/commands/jobs.tsgbrain jobs CLI subcommands + gbrain jobs work daemon. case 'work' wraps worker.start() in try/finally and owns engine lifecycle — calls engine.disconnect() on shutdown with loud error logging (the worker must not disconnect an engine it doesn't own; pool slots free immediately on shutdown rather than waiting for TCP keepalive). jobs submit surfaces the full MinionJobInput retry/backoff/timeout/idempotency surface as flags: --max-stalled, --backoff-type fixed|exponential, --backoff-delay, --backoff-jitter, --timeout-ms, --idempotency-key. jobs smoke --sigkill-rescue is the SIGKILL-rescue regression guard. registerBuiltinHandlers always registers subagent + subagent_aggregator (no env flag — ANTHROPIC_API_KEY is the cost gate, trust is via PROTECTED_JOB_NAMES) and loads GBRAIN_PLUGIN_PATH plugins at startup with a loud per-plugin line; shell handler still gated by GBRAIN_ALLOW_SHELL_JOBS=1 (RCE surface). The autopilot-cycle handler forwards job.data.phases to runCycle, validated against ALL_PHASES from src/core/cycle.ts (invalid names filtered; empty/missing falls back to the default cycle). The sync handler resolves sourceId at entry from sources.local_path (mirrors cycle.ts:480) so multi-source brains read the per-source last_commit anchor; concurrency routes through autoConcurrency() in src/core/sync-concurrency.ts (PGLite stays serial); noEmbed default is true. gbrain jobs supervisor status at jobs.ts:803-826 consumes summarizeCrashes() from src/core/minions/handlers/supervisor-audit.ts for parity with gbrain doctor: JSON adds crashes_by_cause: {runtime_error, oom_or_external_kill, unknown, legacy} + clean_exits_24h; human output gains per-cause + clean-exits lines. Pinned by 4 source-grep wiring assertions in test/doctor.test.ts requiring crashes_by_cause + clean_exits_24h= in both doctor.ts and jobs.ts. gbrain jobs watch decouples its two output axes: --json picks FORMAT (human default, never gated on isTTY), --follow picks LOOP (default isTTY && !json). Non-TTY with no flags prints ONE human snapshot then exits (clean for subagent/pipe/cron); --follow opts into a continuous stream (human plain per tick, or JSONL with --json); a TTY with no flags keeps the live ANSI dashboard. Resolution is the pure resolveWatchMode(opts, isTTY): {json, follow, useAnsiDashboard} in src/commands/jobs-watch.ts; the dispatch wires --follow. Pinned by test/jobs-watch-mode.test.ts (format×loop matrix incl. the TTY+--json-one-shot case) + test/e2e/non-tty-output.serial.test.ts (the cmd </dev/null non-empty-stdout contract).
  • src/commands/features.tsgbrain features --json --auto-fix: usage scan + feature adoption salesman.
  • src/commands/autopilot.tsgbrain autopilot --install: self-maintaining brain daemon (sync+extract+embed). Consumes detectTini() from src/core/minions/spawn-helpers.ts, resolved once at startup. Composes a ChildWorkerSupervisor instance for spawn-and-respawn (no inline crashCount/startWorker/child.on('exit')); --max-rss 2048 and maxCrashes: 5 preserved. onMaxCrashesExceeded routes through autopilot's own shutdown('max_crashes') so the autopilot lockfile gets cleaned up. shutdown() drains via childSupervisor.killChild('SIGTERM') + awaitChildExit(35_000). Pinned by test/autopilot-supervisor-wiring.test.ts (6 static-shape guards: composes ChildWorkerSupervisor not legacy names, --max-rss 2048 in argv, maxCrashes: 5 literal, shutdown-via-callback, no workerProc reference).
  • src/mcp/server.ts — MCP stdio server (generated from operations). Tool-call handler delegates to dispatchToolCall from src/mcp/dispatch.ts so stdio + HTTP transports share one validation, context-build, and error-format path. Stdin 'end' / 'close' shutdown hooks are skipped when process.env.MCP_STDIO === '1' — gateway-piped stdio MCP wrappers (OpenClaw's bundle-mcp) pipe the handshake then close their stdin half, which would otherwise kill the server before the first tool call; signal handlers (SIGTERM/SIGINT/SIGHUP) + the parent-process watchdog still cover legitimate disconnects. src/commands/serve.ts exposes ServeOptions.mcpStdio?: boolean as a test seam so the guard is exercisable without process.env mutation. Pinned by test/serve-stdio-lifecycle.test.ts.
  • src/mcp/dispatch.ts — shared tool-call dispatch consumed by both stdio (server.ts) and HTTP transports. Exports dispatchToolCall(engine, name, params, opts), buildOperationContext(engine, params, opts), validateParams(op, params). Single source of truth for (ctx, params) handler arg order and the 5-field OperationContext shape (engine + config + logger + dryRun + remote). Defaults remote: true (untrusted); local CLI callers pass remote: false. Also exports summarizeMcpParams(opName, params) — privacy-preserving redactor for mcp_request_log and the admin SSE feed, returns {redacted, kind, declared_keys, unknown_key_count, approx_bytes}. Intersects submitted top-level keys against the operation's declared params allow-list (declared keys preserved sorted; unknown keys counted but never named, closing the attacker-controlled-key-name leak). Byte counts bucketed up to nearest 1KB so an attacker can't binary-search secret-content sizes by probing. Raw payload visibility is opt-in via gbrain serve --http --log-full-params (loud stderr warning). New logging paths route through this helper, not JSON.stringify(params).
  • src/mcp/rate-limit.ts — Bounded-LRU token-bucket limiter. buildDefaultLimiters() returns the two-bucket pipeline: pre-auth IP (30/60s, fires BEFORE the DB lookup so brute-force load against access_tokens is capped) + post-auth token-id (60/60s). Tracks lastTouchedMs separately from lastRefillMs so an exhausted key can't be reset by hammering past the TTL. LRU cap bounds memory under attacker-controlled key growth.
  • src/commands/serve-http.ts — Express 5 HTTP MCP server with OAuth 2.1, admin dashboard, and SSE live activity feed. Started via gbrain serve --http [--port N] [--token-ttl N] [--enable-dcr] [--public-url URL] [--bind HOST] [--log-full-params]. Combines MCP SDK's mcpAuthRouter (authorize/token/register/revoke), a custom client_credentials handler running BEFORE the router (SDK's token endpoint throws UnsupportedGrantTypeError for CC; custom handler falls through for auth_code / refresh_token), requireBearerAuth middleware for /mcp with scope enforcement + localOnly rejection before op dispatch, and express-rate-limit at 50 req / 15 min on /token. Serves the built admin SPA from admin/dist/ with SPA fallback. /admin/events SSE broadcasts every MCP request. cookie-parser wired (Express 5 has no built-in). Startup logging prints port, engine, issuer URL (honors --public-url), client count, DCR status, admin bootstrap token. The /mcp request handler's OperationContext literal sets remote: true explicitly (without it submit_job's protected-name guard at operations.ts:1391 saw a falsy undefined and a read+write-scoped OAuth token could submit shell jobs — RCE). summarizeMcpParams from src/mcp/dispatch.ts feeds both mcp_request_log writes and the SSE feed by default (raw via --log-full-params). Cookie Secure flag set behind HTTPS or a public-URL proxy; magic-link nonce store LRU-bounded; DCR disable routes through the GBrainOAuthProvider dcrDisabled constructor option (not a router monkey-patch); transport.handleRequest wrapped in try/catch to return a JSON-RPC 500 envelope; OperationError + unexpected exceptions unified through buildError / serializeError so /mcp always returns the same envelope. /health is liveness-only via probeLiveness(sql, engineName, version, timeoutMs) racing sql\SELECT 1`against the exportedHEALTH_TIMEOUT_MS = 3000(returns the sameProbeHealthResulttagged-union asprobeHealth, single timer-cleanup site, single 503 envelope); body shape {status, version, engine}only. Full stats moved to admin-only/admin/api/full-stats(gated byrequireAdmin, calls probeHealth(engine, ...)) — keeps getStats()'s 6× count(*) off the public route so a saturated pool doesn't trigger orchestrator restart cascades. Every OAuth/admin/audit SQL call routes through sqlQueryForEngine(engine)fromsrc/core/sql-query.tsso it works against PGLite; the fourmcp_request_log.paramsINSERT sites (success / auth_failed / scope_denied / server-error) go throughexecuteRawJsonb(engine, ...) so the column stores real objects (params->>'op'returnssearch, not the quoted string). --bind HOSTdefaults127.0.0.1(self-hosters pass--bind 0.0.0.0); a stderr WARN fires when --public-urlis set without--bind; the banner prints a Bind:line.AuthInfo.sourceId+AuthInfo.allowedSourcesare the typed source of truth, populated byoauth-provider.ts:verifyAccessTokenfrom theoauth_clientsrow. The HTTP MCPtools/listhandler at:837-849usesparamDefToSchema(v)fromsrc/mcp/tool-defs.tsso array params keepitems` (strict-mode OAuth clients otherwise reject the whole tool list).
  • src/core/sql-query.ts — engine-aware tagged-template SQL adapter for OAuth/admin/auth infrastructure. sqlQueryForEngine(engine) returns a SqlQuery ((strings, ...values) => Promise<rows[]>) that walks the template, builds $N positional SQL, asserts every value is a SqlValue (string | number | bigint | boolean | Date | null), and routes through engine.executeRaw(sql, params) (Postgres via postgres.js unsafe(sql, params), PGLite via db.query(sql, params)). Deliberately narrower than postgres.js's sql tag: no nested fragments, sql.json(), sql.unsafe(), sql.begin(), or array binding — the narrow scalar-only surface is the feature (keeps it from drifting into a partial postgres.js clone). JSONB writes go through executeRawJsonb(engine, sql, scalarParams, jsonbParams) which composes positional $N::jsonb casts and passes JS objects through; the double-encode bug class doesn't apply because positional binding through unsafe() reaches the wire protocol with the correct type oid (verified by test/sql-query.test.ts on PGLite, test/e2e/auth-permissions.test.ts:67 on Postgres). scripts/check-jsonb-pattern.sh doesn't fire because executeRawJsonb(...) is a method call, not the banned literal-template-tag interpolation. Consumed by src/commands/auth.ts, src/commands/serve-http.ts, src/core/oauth-provider.ts, src/commands/files.ts, src/mcp/http-transport.ts so all five work uniformly against PGLite and Postgres.
  • src/commands/serve.tsgbrain serve stdio MCP entrypoint with idempotent shutdown across every parent-disconnect signal. Stdio EOF, SIGTERM, SIGINT, SIGHUP, and parent-process death (every reparent case — PID 1, launchd subreaper, systemd, tmux, or a parent shell with PR_SET_CHILD_SUBREAPER) all funnel into one cleanup(reason) that releases the engine and the PGLite write-lock dir within 5 seconds (otherwise the lock is held indefinitely after Claude Desktop / Cursor / launchd-managed gateways disconnect, forcing a 5-minute stale-lock wait on next start). Watchdog reparent check is getParentPid() !== initialParentPid (the === 1 check missed the subreaper case under launchd/systemd). Bun's process.ppid cache is stale across reparenting (oven-sh/bun#30305) so getParentPid() runs spawnSync('ps', ['-o', 'ppid=', '-p', PID]) per tick. Startup probe verifies ps is on PATH; if not (stripped containers, busybox), the watchdog skips installing AND emits a loud [gbrain serve] watchdog disabled: ps unavailable ... stderr line so operators see the degraded mode. Pinned by test/serve-stdio-lifecycle.test.ts (22 cases). Credit @Aragorn2046 + @seungsu-kr.
  • src/core/oauth-provider.tsGBrainOAuthProvider implementing the MCP SDK's OAuthServerProvider + OAuthRegisteredClientsStore. Backed by raw SQL (works on both PGLite and Postgres — OAuth is infrastructure, not a BrainEngine concern). Full OAuth 2.1: authorize + exchangeAuthorizationCode with PKCE, client_credentials, refresh_token with rotation, revokeToken, registerClient (DCR validates redirect_uri is https:// or loopback per RFC 6749 §3.1.2.1). All tokens + client secrets SHA-256 hashed before storage. Auth codes single-use with 10-minute TTL via atomic DELETE...RETURNING (closes RFC 6749 §10.5 TOCTOU); refresh rotation also DELETE...RETURNING (§10.4 stolen-token detection). pgArray() escapes commas/quotes/braces so a comma-bearing redirect_uri can't smuggle a second array element. Legacy access_tokens fallback in verifyAccessToken grandfathers pre-v0.26 bearer tokens as read+write+admin. sweepExpiredTokens() runs on startup in try/catch and returns the count via RETURNING 1 + array length. RFC hardening: client_id folded atomically into the DELETE WHERE for both auth-code exchange and refresh rotation (wrong-client paths don't burn the row); refresh-scope-subset enforced against the original grant on the row (RFC 6749 §6, so revoking a scope shrinks existing refresh tokens); client_id bound on revokeToken (RFC 7009 §2.1); /token redirect_uri validated against the /authorize value (RFC 6749 §4.1.3, empty-string treated as missing not wildcard); bare catch {} in verifyAccessToken/getClient replaced by isUndefinedColumnError from src/core/utils.ts (only SQLSTATE 42703 falls through to legacy; lock timeouts/network blips throw); dcrDisabled constructor option lets serve-http.ts disable /register without monkey-patching the router. Module-private coerceTimestamp() normalizes postgres-driver-as-string BIGINT columns to JS numbers at 5 read sites (getClient for RFC 7591 §3.2.1 numeric timestamps, exchangeRefreshToken + verifyAccessToken for the SDK's typeof === 'number' check); throws on NaN/Infinity (fail loud at boundary), returns undefined for SQL NULL (callers treat NULL as expired). Not promoted to utils.ts — generic BIGINT precision-loss risk. registerClient honors token_endpoint_auth_method: "none" (RFC 7591 §3.2.1): public PKCE clients store client_secret_hash = NULL and the response omits client_secret; confidential clients (client_secret_post / client_secret_basic) keep their one-time-reveal shape; getClient normalizes NULL client_secret_hash to JS undefined so the SDK's clientAuth path accepts public clients. verifyAccessToken JOINs oauth_clients.source_id (write scope, scalar) + oauth_clients.federated_read (read scope, TEXT[]) onto the returned AuthInfo; legacy brains degrade via isUndefinedColumnError fallback.
  • admin/ — React 19 + Vite + TypeScript admin SPA embedded in the binary via admin/dist/ served by serve-http.ts. 7 screens: Login (bootstrap token → session cookie), Dashboard (metrics + SSE feed + token health), Agents (sortable table + sparklines + Register), Register (modal with scope checkboxes + grant type selector), Credentials reveal (Copy + Download JSON + one-time-only warning), Request Log (filterable paginated), Agent Detail drawer (Details / Activity / Config Export tabs + Revoke). Design tokens: #0a0a0f bg, Inter for UI, JetBrains Mono for data, 4-32px spacing scale, rounded pill badges. HTTP-only SameSite=Strict cookie auth. 65KB gzip. Build: cd admin && bun install && bun run build; output at admin/dist/ is committed for self-contained binaries.
  • src/commands/auth.ts — token management. gbrain auth create/list/revoke/test for legacy bearer tokens, plus gbrain auth register-client and gbrain auth revoke-client <client_id> for OAuth 2.1 client lifecycle. revoke-client runs an atomic DELETE...RETURNING on oauth_clients; FK ON DELETE CASCADE on oauth_tokens.client_id and oauth_codes.client_id purges every active token + auth code in one transaction; process.exit(1) on no-such-client (idempotent). Legacy tokens stored as SHA-256 hashes in access_tokens; OAuth clients in oauth_clients; legacy tokens grandfather to read+write+admin scopes on the OAuth HTTP server (no migration). Every SQL site routes through sqlQueryForEngine(engine) from src/core/sql-query.ts (and executeRawJsonb for the takes-holders permissions JSONB column) so gbrain auth works against PGLite; the takes-holders write goes through executeRawJsonb(engine, sql, [name, hash], [{takes_holders:[...]}]) which round-trips with jsonb_typeof = 'object'. register-client accepts --source <id> (write authority, scalar) and --federated-read <S1,S2,...> (read scope, array) and prints the resolved Write source + Federated reads; pre-v0.34 clients backfill to source_id='default' via migration v60. The bare gbrain auth create <name> form (no --takes-holders) mints a token via the exported pure parseAuthCreateArgs(rest) (the inline version used rest[takesIdx + 1] resolving to rest[0] when takesIdx === -1, excluding the name from the positional search). Pinned by test/auth-create-args.test.ts.
  • src/commands/connect.ts + src/core/connect-probe.tsgbrain connect <mcp-url> [--token <bearer>] one-command coding-agent onboarding from a bearer token. Turns an MCP URL + token into a paste-ready claude mcp add ... -H "Authorization: Bearer ..." block (default) or, with --install, runs it directly and smoke-tests the token. Direct HTTP MCP — Claude Code talks straight to a remote gbrain serve --http, no local install needed. Token resolution: --token > $GBRAIN_REMOTE_TOKEN > placeholder (print) / error (install). The generated block tells the agent to call get_brain_identity + list_skills (the LEARN_INSTRUCTION export, which names put_page not capture since capture is CLI-only, not an MCP tool) with a core-tools fallback for hosts without skill publishing. URL normalization appends /mcp to a bare host but REJECTS a scheme-less host; pure helpers (isLinkLocalOrMetadata, URL parse, render) are unit-tested. Flags: --token, --name <id> (default gbrain, validated against NAME_RE), --agent claude-code|codex|perplexity|generic, --install, --yes (required for --install in non-TTY), --force, --json (token redacted unless --show-token), --timeout-ms. connect is in CLI_ONLY + CLI_ONLY_SELF_HELP; dispatched in cli.ts:handleCliOnly with no local DB connect. AGENT_SPECS drives per-agent rendering + --install: claude-codebuildClaudeMcpAddArgv (literal -H "Authorization: Bearer <tok>"); codexbuildCodexMcpAddArgv = codex mcp add <name> --url <url> --bearer-token-env-var GBRAIN_REMOTE_TOKEN (Codex reads the token from the env var at runtime, never written to config; --install runs it and prints an export GBRAIN_REMOTE_TOKEN hint when missing); perplexity + generic are installable:false and reject --install. --oauth (supportsOAuth:true = perplexity/generic only) emits an OAuth 2.1 client-credentials connector block (Issuer URL via issuerFromMcpUrl = mcp-url minus /mcp, Client ID, Client Secret) — least-privilege scopes + short-lived rotating tokens vs a long-lived full-access secret. Creds from --client-id/--client-secret (BYO) or --register (deps.registerOAuthClient shells gbrain auth register-client <name> --grant-types client_credentials --scopes <DEFAULT_SCOPES="read write"> --token-endpoint-auth-method client_secret_post and parses Client ID:/Client Secret:); --oauth rejected for claude-code/codex and incompatible with --install. buildJson is a generic shape (agent, command/command_argv null for perplexity/generic, header, env_var, oauth fields with redaction); the codex command carries only the env-var name, never the token. cmdString(binary, argv) POSIX-single-quotes args. ConnectDeps = {isTTY, promptYesNo, hasBinary(bin), runBinary(bin, argv), probe, env(name)} — binary-generic so claude and codex share the path; env injectable for tests. Security: rendered command single-quotes the token so shell metacharacters can't run code when pasted; token validated before it lands in an HTTP header; link-local / cloud-metadata addresses (incl. IPv4-mapped IPv6 ::ffff:169.254.x.x and AWS IMDSv2-over-IPv6 fd00:ec2::254) refused as a token-exfil guard while localhost/RFC1918/LAN stay allowed; token redacted from all error output. src/core/connect-probe.ts is the raw-bearer MCP smoke probe backing --install: connects the official MCP SDK Client over StreamableHTTPClientTransport with a STATIC Authorization header (no OAuth/discovery — distinct from mcp-client.ts:callRemoteTool which is OAuth-only and remote-mcp-probe.ts:smokeTestMcp which only sends initialize), runs the full initialize handshake via client.connect(), then calls get_brain_identity (read-scope, non-localOnly) to prove a tool call round-trips. Never throws — every failure maps to { ok: false, reason: 'auth' | 'unreachable' | 'timeout' | 'tool_error' | 'unknown', message } so a wrong/expired token fails at setup, not on the agent's first request. DEFAULT_PROBE_TIMEOUT_MS = 15_000 shared with connect.ts. serve-http.ts adds exported pure skillPublishStatus(publishSkills) for the startup banner Skills: published / not published line + a one-line gbrain config set mcp.publish_skills true stderr nudge when publishing is OFF. Docs: docs/mcp/CODEX.md, docs/mcp/PERPLEXITY.md, docs/mcp/CLAUDE_CODE.md, docs/tutorials/connect-coding-agent.md. Pinned by test/connect.test.ts (pure-helper + render, all four agents) + test/e2e/connect-bearer.test.ts (raw-bearer probe + full OAuth chain register→connect→discovery→/token mint→get_brain_identity, client registered in beforeAll before serve takes the PGLite single-writer lock; drives real claude + codex binaries through connect --install with sandboxed HOME/CODEX_HOME, asserts registration + token never in Codex config, skips when a binary is absent) + test/e2e/serve-stdio-roundtrip.test.ts (spawns real gbrain serve stdio against a fresh init --pglite brain, drives the SDK client through initializetools/listtools/call, asserts the advertised core-tool set and that capture is NOT advertised) + test/serve-skills-publish-nudge.test.ts (the test/audit/batch-retry-audit.test.ts ENOENT case was made hermetic — it had read the real ~/.gbrain/audit).
  • src/commands/upgrade.ts — self-update CLI. runPostUpgrade() enumerates migrations from the TS registry (src/commands/migrations/index.ts) and tail-calls runApplyMigrations(['--yes', '--non-interactive']) so the mechanical side of every outstanding migration runs unconditionally.
  • src/commands/migrations/ — TS migration registry (compiled into the binary; no runtime walk of skills/migrations/*.md). index.ts lists migrations in semver order. v0_11_0.ts = Minions adoption orchestrator (8 phases). v0_12_0.ts = Knowledge Graph auto-wire orchestrator (5 phases: schema → config check → backfill links → backfill timeline → verify); phaseASchema has a 600s timeout for duplicate-heavy brains. v0_12_2.ts = JSONB double-encode repair orchestrator (4 phases: schema → repair-jsonb → verify → record). v0_14_0.ts = shell-jobs + autopilot cooperative (pending-host-work ping for skills/migrations/v0.14.0.md). All orchestrators are idempotent and resumable from partial status. The RUNNER owns all ledger writes — orchestrators return OrchestratorResult and apply-migrations.ts persists a canonical {version, status, phases} shape (orchestrators no longer call appendCompletedMigration). statusForVersion prefers complete over partial (never regresses); 3 consecutive partials → wedged → --force-retry <version> writes a 'retry' reset marker. Schema-only migrations v14 (pages_updated_at_index) + v15 (minion_jobs_max_stalled_default_5 with UPDATE backfill) live in the MIGRATIONS array in src/core/migrate.ts. in-process.ts exports runMigrateOnlyCore({timeoutMs?}) — single source of truth for "bring schema to head" (configureGatewaycreateEngineconnectinitSchemadisconnect, idempotent, 600s MIGRATE_ONLY_TIMEOUT_MS guard, throws MigrateOnlyError on no-config / timeout); the orchestrators' 9 schema phases AND init.ts:initMigrateOnly both delegate to it so schema bring-up can't drift (running in-process removes the spawn that died with getaddrinfo ENOTFOUND on Windows + bun + Supabase pooler). runGbrainSubprocess is the diagnostic wrapper for the remaining non-schema spawns (extract/repair/stats): captures child stderr (64MB buffer) into the thrown error. v0_13_1.ts:phaseCGrandfather is a CHUNKED bulk SQL pass keyed on pages.id (globally unique PK, NOT slug — slug uniqueness is (source_id, slug)), filters deleted_at IS NULL (no tombstones), chunked in CHUNK_SIZE batches (DELETE_BATCH_SIZE convention) for bounded lock-hold; the rollback log carries {id, slug, source_id, pre_frontmatter} so rollback is unambiguous across sources; idempotent + resumable (each UPDATE flips its rows out of GRANDFATHER_WHERE). Pinned by test/migration-in-process.serial.test.ts and test/migrations-v0_13_1-grandfather.test.ts.
  • src/commands/repair-jsonb.tsgbrain repair-jsonb [--dry-run] [--json]: rewrites jsonb_typeof='string' rows in place across 5 affected columns (pages.frontmatter, raw_data.data, ingest_log.pages_updated, files.metadata, page_versions.frontmatter). Fixes the double-encode bug on Postgres; PGLite no-ops. Idempotent.
  • src/commands/orphans.tsgbrain orphans [--json] [--count] [--include-pseudo] [--source <id>]: surfaces pages with zero inbound wikilinks, grouped by domain (auto-generated/raw/pseudo filtered by default). Also exposed as find_orphans MCP op. findOrphans/getOrphansData (the canonical pure fn shared with doctor's orphan_ratio) takes { sourceId?, sourceIds? }; BrainEngine.findOrphanPages(opts?) (both engines) scopes ONLY the candidate side (p.source_id = $1 scalar, or = ANY($1::text[]) federated) while still counting inbound links from ANY source — a page in source X linked FROM source Y is reachable, so NOT an orphan of X (deliberate definition; the stricter intra-source-only reading is rejected). --source is an explicit raw-flag parse (NOT resolveSourceWithTier, which would scope bare invocations to a default). The total_linkable denominator enumerates ALL live pages (scoped) and subtracts every excluded-by-slug page (templates/, scratch/, etc.) so excluded NON-orphan pages with inbound links don't inflate it and suppress warnings. The find_orphans MCP op threads sourceScopeOpts(ctx) so a source-bound OAuth client doesn't see brain-wide orphans. gbrain doctor --source <id> scopes orphan_ratio and, under explicit --source below 100 entity pages, reports the ratio with a low-scale caveat (thin-client doctor --source orphan_ratio remains brain-wide — TODO). Pinned by test/orphans-source-scope.test.ts (PGLite) + test/e2e/engine-parity.test.ts (Postgres↔PGLite scalar + federated parity). Contributed by @knee5.
  • src/commands/salience.tsgbrain salience [--days N] [--limit N] [--kind PREFIX] [--json]: pages ranked by emotional + activity salience over a recency window. Mirrors orphans.ts shape (pure data fn + JSON formatter + human formatter). Calls engine.getRecentSalience(opts). Score formula: (emotional_weight × 5) + ln(1 + active_take_count) + 1/(1 + days_since_update).
  • src/commands/anomalies.tsgbrain anomalies [--since YYYY-MM-DD] [--lookback-days N] [--sigma N] [--json]: cohort-level activity outliers. Calls engine.findAnomalies(opts). Two cohort kinds: tag, type.
  • src/commands/whoknows.tsgbrain whoknows <topic> [--explain] [--limit N] [--json]: expertise + relationship-proximity routing. Mirrors salience/anomalies shape (pure rankCandidates() + findExperts() orchestrator + runWhoknows() CLI dispatch + thin-client routing). MCP op = find_experts (scope: read, localOnly: false). Ranking formula: score = log(1 + raw_match) × max(0.1, exp(-days/180)) × (0.5 + 0.5 × salience) where raw_match is hybridSearch's RRF+source-boost score. Filters at SQL via SearchOpts.types: ['person', 'company'] (no post-filter waste); hybridSearch's internal salience+recency boosts are intentionally disabled so the locked formula applies on a clean signal. Floors prevent multiplicative-zero edge cases (cold-start people stay visible); ties break alphabetically by slug for determinism. 16 unit tests in test/whoknows.test.ts pin the math.
  • src/commands/eval-whoknows.tsgbrain eval whoknows <fixture.jsonl> [--json] [--skip-replay]: two-layer eval gate. Layer 1 quality (hand-labeled fixture, top-3 hit rate ≥ 0.8). Layer 2 regression (eval_candidates replay set-Jaccard@3 ≥ 0.4). Sparseness fallback: < 20 replay-eligible rows → Layer 2 auto-skips with stderr warning. Stable JSON envelope with schema_version: 1; exit 0/1/2 for pass/fail/usage. WhoknowsFn callable abstraction makes the gates impl-agnostic; runEvalWhoknows(engine: BrainEngine | null, args) picks the impl at entry — thin-client mode (isThinClient(cfg)) routes per-query through callRemoteTool(cfg, 'find_experts', {topic, limit}), local mode calls findExperts(engine, ...) directly. cli.ts adds a thin-client bypass before connectEngine (dispatch shape under src/commands/eval.ts); the regression gate auto-skips in thin-client mode (no DB access to eval_candidates). Public exports jaccardAtK, topKHit, readFixture, WhoknowsFn, threshold constants pinned by test/eval-whoknows.test.ts (25 cases incl. null-engine signature contract).
  • test/fixtures/whoknows-eval.jsonl — 10-row synthetic placeholder demonstrating the eval-fixture schema ({query, expected_top_3_slugs, notes?} JSONL). End users replace with their own real queries; placeholder uses obviously-example slugs (wiki/people/example-alice). Drives test/e2e/whoknows.test.ts (seeds a matching synthetic brain, asserts the >=80% gate) and the whoknows_health doctor check.
  • src/core/skillopt/ + src/commands/skillopt.ts + skills/skill-optimizer/ — self-evolving skill optimization grounded in the SkillOpt paper (arXiv 2605.23904). gbrain skillopt <skill> treats SKILL.md as trainable parameters of a frozen agent: validation-gated (median-of-3 + epsilon=0.05), budget-capped (preflight estimator), per-skill DB-locked (tryAcquireDbLock('skillopt:<name>', 60min)), atomic-versioned (history-intent-first 5-step commit), body-only mutations (frontmatter forbidden). Rollouts use gateway.toolLoop directly with no-op persistence callbacks (zero subagent_messages pollution) + a read-only tool allowlist derived from BRAIN_TOOL_ALLOWLIST minus put_page/submit_job/file_upload. Two reflect calls per step; rejected-edit buffer LRU-bounded to 100; bundled-skill gate; bootstrap workflow (sentinel + --bootstrap-reviewed); D_sel floor (>=5 with --split override); audit JSONL via audit-writer.ts. Added to ALL_PHASES after patterns (default OFF; opt-in via gbrain config set cycle.skillopt.enabled true); cycle phase wrapper at src/core/skillopt/cycle-phase.ts walks stale skills with per-skill ($0.50) + brain-wide ($2.00) caps. Added to PROTECTED_JOB_NAMES. Surface: dream-cycle phase wrapper; --all batch mode (src/core/skillopt/batch.ts:runBatchAll); --target-models fleet (runFleet parallel per-model receipts under skillopt/fleet/<slug>/); MCP op run_skillopt (admin scope + per-skill skillopt.allowed_skills allowlist, NOT localOnly, validates skill_name kebab-only + confines caller-supplied benchmark/held-out paths to skillsDir for remote callers); Minion skillopt handler + --background with allowProtectedSubmit: true; write-flavored optimization via src/core/skillopt/write-capture.ts:buildWriteCaptureRegistry (virtual put_page/submit_job/file_upload captured in-memory; --write-capture flag); held-out real-user test set via src/core/skillopt/held-out.ts (capture infra at ~/.gbrain/skillopt-captures/<skill>/<run>.jsonl, --held-out <path> flag, runHeldOutGate candidate >= baseline). Hermetic via DI seams (opts.chatFn for optimizer + judge; opts.toolLoopFn for rollouts; no mock.module). --bootstrap-from-skillrunBootstrapFromSkill in src/core/skillopt/bootstrap-benchmark.ts: reads SKILL.md directly (no routing-eval.jsonl), makes ONE LLM call emitting a full starter benchmark (tasks + rule judges) as JSONL, parsed line-by-line with skip-bad-line salvage and a min-2-valid-checks-per-task drop; provider/transport errors PROPAGATE (not collapsed to bootstrap_empty). --bootstrap-tasks N (default 15, capped 50); maxTokens scales min(8000, max(4000, N*220)). The stderr REVIEW line prints the literal gbrain skillopt <name> --bootstrap-reviewed --split 1:1:1 — load-bearing because the default 4:1:5 split makes a 15-task starter's D_sel = floor(15/10) = 1, below the >=5 floor, so a 15-task benchmark needs --split 1:1:1. Both bootstrap generators share assertBenchmarkAbsent + readSkillBodyOrThrow; --bootstrap-from-skill is mutually exclusive with --bootstrap-from-routing/--benchmark/--all/--target-models/--resume. Generated rule judges are explicitly WEAK DRAFTS to be strengthened during the review gate. The F11 held-out gate is wired: --held-out <path> is parsed and threaded through every caller (CLI main + --background held_out_path + batch/fleet heldOutPath + the run_skillopt held_out_path param), running at CHECKPOINT ACCEPTANCE so no-mutate/fleet paths can't promote a held-out-failing candidate. assertBundledMutationHeldOut in bundled-skill-gate.ts: bundled + --allow-mutate-bundled requires a NON-EMPTY held-out (MIN_HELD_OUT_SIZE = D_SEL_MIN_SIZE = 5, derived so they can't desync) or hard-refuses (exit 2), for ALL callers (they funnel through runSkillOpt); held-out must be task_id-DISJOINT from the benchmark (overlap rejected — can't catch overfitting). receipt.baseline_sel_score populated + a real final-test eval (test_score + baseline_test_score) scoring best + baseline on split.test; shared scoreSkillOnTasks primitive (validate-gate.ts) backs baseline/final-test/held-out scoring. --no-mutate writes proposed.md via writeProposed in version-store.ts. maxRuntimeMin ENFORCED (wall-clock deadline between steps → skillopt_runtime_exceeded → outcome aborted). Three eval-internal ablation opts on SkillOptOpts (NOT on CLI): reflectMode ('both'/'failure-only'), disableValidationGate (greedy-accept), optimizerMode ('reflect'/'one-shot-rewrite'), recorded in RunReceipt + audit run_start for replayability; ROLLOUT_SUCCESS_THRESHOLD = 0.5 named constant for the partition; one-shot fence-strip is anchored (^```...```$) so an embedded code sample isn't truncated. Budget no-pricing fix: Claude Haiku 4.5's dateless canonical id claude-haiku-4-5 is in src/core/anthropic-pricing.ts (a BudgetTracker-capped run on Haiku otherwise threw no_pricing on the FIRST chat() of every rollout); runValidationGate (validate-gate.ts) scans settled results for isMustAbortError(error) (from worker-pool.ts; BUDGET_EXHAUSTED is in MUST_ABORT_ERROR_TAGS) and re-throws so the caller aborts loudly instead of recording a hollow selScore:0 — ordinary non-abort rollout errors still fail-open to score:0 (judge-hiccup posture preserved). Pinned by 152 tests across 18 files (foundation + adversarial + v2 surface + E2E PGLite serial), test/skillopt/bootstrap-from-skill.test.ts (20 cases), test/skillopt/rollout.test.ts, test/skillopt/validate-gate-abort.test.ts (3 cases), held-out ENFORCE + one-shot-rewrite unit cases, and e2e (F11 block/allow, bundled no-mutate, runtime deadline, receipt honesty, held-out disjointness, no-DB-pollution). Drives the Track B SkillOpt benchmark suite in the sibling gbrain-evals repo.
  • src/core/brainstorm/{domain-bank,orchestrator,judges}.ts + src/commands/{brainstorm,lsd,eval-brainstorm}.ts + src/core/last-retrieved.ts — bisociation-grounded idea generation pair: gbrain brainstorm <question> (defensible, cite-heavy, 4 close × 6 far, judge threshold 4.0/5, save by default) and gbrain lsd <question> (Lateral Synaptic Drift — inverted judge rejecting ideas with resistance >4.5 "too obvious", stale-page bias via pages.last_retrieved_at, 2 close × 12 far, axiomatic inversions required, ephemeral by default). The "domain bank" is prefix-stratified sampling from the user's own brain (SELECT DISTINCT substring(slug from '^[^/]+/[^/]+') cached 1h-TTL in config per source) tiebroken by JOIN page_links connection_count, with corpus-sampling fallback when fewer prefixes than M exist. Distance normalized to [0,1] via 1 - clamp(cosine_distance, 0, 2) / 2. judges.ts exports runJudge(config, ideas) + two configs (BRAINSTORM_JUDGE_CONFIG weighted originality/resistance/thesis_density/concrete_grounding/cognitive_load 0.25/0.20/0.20/0.20/0.15 vs LSD_JUDGE_CONFIG cognitive_load 0.50 + inversion rule). Calibration cold-start fallback: when calibration_profiles.active_bias_tags is empty, judge runs without anti-bias context AND stderr-warns. Op-layer write-back in src/core/operations.ts search/query/get_page handlers fires bumpLastRetrievedAt(engine, pageIds) (fire-and-forget, 5-min throttled via SQL clause, default-on with search.track_retrieval config escape hatch); internal callers (sync, migrations, dream cycle) bypass the op layer so the LSD stale signal stays clean. The fire-and-forget IIFE is tracked in a module-scoped Set<Promise<unknown>>; awaitPendingLastRetrievedWrites(timeoutMs?: number): Promise<{outcome, pending}> resolves once all tracked promises settle, bounded by a 5s Promise.race timeout that stderr-warns the pending count. src/cli.ts awaits the drain unconditionally for every op in the op-dispatch finally block BEFORE engine.disconnect(), then a fallback process.exit(0) fires ONLY when outcome === 'timeout' AND shouldForceExitAfterMain(argv) (excludes serve so daemons stay alive) — closes the PGLite CLI search/query/get-hang class where the IIFE raced disconnect and PGLite's WASM kept Bun's event loop alive. pages.last_retrieved_at TIMESTAMPTZ NULL has a full (NOT partial) B-tree index covering both NULL and range branches; full forward-reference bootstrap probe on both engines. Frontmatter mode: lsd makes the dream-cycle synthesize phase skip LSD output via isLsdOutput() in src/core/cycle/transcript-discovery.ts short-circuiting isDreamOutput(). gbrain eval brainstorm <fixture.jsonl> is a three-axis conjunctive gate (distance + usefulness + grounding — distance alone is gameable). gbrain doctor has a brainstorm_health check (migration applied, search.track_retrieval setting, calibration cold-start status). judges.ts computes the judge token budget via computeJudgeMaxTokens(ideaCount, modelId) (named constants TOKEN_BUDGET_PER_IDEA, TOKEN_BUDGET_ENVELOPE, LEGACY_MIN_MAX_TOKENS, MAX_OUTPUT_TOKENS_CEIL; ANTHROPIC_OUTPUT_CAPS map: Opus 4.7 32K, Sonnet 4.6 / Haiku 4.5 64K, legacy Claude 3.5 8K) so a large multi-call judge doesn't truncate mid-JSON; with no modelOverride the cap routes through the gateway's actual configured chat model via getChatModel(). --save for both commands persists through the canonical ingestion path: persistSavedIdea(engine, {slug, content, provenanceVia}) calls importFromContent({noEmbed:true, sourcePath}) (chunked + tagged + content_hash so search finds it, no embedding cost at save) THEN renders the saved row to disk via the shared writePageThrough helper (file rendered FROM the row so the two sinks can't diverge and gbrain sync doesn't churn it). formatSaveOutcome(outcome, ctx) returns an honest per-branch message (both-sinks, DB-only when no sync.repo_path/repo-not-a-dir, DB-saved-but-file-errored, total-failure → loud save FAILED … NOT persisted on stderr + nonzero exit) — closes the silent-false-success class where --save printed "Saved" unconditionally even when the DB write failed. buildIdeaSlug(question, label, nonce?) adds a random nonce suffix (injectable for tests) so two same-day runs sharing the first 60 slug chars don't clobber. --json callers stay DB-only. buildBrainstormFrontmatterObject(result) in orchestrator.ts returns the object form for serializeMarkdown (string buildBrainstormFrontmatter untouched). Pinned by test/last-retrieved.test.ts, test/e2e/pglite-cli-exit.serial.test.ts (IRON-RULE: real bun src/cli.ts subprocess against a hermetic PGLite tempdir asserts search/get/query exit 0 in <15s + daemon-survival), test/fix-wave-structural.test.ts (asserts the drain await is textually BEFORE engine.disconnect), test/brainstorm/{distance,lsd-mode-skip,eval-brainstorm,judges-maxtokens,save}.test.ts. Open Collider source: github.com/CL-ML/open-collider.
  • src/core/write-through.ts — shared atomic disk write-through for the canonical ingestion path. writePageThrough(engine, slug, {sourceId?, frontmatterOverrides?, logger?}) reads sync.repo_path, re-reads the just-written DB row (getPage), renders it via serializePageToMarkdown + resolvePageFilePath, and writes the .md under the repo so the brain has a committable artifact that round-trips through gbrain sync. Rendering FROM the row means file and row cannot diverge. ATOMIC: writes to a unique temp sibling (<file>.tmp.<pid>.<rand>) + renameSync, cleaning up temp on any failure, so a crash or concurrent gbrain sync/autopilot walking the live git tree never reads a half-written .md (matches the .tmp + rename convention in import-checkpoint.ts / op-checkpoint.ts). Never throws — returns WriteThroughResult { written, path?, skipped?: 'no_repo_configured' | 'repo_not_found' | 'page_not_found_after_write', error? } so the caller decides messaging + exit codes. Trust gating (subagent sandbox, dry-run) stays at the CALLER. Consumers: put_page op and gbrain brainstorm/lsd --save via persistSavedIdea. Pinned by test/write-through.test.ts.
  • src/core/model-id.tssplitProviderModelId(input: string | null | undefined): {provider: string | null, model: string} shared parser for the pricing side. Splits on : first, then /. Defensive contract: null/undefined/empty/whitespace returns {provider: null, model: ''}. Five sites consume it (src/core/anthropic-pricing.ts:estimateMaxCostUsd, src/core/budget/budget-tracker.ts:lookupPricing, src/core/eval-contradictions/cost-tracker.ts:pricingFor, src/core/minions/batch-projection.ts at two call sites, src/core/model-config.ts:isAnthropicProvider) so the pricing + classification surface has no parallel re-implementations of provider:model splitting — slash-form ids (anthropic/claude-sonnet-4-6) classify correctly instead of falling through to "unknown model". Distinct from the gateway-side parseModelId in src/core/ai/model-resolver.ts, which throws on bare names because routing needs an explicit provider; this one returns {provider: null, model: 'bare'} because pricing lookups happen against bare model ids. Pinned by test/model-id.test.ts.
  • src/core/ai/model-resolver.ts:parseModelId — gateway-side resolver accepts both colon and slash form (provider:model and provider/model) so a slash-form id resolves to the same recipe at every gateway entry point (chat / embed / rerank) instead of throwing AIConfigError: model id must be in format provider:model. Bare names without ANY separator still throw — gateway routing always needs an explicit provider. Pinned by test/ai/model-resolver-slash.test.ts including a resolveRecipe round-trip asserting slash form resolves to the same recipe object as colon form.
  • src/commands/transcripts.tsgbrain transcripts recent [--days N] [--full] [--json]: recent raw .txt transcripts from the dream-cycle corpus dirs. Imports listRecentTranscripts from src/core/transcripts.ts (the same library the gated get_recent_transcripts MCP op uses). Local-only by construction — the CLI always runs with ctx.remote=false.
  • src/commands/integrity.tsgbrain integrity check|auto|review|extract: bare-tweet detection, dead-link detection, three-bucket repair (auto-repair / review-queue / skip). scanIntegrity() is the shared library function called from gbrain doctor (sampled at limit=500) and cmdCheck (full scan). Batch-load fast path on Postgres uses a single SQL query (fixes the PgBouncer round-trip timeout, ~60s → ~6s), gated by engine.kind === 'postgres' at the call site so PGLite never enters batch; fallback catch logs at GBRAIN_DEBUG=1. Batch projection is SELECT ... ORDER BY source_id, slug (NOT SELECT DISTINCT ON (slug), which collapsed same-slug-different-source pages into one scan) so multi-source brains scan each (source, slug) row independently. Sequential and auto-repair loops use listAllPageRefs() to enumerate (slug, source_id) pairs and thread sourceId to getPage; batch + sequential paths report the same page count on multi-source brains.
  • src/commands/doctor.tsgbrain doctor [--json] [--fast] [--fix] [--dry-run] [--index-audit]: health checks. Checks include jsonb_integrity + markdown_body_completeness (reliability), schema_version (fails loudly when version=0, routes to gbrain apply-migrations --yes), queue_health (Postgres-only: stalled-forever active jobs started_at > 1h, waiting-depth-per-name > threshold default 10 via GBRAIN_QUEUE_WAITING_THRESHOLD, and dead-lettered subagent jobs with last_error matching the prompt_too_long classifier in last 24h), sync_failures ([CODE=N, ...] breakdown for unacked-warn + acked-ok; severity comes from the shared decideSyncFailureSeverity in src/core/sync-failure-ledger.ts so the LOCAL and REMOTE/thin-client doctor surfaces can never drift — a stuck bookmark escalates to FAIL once an OPEN failure has blocked past the staleness window or ≥10 files block, while already auto_skipped rows stay a visible WARN), rls_event_trigger (healthy evtenabled set is ('O','A') only; fix hint gbrain apply-migrations --force-retry 35), graph_coverage (short-circuits to ok when SELECT COUNT(*) FROM pages WHERE type IN ('entity','person','company','organization') returns 0; WARN hint is gbrain extract all), embedding_column_registry (probes each declared column via Postgres format_type(atttypid, atttypmod) to catch dim mismatch with a paste-ready gbrain config set embedding_columns '{...}' hint, probes HNSW index presence via pg_indexes, computes default-column population via COUNT(*) FILTER (WHERE <col> IS NOT NULL) / COUNT(*) warning below 90% except empty brains where chunk_count=0 short-circuits to ok; PGLite parity via executeRaw), and skill_brain_first (walks SKILL.md via autoDetectSkillsDirReadOnly, calls analyzeSkillBrainFirst() from src/core/skill-brain-first.ts per file with structured Check.issues[]; warn states missing_brain_first/brain_first_typo, ok states compliant_callout/compliant_phase/compliant_position/exempt_frontmatter/no_external; snapshot+diff audit at ~/.gbrain/audit/skill-brain-first-YYYY-Www.jsonl). --fix delegates inlined cross-cutting rules to > **Convention:** see [path](path). callouts via src/core/dry-fix.ts (and MISSING_RULE_PATTERNS for the brain-first callout); --fix --dry-run previews. --index-audit (Postgres-only, informational, no auto-drop) reports zero-scan indexes from pg_stat_user_indexes. Every DB check runs under a progress phase; markdown_body_completeness runs under a 1s heartbeat. runDoctor uses autoDetectSkillsDirReadOnly (from src/core/repo-root.ts; install-path fallback so cd ~ && gbrain doctor finds bundled skills); --fix carries a D6 install-path safety gate that refuses auto-repair when detected.source === 'install_path' (would rewrite the bundled tree). The Lane D supervisor check at doctor.ts:1011-1043 consumes summarizeCrashes(events) from src/core/minions/handlers/supervisor-audit.ts (warn at >=1 real crash; ok message has clean_exits_24h=N; warn message has runtime=A oom=B unknown=C legacy=D per-cause breakdown) so OOM/runtime/unknown crashes are distinguishable from clean code=0 worker drains; cross-surface parity with gbrain jobs supervisor status is pinned by source-grep wiring assertions requiring the breakdown substrings in BOTH doctor.ts and jobs.ts. checkSyncFreshness (exported, in runDoctor local + doctorReportRemote thin-client) is a staleness probe: warns at 24h, fails at 72h or never-synced; future-last_sync_at warns ("clock skew") instead of falling through ok; env overrides GBRAIN_SYNC_FRESHNESS_WARN_HOURS/GBRAIN_SYNC_FRESHNESS_FAIL_HOURS (invalid fall back with once-per-process stderr warn via _resolveSyncFreshnessHours); failure messages embed source.id so the printed gbrain sync --source <id> matches. It has a localOnly-gated git short-circuit (runDoctor passes localOnly: true; doctorReportRemote runs in the HTTP MCP server src/commands/serve-http.ts and keeps default false so that path never walks DB-supplied local_path via subprocess — trust boundary). The local predicate mirrors sync's "do work?" gate (HEAD == last_commit AND working tree clean via requireCleanWorkingTree: 'ignore-untracked' so a quiet repo with only untracked dirs is unchanged not SEVERE, AND chunker_version === CURRENT); the inline SELECT carries last_commit + chunker_version + newest_content_at. The REMOTE path computes lag via lagFromContentMs(newest_content_at, lastSync, now) from the stored column, NO git subprocess; LOCAL fall-through and the < 0 clock-skew check stay on raw wall-clock. Three-bucket count math populates Check.details = {unchanged_count, synced_recently_count, stale_count} with the invariant sum === sources.length. checkCycleFreshness is DELIBERATELY NOT git-short-circuited or content-relativized (last_commit == HEAD can't answer "did the full cycle complete?"; a sync can succeed while later cycle phases fail; different axis last_full_cycle_at). Pinned by test/doctor.test.ts (incl. IRON-RULE regression banning stale verb names, the sync_freshness boundary matrix, the D4 regression guard verifying git probes are NEVER called when localOnly is unset/false, the three-bucket invariant, and the untracked-folders / remote-never-shells-out trust-boundary cases).
  • src/core/migrate.ts — schema-migration runner. Owns the MIGRATIONS array (source of truth for schema DDL). Migration interface carries sqlFor?: { postgres?, pglite? } (engine-specific SQL overrides sql) and transaction?: boolean (false for CREATE INDEX CONCURRENTLY, which Postgres refuses in a transaction; ignored on PGLite). Key migrations: v14 (handler branches on engine.kind for CONCURRENTLY-on-Postgres with invalid-remnant pre-drop via pg_index.indisvalid, plain CREATE INDEX on PGLite); v15 (minion_jobs.max_stalled default 1→5 + backfill non-terminal rows); v24 rls_backfill_missing_tables (sqlFor: { pglite: '' } no-op — PGLite has no RLS engine, targets subagent tables absent from pglite-schema.ts); v30 dream_verdicts (file_path TEXT, content_hash TEXT, worth_processing BOOL, reasons JSONB, judged_at TIMESTAMPTZ, PK(file_path, content_hash)) (RLS-enabled under BYPASSRLS; synthesize reads/writes to avoid re-judging); v35 auto-RLS event trigger auto_rls_on_create_table fires on ddl_command_end for WHEN TAG IN ('CREATE TABLE','CREATE TABLE AS','SELECT INTO') running ALTER TABLE … ENABLE ROW LEVEL SECURITY on new public.* tables (no FORCE) + one-time backfill on every existing public.* base table whose comment doesn't match ^GBRAIN:RLS_EXEMPT\s+reason=\S.{3,} (per-table failure aborts the offending CREATE TABLE; no EXCEPTION wrap; PGLite no-op via sqlFor.pglite: ''; breaking change: intentionally-RLS-off public tables need the GBRAIN:RLS_EXEMPT comment before upgrade); v40 pages_emotional_weight (pages.emotional_weight REAL NOT NULL DEFAULT 0.0, column-only metadata-only); v46 mcp_request_log_params_jsonb_normalize (UPDATE ... SET params = params::text::jsonb WHERE jsonb_typeof(params) = 'string', idempotent); v60-v65 six-migration chain wiring source-scoping into oauth_clients — v60 (oauth_clients_source_id_fk: source_id TEXT NULL→'default' backfill + FK to sources(id) ON DELETE SET NULL), v61 (federated_read TEXT[] NOT NULL DEFAULT '{}'), v62 (explicit-CASE backfill so source_id IS NULL'{}'), v63 (fail-loud check every row's source_id is in its federated_read array), v64 (FK flipped to ON DELETE RESTRICT), v65 (GIN index for array-containment); v68 eval_candidates_embedding_column (eval_candidates.embedding_column TEXT NULL per-row provenance for gbrain eval replay to reproduce the same retrieval space; NULL-tolerant); v108 pages_embedding_signature (pages.embedding_signature TEXT NULL = <provider:model>:<dims> stamped via setPageEmbeddingSignature; GRANDFATHER — stale predicate is embedding_signature IS NOT NULL AND embedding_signature <> $current so NULL is NEVER stale and upgrade never re-embeds the whole corpus; no index; metadata-only); v109 sources_newest_content_at (sources.newest_content_at TIMESTAMPTZ durable newest-COMMIT HEAD committer time written by writeSyncAnchor, read by the REMOTE staleness path instead of shelling to git; mirror in pglite-schema.ts + schema.sql + bootstrap probe); v110 page_aliases ((id, source_id, alias_norm, slug, ...) with UNIQUE (source_id, alias_norm, slug) + lookup indexes on (source_id, alias_norm) and (source_id, slug); alias_norm is normalizeAlias() output so WRITE/READ key on the same form; also in src/core/pglite-schema.ts); v111 search_telemetry_rank1_columns (ADD COLUMN IF NOT EXISTS on both engines: sum_rank1_score, count_rank1, three buckets rank1_lt_solid/rank1_solid/rank1_high on search_telemetry — aggregate not per-query rows so rank-1 median drift is bounded-growth; ALTERs right after v57 which created the table); v114 links_link_source_check_kebab_regex (#1941, opens link_source from the closed allowlist to a kebab-case format gate ^[a-z][a-z0-9]*(-[a-z0-9]+)*$ + char_length<=64; Postgres branch uses NOT VALID + VALIDATE CONSTRAINT with transaction:false, PGLite plain DROP+ADD; existing built-ins all satisfy the regex so VALIDATE never fails on existing data).
  • src/core/progress.ts — Shared bulk-action progress reporter. Writes to stderr. Modes: auto (TTY \r-rewriting; non-TTY plain lines), human, json (JSONL), quiet. Rate-gated by minIntervalMs and minItems. startHeartbeat(reporter, note) for single long queries. child() composes phase paths. Singleton SIGINT/SIGTERM coordinator emits abort events for every live phase. EPIPE defense on both sync throws and stream 'error' events. Zero dependencies. emitHumanLine is prefix-aware — inside a withSourcePrefix(id, ...) scope from src/core/console-prefix.ts it prepends [id] (and TTY-rewrite mode \r\x1b[2K carries the prefix inside the clear-to-EOL escape); emitJson is intentionally NOT prefixed so NDJSON consumers don't choke on a [id] {...} shape.
  • src/core/console-prefix.tsAsyncLocalStorage<string>-backed per-source line-prefix helper. Exports withSourcePrefix(id, fn) (runs fn with id as active prefix; nested wraps replace then restore), getSourcePrefix() (read-only accessor; test seam), and slog(...) / serr(...) (prefix-aware console.log/console.error). Embedded-newline-safe: a multi-line string under prefix [foo] emits [foo] line1\n[foo] line2. Outside a wrap, slog/serr fall through to bare console.log/console.error so single-source callers see identical output (back-compat invariant). Use src.id (slug-validated by sources add) NOT src.name (free-form) to defeat log-injection through newline/control-character names. Coverage: src/commands/sync.ts performSync + callees, src/commands/embed.ts runEmbedCore + helpers, src/core/progress.ts emitHumanLine.
  • src/core/cli-options.ts — Global CLI flag parser. parseGlobalFlags(argv) returns {cliOpts, rest} with --quiet / --progress-json / --progress-interval=<ms> stripped. getCliOptions() / setCliOptions() expose a module-level singleton so commands reach resolved flags without parameter threading. cliOptsToProgressOptions() maps to reporter options. childGlobalFlags() returns the flag suffix to append to execSync('gbrain ...') calls in migration orchestrators. OperationContext.cliOpts extends shared-op dispatch for MCP callers.
  • src/core/db-lock.ts — generic tryAcquireDbLock(engine, lockId, ttlMinutes) over the gbrain_cycle_locks table. Parameterized lock id so scopes nest cleanly: gbrain-cycle for the broad cycle (held by cycle.ts) and gbrain-sync (SYNC_LOCK_ID) for performSync's narrower writer window. UPSERT-with-TTL semantics survive PgBouncer transaction pooling (unlike session-scoped pg_try_advisory_lock); crashed holders auto-release once their TTL expires. It also does automatic same-host dead-pid takeover: when the upsert finds a held, NOT-TTL-expired lock whose holder is on this host and provably dead, it reclaims via a guarded DELETE WHERE id=$1 AND holder_pid=$2 + one normal-upsert retry returning the standard handle (refresh/release intact). The liveness check is the exported classifyHolderLiveness(pid, host, ageMs, opts?) / isHolderDeadLocally(...) (injectable process.kill seam; HOLDER_TAKEOVER_GRACE_MS = 60_000 PID-reuse guard; EPERM classified as alive so a live process you don't own is never stolen). TTL-expired locks stay the upsert's job; cross-host stays TTL-only. runBreakLock (src/commands/sync.ts) consumes the same predicate. Pinned by test/db-lock-auto-takeover.test.ts.
  • src/core/sync-concurrency.ts — single source of truth for the parallel-sync policy. Exports autoConcurrency(engine, fileCount, override?) (PGLite always serial; explicit override clamped to >=1; auto path returns DEFAULT_PARALLEL_WORKERS=4 when fileCount > AUTO_CONCURRENCY_FILE_THRESHOLD=100), shouldRunParallel(workers, fileCount, explicit) (explicit --workers bypasses the >50-file floor), and parseWorkers(s) (rejects '0', '-3', 'foo', '1.5', trailing chars). Used by performSync, performFullSync, runImport, and the Minion sync handler so the sites can't drift. DEFAULT_PARALLEL_SOURCES = 4 is a SEPARATE constant for the per-source fan-out under gbrain sync --all — kept distinct from DEFAULT_PARALLEL_WORKERS because total live Postgres connections per wave ≈ DEFAULT_PARALLEL_SOURCES × DEFAULT_PARALLEL_WORKERS × 2 (per-file pool) = 32 at both defaults (each per-file worker opens its own PostgresEngine with poolSize = min(2, resolvePoolSize(2))); sync.ts warns when parallel × workers × 2 > 16. resolveWorkersWithClamp(engine, override, commandName, fileCount) wraps autoConcurrency with a per-command stderr clamp warning on PGLite (per-(command, requested) dedup via module-scoped warned-once set with _resetWorkersClampWarningsForTest() seam) and is the canonical surface for every bulk-command --workers N flag (extract-conversation-facts, extract, edges-backfill, reindex-multimodal, reindex, reindex-code); embed.ts deliberately bypasses it and keeps GBRAIN_EMBED_CONCURRENCY || 20. resolveMaxConnections() (reads GBRAIN_MAX_CONNECTIONS, undefined when unset) + clampWorkersForConnectionBudget(workers, perWorkerPool, maxConnections, parentPool) back the opt-in single-sync connection-footprint clamp so a big sync stays under a low pooler cap (parent_pool + workers×perWorkerPool ≤ budget); gbrain doctor's pool_budget check (computePoolBudgetCheck / checkPoolBudget in src/commands/doctor.ts) warns when the budget leaves no room for a worker, pointing at GBRAIN_POOL_SIZE=2. Pinned by test/pglite-workers-clamp.test.ts.
  • src/core/worker-pool.ts — Canonical sliding-pool + bounded-semaphore primitive (extracted from src/commands/embed.ts sliding-pool sites and src/commands/eval-cross-modal.ts runWithLimit semaphore). Two exports: runSlidingPool<T>({items, workers, onItem, signal?, onError?, failureLabel?, onProgress?}) + runWithLimit<TIn, TOut>({items, limit, fn, signal?}). Atomicity invariant: const idx = nextIdx++ is one synchronous JS statement (no await between read and write — guaranteed by the single-threaded event loop), documented in the module header AND enforced by scripts/check-worker-pool-atomicity.sh (wired into bun run verify), which rejects importing worker_threads in any consuming file and inserting await between the nextIdx read and write. MUST_ABORT_ERROR_TAGS set is seeded with BUDGET_EXHAUSTED from src/core/budget/budget-tracker.ts; tagged errors (matched via err.tag === 'BUDGET_EXHAUSTED' to avoid cross-module import) bypass onError and hard-abort the pool via AbortController.abort() to in-flight onItem — the budget cap is a structural ceiling under concurrency. failures[] shape is {idx, label, error} records (NOT full items; callers supply failureLabel(item) => string) for bounded memory under huge brains. Pinned by test/worker-pool.test.ts + test/scripts/check-worker-pool-atomicity.test.ts. Drives every --workers N bulk command.
  • src/commands/embed.ts extension — both inline sliding-pool sites (embedAll simple at :458-467 and embedAllStale paginated + AbortSignal at :586-632) call runSlidingPool from the shared worker-pool helper. Invariant-level contract preserved (counts + cost + AbortSignal propagation + per-batch rate-limit retry via embedBatchWithBackoff); byte-equality on progress-event ORDERING is NOT promised. The GBRAIN_EMBED_CONCURRENCY || 20 default is preserved and embed bypasses resolveWorkersWithClamp because the 20-worker default would otherwise silently change every brain's embed hot path. Pinned by test/embed-helper-migration.test.ts (asserts the helper is wired in AND the pre-migration let nextIdx = 0 + Promise.all(Array.from({length: numWorkers}, ...)) shapes are gone).
  • src/commands/extract-conversation-facts.ts extension — --workers N for LLM-bound fact extraction over conversation pages, with a per-page advisory lock via src/core/db-lock.ts:withRefreshingLock (lock id extract-conversation-facts:<source>:<slug>, TTL PER_PAGE_LOCK_TTL_MINUTES=2 with 20s refresh via Math.max(15s, 120s/6); LockUnavailableError triggers skip-and-continue with rate-limited log per (source, minute) + pages_lock_skipped counter + CLI exits 3 when non-zero AND no hard failures). deleteOrphanFactsForPage(engine, sourceId, slug) provides delete-orphans-first replay safety — wipes facts from a prior crashed run for this (sourceId, slug) before re-extracting, closing the "terminal audit row written after partial insertFacts failure" class. assertFactsEmbeddingDimMatchesConfig(engine) is the startup preflight (throws FactsEmbeddingDimMismatchError with paste-ready ALTER hint BEFORE the first insert; cached per engine via WeakMap). Result type carries pages_lock_skipped + orphan_facts_cleaned. Checkpoint state is a shared cpMap: Map<slug, endIso> (NOT a per-page-mutated cpEntries: string[]) so atomic Map.set survives parallel workers. Minion handler extract-conversation-facts in src/commands/jobs.ts round-trips workers via job.data.workers for --background --workers 20. Cycle config key cycle.conversation_facts_backfill.workers (default 1; opt-in concurrency under brain-wide cost + walltime caps). Pinned by test/extract-conversation-facts-workers.test.ts + the existing extract-conversation-facts behavioral tests.
  • src/core/embedding-dim-check.ts extension — facts.embedding dim drift surface. readFactsEmbeddingDim(engine): Promise<FactsColumnDimResult> covers both vector(N) and halfvec(N) shapes (migration v40 falls back to vector on pgvector < 0.7); regex ordering is halfvec-before-vector (substring "vec" appears in "halfvec"; naive /vector/i would shadow). buildFactsAlterRecipe(dims, configured, type) emits the paste-ready DROP INDEX IF EXISTS idx_facts_embedding_hnsw; ALTER TABLE facts ALTER COLUMN embedding TYPE halfvec(N) USING embedding::halfvec(N); CREATE INDEX idx_facts_embedding_hnsw ON facts USING hnsw (embedding halfvec_cosine_ops) WHERE ... flow (NOT bare REINDEX, which doesn't rewrite the index after a column-type change). assertFactsEmbeddingDimMatchesConfig(engine) is the preflight — throws FactsEmbeddingDimMismatchError (tagged tag: 'FACTS_EMBEDDING_DIM_MISMATCH' for parity with the worker-pool MUST_ABORT semantics) when configured dim ≠ column width; cached per-engine via WeakMap; PGLite engines silently skip. Doctor check facts_embedding_width_consistency (registered after embedding_width_consistency) reuses the same helpers with an identical ALTER recipe. Pinned by test/embedding-dim-check-facts.test.ts.
  • src/core/postgres-engine.ts extension — insertFact + insertFacts no longer hardcode tx.unsafe(\'${embedLit}'::vector`)for the embedding column.resolveFactsEmbeddingCast()(private) probespg_attributeonce per engine instance (cached in_factsEmbeddingCastSuffix) and returns '::halfvec'when migration v40 created the column as halfvec, else'::vector'; both insert paths use the cached suffix so the cast matches the actual column type (works on older pgvector that lacks implicit auto-cast). Test seam __resetFactsEmbeddingCastCacheForTest()` clears the per-engine cache.
  • src/core/cycle.ts + src/core/cycle/extract-atoms.ts + src/core/cycle/synthesize-concepts.ts + src/commands/extract.ts + src/commands/doctor.ts + src/core/op-checkpoint.ts extensions — six daily-driver ops fixes. (1) Batch idempotency: atomsExistingForHashes(engine, sourceId, hashes[]) (exported from src/core/cycle/extract-atoms.ts) replaces the per-hash loop (7K individual queries) with one batched SQL roundtrip returning already-extracted content_hash16 values; fail-open (SQL error → empty set, extraction proceeds); powered by migration v104 pages_atom_source_hash_idx (partial expression index on frontmatter->>'source_hash' for atom rows where deleted_at IS NULL; Postgres CREATE INDEX CONCURRENTLY with invalid-remnant pre-drop, PGLite plain). (2) Shorter cycle lock TTL + active in-phase refresh: LOCK_TTL_MINUTES = 5 (was 30); buildYieldDuringPhase(lock, outer) (exported, with LockHandle) calls lock.refresh() + any external hook on every fire, throttled to 30s via maybeYield, firing both in the main loop AND immediately after every await chat(...); synthesize_concepts uses the same throttled hook. A crashed cycle releases its lock 6x faster while a healthy long-running cycle keeps it alive (residual: a single await chat() past 5 min can expire the lock mid-await — TODO-OPS-2). (3) Progress wiring: progress?: ProgressReporter opt on ExtractAtomsOpts and SynthesizeConceptsOpts; cycle.ts passes its phase-level reporter down (NOT a child reporter, which would collide on cycle.extract_atoms.extract_atoms.work); phases only call tick()/heartbeat(), cycle.ts owns start()/finish(). (4) by-mention resume: mentionsFingerprint({source, type, since, gazetteerHash}) in src/core/op-checkpoint.ts — the gazetteer hash is load-bearing (adding entity pages mid-pause shifts the hash → new fingerprint → fresh scan against the new gazetteer, never silent skip); gbrain extract links --by-mention resumes via op_checkpoints with flushAndCheckpoint ordering (links flush to DB FIRST, page keys commit to checkpoint SECOND, persist THIRD, so a crash mid-batch leaves the page un-checkpointed and resume re-scans it); persist every 1000 items OR 30s; clean exit clears the checkpoint; --dry-run skips both load and write. (5) sync_consolidation doctor check (multi-source brains see a paste-ready gbrain sync --all --parallel 4 --workers 4 --skip-failed; single-source "not applicable"; SQL errors return warn via the check's own try/catch). (6) Test-isolation: test/cycle-last-full-cycle-at.test.ts + test/schema-cli.test.ts use per-test GBRAIN_HOME=tempdir. Pinned by test/cycle/extract-atoms-batch.test.ts, test/cycle/cycle-lock-ttl.test.ts (regression pin on LOCK_TTL_MINUTES === 5), test/op-checkpoint-mentions-fingerprint.test.ts, test/cycle/extract-atoms-progress.test.ts, test/cycle/synthesize-concepts-progress.test.ts, test/cycle/yield-during-phase-refresh.test.ts, test/cycle/yield-during-phase-throttle.test.ts, test/extract-by-mention-resume.test.ts, test/doctor-sync-consolidation.test.ts. Companion sync --all recipe block in skills/cron-scheduler/SKILL.md.
  • src/commands/sync.tsgbrain sync CLI + the performSync / performFullSync library entrypoints (consumed by the autopilot cycle and the Minion sync handler). performSync runs under a writer lock: per-source gbrain-sync:<sourceId> whenever opts.sourceId is set, wrapped in withRefreshingLock from src/core/db-lock.ts so long-running sources (250K+ chunks) don't lose the lock at the 30-min TTL mid-run; the bare no-source path uses the SAME refreshing lock (#1794 — it was previously a non-refreshing tryAcquireDbLock, stealable mid-run during an incident); SyncOpts.lockId?: string is the explicit override. The lock refresh AND its health probe route through the DIRECT session pool so Supavisor transaction-pool exhaustion (EMAXCONNSESSION) can't kill renewal; takeover is heartbeat-aware (it will NOT steal a holder whose last_refreshed_at is within GBRAIN_LOCK_STEAL_GRACE_SECONDS, defending an alive-but-starved holder); the import loop yields the event loop every GBRAIN_SYNC_YIELD_EVERY files (setTimeout(0), not setImmediate — Bun starves the timers phase) so the refresh setInterval heartbeat fires mid-import. This lock-identity invariant prevents a sync --all per-source worker racing sync --source foo on the global lock from corrupting the same source. performSync throws a typed SyncLockBusyError when the writer lock is held; the Minion sync handler (src/commands/jobs.ts) catches it and marks the job SKIPPED (not failed) so a cron/autopilot tick defers to the holder without polluting crash metrics. performSyncInner is RESUMABLE (incremental path): it drains a PINNED target commit (lastCommit..pin), banking drained file paths via appendCompleted (append-only delta into the op_checkpoint_paths child table, migration v115 — one row per path, O(delta) not the old O(N²) full-array rewrite), keyed by syncFingerprint({sourceId, lastCommit}) from src/core/op-checkpoint.ts (paths under op:'sync'; the pinned target under op:'sync-target'), and advances last_commit/last_sync_at ONLY at full import completion. Checkpoint writes route through the DIRECT session pool + bounded retry so they survive EMAXCONNSESSION; the flush cadence is first-file then every GBRAIN_SYNC_CHECKPOINT_EVERY (default 1000) files OR GBRAIN_SYNC_CHECKPOINT_SECONDS (default 10s), with a race-safe pendingCheckpointPaths delta (single-flight swap, re-merge on failure) under parallel workers; a SIGTERM banks the in-flight delta via a no-retry one-shot (appendCompletedOnce, ordered before lock release through registerCleanup); and sustained flush failure aborts the run with reason:'checkpoint_unavailable' after GBRAIN_SYNC_MAX_CHECKPOINT_FAILURES consecutive fails rather than importing work it can never bank (every partial/blocked exit logs the banked-file count). A sync killed mid-import banks its progress, leaves the anchor unmoved (the source stays correctly stale to the autopilot scheduler — last_sync_at is never bumped on a partial), and the next run resumeFilters the same fixed diff to skip done files. The pin is the checkpoint's stored target when still reachable from HEAD, else live HEAD (a history rewrite / reset re-pins); completion advances to the pin, NOT live HEAD, so commits landing past the pin are a clean next-sync diff — this closes the cross-run staleness window. After import a pin-reachability gate (git merge-base --is-ancestor pin HEAD) replaces the old strict head-drift gate: forward commits on top of the pin (e.g. a background enrich process committing to the same repo every ~2 min) are SAFE and no longer block the run; only a real rewrite (pin not an ancestor of HEAD) blocks and discards the checkpoint. A file added in lastCommit..pin but gone from disk (deleted by a commit after the pin) is SKIPPED and marked completed, not recorded as a failure. Downstream extract/facts/embed are size-gated: inline only for totalChanges <= 100; large syncs defer to the resumable extract --stale watermark + embed --stale/backfill + the facts cycle phases, so a 44K-page facts/embed pass never re-blocks import convergence (sync convergence == import convergence). Worker engines wrap in try/finally so disconnect always fires; both PGLite-detection sites use engine.kind === 'pglite'. CLI accepts --workers N (alias --concurrency N) validated via parseWorkers (explicit bypasses the file-count floor; auto path defers to autoConcurrency()). The newest-first descending-lex order uses sortNewestFirst(addsAndMods) from src/core/sort-newest-first.ts (shared with gbrain import). gbrain sync --all runs a continuous worker pool: parseWorkers-validated --parallel N (default min(sourceCount, --workers, DEFAULT_PARALLEL_SOURCES=4)), long-lived async workers pulling from a shared FIFO queue (no head-of-line blocking), per-source withSourcePrefix(src.id, ...) so every slog/serr line carries [<source-id>]; --skip-failed/--retry-failed reject when combined with --parallel > 1 (the brain-global sync-failures.jsonl has no per-source scope); a connection-budget stderr warning fires when parallel × workers × 2 > 16 (the × 2 per-file pool factor: each per-file worker opens its own PostgresEngine with poolSize=2). Exports resolveParallelism, syncOneSource, buildSyncStatusReport, printSyncStatusReport, SyncStatusReport back the gbrain sources status dashboard. --json envelope {schema_version: 1, sources, parallel, ok_count, error_count} on stdout; human banners route to stderr via humanSink so jq parses cleanly. Exit matrix: 0 all ok, 1 any error, 2 cost-prompt-not-confirmed. The dashboard SQL is content_chunks ch JOIN pages pg ON pg.id = ch.page_id WHERE pg.deleted_at IS NULL with archived = false at the caller; embedding column resolved via resolveEmbeddingColumn(undefined, cfg) from src/core/search/embedding-column.ts so Voyage/multimodal/non-default-column brains count against the column they use; errors propagate (no swallow-catch). The sync delete loop is interleaved per-batch resolve+delete using engine.resolveSlugsByPaths + engine.deletePages from src/core/engine.ts (73K-delete commit: ~146K SQL round-trips → ~292, closing the cascade-staleness class where one big-delete commit jammed every other source's sync); per-batch try-catch decomposes batch DELETE failures to per-slug deletePage fallback, unrecoverable per-slug failures land in failedFiles; pagesAffected filters to confirmed-deleted slugs. The rename loop gets the same batched slug-resolve; a rename whose destination is un-syncable folds the source path into the delete set so the old page can't orphan (#1970). An entry-time bookmark-reachability guard distinguishes a gc'd anchor (cat-file fails → performFullSync) from a history-rewrite anchor that is merely no-longer-an-ancestor but still on disk: the latter is diffed tree-to-tree directly (git diff lastCommit..pin is an endpoint-tree compare, ancestry not required) so a force-push / mastermain consolidation imports only the real delta instead of re-walking the whole tree forever (#1970); an oversized or failed diff degrades to performFullSync. performFullSync is itself authoritative for deletes — after an advancing full import it purges file-backed pages (source_path != null AND strategy-aware isSyncable) whose source file no longer exists, sparing put_page/manual pages (null source_path) and metafiles. resolveSlugByPathOrSourcePath at sync.ts:267 delegates to engine.resolveSlugsByPaths when sourceId is set, keeping legacy executeRaw fallback for the no-sourceId path. failedFiles is hoisted to the top of performSyncInner so both delete-decompose and import loops feed the same bookmark gate. The sync --all cost gate is mode-aware via willEmbedSynchronously + shouldBlockSync from src/core/embedding.ts (federated_v2 resolved once up front): the DEFERRED path (v2 on, parallel) is INFORMATIONAL (embedding goes to per-source embed-backfill jobs with their own $X/source/24h cap, default $25 via SPEND_CAP_CONFIG_KEY from embed-backfill-submit.ts; prints the cap + backlog sumStaleChunkChars({signature}) priced via estimateCostFromChars at currentEmbeddingPricePerMTok() + queued-job count, NEVER exits 2); the INLINE path BLOCKS only when the new-content estimate exceeds sync.cost_gate_min_usd (default $0.50). The new-content estimate (estimateInlineNewTokens) contributes ZERO for sources provably unchanged since last sync (isSourceUnchangedSinceSync HEAD==last_commit + clean tree AND chunker_version === CURRENT) and the full-tree ceiling for changed sources; the pre-existing stale backlog is shown informationally but NEVER gated on (sync doesn't sweep it inline). Helpers resolveCostGateFloorUsd(engine) (fail-open $0.50, accepts 0) + resolveBackfillCapUsd(engine). JSON envelopes gain mode + gate discriminators (dry_run | deferred_notice | below_floor | confirmation_required); SyncStatusReportSource gains backfill_queued/backfill_active/backfill_last_completed_at; cost previews read getEmbeddingModelName() (no hardcoded OpenAI). SyncOpts.noSchemaPack (CLI --no-schema-pack, threaded through performSync AND syncOneSource) skips loadActivePack so pages fall back to legacy prefix typing — an escape hatch when a suspect pack regex wedges a sync. A per-file BEGIN heartbeat if (process.env.GBRAIN_SYNC_TRACE) serr('[sync] begin import: <path>') fires BEFORE importFile (the progress.tick fires only AFTER) so a stuck file is a begin-line with no matching completion. Triage doc: docs/architecture/serve-sync-concurrency.md (PGLite single-writer serve↔sync contention + the GBRAIN_SYNC_TRACE + --no-schema-pack recipes). Pinned by test/e2e/sync-status-pglite.test.ts (IRON-RULE: PGLite seeds 2 sources × pages × chunks, soft-deletes 1 page, archives 1 source, validates the SQL excludes both AND uses the active embedding column), test/sync-cost-gate.serial.test.ts, test/sync-cost-preview.test.ts. Runaway-sync protection: resolveSyncHardDeadline(args, {isTty, env, defaultNonTtySec?}) resolves a wall-clock hard deadline (precedence --no-hard-deadline > --hard-deadline <s> > --timeout <s>(non---all, which auto-arms the backstop) > GBRAIN_SYNC_MAX_RUNTIME_SECONDS env > non-TTY default 3600s > none; HARD_DEADLINE_GRACE_SEC=30). src/cli.ts installs the out-of-band watchdog (see src/core/process-watchdog.ts) for the sync command BEFORE connectEngine and disposes it in the dispatch finally, so even an event-loop-starved sync — or a connect-phase hang — is SIGTERM-then-SIGKILLed by the deadline instead of orphaning under cron. runSync registers a SIGINT handler that aborts an interrupt AbortController composed via composeAbortSignals(...) (an AbortSignal.any wrapper over the defined signals) with the per-source --timeout signal, so Ctrl-C returns a clean partial and releases the lock through the normal finally (process-cleanup.ts owns SIGTERM lock-release; the watchdog owns the hard kill). withRefreshingLock unref()s its refresh setInterval. The spin's own root cause is not yet pinned (leading lead is catastrophic-backtracking in a pack link-inference regex, bounded by the redos-guard); the watchdog heartbeat plus the existing [gbrain phase] breadcrumbs are the diagnosis surface. Pinned by test/sync-hard-deadline.test.ts (resolution precedence + composeAbortSignals).
  • src/commands/import.tsgbrain import CLI + runImport library entrypoint. Uses a path-set checkpoint via src/core/import-checkpoint.ts (the walk still applies sortNewestFirst() for embed-cost ordering, but checkpoint correctness no longer depends on sort order). A file enters completed: Set<relativePath> only when its processFile returns success (including content-hash short-circuit no-ops); failed files never enter the set so the next run retries them automatically with no manual ~/.gbrain/import-checkpoint.json delete. This closes three classes: parallel-import-with-slow-worker dropping the slow file on crash-resume (the slow file isn't in completed until its own processFile resolves), failed-file-bumps-counter-past-itself (failures don't add to completed), and sort-flip-drops-newest-N-on-cross-version-resume (order is no longer part of the checkpoint). Old positional checkpoints are detected and discarded with a stderr line on first resume (re-walking is cheap because content_hash short-circuits unchanged files). Checkpoint persists every 100 successful adds, not every 100 processed files. The managedBookmark opt (set by performFullSync when runImport is the full-sync engine) suppresses runImport's own sync.last_commit advance so the shared applySyncFailureGate (src/core/sync-failure-ledger.ts) owns the bookmark + failure-ledger gating on that path — one gate decides advance/block/auto-skip across both sync paths. Pinned by test/import-checkpoint.test.ts + test/import-resume.test.ts (incl. the SLUG_MISMATCH retry regression).
  • src/core/import-checkpoint.tsloadCheckpoint(brainDir), saveCheckpoint(brainDir, completed), resumeFilter(files, completed, brainDir), clearCheckpoint(), plus the ImportCheckpoint type. Path-set format {schema_version, brainDir, completed: string[]}. Atomic write via .tmp + rename() so a mid-write crash never leaves a partial JSON. loadCheckpoint returns null on: missing file, malformed JSON, brainDir mismatch (ran against a different brain), and the old positional format (logged to stderr before discard). resumeFilter returns {toProcess, skippedCount} — pure, no I/O, deterministic. clearCheckpoint is no-op-on-missing for clean-exit cleanup. Honors GBRAIN_HOME via gbrainPath() so withEnv({GBRAIN_HOME: tmpdir}) test isolation works without monkey-patching fs. Best-effort persistence — saveCheckpoint logs warnings on write errors but never throws.
  • src/core/sort-newest-first.ts — single source of truth for the descending-lex sort that gbrain import and gbrain sync both apply. Mutates in place (Array.prototype.sort semantics), returns the same array reference for fluent chaining. Empty/single-element inputs short-circuit. Future ordering changes flip one line here instead of touching two CLI commands. Pinned by test/sort-newest-first.test.ts (descending order, mixed prefixes, empty, single-element, in-place-mutation contract).
  • src/core/cycle.ts — brain maintenance cycle primitive (9 phases). runCycle(engine: BrainEngine | null, opts: CycleOpts): Promise<CycleReport> composes phases in semantic order: lint → backlinks → sync → synthesize → extract → patterns → recompute_emotional_weight → embed → orphans. synthesize runs after sync (cross-references see a fresh brain) and before extract (auto-link materializes its writes); patterns runs after extract so it reads a fresh graph (subagent put_page sets ctx.remote=true and skips auto-link/timeline by default, so extract is the canonical materialization); recompute_emotional_weight sees the union of syncPagesAffected + synthesizeWrittenSlugs incrementally, or all pages when neither anchor is set (full backfill via gbrain dream --phase recompute_emotional_weight). CycleReport.schema_version: "1" is stable; totals is additive (pages_emotional_weight_recomputed, transcripts_processed, synth_pages_written, patterns_written). Three callers: gbrain dream CLI, gbrain autopilot daemon inline path, the Minions autopilot-cycle handler. Coordination via gbrain_cycle_locks DB table + ~/.gbrain/cycle.lock file lock with PID-liveness for PGLite. yieldBetweenPhases runs between phases; yieldDuringPhase is in-phase keepalive (synthesize/patterns renew the cycle-lock TTL during long waits). Engine nullable; lock-skip on read-only phase selections. CycleOpts.signal?: AbortSignal propagates the worker's abort signal with checkAborted() between every phase. runPhaseSync returns pagesAffected via SyncPhaseResult (threaded to runPhaseExtract as the 4th arg) and takes willRunExtractPhase: boolean setting noExtract: phases.includes('extract') so gbrain dream --phase sync doesn't silently lose extraction. resolveSourceForDir(engine, brainDir) threads sourceId to performSync() so sync reads the per-source sources.last_commit anchor (not the drift-prone global config.sync.last_commit). CycleOpts.brainDir is string | null; when null (checkout-less postgres/Supabase brain) the 6 filesystem phases (lint/backlinks/sync/synthesize/extract/patterns) skip with details.reason: 'no_brain_dir' and the DB-only phases run; resolveSourceForDir is null-tolerant. cycleSourceId = opts.sourceId ?? resolveSourceForDir(engine, brainDir) is the canonical per-source scope for extract_facts/extract_atoms/calibration so gbrain dream --source repo-a reconciles repo-a's facts even with no checkout (instead of scoping to 'default' while stamping repo-a fresh). deriveStatus counts edges_resolved/edges_ambiguous as work so an edges-only cycle reports ok not clean; the jobs.ts autopilot-cycle + phase-wrapper handlers pass null (not '.') when no repo is configured. Pinned by test/dream-postgres.serial.test.ts + test/jobs-autopilot-cycle-braindir.serial.test.ts.
  • src/core/cycle/synthesize.ts — Synthesize phase: conversation-transcript-to-brain pipeline. Reads dream.synthesize.session_corpus_dir, runs a cheap Haiku verdict (cached in dream_verdicts), then fans out one Sonnet subagent per worth-processing transcript with allowed_slug_prefixes (sourced from skills/_brain-filing-rules.json dream_synthesize_paths.globs). Orchestrator collects slugs from subagent_tool_executions (NOT pages.updated_at) and reverse-renders DB → markdown via serializeMarkdown. Cooldown via dream.synthesize.last_completion_ts, written ONLY on success. Idempotency key dream:synth:<file_path>:<content_hash>. --dry-run runs Haiku, skips Sonnet. Subagent never gets fs-write access. renderPageToMarkdown (exported) stamps dream_generated: true + dream_cycle_date into every reverse-write's frontmatter; writeSummaryPage does the same on the summary index — this marker is the explicit identity surface isDreamOutput checks in transcript-discovery.ts. judgeSignificance and JudgeClient are exported; judgeSignificance takes a verdictModel param loaded from dream.synthesize.verdict_model via loadSynthConfig. splitTranscriptByBudget(content, contentHash, maxChars) splits oversized transcripts at paragraph boundaries (## Topic:---\n ladder) using a deterministic offset seeded from the first 32 bits of contentHash so retries chunk identically; per-chunk char budget = MODEL_CONTEXT_TOKENS[resolvedModel] × 0.9 × 3.5 chars/token (non-Anthropic ids fall back to a 180K-token safe default + once-per-process stderr warn); operator overrides dream.synthesize.max_prompt_tokens (floor 100K, wins) and dream.synthesize.max_chunks_per_transcript (default 24). Per-chunk idempotency keys dream:synth:<filePath>:<hash16>:c<i>of<n>; single-chunk transcripts preserve the legacy dream:synth:<filePath>:<hash16> key byte-for-byte so existing brains skip with already_synthesized_legacy_single_chunk instead of re-spending Sonnet. collectChildPutPageSlugs raw-fetches every (job_id, slug) pair (not SELECT DISTINCT) and rewrites bare-hash6 slugs to <hash6>-c<idx> for chunked children (orchestrator-side, zero Sonnet trust). Cap-hit skips don't write to dream_verdicts so raising the cap on next run re-attempts cleanly. Bounds INITIAL prompt size only; tool-loop turn-N accumulation is caught by terminal-error classification in subagent.ts. Verdict routing is gateway-routed: makeJudgeClient(verdictModel) (exported, replacing makeHaikuClient()) mirrors tryBuildGatewayClient in src/core/think/index.ts — a construction-time provider/key probe returns null on a clear miss (unknown provider id via resolveRecipe AIConfigError, or Anthropic provider with no key via hasAnthropicKey()). The verdict loop wraps judgeSignificance in try/catch for AIConfigError so mid-run provider failures surface as per-transcript worth=false, reasons=['gateway error: ...'] instead of crashing the phase. Canonical config key models.dream.synthesize_verdict (per PER_TASK_KEYS in src/core/model-config.ts); JudgeClient signature preserved verbatim for test-seam stability; CI guard scripts/check-gateway-routed-no-direct-anthropic.sh prevents reintroducing new Anthropic() here or in think/index.ts. At the queue.add boundary (lines 395-404) a conditional anthropic: prefix is applied ONLY when the resolved model has no colon AND starts with claude- (because resolveModel returns bare ids from TIER_DEFAULTS/DEFAULT_ALIASES and the subagent validator requires provider:model form) — avoids changing the shared constants which would ripple across every resolveModel caller. Pinned by test/cycle/synthesize-gateway-adapter.test.ts, test/e2e/dream-synthesize-pglite.test.ts (gateway-adapter mid-run AIConfigError catch), test/cycle/regression-pr-wave-r1-r2-r4.test.ts.
  • scripts/check-gateway-routed-no-direct-anthropic.sh — CI guard that fails the build if src/core/cycle/synthesize.ts or src/core/think/index.ts reintroduces a runtime new Anthropic() constructor call or a value-shaped import Anthropic from '@anthropic-ai/sdk' import. Type-only imports (import type Anthropic from '@anthropic-ai/sdk') stay allowed for adapter types; comment lines (// or * prefixes) are excluded so historical JSDoc doesn't false-fire. Mirrors scripts/check-jsonb-pattern.sh. Wired into bun run verify and bun run check:all. Extend GUARDED_FILES when migrating another file off direct SDK construction.
  • src/core/cycle/patterns.ts — Patterns phase: cross-session theme detection over reflections within dream.patterns.lookback_days (default 30). Names a pattern only when ≥dream.patterns.min_evidence (default 3) reflections support it. Single Sonnet subagent; same allow-list path as synthesize. Runs AFTER extract so the graph is fresh.
  • src/core/cycle/extract-facts.ts — extract_facts cycle phase. Fence is canonical: per-page wipe (deleteFactsForPage) + reinsert from parseFactsFence + extractFactsFromFenceText + engine.insertFacts. Empty-fence guard refuses when legacy rows (row_num IS NULL AND entity_slug IS NOT NULL) pend backfill (status: warn, hint: gbrain apply-migrations --yes). A phantom-redirect pre-pass runs AFTER the legacy-row guard, BEFORE the main reconcile loop: when opts.brainDir is set, runPhantomRedirectPass(engine, brainDir, sourceId, dryRun) walks unprefixed-slug pages capped by GBRAIN_PHANTOM_REDIRECT_LIMIT (default 50). The pass returns touched_canonicals — canonical slugs whose disk fence merged with phantom rows; runExtractFacts UNIONs them into the main reconcile slug set so canonical's DB facts derive from the merged fence in the same cycle (handles phantom-had-only-on-disk-fence). ExtractFactsResult gains six phantom fields: phantomsScanned, phantomsRedirected, phantomsAmbiguous, phantomsSkippedDrift, phantomsLockBusy, phantomsMorePending. Three bubble to CycleReport.totals (phantoms_redirected, phantoms_ambiguous, phantoms_skipped_drift).
  • src/core/entities/resolve.ts — Free-form entity name → canonical slug resolution. resolveEntitySlug(engine, source_id, raw): exact slug → fuzzy (pg_trgm @ 0.4 threshold) → bare-name prefix expansion (people/<token>-% then companies/<token>-%, connection_count correlated-subquery tiebreaker) → deterministic slugify fallback. Two helpers for the phantom-redirect pass: resolvePhantomCanonical(engine, sourceId, phantomSlug) SKIPS the exact-slug step (a phantom slug 'alice' would exact-match itself and no-op the redirect); returns the canonical only when non-null AND contains /. findPrefixCandidates(engine, sourceId, token) is a standalone SQL query returning ALL candidates across PREFIX_EXPANSION_DIRS (hardcoded ['people', 'companies']) via slug LIKE ANY($N::text[]) over patterns dir/token + dir/token-%, cap of 10 ordered by connection_count DESC, slug ASC — NOT a wrapper around tryPrefixExpansion (that path returns per-dir top-1 and suppresses ambiguity by design). Pinned by test/phantom-redirect.test.ts (resolvePhantomCanonical 3 cases + findPrefixCandidates 6 cases incl. multi-dir ambiguity and the people/aliceberg-doesn't-match-alice false-positive guard).
  • src/core/cycle/phantom-redirect.ts — Phantom-redirect orchestrator. Exports runPhantomRedirectPass(engine, brainDir, sourceId, dryRun): Promise<PhantomPassResult> (per-cycle wrapper acquiring the gbrain-sync writer lock once for the whole pass, 30s bounded retry, walks up to GBRAIN_PHANTOM_REDIRECT_LIMIT unprefixed phantoms) + tryRedirectPhantom(engine, page, sourceId, brainDir, dryRun): Promise<RedirectResult> + stripFenceAndFrontmatterAndLeadingH1 (pure body-shape gate helper — strips facts fence incl. preceding ## Facts heading and the leading H1; zero residue = phantom). Handler order: body-shape gate → resolvePhantomCanonical (bypasses exact-self-match) → findPrefixCandidates ambiguity check → fenceDbDrift bi-directional check → dry-run early exit → materialize canonical via serializeMarkdown if DB-only → append phantom fence rows to canonical's disk fence with (claim, valid_from) dedup-guard + row_num continuation → engine.refreshPageBody with SHA-256 content_hash recomputed via the import-file shape → engine.migrateFactsToCanonical (lossless) → engine.rewriteLinks (DB FK rewrite; wiki-link text rewrite is a documented follow-up) → engine.softDeletePage + engine.deleteFactsForPage(phantom) + fs.unlinkSync(phantomPath). RedirectResult.canonical populated on 'redirected' (incl. dry-run preview) so the caller builds touched_canonicals. Idempotent on re-run: phantom soft-deleted → predicate fails (deleted_at IS NULL); migrate UPDATE matches no rows; dedup-guard prevents double-append.
  • src/core/facts/phantom-audit.ts — JSONL audit at ${resolveAuditDir()}/phantoms-YYYY-Www.jsonl. Pattern copy of src/core/audit-slug-fallback.ts (ISO-week rotation, honors GBRAIN_AUDIT_DIR). Exports logPhantomEvent(record) + readRecentPhantomEvents(days) + computePhantomAuditFilename(now?). Records every outcome: redirected | ambiguous | drift | no_canonical | not_phantom_has_residue | pass_skipped_lock_busy. Best-effort writes — stderr warn on failure, never throws. Separate file from stub-guard-audit.ts (distinct consumer + lifecycle: stub-guard logs PREVENTIVE blocks; phantom-audit logs CLEANUP decisions, to be read by a future phantoms_pending doctor check).
  • src/core/cycle/emotional-weight.ts — Pure function computeEmotionalWeight({tags, takes}, {highEmotionTags?, userHolder?}). Deterministic 0..1 score: tag-emotion boost (max 0.5, case-insensitive match against HIGH_EMOTION_TAGS seed list), take density (0.1/take, capped at 0.3), take avg weight (0..0.1), user-holder ratio (0..0.1 over active takes; default holder 'garry'). Total clamped to [0..1]. Anglocentric / personal-life-biased seed list intentional; override via config emotional_weight.high_tags (JSON array). userHolder overridable via emotional_weight.user_holder.
  • src/core/cycle/anomaly.ts — Pure stats helpers for find_anomalies. meanStddev returns sample stddev (n-1 denominator) and (0,0) for empty input. computeAnomaliesFromBuckets(baseline, today, sigma, limit) takes densified daily-count buckets + today's counts per cohort, returns AnomalyResult[]. Zero-stddev fallback: cohort fires when count > mean + 1, with sigma_observed = count - mean as a finite sort proxy (no NaN). Brand-new cohorts (no baseline) have mean=0, stddev=0 so the fallback fires at count >= 2. Sorted by sigma_observed desc, top limit (default 20). page_slugs capped at 50 per cohort.
  • src/core/cycle/recompute-emotional-weight.ts — Cycle phase orchestrator. Two SQL round-trips: engine.batchLoadEmotionalInputs(slugs?)computeEmotionalWeight (per-row pure function) → engine.setEmotionalWeightBatch(rows). Reads config emotional_weight.high_tags (JSON array, falls back to default seed list on parse error) and emotional_weight.user_holder. Empty affectedSlugs short-circuits with zero-work success. dry-run reports the would-write count without touching the DB. Engine throw bubbles into status: 'fail' with code RECOMPUTE_EMOTIONAL_WEIGHT_FAIL so the cycle continues.
  • src/core/transcripts.tslistRecentTranscripts(engine, opts) library reused by both the gbrain transcripts recent CLI and the get_recent_transcripts MCP op. Reads dream.synthesize.session_corpus_dir + dream.synthesize.meeting_transcripts_dir config (same as discoverTranscripts); walks .txt files within days; applies the isDreamOutput guard from transcript-discovery.ts (skips dream-generated files); returns {path, date, mtime, length, summary}[] sorted newest-first. Summary mode (default true) = first non-empty line + ~250 trailing chars; full mode caps at 100KB/file. Missing/non-existent corpus dirs return [], not error. Trust gate lives in the op handler, not here: the op throws permission_denied for ctx.remote === true; this is a trusted library function used by both the gated op and the local CLI.
  • src/core/operations-descriptions.ts — Constants module for tool descriptions. Pinned via test/operations-descriptions.test.ts. Houses GET_RECENT_SALIENCE_DESCRIPTION, FIND_ANOMALIES_DESCRIPTION, GET_RECENT_TRANSCRIPTS_DESCRIPTION plus LIST_PAGES_DESCRIPTION, QUERY_DESCRIPTION, SEARCH_DESCRIPTION. Stable surface for the Tier-2 LLM routing eval — extracting them keeps the test from binding to whatever was in operations.ts at test-run time.
  • src/core/cycle/transcript-discovery.ts — Pure filesystem walk for synthesize. discoverTranscripts(opts) filters .txt files by date range, min_chars, and word-boundary regex excludePatterns (medical matches "medical advice" but NOT "comedical"; power users may pass full regex). readSingleTranscript(path) is the gbrain dream --input <file> ad-hoc path. Self-consumption guard: DREAM_OUTPUT_MARKER_RE (anchored at frontmatter open ---\n, optional BOM + CRLF tolerance, scans first 2000 chars for dream_generated: true with case-insensitive value and word boundary on true) drives isDreamOutput(content, bypass=false). Both functions skip matching files and emit a [dream] skipped <basename>: dream_generated marker stderr log (no silent skips). bypassGuard?: boolean on DiscoverOpts and readSingleTranscript's opts disables the guard for the explicit --unsafe-bypass-dream-guard escape hatch only — never auto-applied for --input.
  • src/commands/dream.tsgbrain dream CLI; thin alias over runCycle. Flags: --dry-run, --json, --phase <name>, --pull, --dir <path>, --input <file> (ad-hoc transcript, implies --phase synthesize), --date YYYY-MM-DD, --from <d> --to <d> (backfill range), --unsafe-bypass-dream-guard (plumbed through runCycle.synthBypassDreamGuardSynthesizePhaseOpts.bypassDreamGuarddiscoverTranscripts({bypassGuard}) / readSingleTranscript({bypassGuard}); loud stderr warning at synthesize-phase entry; never auto-applied for --input). Conflict detection: --input + --date exits 2. ISO date validation. --dry-run runs the Haiku significance verdict but skips Sonnet synthesis (NOT zero LLM calls). Exit 1 on status=failed. resolveBrainDir returns string | null (order: --dir → resolved --source's local_path → global sync.repo_path → null); a checkout-less postgres/Supabase brain runs DB-only phases (incl. resolve_symbol_edges) and skips the 6 filesystem phases with details.reason: 'no_brain_dir'; runDream owns the only hard error (no checkout AND no engine). When --source resolves but has no on-disk checkout, returns null (DB-only) rather than borrowing another source's global sync.repo_path (would mix scopes). Pinned by test/dream-postgres.serial.test.ts. --drain [--window <seconds>] for --phase extract_atoms: runDrain() bypasses the pack-gate and runs the single-hold bounded drain from src/core/cycle/extract-atoms-drain.ts under the same cycleLockIdFor(sourceId) the routine cycle uses (concurrent autopilot tick defers with cycle_already_running), reporting {extracted, skipped, remaining}. Exits EXIT_DRAIN_INCOMPLETE=3 while remaining > 0; a null backlog count (count query FAILED) is also exit 3, never a drained success; LockUnavailableErrorcycle_already_running skip (also exit 3). The extract_atoms_backlog doctor check (computeExtractAtomsBacklogCheck) surfaces the silent pack-gated backlog with the exact --drain command; pack-gated cycle skips carry a greppable pack_gated:true marker.
  • src/commands/friction.ts + src/core/friction.tsgbrain friction {log,render,list,summary} reporter. Append-only JSONL under $GBRAIN_HOME/friction/<run-id>.jsonl. Schema is a flat extension of StructuredAgentError. Render groups by severity → phase, defaults to --redact for md output (strips $HOME/$CWD to placeholders so reports paste safely in PRs). Run-id resolves from --run-id > $GBRAIN_FRICTION_RUN_ID > standalone.jsonl. Skills the claw-test exercises gain a _friction-protocol.md callout so agents know when to log friction.
  • src/commands/claw-test.ts + src/core/claw-test/gbrain claw-test [--scenario <name>] [--live --agent openclaw]. End-to-end "fresh user" friction harness. Two modes: scripted (CI gate, agent-free) and live (real openclaw subprocess, $1–2 in tokens). Sets GBRAIN_HOME=<tempdir> for hermeticity and captures gbrain's --progress-json events from each child's stderr to verify expected phases ran (import.files, extract.links_fs, doctor.db_checks). Scripted phases: setup → install_brain (gbrain init --pglite) → import (--no-embed) → query → extract → verify (gbrain doctor --json, asserts status: 'ok') → render. Live mode hands BRIEF.md from test/fixtures/claw-test-scenarios/<name>/ to the agent runner. Ships with the OpenClaw runner only (src/core/claw-test/runners/openclaw.ts, invokes openclaw agent --local --agent <name> --message <brief>); hermes runner deferred. Transcript capture (transcript-capture.ts) uses fs.createWriteStream with 'drain'-event backpressure (256KB-burst child-stall fix). Upgrade scenario seeded via seed-pglite.ts SQL replay.
  • skills/_friction-protocol.md — shared cross-cutting convention skill (like _brain-filing-rules.md). Tells agents when to call gbrain friction log and how to choose a severity. Routes to friction CLI from any skill the claw-test exercises.
  • scripts/check-progress-to-stdout.sh — CI guard against regressing to \r-on-stdout progress. Wired into bun run test via scripts/check-progress-to-stdout.sh && bun test in package.json.
  • docs/progress-events.md — Canonical JSON event schema reference. Additive only.
  • src/core/markdown.ts — Frontmatter parsing + body splitter. coerceFrontmatterString(v) coerces a non-string title/slug/type to a deterministic string at parse time so a YAML-typed value never reaches .toLowerCase() and throws (the #1939 wedge: title: 2024-06-01 parsed as a Date, title: 1458 as a number, and the throw blocked the sync bookmark from advancing); a Date becomes its UTC ISO date (2024-06-01, machine-independent and matching the on-disk token, unlike String(date)), null/undefined become '', everything else uses String(). splitBody requires an explicit timeline sentinel (<!-- timeline -->, --- timeline ---, or --- immediately before ## Timeline/## History). Plain --- in body text is a markdown horizontal rule, not a separator. inferType auto-types /wiki/analysis/ → analysis, /wiki/guides/ → guide, /wiki/hardware/ → hardware, /wiki/architecture/ → architecture, /writing/ → writing (plus existing people/companies/deals/etc heuristics).
  • scripts/check-jsonb-pattern.sh — CI grep guard. Fails the build if anyone reintroduces (a) the ${JSON.stringify(x)}::jsonb interpolation pattern (postgres.js v3 double-encodes it), or (b) max_stalled INTEGER NOT NULL DEFAULT 1 in any schema source file (must be DEFAULT 5 to preserve SIGKILL-rescue). Wired into bun test.
  • scripts/check-source-id-projection.sh — CI grep guard for the multi-source bug class. Greps src/core/postgres-engine.ts + src/core/pglite-engine.ts for SELECT.*FROM pages projections matching the rowToPage feeder shape (id + slug + type + title) and fails if source_id is missing. Page.source_id is required at the type level; a projection dropping the column produces Page rows with source_id: undefined while TypeScript's : string lies about it. Wired into bun run verify + bun run check:all.
  • docker-compose.ci.yml + scripts/ci-local.sh — Local CI gate. bun run ci:local spins up pgvector/pgvector:pg16 + oven/bun:1 with named volumes (gbrain-ci-pg-data, gbrain-ci-node-modules, gbrain-ci-bun-cache), runs gitleaks on host, smoke-tests scripts/run-e2e.sh argv handling, runs unit tests with DATABASE_URL unset, then runs all 29 E2E files sequentially. --diff swaps in the diff-aware selector; --no-pull skips upstream pulls; --clean nukes named volumes. Postgres host port defaults to 5434; override with GBRAIN_CI_PG_PORT=NNNN. Stronger gate than PR CI's 2-file Tier 1 set.
  • scripts/select-e2e.ts + scripts/e2e-test-map.ts — Diff-aware E2E test selector. Reads three git sources (committed origin/master...HEAD, working-tree HEAD, and git ls-files --others --exclude-standard for untracked NOT-gitignored files), classifies as EMPTY / DOC_ONLY / SRC. Fail-closed: EMPTY → all 29 files; DOC_ONLY (every path matches the README/CLAUDE/AGENTS/CHANGELOG/TODOS allowlist) → empty stdout; SRC → escape-hatch paths (schema, package.json, skills/) trigger all, else the hand-tuned E2E_TEST_MAP glob narrows, and an unmapped src/ change still emits ALL files (never silently nothing). Pure-function exports selectTests, classify, matchGlob. bun run ci:select-e2e prints the current selection on stdout. test/select-e2e.test.ts covers all 4 branches plus 3 regression guards (skills/, untracked files, unmapped src/) — 24 cases.
  • scripts/run-e2e.sh — Sequential E2E runner. Accepts an optional argv-driven file list (used by ci:local:diff) and a --dry-run-list flag that prints the resolved file list and exits (used by ci-local.sh's startup smoke-test). Falls back to test/e2e/*.test.ts when invoked with no args.
  • scripts/llms-config.ts + scripts/build-llms.ts — Generator for llms.txt (llmstxt.org-spec web index) + llms-full.txt (inlined single-fetch bundle). Curated config drives both. Run bun run build:llms after adding a new doc. LLMS_REPO_BASE env lets forks regenerate with their own URL base. FULL_SIZE_BUDGET (600KB) caps the inline bundle; generator WARNs if exceeded. Committed output has no runtime consumer; committed for GitHub browsing and fork-safe fetching.
  • AGENTS.md — Local-clone entry point for non-Claude agents (Codex, Cursor, OpenClaw, Aider). Mirrors CLAUDE.md intent via relative links. Claude Code keeps using CLAUDE.md.
  • docs/UPGRADING_DOWNSTREAM_AGENTS.md — Patches for downstream agent skill forks to apply when upgrading. Each release appends a new section; includes diffs for brain-ops, meeting-ingestion, signal-detector, enrich.
  • src/core/schema-embedded.ts — AUTO-GENERATED from schema.sql (run bun run build:schema)
  • src/schema.sql — Full Postgres + pgvector DDL (source of truth, generates schema-embedded.ts)
  • src/commands/integrations.ts — Standalone integration recipe management (no DB needed). Exports getRecipeDirs() (trust-tagged recipe sources), SSRF helpers (isInternalUrl, parseOctet, hostnameToOctets, isPrivateIpv4). Only package-bundled recipes are embedded=true; $GBRAIN_RECIPES_DIR and cwd ./recipes/ are untrusted and cannot run command/http/string health checks.
  • src/core/search/expansion.ts — Multi-query expansion via Haiku. Exports sanitizeQueryForPrompt + sanitizeExpansionOutput (prompt-injection defense-in-depth). Sanitized query is only used for the LLM channel; the original query still drives search.
  • recipes/ — Integration recipe files (YAML frontmatter + markdown setup instructions)
  • docs/guides/ — Individual SKILLPACK guides (broken out from monolith)
  • docs/integrations/ — "Getting Data In" guides and integration docs
  • docs/architecture/infra-layer.md — Shared infrastructure documentation
  • docs/ethos/THIN_HARNESS_FAT_SKILLS.md — Architecture philosophy essay
  • docs/ethos/MARKDOWN_SKILLS_AS_RECIPES.md — "Homebrew for Personal AI" essay
  • docs/guides/repo-architecture.md — Two-repo pattern (agent vs brain)
  • docs/guides/sub-agent-routing.md — Model routing table for sub-agents
  • docs/guides/skill-development.md — 5-step skill development cycle + MECE
  • docs/guides/idea-capture.md — Originality distribution, depth test, cross-linking
  • docs/guides/quiet-hours.md — Notification hold + timezone-aware delivery
  • docs/guides/diligence-ingestion.md — Data room to brain pages pipeline
  • docs/designs/HOMEBREW_FOR_PERSONAL_AI.md — 10-star vision for integration system
  • docs/mcp/ — Per-client setup guides (Claude Desktop, Code, Cowork, Perplexity)
  • BrainBench (benchmark suite + corpus): lives in the separate gbrain-evals repo. Not installed alongside gbrain.
  • skills/_brain-filing-rules.md — Cross-cutting brain filing rules (referenced by all brain-writing skills)
  • skills/RESOLVER.md — Skill routing table (based on the agent-fork AGENTS.md pattern)
  • skills/conventions/ — Cross-cutting rules (quality, brain-first, model-routing, test-before-bulk, cross-modal)
  • skills/_output-rules.md — Output quality standards (deterministic links, no slop, exact phrasing)
  • skills/signal-detector/SKILL.md — Always-on idea+entity capture on every message
  • skills/brain-ops/SKILL.md — Brain-first lookup, read-enrich-write loop, source attribution
  • skills/idea-ingest/SKILL.md — Links/articles/tweets with author people page mandatory
  • skills/media-ingest/SKILL.md — Video/audio/PDF/book with entity extraction
  • skills/meeting-ingestion/SKILL.md — Transcripts with attendee enrichment chaining
  • skills/citation-fixer/SKILL.md — Citation format auditing and fixing
  • skills/repo-architecture/SKILL.md — Filing rules by primary subject
  • skills/skill-creator/SKILL.md — Create conforming skills with MECE check
  • skills/daily-task-manager/SKILL.md — Task lifecycle with priority levels
  • skills/daily-task-prep/SKILL.md — Morning prep with calendar context
  • skills/cross-modal-review/SKILL.md — Quality gate via second model
  • skills/cron-scheduler/SKILL.md — Schedule staggering, quiet hours, idempotency
  • skills/reports/SKILL.md — Timestamped reports with keyword routing
  • skills/testing/SKILL.md — Skill validation framework
  • skills/soul-audit/SKILL.md — 6-phase interview for SOUL.md, USER.md, ACCESS_POLICY.md, HEARTBEAT.md
  • skills/webhook-transforms/SKILL.md — External events to brain signals
  • skills/data-research/SKILL.md — Structured data research: email-to-tracker pipeline with parameterized YAML recipes
  • skills/minion-orchestrator/SKILL.md — Unified background-work skill (consolidation of the former minion-orchestrator + gbrain-jobs split). Two lanes: shell jobs via gbrain jobs submit shell --params '{"cmd":"..."}' (operator/CLI only; MCP throws permission_denied for protected names) and LLM subagents via gbrain agent run (user-facing entrypoint). Shared Preconditions block, parent-child DAGs with depth/cap/timeouts, child_done inbox for fan-in, PGLite --follow inline path for dev. Triggers narrowed to "gbrain jobs submit" + "submit a gbrain job" so stats/prune/retry questions fall through to gbrain --help.
  • templates/ — SOUL.md, USER.md, ACCESS_POLICY.md, HEARTBEAT.md templates
  • skills/migrations/ — Version migration files with feature_pitch YAML frontmatter
  • src/commands/publish.ts — Deterministic brain page publisher (code+skill pair, zero LLM calls)
  • src/commands/backlinks.ts — Back-link checker and fixer (enforces Iron Law)
  • src/commands/lint.ts — Page quality linter (catches LLM artifacts, placeholder dates)
  • src/commands/report.ts — Structured report saver (audit trail for maintenance/enrichment)
  • src/core/destructive-guard.ts — three-layer protection against accidental data loss. assessDestructiveImpact(engine, sourceId) counts pages/chunks/embeddings/files for a source. checkDestructiveConfirmation(impact, opts) is the fail-closed gate (--confirm-destructive required when data is present; --yes alone is rejected). softDeleteSource / restoreSource / listArchivedSources / purgeExpiredSources drive the source-level archive lifecycle via sources.archived BOOLEAN, archived_at TIMESTAMPTZ, archive_expires_at TIMESTAMPTZ. Page-level analog: BrainEngine.softDeletePage / restorePage / purgeDeletedPages plus pages.deleted_at TIMESTAMPTZ and a partial purge index. The MCP delete_page op rewires to softDeletePage; ops restore_page (scope: write) and purge_deleted_pages (scope: admin, localOnly: true) round out the surface. Search visibility (buildVisibilityClause in src/core/search/sql-ranking.ts) hides soft-deleted pages and archived sources from searchKeyword / searchKeywordChunks / searchVector in both engines. The autopilot cycle's purge phase calls purgeExpiredSources + engine.purgeDeletedPages(72) so the 72h TTL is real.
  • src/commands/pages.tsgbrain pages purge-deleted [--older-than HOURS|Nd] [--dry-run] [--json] operator escape hatch. Mirror of gbrain sources purge for the page-level lifecycle. Hard-deletes pages whose deleted_at is older than the cutoff; cascades to content_chunks/page_links/chunk_relations.
  • src/core/op-checkpoint.ts — DB-backed checkpoint primitive for long-running ops. Migration v67 introduces op_checkpoints (op TEXT, fingerprint TEXT, completed_keys JSONB, updated_at TIMESTAMPTZ, PK(op, fingerprint)). Per-op fingerprint helpers (embedFingerprint, extractFingerprint, reindexFingerprint, integrityFingerprint, purgeFingerprint) compute sha8(canonical-JSON(relevant-params)) so re-running with the same params resumes from completed_keys and re-running with different params (e.g. --limit 100 vs --limit 200) starts fresh. Cross-worker safe on Postgres (DB row, no file-lock race); PGLite degrades gracefully. Replaces per-op file-backed JSON checkpoints scattered across import.ts, embed.ts, reindex.ts. The 7-day TTL GC runs in the cycle's purge phase. All writes (recordCompleted, clearOpCheckpoint) route through engine.executeRawDirect + withRetry(BULK_RETRY_OPTS) so they survive Supavisor pool exhaustion, and recordCompleted returns boolean (banked vs failed-after-retries) — the 9 non-sync consumers keep its REPLACE-into-completed_keys semantics. Resumable sync uses the additive appendCompleted(key, deltaKeys) / appendCompletedOnce (the latter no-retry for the SIGTERM path) which INSERT a delta into the op_checkpoint_paths child table (migration v115: (op, fingerprint, path) PK, FK to op_checkpoints ON DELETE CASCADE) via a single writable-CTE unnest($3::text[]) write — O(delta), killing the old O(N²) full-set rewrite. loadOpCheckpoint returns the UNION ALL of legacy completed_keys + child-table paths (deduped in JS), so an in-flight upgrade loses nothing. syncFingerprint({sourceId, lastCommit}) keys the sync rows. Pinned by test/op-checkpoint.test.ts (incl. delta-append, union read, cascade clear, durable-write boolean). import-checkpoint.ts was NOT migrated to this primitive — both checkpoint systems coexist without conflict; migrating requires async-propagating 4 sync call sites in src/commands/import.ts and rewriting 18 tests, deferred.
  • src/core/brain-score-recommendations.ts — pure data layer consumed by both gbrain doctor --remediation-plan / --remediate and gbrain features. computeRecommendations(checks, opts) returns Remediation[] with stable id, content-hash idempotency_key, severity, est_seconds, est_usd_cost, depends_on (references stable ids, not check names — so plan order is reproducible). classifyChecks(report) triages every doctor check three-state into remediable | human_only | blocked (human_only covers RLS warnings and other human-judgment gates; blocked covers dependency chains where a parent check failed). maxReachableScore(checks) computes the ceiling for empty/under-configured brains (no entity pages → graph_coverage caps at 70; no embedding key → embedding_coverage caps at 60). Cost estimates pull from anthropic-pricing.ts (synthesize/patterns/consolidate) and embedding-pricing.ts (embed jobs). Pinned by test/brain-score-recommendations.test.ts (~27 cases incl. determinism, content-hash idempotency, DB-backed checkpoint provenance, three-state triage).
  • src/commands/doctor.ts extension — --remediation-plan [--json] [--target-score N] prints what would run (stable id, idempotency_key, severity, est_seconds, est_usd_cost, depends_on); --remediate [--yes] [--target-score N] [--max-usd N] submits each plan step as a Minion job in dependency order, re-checking score between steps. --target-score N defaults to 90; refuses to start when target exceeds maxReachableScore() and lists what's missing. --max-usd N is the cron-safety guard — submission refuses when the plan's est_total_usd_cost exceeds the cap. JSON envelope adds a Check.remediation field (additive, schema_version unchanged). Pinned by tests in test/doctor.test.ts.
  • src/commands/jobs.ts extension — registers 11 Minion handlers: reindex, repair-jsonb, orphans, integrity, purge, synthesize (PROTECTED), patterns (PROTECTED), consolidate (PROTECTED), extract_facts, resolve_symbol_edges, recompute_emotional_weight. Phase wrappers delegate to runCycle({phases:[name]}) so src/core/cycle.ts stays the single source of truth for phase semantics. The standalone sync handler passes noExtract: true to match runPhaseSync's contract (doctor's remediation plan emitting [sync, extract] would otherwise double-extract).
  • src/core/minions/protected-names.ts extension — PROTECTED_JOB_NAMES includes synthesize, patterns, consolidate. These phases internally submit subagent children with allowProtectedSubmit=true and can spend Anthropic credits. Only trusted local callers (CLI, autopilot, doctor --remediate) can submit them; MCP requests are rejected by submit_job's protected-name guard.
  • src/commands/autopilot.ts extension — targeted-submit loop instead of blanket autopilot-cycle dispatch. Each tick: cheap engine.getHealth() (single SQL count) + computeRecommendations(), then route by shape — score >= 95 AND no plan AND <60min since last full → sleep; score >= 95 AND >=60min → submit autopilot-cycle (60-min floor exercises phase-coupling invariants on healthy brains); plan <= 3 steps AND est <5min → submit individual handlers; plan large OR score < 70 → submit full autopilot-cycle. The gbrain-cycle lock ensures targeted submissions and the full cycle can't run concurrently. maxWaiting: 1 per submit closes the queue-fan-out vector.
  • src/core/abort-check.ts (#1737) — one canonical place for cooperative-abort checks across gbrain's long loops. isAborted(signal?) → boolean (for loops that break and return partial progress). throwIfAborted(signal?, label?) throws an AbortError (name === 'AbortError') at phase boundaries, preferring the signal's reason ('wall-clock'/'lock-lost'/'shutdown') so the unwind self-describes. anySignal(internal, external?) composes two signals into one that fires when EITHER does (platform AbortSignal.any with a manual-relay fallback), returning the internal unchanged when there's no external so non-aborting callers pay nothing. The fix for the #1737 cycle-wedge: the embed phase ignored its abort signal and ran to completion, so gbrain_cycle_locks stayed held and later autopilot cycles skipped with cycle_already_running; threading these checks through runPhaseEmbed → runEmbedCore → embedAll(Stale)/embedPage lets the phase bail and release the lock immediately. Pinned by test/abort-check.test.ts.
  • src/core/cycle.ts extension — purge phase (the cycle's 9th phase, for soft-delete TTLs) also GCs stale op_checkpoints rows older than 7 days. Non-fatal on pre-v67 brains (DROP-target-table check before DELETE). #1737: the cycle threads its abort signal into the embed phase (runPhaseEmbed(engine, dryRun, signal)) so a timed-out cycle's long embed phase honors cancellation and releases gbrain_cycle_locks right away instead of after a full backlog run.
  • src/commands/embed.ts extension — wires --background as the reference integration for the maybeBackground() helper. gbrain embed --stale --background submits as a Minion job, prints job_id=N to stdout, exits 0. Composable: JOB=$(gbrain embed --stale --background | grep -oE 'job_id=[0-9]+' | cut -d= -f2); gbrain jobs follow $JOB. The other six commands (extract, lint, backlinks, reindex, integrity, pages) adopt the same pattern in a follow-up wave. #1737: runEmbedCore accepts an optional signal threaded down both the --stale and --all paths (embedAllStale/embedAll/embedPage); each composes it with the internal wall-clock budget via anySignal and checks isAborted/effectiveSignal.aborted in every per-slug loop, page-claim pool, and embedBatch call, so a worker abort (wall-clock timeout / lock loss / SIGTERM) stops embedding within a batch. Pinned by test/embed.serial.test.ts.
  • src/core/cli-options.ts extension — maybeBackground(opName, fingerprintArgs, runDirect) helper. Same semantics in TTY and cron (no --no-tty-detect flag, no surprise behavior change between contexts): when --background is passed, submits the op as a Minion job via op_checkpoints for resumability and returns the job_id. --background --follow execs gbrain jobs follow <id> so the user sees the same stderr stream they'd get from a direct call. PGLite degrades to inline execution with a clear stderr note ("PGLite worker pool not yet supported; running inline"). Returns a tagged union the caller dispatches on.
  • openclaw.plugin.json — ClawHub bundle plugin manifest
  • src/commands/capture.ts + src/commands/serve-http.ts + src/core/{operations,import-file,types,utils,facts/absorb-log,brainstorm/{orchestrator,error-classify},scope,postgres-engine,pglite-engine}.ts extensions — ingestion-cathedral productionization after a smoke test against Supabase+PgBouncer. Capture frontmatter merge via mergeCaptureFrontmatter (uses gray-matter directly, NOT the lossy parseMarkdown); /ingest null-guard + outer try/catch envelope with !res.headersSent guard; dedup via separate normalize-for-hash (normalizeForHash strips BOM/CRLF/whitespace/NFKC) + body-after-frontmatter-strip on the DB hash (excludes captured_at + ingested_at so capture-cli timestamp variations don't invalidate the chunk cache); friendly pages_source_id_fk rewrite via maybeRewriteSourceFkError on BOTH local + thin-client callRemoteTool catch blocks; facts:absorb 'No database connection' suppression via typed instanceof GBrainError && e.problem check + first-occurrence stack-trace info log (module-scoped _hasLoggedDisconnectedFactsAbsorb flag, test seam _resetFactsAbsorbDisconnectedFlagForTests); CLI help discoverability (capture added to CLI_ONLY_SELF_HELP + pre-engine-bind --help short-circuit in handleCliOnly + a BRAIN section in printHelp); binary-file guard via detectBinaryNullByte(buf) first-8KB NUL scan on --file (Buffer-read, no encoding) and --stdin (readStdinBuffer accumulator); provenance write-through — put_page accepts 3 optional params (source_kind, source_uri, ingested_via; ingested_at server-stamped) + trust gate (when ctx.remote !== false IGNORE client params, server stamps mcp:put_page, fail-closed) + COALESCE-preserve UPDATE semantics (omitting params on a later put_page preserves prior values; first-write-wins); /admin/api/register-client scopes normalization via normalizeScopesInput(raw: unknown) in src/core/scope.ts (accepts string/string[]/missing; rejects ['read write'] space-in-element shape, non-string elements, empty array, unknown scopes; deduped + sorted); brainstorm timeout surfacing via an orchestrator-level try/catch at runBrainstorm entry (single-point wrap covers every internal SQL site, classifies SQLSTATE 57014 via postgres.js .code / .sqlState / message fallback into StructuredAgentError code brainstorm_timeout with a hint covering all 3 PG cancel sub-causes); read-path surfaces all 4 provenance columns via getPage projection + rowToPage 3-state optional read + Page interface; canonical source resolver routes capture through resolveSourceWithTier(engine, parsed.source, cwd); thin-client --source rejection (server-side OAuth client registration owns source scope); the source_kind taxonomy is closed (capture-cli | put_page | mcp:put_page | webhook | file-watcher | inbox-folder | cron-scheduler), --source maps to source_id only. Tests: test/capture-build-content.test.ts, test/capture-runcapture.test.ts, test/put-page-provenance.test.ts, test/scope-normalize.test.ts, test/cli-help-discoverability.test.ts, test/brainstorm-timeout.test.ts; extended test/facts-absorb-log.test.ts, test/import-file.test.ts, test/e2e/engine-parity.test.ts, test/e2e/serve-http-ingest-webhook.test.ts. Report at docs/v0.38-smoke-test-report.md. Follow-ups in TODOS.md: SQL-shape rewrite of listPrefixSampledPages for PgBouncer, magic-byte allowlist for binary detection, --source-kind override flag, ingest_capture handler migration, provenance-history table, facts:absorb root-cause trace.

BrainBench — in a sibling repo (v0.20+)

BrainBench — the public benchmark for personal-knowledge agent stacks — lives in github.com/garrytan/gbrain-evals. It depends on gbrain as a consumer; gbrain never pulls in the ~5MB eval corpus or the pdf-parse dev dep at install time.

gbrain's public API surface (the exports map in package.json) is what gbrain-evals consumes: gbrain/engine, gbrain/types, gbrain/operations, gbrain/pglite-engine, gbrain/link-extraction, gbrain/import-file, gbrain/transcription, gbrain/embedding, gbrain/config, gbrain/markdown, gbrain/backoff, gbrain/search/hybrid, gbrain/search/expansion, gbrain/extract. Removing any of these is a breaking change for the gbrain-evals consumer.

v0.36.1.0 Hindsight calibration wave (key files cluster)

The wave that taught gbrain to know how the user tends to be wrong + use that knowledge at every advice surface. Six-migration schema (v67-v72), three new cycle phases, eight expansions, one admin tab. Plan persisted at ~/.claude/plans/system-instruction-you-are-working-rippling-knuth.md. Convention skill at skills/conventions/calibration.md has the agent- facing rules.

Hotfix (migration v80): takes_resolution_consistency CHECK widened to accept quality='unresolvable' AND outcome=NULL as the 4th valid resolution state. The column-level CHECK on resolved_quality (takes_resolved_quality_values) enumerates all 4 states. Take.resolved_quality, TakeResolution.quality, and takes-fence.ts:TakeQuality are 4-state. TakesScorecard gains unresolvable_count

  • unresolvable_rate; resolved stays 3-state (correct+incorrect+partial) so historical comparisons hold. finalizeScorecard: unresolvable_rate = unresolvable_count / (resolved + unresolvable_count), NULL when both 0. Spec doc at docs/architecture/calibration-quality-gate-spec.md (falsifiability + per-category calibration ship on top in a follow-up). Pinned by R1-R5 in test/takes-resolution.test.ts and test/migrate.test.ts's v80 structural + PGLite round-trip suite (CHECK admits unresolvable+NULL, still rejects partial+true and unresolvable+true|false, pre-v80 NULL/NULL rows survive).
  • src/core/cycle/base-phase.ts — abstract BaseCyclePhase class. Enforces sourceScopeOpts(ctx) threading at the type level; closes the source-isolation leak class structurally for every new phase. Inherits source-scope, budget meter, error envelope, progress reporter. propose_takes / grade_takes / calibration_profile all extend it.
  • src/core/cycle/propose-takes.ts — LLM scans markdown prose, proposes gradeable claims to the take_proposals queue. Idempotency cache on (source_id, page_slug, content_hash, prompt_version) composite unique index. Fence-dedup: existing canonical takes passed to the extractor as context. Ships a stub prompt; tuned prompt arrives via the synthetic corpus build.
  • src/core/cycle/grade-takes.ts — walks unresolved takes older than 6 months, retrieves evidence, asks judge model, caches verdict. Auto-resolve DISABLED by default. Conservative thresholds: >=0.95 single OR >=0.85 ensemble 3/3 unanimous. aggregateEnsemble reuses the cross-modal substrate; fires on the borderline 0.6-0.95 band. Writes to take_grade_cache.
  • src/core/cycle/calibration-profile.ts — aggregates resolved takes into 2-4 narrative pattern statements + active bias tags. Voice-gated via gateVoice(). Cold-brain skip when <5 resolved. Writes to calibration_profiles with audit columns (voice_gate_passed, voice_gate_attempts, grade_completion).
  • src/core/calibration/voice-gate.ts — single gateVoice() function, mode parameter (pattern_statement | nudge | forecast_blurb | dashboard_caption | morning_pulse). 2 regens then template fallback from src/core/calibration/templates.ts. Haiku judge with mode-specific rubrics; all rubrics structurally forbid clinical/preachy voice.
  • src/core/calibration/cross-brain.ts — 4-rule contract for cross-brain calibration reads. Local-first → mount-fallback (only with canReadMountsForCtx(ctx) true) → cross-brain attribution via source_brain_id + from_mount → subagent prohibition closes the OAuth-token-to-cross-brain-leak surface. All 4 rules pinned in test/cross-brain-calibration.test.ts.
  • src/core/calibration/nudge.ts — real-time pattern surfacing. evaluateAndFireNudge(opts): threshold check (conviction > 0.7, holder match, slug-derived domain hint matches active bias tag) → cooldown probe (14d via take_nudge_log) → fire + log. STDERR-only output; multi-channel deferred.
  • src/core/calibration/take-forecast.ts — Brier-trend at write time. Pure math over existing TakesScorecard; no LLM. Returns predicted_brier, bucket_n, overall_brier. Insufficient-data branch at MIN_BUCKET_N = 5. batchForecast memoizes per (holder, domain) tuple.
  • src/core/calibration/gstack-coupling.ts — outcome-driven learnings coupling. writeIncorrectResolution(opts) shells out to the gstack-learnings-log binary. Config gate cycle.grade_takes.write_gstack_learnings (default false for external users). Namespace prefix gbrain:calibration:v0.36.1.0: so --undo-wave can scrub.
  • src/core/calibration/svg-renderer.ts — server-rendered SVG for the admin SPA Calibration tab. Pure functions: data → SVG string. Inlines design tokens; XSS-safe via escapeXml(). Four renderers: renderBrierTrend, renderDomainBars, renderAbandonedThreadsCard, renderPatternStatementsCard. SPA renders via <TrustedSVG> wrapper behind requireAdmin.
  • src/core/calibration/undo-wave.tsundoWave reverses the wave's mutations: unsets takes.resolved_* for wave-applied resolutions (cross-checks resolved_by so manual writes persist), deletes calibration_profiles, purges nudge logs, marks grade-cache rows applied=false. --dry-run shows counts without writing. Idempotent on wave_version match.
  • src/core/calibration/think-ab.ts — A/B harness. runAbTrial calls thinkRunner twice (baseline + with-calibration), records preference to think_ab_results. buildAbReport aggregates over a 30-day window; flags calibration_net_negative when n>=20 + win rate < 45% on decisive trials.
  • src/core/calibration/recall-footer.ts — formatter for the morning-pulse calibration block. Cold-brain branch when <5 resolved. Opt-in via the wiring layer.
  • src/core/eval-contradictions/calibration-join.ts — cross-reference. tagFindingWithCalibration(finding, profile) returns bias-tag context for contradictions matching active patterns. Returns null when profile missing (output byte-identical to the pre-calibration baseline).
  • src/core/think/prompt.ts extension — anti-bias rewrite. withCalibration option on buildThinkSystemPrompt adds anti-bias rules. buildCalibrationBlock() emits the <calibration> XML. buildThinkUserMessage has TWO shapes: default (question first), and with-calibration (retrieval → calibration → question) when opt-in. Wired into runThink via opts.withCalibration + opts.calibrationHolder.
  • src/commands/calibration.ts — CLI: gbrain calibration (read + print), --regenerate, --undo-wave <ver>, ab-report. MCP op get_calibration_profile (scope: read) backs the same data path. Source-scoped via sourceScopeOpts(ctx).
  • src/commands/serve-http.ts extension — three admin routes: /admin/api/calibration/profile, /admin/api/calibration/charts/:type (image/svg+xml; type in {brier-trend, domain-bars, pattern-statements, abandoned-threads}), /admin/api/calibration/pattern/:id (drill-down).
  • src/commands/takes.ts extension — gbrain takes revisit <slug> opens $EDITOR on the source page with a <!-- gbrain:revisit --> cursor marker.
  • src/commands/doctor.ts extension — 4 checks: abandoned_threads, calibration_freshness, grade_confidence_drift (mitigation surface; math ships later), voice_gate_health.
  • admin/src/pages/Calibration.tsx — Calibration tab. Single-column layout. <TrustedSVG> wrapper handles dangerouslySetInnerHTML for the server-rendered SVG.
  • admin/src/index.css extension — --text-muted: #777 (WCAG AA contrast bump to ~5.5 on the #0a0a0f bg).
  • test/fixtures/calibration/extract-takes-corpus/ — synthetic prompt-tuning corpus. Ships 5 representative pages; full 50-page + 10-page holdout generated by gbrain calibration build-corpus. All anonymized per CLAUDE.md placeholder list.
  • scripts/check-synthetic-corpus-privacy.sh — CI guard in bun run verify. Greps for explicit dollar amounts + verifies non-essay fixtures reference at least one placeholder name.
  • test/regressions/v0.36.1.0-iron-rule.test.ts — R1-R5 regression inventory; pins all 5 IRON-RULE regressions in one place for future bisects.
  • DESIGN.md — repo-root design system. Formalizes the de facto admin tokens. Calibration target for future /plan-design-review and /design-review.

Schema Cathedral v3 (v0.40.7.0)

The schema-pack mutation surface shipped in v0.40.7.0 as the production rebuild of closed community PR #1321 (@garrytan-agents). Six new foundation modules + a mutate skeleton + stats/sync data plane + 14 CLI verbs + 9 MCP ops + a first-class agent skill. See ~/.claude/plans/system-instruction-you-are-working-recursive-thacker.md for the full plan + 21 captured design decisions.

Key files (v0.40.7.0 additions):

  • src/core/schema-pack/pack-lock.ts — Atomic O_CREAT|O_EXCL per-pack lock. DELIBERATELY NOT the existsSync + writeFileSync TOCTOU shape from src/core/page-lock.ts. Default 60s TTL, refresh every 10s while withPackLock(fn) runs, --force semantics = "steal stale lock" NOT "skip locking." Lock path per-pack so two packs never block each other.
  • src/core/schema-pack/mutate-audit.ts — ISO-week JSONL at ~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl. Privacy-redacted: type names → sha8, prefixes → first slug segment only, matches candidate-audit.ts privacy posture. Logs BOTH success AND failure events so the schema_pack_writability doctor check has signal. summarizeMutations() is the cross-surface parity primitive.
  • src/core/schema-pack/registry.ts extensions — invalidatePackCache(name?) walks the extends-chain reverse-graph (editing a parent pack must not leave children stale). tryCachedPack(name) TTL-gated fast path: inside STAT_TTL_MS (default 1000ms, env GBRAIN_PACK_STAT_TTL_MS) returns cached without statting; outside the window it stats every file in the chain and cascade-invalidates on mtime change (cross-process detection).
  • src/core/schema-pack/best-effort.tsloadActivePackBestEffort(ctx) returns ResolvedPack | null. Single source of truth for the T1.5 wiring sites. null means EMPTY FILTER (NOT hardcoded defaults — closes the silent-violation bug class).
  • src/core/schema-pack/lint-rules.ts — 12 pure rule functions. withMutation's pre-write validation gate composes the 10 file-plane rules; the 2 DB-aware rules (extractable_empty_corpus, mutation_count_anomaly) need an engine. Single source of truth consumed by CLI lint + MCP schema_lint + the pre-write validation gate. New file-plane rule link_regex_catastrophic_backtrack — advisory ReDoS pre-screen flagging the classic nested-quantifier shapes ((a+)+, (a*)*, (a+)*, (\w+)+) in a link_type's inference.regex via NESTED_QUANTIFIER_RE. WARNING not error: a hard reject would disable the whole pack on upgrade (pages fall back to legacy typing). The runtime input-length cap in redos-guard.ts is the actual safety net; this rule tells the pack author to fix the pattern.
  • src/core/schema-pack/redos-guard.ts + src/core/schema-pack/link-inference.ts — ReDoS hardening for pack inference regexes. redos-guard.ts adds MAX_REGEX_INPUT_CHARS (default 64_000, env GBRAIN_MAX_REGEX_INPUT_CHARS) — a hard input-length cap, the real runtime safety net (catastrophic backtracking needs a long input; a link-extraction context is normally a sentence or short paragraph). Over the cap, runRegexBounded throws the tagged RegexInputTooLargeError and the regex is skipped (degrade-to-mentions) without entering the node:vm. link-inference.ts:inferLinkTypeFromPack no-budget branch (test contexts) now routes through runRegexBounded so the input-length cap + per-regex vm timeout (PER_REGEX_TIMEOUT_MS = 50) apply on every path (previously this branch ran new RegExp(pattern).test(context) unbounded — the one ReDoS hole with no timeout). Defensive hardening + diagnostics; the deterministic ~3100-file sync-wedge root cause remains open. Pinned by test/redos-hardening.test.ts + test/schema-pack-lint-rules.test.ts.
  • src/core/schema-pack/query-cache-invalidator.tsinvalidateQueryCache(engine, sourceId?) DELETEs query_cache rows so cached search results bound to old page types don't survive a schema mutation.
  • src/core/schema-pack/mutate.ts — 8-step withMutation skeleton (bundled-guard → lock → read → mutator → validate → atomic write → audit → invalidate). 11 mutation primitives: addTypeToPack, removeTypeFromPack (with reference check), updateTypeOnPack, addAliasToType, removeAliasFromType, addPrefixToType, removePrefixFromType, addLinkTypeToPack, removeLinkTypeFromPack, setExtractableOnType, setExpertRoutingOnType. Atomic write via .tmp + fsync + rename — the pack file on disk is NEVER partial. Inline minimal JSON→YAML emitter so YAML packs stay YAML (does NOT preserve comments — pin pack.json if you care about layout).
  • src/core/schema-pack/stats.tsrunStatsCore(engine, opts) returns per-source + aggregate page counts + coverage % + dead_prefixes (declared prefixes with zero matching pages — agent drilldown signal). Multi-source aware (sourceIds[] federated, sourceId single, or whole-brain). PGLite + Postgres parity via executeRaw. Empty brain → coverage:1.0 (vacuous truth).
  • src/core/schema-pack/sync.tsrunSyncCore(engine, opts) chunked UPDATE in 1000-row batches per declared prefix. Concurrent writers never block on a single row >100ms. Write-side scoping via ctx.sourceId directly (NOT sourceScopeOpts, which inherits OAuth read federation). Idempotent on --apply re-run.
  • src/commands/schema.ts extension — 14 CLI verbs in the dispatch table: add-type, remove-type, update-type, add-alias, remove-alias, add-prefix, remove-prefix, add-link-type, remove-link-type, set-extractable, set-expert-routing, stats, sync, reload. withConnectedEngine defensive fix retained. Lifecycle-grouped help text (Inspection / Activation / Authoring / Discovery+repair).
  • src/core/operations.ts extension — 9 MCP ops: get_active_schema_pack, list_schema_packs, schema_stats, schema_lint, schema_graph, schema_explain_type, schema_review_orphans (all read-scope, NOT localOnly), plus schema_apply_mutations (admin scope, NOT localOnly so remote agents can author packs over HTTPS MCP — batched, one MCP tool taking a mutations[] array atomically inside ONE withPackLock, audit log captures actor: mcp:<clientId8>) and reload_schema_pack (admin, NOT localOnly). Trust posture: per-call schema_pack opt STAYS rejected for remote callers via op-trust-gate.ts.
  • src/commands/whoknows.ts + src/core/operations.ts:find_experts — T1.5 wiring sites. Pack-aware via expertTypesFromPack(pack.manifest) from best-effort.ts. Pack-load failure → EMPTY filter (NOT hardcoded ['person', 'company'] defaults). A researcher type declared --expert now surfaces in whoknows results.
  • skills/schema-author/SKILL.md — Agent dispatcher for "evolve the schema pack." Triggers: 15+ phrasings incl. "add a page type", "my brain has untyped pages", "propose new types from my corpus", "backfill page types". Explicit Non-goals callout to brain-taxonomist (files one page) and eiirp (schema-check during iteration) so agents pick the right surface. 7-phase workflow: brain → assess → propose → apply → sync → verify → commit. Lists every gbrain schema CLI verb + every MCP op the skill uses. brain_first: exempt frontmatter. Required conformance sections: Contract, Anti-Patterns, Output Format.
  • skills/conventions/schema-evolution.md — Canonical convention: "when to add a type vs alias vs prefix." Decision tree: <20 pages → don't pack-codify; 20-100 → alias or narrow prefix on existing type; 100+ → first-class type. Don'ts section + "when to remove a type" + "when to commit the pack" all answered in one place.
  • skills/RESOLVER.md + skills/manifest.json — schema-author wired into the dispatcher with the full functional-area trigger list (compressed routing pattern per the dispatcher convention).

T1.5 wiring is partial in v0.40.7.0. Three follow-ups filed in TODOS.md under "v0.40.7.0 Schema Cathedral v3 follow-ups (v0.40.7+)" — enrichment-service.ts union widening ('person' | 'company'string), facts/eligibility.ts pack-aware ELIGIBLE_TYPES wiring, and 3 doctor checks (schema_pack_coverage, schema_pack_writability, schema_pack_mutation_audit).