mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* v0.36.5.0 feat: secure DATABASE_URL access for shell jobs (inherit: ["database_url"]) Replaces PR #1137's plaintext-config / plaintext-env workarounds with code. Shell-job params gain `inherit: ["database_url"]`, validated pre-enqueue in both the CLI (`gbrain jobs submit`) and `submit_job` MCP op handler. Worker resolves the value from its own loadConfig() at child-spawn time; the persisted `minion_jobs.data` row stores only the name. Plain `env: { GBRAIN_DATABASE_URL: ... }` / `env: { DATABASE_URL: ... }` / `env: { GBRAIN_DIRECT_DATABASE_URL: ... }` are rejected pre-enqueue with a paste-ready hint pointing at `inherit:`. Codex pre-landing review caught two bypasses + one missing shadow name: - H1: cmd/argv inline-secret regex scan (cmd:"GBRAIN_DATABASE_URL=... gbrain sync" was a clean bypass — fixed) - H3: GBRAIN_DIRECT_DATABASE_URL added to shadowKeys - H2: honest docs about output-side leakage (stdout_tail/stderr_tail can still carry the value if the script prints it; that's the script author's responsibility, not gbrain's) Also: gbrain doctor learns home_dir_in_worktree (warns when ~/.gbrain lives inside a git worktree); ~/.gbrain/.gitignore retroactive via saveConfig + post-upgrade. New canonical guide: docs/guides/agent-to-gbrain.md (two-domain framing for downstream agent authors: MCP ops via OAuth vs localOnly admin ops via shell-job inherit:). Closes #1137. Tests: +53 new (21 validator + 12 inherit-record + 6 ensureGitignore + 5 doctor + 2 PGLite E2E + 7 codex-driven H1/H3 cases). Credit: @wintermute filed PR #1137 which made the env-stripping gap visible enough to fix in code. Thank you. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * v0.36.5.0 redesign: free-form inherit:, drop closed enum User feedback: "agent spawning minions should have agency to do what it wants with secrets and pass only the ones that it needs. don't be a security nazi please." Replaces the closed INHERITABLE enum (database_url only) with three small helpers in shell-inherit.ts: - INHERIT_NAME_RE: snake_case shape guard. Rejects __proto__, leading underscore, uppercase, path-traversal. Prototype-pollution defense. - deriveEnvKey(name): config-key → child-env-key. Uppercase by default with one override: database_url → GBRAIN_DATABASE_URL. - resolveInheritValue(cfg, name): value lookup with Object.hasOwn. inherit: now accepts any snake_case config-key the worker has. Agent picks what it needs per-job (database_url, anthropic_api_key, voyage_api_key, or any custom field). Validator does NOT police WHICH keys — single-uid trust model treats agent as peer of worker. Drops the v0.36.5.0-RC rules that were paternalistic for the actual threat model: - closed-enum check - env-shadow rejection - cmd/argv inline-secret scan Keeps the parts that defend real problems: - pre-enqueue validation (closes the persistence-before-throw window) - snake_case regex (prototype-pollution + audit-log readability) - fail-fast on missing config value (UX guardrail, not security) Tests: shell-validate (existing rules + new free-form + prototype-pollution defense + T1 regression guard) and shell-inherit (regex matrix, deriveEnvKey per-name, resolveInheritValue with hasOwn defense). E2E case now exercises inherit:["anthropic_api_key"] to prove genuinely free-form. Docs and CHANGELOG rewritten to reflect the open design + the design-arc story (closed → cut → free-form). Migration file too. 7653 unit tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * v0.36.5.0 add: redact_secrets opt-in for stdout/stderr scrubbing Honest defense for the documented output-side leakage. When a script prints an inherited secret, the value lands plaintext in result.stdout_tail / result.stderr_tail / error_text. v0.36.5.0 adds: - `redact_secrets: true` ShellJobParams field - `--redact-secrets` CLI convenience flag on `gbrain jobs submit shell` - shell-redact.ts: pure `redactSecretsInText(text, secrets)` helper (string-mode replaceAll; regex metachars in values stay literal) - Handler post-processes both tails before throw/return, so the persisted row carries `<REDACTED:name>` tokens instead of values Only inherit-resolved values are scrubbed. env: values are not (those are the agent's "fine in the row" channel by design). Heuristic — defeats accidental `echo "$GBRAIN_DATABASE_URL"`, not adversarial encode-then-print. Default false for back-compat. Tests: - test/minions-shell-redact.test.ts (9 cases): pure-function behavior, regex-metachar safety, multi-secret independent redaction, substring overlap, empty-input/map edge cases - test/minions-shell-validate.test.ts: +4 cases for redact_secrets shape - test/e2e/minions-shell-pglite.test.ts: +2 cases proving redact_secrets: true scrubs persisted row AND redact_secrets:false preserves plaintext (back-compat regression guard) Docs + CHANGELOG + migration file + CLAUDE.md updated. 7667 unit tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
6068 lines
505 KiB
Plaintext
6068 lines
505 KiB
Plaintext
# GBrain — Full Context
|
||
|
||
> GBrain is a personal knowledge brain and GStack mod for agent platforms. Pluggable engines (PGLite default, Postgres+pgvector for scale), contract-first operations, 26 fat-markdown skills. Teaches agents brain ops, ingestion, enrichment, scheduling, identity, and access control.
|
||
|
||
This file concatenates core GBrain documentation for single-fetch ingestion.
|
||
For the link-only index, see `llms.txt`. Source of truth: https://github.com/garrytan/gbrain.
|
||
|
||
# Core entry points
|
||
|
||
## AGENTS.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md
|
||
|
||
# Agents working on GBrain
|
||
|
||
This is your install + operating protocol. Claude Code reads `./CLAUDE.md` automatically.
|
||
Everyone else (Codex, Cursor, OpenClaw, Aider, Continue, or an LLM fetching via URL):
|
||
start here.
|
||
|
||
## Install (5 min)
|
||
|
||
1. Install gbrain via Bun (the canonical path):
|
||
```bash
|
||
curl -fsSL https://bun.sh/install | bash
|
||
export PATH="$HOME/.bun/bin:$PATH"
|
||
bun install -g github:garrytan/gbrain
|
||
```
|
||
If `bun install -g` aborts or `gbrain doctor` reports `schema_version: 0`,
|
||
the CLI prints a recovery hint pointing at [#218](https://github.com/garrytan/gbrain/issues/218).
|
||
Run `gbrain apply-migrations --yes` to recover, or fall back to the
|
||
deterministic install: `git clone https://github.com/garrytan/gbrain.git ~/gbrain && cd ~/gbrain && bun install && bun link`.
|
||
2. Init the brain: `gbrain init` (defaults to PGLite, zero-config). For 1000+ files or
|
||
multi-machine sync, init suggests Postgres + pgvector via Supabase.
|
||
3. **STOP — ask the user about search mode.** `gbrain init` auto-applied a
|
||
default but printed a 9-cell cost matrix (mode × downstream model)
|
||
preceded by `[AGENT]` markers. You MUST relay the matrix to the operator
|
||
and confirm their choice before continuing. Cost spread between corners
|
||
is 25x — silent acceptance is the wrong default. See
|
||
[`./INSTALL_FOR_AGENTS.md`](./INSTALL_FOR_AGENTS.md) Step 3.5 for the
|
||
exact ask-the-user protocol. Same banner fires on `gbrain post-upgrade`
|
||
for existing users (search modes were added in v0.32.3).
|
||
4. Read [`./INSTALL_FOR_AGENTS.md`](./INSTALL_FOR_AGENTS.md) for the full 9-step flow
|
||
(API keys, identity, cron, verification).
|
||
|
||
## Read this order
|
||
|
||
1. `./AGENTS.md` (this file) — install + operating protocol.
|
||
2. [`./CLAUDE.md`](./CLAUDE.md) — architecture reference, key files, trust boundaries,
|
||
test layout.
|
||
3. [`./docs/architecture/brains-and-sources.md`](./docs/architecture/brains-and-sources.md)
|
||
— the two-axis mental model (brain = which DB, source = which repo in the DB). Every
|
||
query routes on both axes. Read before writing anything that touches brain ops.
|
||
4. [`./skills/conventions/brain-routing.md`](./skills/conventions/brain-routing.md) —
|
||
agent-facing decision table: when to switch brain, when to switch source, how
|
||
cross-brain federation works (latent-space only; the agent decides).
|
||
5. [`./skills/RESOLVER.md`](./skills/RESOLVER.md) — skill dispatcher. Read before any task.
|
||
|
||
## Trust boundary (critical)
|
||
|
||
GBrain distinguishes **trusted local CLI callers** (`OperationContext.remote = false`,
|
||
set by `src/cli.ts`) from **untrusted agent-facing callers** (`remote = true`, set by
|
||
`src/mcp/server.ts`). Security-sensitive operations like `file_upload` tighten filesystem
|
||
confinement when `remote = true` and default to strict behavior when unset. If you are
|
||
writing or reviewing an operation, consult `src/core/operations.ts` for the contract.
|
||
|
||
## Common tasks
|
||
|
||
- **Configure:** [`docs/ENGINES.md`](./docs/ENGINES.md),
|
||
[`docs/guides/live-sync.md`](./docs/guides/live-sync.md),
|
||
[`docs/mcp/DEPLOY.md`](./docs/mcp/DEPLOY.md).
|
||
- **Debug:** [`docs/GBRAIN_VERIFY.md`](./docs/GBRAIN_VERIFY.md),
|
||
[`docs/guides/minions-fix.md`](./docs/guides/minions-fix.md), `gbrain doctor --fix`.
|
||
- **Migrate / upgrade:** `gbrain upgrade` (binary self-update + schema migrations + post-upgrade prompts),
|
||
[`docs/UPGRADING_DOWNSTREAM_AGENTS.md`](./docs/UPGRADING_DOWNSTREAM_AGENTS.md),
|
||
[`skills/migrations/`](./skills/migrations/), `gbrain apply-migrations --yes` (manual schema-only).
|
||
- **Eval retrieval changes:** capture is off by default. To benchmark a
|
||
retrieval change against real captured queries, set
|
||
`GBRAIN_CONTRIBUTOR_MODE=1`, then `gbrain eval export --since 7d > base.ndjson`
|
||
and `gbrain eval replay --against base.ndjson`. For public benchmark
|
||
coverage (LongMemEval, ground-truth scoring), `gbrain eval longmemeval
|
||
<dataset.jsonl>` (v0.28.8) runs against an isolated in-memory PGLite
|
||
per question — your `~/.gbrain` is never opened. Full guide:
|
||
[`docs/eval-bench.md`](./docs/eval-bench.md).
|
||
- **Drive the brain to a target health score (v0.36.4.0):** the one-command
|
||
loop. `gbrain doctor --remediation-plan --json` previews what would be
|
||
fixed; `gbrain doctor --remediate --yes --target-score 90 --max-usd 5`
|
||
walks a dependency-ordered plan (sync before extract, embed after
|
||
consolidate), re-checking score between every step, refusing to spend
|
||
past the cost cap. Empty brains (no entity pages) or unconfigured embedding
|
||
keys hit a `max_reachable_score` ceiling and bail with what's missing.
|
||
Three phase handlers (synthesize / patterns / consolidate) are
|
||
PROTECTED — only trusted local callers can submit them; MCP cannot.
|
||
Reference: [`docs/architecture/topologies.md`](./docs/architecture/topologies.md)
|
||
and the CHANGELOG entry for v0.36.4.0.
|
||
- **Track a founder/company over time (v0.35.7):** when an entity has
|
||
typed metric claims in its `## Facts` fence (`metric: mrr`, `value: 50000`,
|
||
`unit: USD`, `period: monthly` columns), run
|
||
`gbrain eval trajectory <entity-slug>` for the chronological history
|
||
with regressions auto-flagged, or `gbrain founder scorecard <entity-slug>`
|
||
for a four-signal JSON rollup (claim_accuracy / consistency /
|
||
growth_trajectory / red_flags). MCP op `find_trajectory` exposes the
|
||
same data — read scope, visibility-filtered for remote callers.
|
||
- **Everything else:** [`./llms.txt`](./llms.txt) is the full documentation map.
|
||
[`./llms-full.txt`](./llms-full.txt) is the same map with core docs inlined for
|
||
single-fetch ingestion.
|
||
|
||
## Before shipping
|
||
|
||
Easiest path: `bun run ci:local` runs the full CI gate inside Docker (gitleaks,
|
||
unit tests with `DATABASE_URL` unset, then all 29 E2E files sequentially against a
|
||
fresh pgvector container) and tears down. Use `bun run ci:local:diff` for the
|
||
diff-aware subset during fast iteration on a focused branch. Requires Docker
|
||
(Docker Desktop / OrbStack / Colima) and `gitleaks` (`brew install gitleaks`).
|
||
|
||
Manual path: `bun test` plus the E2E lifecycle described in `./CLAUDE.md` (spin
|
||
up the test Postgres container, run `bun run test:e2e`, tear it down).
|
||
|
||
Ship via the `/ship` skill, not by hand.
|
||
|
||
## Privacy
|
||
|
||
Never commit real names of people, companies, or funds into public artifacts. See the
|
||
Privacy rule in `./CLAUDE.md`. GBrain pages reference real contacts; public docs must
|
||
use generic placeholders (`alice-example`, `acme-example`, `fund-a`).
|
||
|
||
## Forks
|
||
|
||
If you are a fork, regenerate `llms.txt` + `llms-full.txt` with your own URL base before
|
||
publishing: `LLMS_REPO_BASE=https://raw.githubusercontent.com/your-org/your-fork/main bun run build:llms`.
|
||
|
||
---
|
||
|
||
## CLAUDE.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/CLAUDE.md
|
||
|
||
# CLAUDE.md
|
||
|
||
GBrain is a personal knowledge brain and GStack mod for agent platforms. Pluggable
|
||
engines: PGLite (embedded Postgres via WASM, zero-config default) or Postgres + pgvector
|
||
+ hybrid search in a managed Supabase instance. `gbrain init` defaults to PGLite;
|
||
suggests Supabase for 1000+ files. GStack teaches agents how to code. GBrain teaches
|
||
agents everything else: brain ops, signal detection, content ingestion, enrichment,
|
||
cron scheduling, reports, identity, and access control.
|
||
|
||
## Two organizational axes (read this first)
|
||
|
||
GBrain knowledge is organized along two orthogonal axes. Users AND agents must
|
||
understand both, or queries misroute silently.
|
||
|
||
- **Brain** — WHICH DATABASE. Your personal brain is `host`. You can mount
|
||
additional brains (team-published, each with their own DB and access policy)
|
||
via `gbrain mounts add` (v0.19+). Routing: `--brain`, `GBRAIN_BRAIN_ID`,
|
||
`.gbrain-mount` dotfile.
|
||
- **Source** — WHICH REPO INSIDE THE DATABASE. A brain can hold many sources
|
||
(wiki, gstack, openclaw, essays). Slugs scope per source. Routing:
|
||
`--source`, `GBRAIN_SOURCE`, `.gbrain-source` dotfile.
|
||
|
||
Both axes follow the same 6-tier resolution pattern. Read
|
||
`docs/architecture/brains-and-sources.md` for topology diagrams (personal, team
|
||
mount, CEO-class with multiple team brains) and
|
||
`skills/conventions/brain-routing.md` for the agent-facing decision table.
|
||
|
||
## Architecture
|
||
|
||
Contract-first: `src/core/operations.ts` defines ~47 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`). CLI and MCP
|
||
server are both generated from this single source. Engine factory (`src/core/engine-factory.ts`)
|
||
dynamically imports the configured engine (`'pglite'` or `'postgres'`). Skills are fat
|
||
markdown files (tool-agnostic, work with both CLI and plugin contexts).
|
||
|
||
**Trust boundary:** `OperationContext.remote` distinguishes trusted local CLI callers
|
||
(`remote: false` set by `src/cli.ts`) from untrusted agent-facing callers
|
||
(`remote: true` set by `src/mcp/server.ts`). Security-sensitive operations like
|
||
`file_upload` tighten filesystem confinement when `remote=true` and default to
|
||
strict behavior when unset.
|
||
|
||
## Key files
|
||
|
||
- `src/core/operations.ts` — Contract-first operation definitions (the foundation). Also exports upload validators: `validateUploadPath`, `validatePageSlug`, `validateFilename`, plus `matchesSlugAllowList(slug, prefixes)` (v0.23 glob matcher: `<prefix>/*` matches recursive children; bare `<prefix>` matches exact only). `OperationContext.remote` flags untrusted callers; `OperationContext.allowedSlugPrefixes` (v0.23) is the trusted-workspace allow-list set by the dream cycle. `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 enabled for trusted-workspace writes (skipped only when `remote=true && !trustedWorkspace`). As of v0.26.0, every `Operation` also carries `scope?: 'read' | 'write' | 'admin'` + `localOnly?: boolean`. All ops are annotated; `sync_brain`, `file_upload`, `file_list`, and `file_url` are `admin + localOnly` (rejected over HTTP). `OperationContext.auth?: AuthInfo` is threaded through HTTP dispatch for scope enforcement in `serve-http.ts` before the op runs. **v0.26.9 (D12 + F7b):** `OperationContext.remote` is now a REQUIRED field in the TypeScript type — the compiler is the first defense against transports that forget to set it. Four trust-boundary call sites (`put_page` allowlist, file_upload trust-narrowing, submit_job protected-name guard, auto-link skip) flipped from falsy-default (`!ctx.remote`) to fail-closed semantics (`ctx.remote === false` for "trusted-only" sites and `ctx.remote !== false` for "untrust unless explicit-false"). Anything that isn't strictly `false` is now treated as remote. Closed an HTTP MCP shell-job RCE: a `read+write`-scoped OAuth token could submit `shell` jobs because the HTTP request handler's literal context skipped `remote: true` and `submit_job`'s protected-name guard saw a falsy undefined. Stdio MCP set the field correctly via dispatch.ts; HTTP inlined a parallel context-builder for several releases and lost it. **v0.34.1.0 (#861 + #876):** new helper `sourceScopeOpts(ctx)` encodes the precedence ladder for source-scoped reads — federated array (`ctx.auth.allowedSources`) wins over scalar (`ctx.sourceId` / `ctx.auth.sourceId`) over nothing. Every read-side op handler routes through it so future ops can't silently drift from the canonical v0.31.8 thread. Closes the source-isolation leak on the read path: a `read+write`-scoped OAuth client bound to `--source dept-x` no longer sees rows from neighboring sources via `search` / `query` / `list_pages` / `get_page` / `find_experts` / `query`'s image path.
|
||
- `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 v0.12.1 bulk-insert API (`addLinksBatch` / `addTimelineEntriesBatch`). As of v0.13.1, `BrainEngine` has a `readonly kind: 'postgres' | 'pglite'` discriminator so migrations (`src/core/migrate.ts`) and other consumers can branch on engine without `instanceof` + dynamic imports. **v0.29:** four new methods — `batchLoadEmotionalInputs(slugs?)` (CTE-shaped read with per-table aggregates so a 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)` for multi-source safety), `getRecentSalience(opts)`, `findAnomalies(opts)`. `PageFilters` extended with `sort?: 'updated_desc' | 'updated_asc' | 'created_desc' | 'slug'` + `PAGE_SORT_SQL` whitelist consumed by both engines (was hardcoded `ORDER BY updated_at DESC`). **v0.32.8 (PR #860):** new `listAllPageRefs(): Promise<Array<{slug, source_id}>>` ordered by `(source_id, slug)`. Cheap cross-source enumeration for hot loops on large brains — replaces the `getAllSlugs()→getPage(slug)` N+1 pattern in extract-takes, extract, integrity, which silently defaulted to `source_id='default'` for non-default-source pages. Implementation parity across postgres-engine.ts + pglite-engine.ts. Pinned by `test/e2e/multi-source-bug-class.test.ts`. **v0.34.1.0 (#861):** `SearchOpts` + `PageFilters` add `sourceIds?: string[]` for the federated read axis; both engines apply `WHERE source_id = ANY($N::text[])` when the array is set and preserve the scalar `sourceId` fast path when unset. `traverseGraph(slug, depth, opts?)` and `traversePaths(slug, opts?)` accept `opts.sourceId` / `opts.sourceIds` so graph walks respect the caller's scope. **v0.35.6.0:** two new methods supporting the phantom-redirect cycle pass — `refreshPageBody(slug, sourceId, compiled_truth, timeline, content_hash)` narrow-UPDATEs three columns + updated_at, skipping soft-deleted rows (codex #7: content_hash refresh is 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 — codex #3 fix for the writeFactsToFence lossy-migration trap. Both methods have engine parity tests at `test/phantom-redirect-engine-parity.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 40 BrainEngine methods. `addLinksBatch` / `addTimelineEntriesBatch` use multi-row `unnest()` with manual `$N` placeholders. As of v0.13.1, `connect()` wraps `PGlite.create()` in a try/catch that emits an actionable error naming the macOS 26.3 WASM bug (#223) and pointing at `gbrain doctor`; the lock is released on failure so the next process can retry cleanly. v0.22.0: `searchKeyword` and `searchKeywordChunks` multiply `ts_rank` by the source-factor CASE expression at the chunk-grain level; `searchVector` becomes 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 contract. As of v0.22.6.1, `initSchema()` calls `applyForwardReferenceBootstrap()` BEFORE replaying SCHEMA_SQL — probes for the specific forward-referenced state the embedded schema blob needs (`pages.source_id`, `links.link_source`, `links.origin_page_id`, `content_chunks.symbol_name`, `content_chunks.language`, `sources` FK target table) and adds only what's missing. Closes the upgrade-wedge bug class that bit users 10+ times across 6 schema versions over 2 years (#239/#243/#266/#357/#366/#374/#375/#378/#395/#396). No-op on fresh installs and modern brains. **v0.35.5.0:** probe set extended in parity with postgres-engine.ts — `files.source_id`, `files.page_id`, `oauth_clients.source_id`, `oauth_clients.federated_read`, `sources.archived`, `sources.archived_at`, `sources.archive_expires_at`. Bootstrap also threads the DDL connection from `initSchema` so probes run inside the advisory-lock scope. Closes #1018, #974, #820.
|
||
- `src/core/pglite-schema.ts` — PGLite-specific DDL (pgvector, pg_trgm, triggers)
|
||
- `src/core/postgres-engine.ts` — Postgres + pgvector implementation (Supabase / self-hosted). `addLinksBatch` / `addTimelineEntriesBatch` use `INSERT ... SELECT FROM unnest($1::text[], ...) JOIN pages ON CONFLICT DO NOTHING RETURNING 1` — 4-5 array params regardless of batch size, sidesteps the 65535-parameter cap. As of v0.12.3, `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 (contributed by @garagon). `getEmbeddingsByChunkIds` uses `tryParseEmbedding` so one corrupt row skips+warns instead of killing the query. v0.22.0: `searchKeyword`, `searchKeywordChunks`, and `searchVector` apply source-aware ranking by inlining the source-factor CASE and `NOT (col LIKE …)` hard-exclude clause from `src/core/search/sql-ranking.ts`. `searchVector` switches to a two-stage CTE (HNSW-safe inner ORDER BY, source-boost re-rank in the outer SELECT) and carries `p.source_id` through inner→outer for v0.18 multi-source callers. v0.22.1 (#406): `_savedConfig` retains the connect config; `reconnect()` tears down + recreates the pool from saved config (called by supervisor watchdog after 3 consecutive health-check failures). `executeRaw` is a single-statement passthrough — no per-call retry (D3 dropped that as unsound for non-idempotent statements; recovery is supervisor-driven). v0.22.1 (#363, contributed by @orendi84): `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. v0.22.1 (#409, contributed by @atrevino47): `countStaleChunks()` + `listStaleChunks()` server-side-filter on `embedding IS NULL` for `embed --stale`, eliminating ~76 MB/call client-side pull on a fully-embedded brain; `upsertChunks()` resets both `embedding` AND `embedded_at` to NULL when chunk_text changes without a new embedding (consistency). As of v0.22.6.1, `initSchema()` calls `applyForwardReferenceBootstrap()` BEFORE replaying SCHEMA_SQL on the same forward-reference probe set as the PGLite engine, so old Postgres brains pinned at v0.13/v0.18/v0.19 walk forward cleanly instead of wedging on `column "..." does not exist`. **v0.35.5.0:** probe set extended for the column-only forward-reference cases the original v0.22.6.1 sweep missed — `files.source_id`, `files.page_id` (pre-v0.18 brains where `idx_files_source_id` was the choke point), `oauth_clients.source_id`, `oauth_clients.federated_read` (pre-v0.34 brains where v60+v61+v65 chain failed), and `sources.archived` + `sources.archived_at` + `sources.archive_expires_at` (pre-v0.26.5 brains where `CREATE TABLE IF NOT EXISTS sources` was a no-op on existing tables so the archive lifecycle columns never landed). Also (Codex P1 from pre-landing review): the entire probe path now runs on the DDL connection threaded down from `initSchema` — previously probes ran through the instance pool while the advisory lock sat on a different connection, opening a concurrent-bootstrap race for Supabase pooler users. Closes #1018, #974, #820. **v0.28.1:** `disconnect()` is now idempotent. New `_connectionStyle` instance field 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 falling through to `db.disconnect()` and clobbering the singleton. Pinned by `test/e2e/postgres-engine-disconnect-idempotency.test.ts` (2 cases). Closes the bug class where any test sharing an engine across multiple `worker.start()` / `worker.stop()` cycles silently broke its own DB connectivity.
|
||
- `src/core/cjk.ts` (v0.32.7 CJK wave) — Single source of truth for CJK detection across the codebase. 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 '\\'`). Replaces the inline hasCJK regex previously duplicated at `expansion.ts:58`. BMP-only ranges (Han / Hiragana / Katakana / Hangul Syllables); widening to Unicode property escapes is a v0.33+ TODO. 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` (v0.32.7 CJK wave) — 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()` from shell-audit.ts. Separate surface from `sync-failures.jsonl` per codex outside-voice review — that file carries bookmark-gating semantics that info events shouldn't trigger.
|
||
- `src/core/embedding-pricing.ts` (v0.32.7 CJK wave) — `EMBEDDING_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 approximation. Unknown providers degrade gracefully to "estimate unavailable" instead of fabricating numbers.
|
||
- `src/core/post-upgrade-reembed.ts` (v0.32.7 CJK wave) — 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 out with a doctor-warning marker; `GBRAIN_REEMBED_GRACE_SECONDS=0` skips the wait.
|
||
- `src/commands/reindex.ts` (v0.32.7 CJK wave) — `gbrain 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` against the stored `compiled_truth`. **Both paths pass `forceRechunk: true`** to bypass `importFromContent`'s `content_hash` short-circuit — without that flag (codex post-merge F1), the chunker version bump never reaches pages whose source content hasn't changed since last sync, AND master's v0.32.2 stripFactsFence privacy strip never applies to pre-strip chunks. Idempotent — partial-completion re-runs pick up where they left off via id-ordered batches. Wired into `src/commands/upgrade.ts:runPostUpgrade` after `apply-migrations`.
|
||
- `src/commands/sync.ts:resolveSlugByPathOrSourcePath` (v0.32.7 CJK wave, codex post-merge F4) — 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/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 as of v0.12.3 `tryParseEmbedding(value)` (returns `null` + warns once per process, used by search/rescore paths where availability matters more than strictness). **v0.26.9 (D14):** adds `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 path. Reusable from any future code that needs the same column-existence probe semantics. **v0.32.8 (PR #860):** adds `validateSourceId(id)` that 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')` call so source_id can't traverse out of brainDir. `rowToPage` updated to populate the now-required `Page.source_id` field from the SELECT projection (`scripts/check-source-id-projection.sh` enforces that every projection feeding `rowToPage` includes the column).
|
||
- `src/core/db.ts` — Connection management, schema initialization. v0.22.1 (#363, contributed by @orendi84): `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 (unlike the prior `setSessionDefaults` post-pool SET, kept as a back-compat no-op shim).
|
||
- `src/commands/migrate-engine.ts` — Bidirectional engine migration (`gbrain migrate --to supabase/pglite`)
|
||
- `src/core/import-file.ts` — importFromFile + importFromContent (chunk + embed + tags)
|
||
- `src/core/sync.ts` — Pure sync functions (manifest parsing, filtering, slug conversion). **v0.35.5.0:** new exported `pruneDir(name: string): boolean` helper is the single source of truth for descent-time directory exclusion across walkers. Blocks `node_modules` (no leading dot, so pre-v0.35.5 walkers slipped through and inflated MISSING_OPEN counts via vendor packages), dot-prefix dirs, `ops/`, and `*.raw` sidecars. `isSyncable` now applies it per path segment; `walkMarkdownFiles` in `src/commands/extract.ts` and `listTextFiles` in `src/core/cycle/transcript-discovery.ts` consult it BEFORE recursing so the IO cost of walking thousands of vendor files is saved. Closes #923 + #202. `manageGitignore` worktree fix in same wave: discriminator now matches the gitdir path segment (`/modules/<name>` = submodule, `/worktrees/<name>` = worktree, per Git's documented layout) instead of the legacy absolute-vs-relative check that misclassified absorbed submodules and worktrees both. Conductor worktrees are first-class repos and now get `.gitignore` management for storage-tiering. Closes #889. v0.22.12 (#500, foundation by @wintermute via #501): `classifyErrorCode(errorMsg)` regex-based 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` fallback. `summarizeFailuresByCode(failures)` returns sorted `[{code, count}]`. `code?` optional field on `SyncFailure`; backfilled at ack time on pre-v0.22.12 entries. `acknowledgeSyncFailures()` returns `AcknowledgeResult { count, summary }`. Three regexes (`MISSING_OPEN`, `MISSING_CLOSE`, `EMPTY_FRONTMATTER`) broadened to match actual `markdown.ts:159-244` validator message strings, not just the literal code-name prefix. `FILE_TOO_LARGE` covers all three production size sites in `import-file.ts:199, 352, 401`; `SYMLINK_NOT_ALLOWED` covers the rejection at `:347`. Closes the silent-skip pattern that motivated #500.
|
||
- `src/core/storage.ts` — Pluggable storage interface (S3, Supabase Storage, local)
|
||
- `src/core/storage-config.ts` (v0.22.11) — 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`. Replaces gray-matter (broken on delimiter-less YAML) with a dedicated parser for the `gbrain.yml` shape.
|
||
- `src/core/disk-walk.ts` (v0.22.11) — `walkBrainRepo(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-remote.ts` (v0.35.3.0) — 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. New `GIT_SSRF_SUBCOMMAND_FLAGS = ['--no-recurse-submodules']` is **subcommand-scoped**, spread AFTER the verb. Pre-v0.35.3 a single combined `GIT_SSRF_FLAGS` array spread `--no-recurse-submodules` before the verb where real git rejects it with exit 129 ("unknown option"); the fake-git test harness exited 0 regardless of argv shape, so CI missed it for ~7 months and every remote-source clone/pull was silently broken. `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.ts` (v0.22.11) — `gbrain storage status [--repo P] [--json]`. Split into pure data (`getStorageStatus`) + JSON formatter + human formatter (ASCII-only per D10) matching the `orphans.ts` pattern. `PageCountsByTier` and `DiskUsageByTier` are distinct nominal types so swaps fail at compile time.
|
||
- `gbrain.yml` (brain repo root, v0.22.11) — 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). v0.19.0 adds `code.ts` — tree-sitter-based semantic chunker for 29 languages with embedded-asset WASMs (`src/assets/wasm/`), `@dqbd/tiktoken` cl100k_base tokenizer, small-sibling merging. `CHUNKER_VERSION` constant folded into `importCodeFile`'s `content_hash` so chunker shape changes force clean re-chunks across releases.
|
||
- `src/core/errors.ts` (v0.19.0) — `StructuredAgentError` + `buildError` + `serializeError`. Every new v0.19.0 agent-facing surface (code-def, code-refs, usage errors) uses this envelope; matches v0.17.0 `CycleReport.PhaseResult.error` shape.
|
||
- `src/assets/wasm/` (v0.19.0) — 36 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.
|
||
- `src/commands/code-def.ts` + `src/commands/code-refs.ts` (v0.19.0) — 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.
|
||
- `src/core/search/` — Hybrid search: vector + keyword + RRF + multi-query expansion + dedup. As of v0.22.0, `searchKeyword` / `searchKeywordChunks` / `searchVector` apply source-aware ranking at the SQL layer (curated content like `originals/`, `concepts/`, `writing/` outranks bulk content like `wintermute/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` (v0.22.0) — 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, wintermute/chat/ 0.5) and `DEFAULT_HARD_EXCLUDES` (test/, archive/, attachments/, .raw/). `parseSourceBoostEnv` / `parseHardExcludesEnv` parse comma-separated `prefix:factor` pairs from `GBRAIN_SOURCE_BOOST` / `GBRAIN_SEARCH_EXCLUDE` env vars. `resolveBoostMap` and `resolveHardExcludes` merge defaults + env + caller `SearchOpts.exclude_slug_prefixes`/`include_slug_prefixes`.
|
||
- `src/core/search/sql-ranking.ts` (v0.22.0) — Pure SQL string builders. `buildSourceFactorCase(slugColumn, boostMap, detail)` emits a CASE expression 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 matters because it's Postgres LIKE's default escape char). Single-quote doubling on SQL string literals so injection-style inputs are inert text.
|
||
- `src/commands/eval.ts` — `gbrain eval` command: single-run table + A/B config comparison. v0.25.0 adds sub-subcommand dispatch on `args[0]` so `gbrain eval export` + `gbrain eval prune` + `gbrain eval replay` route into session-capture handlers; bare `gbrain eval --qrels …` fall-through preserves the legacy IR-metrics flow. v0.27.x adds `gbrain eval cross-modal` to 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` (v0.27.x) — 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 per Q3=A in plans/radiant-napping-lerdorf.md). 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 (T11=B partial cost guardrail). Receipts land at `gbrainPath('eval-receipts')/<slug>-<sha8-of-output>.json`. The full `--budget-usd` cap is a v0.27.x follow-up TODO.
|
||
- `src/core/cross-modal-eval/json-repair.ts` (v0.27.x) — `parseModelJSON(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` (v0.27.x) — pure verdict logic. Pass criterion: `(successes >= 2) AND (every dim mean >= 7) AND (every dim min across models >= 5)` (Q2=A floor). Inconclusive when <2/3 models returned parseable scores (Q3=A regression guard for the v1 .mjs `Object.values({}).every(...) === true` empty-array PASS bug).
|
||
- `src/core/cross-modal-eval/runner.ts` (v0.27.x) — orchestrator. Each cycle runs `Promise.allSettled([gwChat(slotA), gwChat(slotB), gwChat(slotC)])` (T4=A — bare allSettled, no rate-leases for the CLI path; minion-integration TODO recovers cross-process concurrency). 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` (v0.27.x) — receipt filename binds (slug, SKILL.md sha-8). `findReceiptForSkill(skillPath, receiptDir)` returns `'found' | 'stale' | 'missing'` (T10=A). Skillify-check item 11 surfaces the status as informational (T7=C); the audit does NOT fail on missing/stale receipts.
|
||
- `src/core/cross-modal-eval/receipt-write.ts` (v0.27.x) — wraps `fs.writeFileSync` with `mkdirSync({recursive:true})` ahead of every write (T5 correction; `gbrainPath()` does NOT auto-mkdir).
|
||
- `src/commands/eval-export.ts` (v0.25.0) — 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` (v0.25.0) — explicit retention cleanup. Requires `--older-than DUR`. `--dry-run` reports would-delete count.
|
||
- `src/commands/eval-replay.ts` (v0.25.0) — 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 that closes the gap between "data captured" and "data used to gate a PR." See `docs/eval-bench.md` for the workflow.
|
||
- `src/commands/eval-trajectory.ts` + `src/commands/founder-scorecard.ts` + `src/core/trajectory.ts` (v0.35.7) — temporal trajectory + founder scorecard. The wave that turns the v0.35.3.1 date-aware contradiction probe into a useful temporal substrate. `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 lives 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` (R3). Source-scoped via the v0.34.1.0 `sourceId` scalar / `sourceIds` array dual pattern (D-CDX-6); visibility-filtered for remote callers (D-CDX-1) — `recall`-equivalent posture. MCP op `find_trajectory` (read scope, NOT localOnly) registered after `find_experts`. Migration v67 adds four 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 for common founder metrics). The `consolidate` cycle phase gains semantic upsert keyed on `(page_id, claim, since_date)` — fixes the pre-existing F4 duplicate-takes bug where re-running the full cycle after `extract_facts` cleared `consolidated_at` would silently append duplicate takes via `MAX(row_num)+1`. Also 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 chain: fence-row > pageEffectiveDate > now()). The contradiction probe MUST NOT write `valid_until` — R1+R8 grep guard at `test/eval-contradictions/no-valid-until-write.test.ts` pins this. Codex outside-voice round caught F1 (v66 collision → v67), F2 (Haiku lives in `facts/extract.ts` not `extract-facts.ts` cycle phase), F3 (cycle didn't embed before insert), F4 (idempotency bug), F5+F6 (missed `fence-write.ts` caller + no Page object there → pageEffectiveDate is OPTIONAL), F7 (privacy regression — visibility filter added), F8 (ParsedFact needed typed-field extension for markdown system-of-record), F9 (dual scalar+federated sourceId). Plan: `~/.claude/plans/system-instruction-you-are-working-curious-jellyfish.md`. Tests: 258 across 12 files.
|
||
- `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}.ts` (v0.32.6) — `gbrain 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 per Codex; UTF-8-safe truncation; C1 confidence-floor double-enforcement; resolution_kind output drives M7 paste-ready commands), persistent cache keyed on `(chunk_a_hash, chunk_b_hash, model_id, prompt_version, truncation_policy)` (Codex outside-voice fix — 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 — Codex fix to bias from silent skip), M5 trend writes to `eval_contradictions_runs`, M6 source-tier breakdown reuses `DEFAULT_SOURCE_BOOSTS` prefix logic, deterministic sampling (combined_score DESC + lex tiebreaker — stable cache hit-rate across re-runs). Hermetic via `judgeFn` + `searchFn` DI in the runner; never touches the real gateway in tests. Engine surface: `BrainEngine.listActiveTakesForPages` (P1 batched), `writeContradictionsRun` + `loadContradictionsTrend` (M5), `getContradictionCacheEntry` + `putContradictionCacheEntry` + `sweepContradictionCache` (P2). Schema migrations v51 + v52. MCP op `find_contradictions` (read scope, NOT localOnly, NOT in subagent allowlist — user-initiated only). M1 doctor check surfaces high-severity findings with paste-ready resolution commands. M2 synthesize phase pre-fetches latest probe's top-5-by-severity findings and threads them into `buildSynthesisPrompt` as an informational block. 226 hermetic unit tests + 12 real-Postgres E2E. Plan: `~/.claude/plans/system-instruction-you-are-working-hashed-dewdrop.md`. Architecture doc: `docs/contradictions.md`.
|
||
- `src/core/think/index.ts` (v0.35.5.0 — gateway adapter) — `runThink` no longer instantiates `new Anthropic()` directly. The internal `LLMClient` instance is now built by a small adapter that wraps `gateway.chat()` from `src/core/ai/gateway.ts`, the canonical AI seam v0.31.12 established for chat/embed/expansion. Closes #952: stdio MCP launches (Claude Desktop, Cursor) don't inherit shell env, so the Anthropic SDK's env-only key resolution lost the key any user had set via `gbrain config set anthropic_api_key`. The gateway reads from `~/.gbrain/config.json` AND from env, so both paths work. Test seam preserved: `opts.client?: ThinkLLMClient` injection still works for the 12+ existing tests (`test/think-pipeline.serial.test.ts`, `test/think-gateway-adapter.test.ts`, etc.); `opts.stubResponse` continues to short-circuit before any LLM call. When neither key nor client is available, the graceful "no LLM available" stub still fires with the same `NO_ANTHROPIC_API_KEY` warning. v0.36.x TODO: drop `ThinkLLMClient` indirection entirely, migrate tests to `__setChatTransportForTests` seam from `src/core/ai/gateway.ts`.
|
||
- `src/core/operations.ts` extension (v0.35.5.0 orphans fix) — `findOrphanPages` (both engines) now 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. Pre-v0.35.5 the query filtered nothing on `deleted_at`, so soft-deleted pages (v0.26.5 soft-delete shipped without updating this query) appeared as orphans AND links from soft-deleted source pages still suppressed live pages from orphan results. Closes #1021. Pinned by `test/orphans.test.ts`'s soft-delete cases.
|
||
- `src/commands/eval-longmemeval.ts` + `src/eval/longmemeval/{harness,adapter,sanitize}.ts` (v0.28.1) — `gbrain eval longmemeval <dataset.jsonl>` runs the public [LongMemEval](https://huggingface.co/datasets/xiaowu0162/longmemeval) benchmark against gbrain's hybrid retrieval. Architecture: one in-memory PGLite per benchmark run created via `createBenchmarkBrain` + `withBenchmarkBrain` (NO `EphemeralBrain` class). Between questions, `TRUNCATE` over runtime-enumerated `pg_tables` so future schema migrations don't silently leak data across questions; infrastructure tables (`sources`, `config`, `gbrain_cycle_locks`, `subagent_rate_leases`) are preserved. `cli.ts` has a pre-dispatch bypass so `eval longmemeval` skips `connectEngine()` — the user's `~/.gbrain` brain is never opened. `--expansion` defaults to OFF (deterministic, no per-query Haiku call); pass `--expansion` to opt in. Default model resolves through `resolveModel()` 6-tier chain with `models.eval.longmemeval` as the new config key. Sanitization parity: `harness.ts` re-uses `INJECTION_PATTERNS` from `src/core/think/sanitize.ts` (now exported, line 22) so adding a pattern automatically covers takes AND benchmarks. Retrieved chat content is wrapped in `<chat_session id="..." date="...">` framing; the answer-gen system prompt declares the content UNTRUSTED. LLM injection seam: `runEvalLongMemEval(args, {client?: ThinkLLMClient})` lets tests stub the client so the full pipeline runs without an Anthropic API key. p50 25.9ms / p99 30.3ms warm reset+import+search on Apple Silicon (per `test/eval-longmemeval.test.ts` perf gate). Hand the JSONL output to LongMemEval's `evaluate_qa.py` to score (their published evaluator, not bundled — needs OpenAI gpt-4o per their spec).
|
||
- `docs/eval-bench.md` (v0.25.0) — 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` (v0.25.0) — 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. Production users get a quiet brain; contributors set `export GBRAIN_CONTRIBUTOR_MODE=1` in `.zshrc` to enable the dev loop. PII scrubber gate is independent and defaults to true regardless of CONTRIBUTOR_MODE.
|
||
- `src/core/eval-capture-scrub.ts` (v0.25.0) — 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 unchanged in v0.25.0. Adds `onMeta?: (m: HybridSearchMeta) => void` callback so op-layer capture can record what hybridSearch actually did. Existing callers leave it undefined. **v0.33:** `HybridSearchOpts.types?: PageType[]` (defined on `SearchOpts`) threads a multi-type filter through to per-engine `searchKeyword` + `searchVector` + `searchKeywordChunks`, where it lands as `AND p.type = ANY($N::text[])`. Primary consumer is `gbrain whoknows` (filters to `['person','company']`). AND-applies alongside the existing single-value `type` filter; either or both can be used. **v0.36.3.0:** `hybridSearch` now 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 into per-engine `searchVector` (not a raw string), and uses `isCacheSafe(resolved, cfg)` for the cache-skip decision (replaces the prior name-based `isDefaultColumn` check that leaked across vector spaces when a user repointed the `embedding` builtin). `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 benchmarking; `search` (keyword-only) deliberately rejects the param.
|
||
- `docs/eval-capture.md` (v0.25.0) — stable NDJSON schema reference for gbrain-evals consumers.
|
||
- `test/public-exports.test.ts` (v0.25.0 / R2) — runtime contract test. 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. **v0.28.7:** `BATCH_SIZE` reverted 50→100 — the original Voyage safety guard halved OpenAI throughput on every page. Per-recipe pre-split + recursive halving + adaptive shrink-on-miss now live in the gateway, so the outer paginator goes back to its original purpose: progress-callback granularity, not batch protection.
|
||
- `src/core/ai/dims.ts` (v0.33.1.1, PR #962 + #866) — 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)`, and `isValidVoyageOutputDim(dims)`. **Voyage path uses the SDK-supported `dimensions` field** (`{ openaiCompatible: { dimensions: N } }`), NOT Voyage's `output_dimension` wire-key — the existing `voyageCompatFetch` shim in `gateway.ts:541` translates `dimensions → output_dimension` before the HTTP body is built. The reverse (sending `output_dimension` from here) was the v0.33.1.0 bug class: the AI SDK's openai-compatible adapter doesn't recognize the wire-key so it was silently dropped, Voyage returned its default 1024-dim, and the gateway dimension check threw on every embed call. 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 — fail-loud instead of opaque Voyage HTTP 400. Most common trigger: `embedding_model: voyage:voyage-4-large` configured without `embedding_dimensions` (falls back to `DEFAULT_EMBEDDING_DIMENSIONS=1536`, an OpenAI default not a Voyage one). Eva (@100yenadmin) shipped the wire-key fix in #866; Codex P3 follow-up landed the validator + valid-dims allowlist in #962.
|
||
- `src/core/ai/types.ts` — provider/recipe types. **v0.28.7 (#680):** `EmbeddingTouchpoint` extended with optional `chars_per_token` (default 4 chars/token, 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) that overshoot tiktoken. The pre-split budget is `max_batch_tokens × safety_factor / chars_per_token`. **v0.28.11 (#719):** `EmbeddingTouchpoint.multimodal_models?: string[]` model-level allow-list for recipes that mix 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` — brains using OpenAI for text can use Voyage for images without flipping the primary embedding pipeline.
|
||
- `src/core/ai/gateway.ts` — unified seam for every AI call. **v0.36.3.0:** `embedQuery(text, opts?)` and `isAvailable(touchpoint, modelOverride?)` accept a model override so the resolved-column path can embed via the column's provider (Voyage / ZeroEntropy / OpenAI) instead of the global default. The hybrid path passes `{embeddingModel: resolved.provider, dimensions: resolved.dimensions}`; the gateway resolves the matching recipe and routes through its `instantiateEmbedding()` branch. `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 — fixes the CDX-4 bug where a healthy Voyage column would skip vector retrieval because OpenAI happened to be unconfigured. **v0.35.0.0:** ZeroEntropy support lands. New `zeroEntropyCompatFetch` shim (sibling to `voyageCompatFetch`) handles ZE's non-OpenAI-compatible wire shape — rewrites the request URL from `/embeddings` to `/models/embed`, injects `input_type` (default `'document'`; `'query'` when threaded via `providerOptions.openaiCompatible.input_type`) and explicit `encoding_format: 'float'`, and rewrites the response from `{results: [{embedding}], usage: {total_bytes, total_tokens}}` to `{data: [{embedding, index}], usage: {prompt_tokens, total_tokens}}` so the SDK's openai-compatible Zod schema validates (Voyage's shim hit the same `prompt_tokens` requirement at `:655`). Layer 1 (Content-Length) + Layer 2 (per-embedding) OOM caps via a new tagged `ZeroEntropyResponseTooLargeError` class (kept separate from `VoyageResponseTooLargeError` because `test/voyage-response-cap.test.ts` does structural source-text greps pinning the Voyage name — class unification is a deferred cleanup). Wired in `instantiateEmbedding()` via the same `recipe.id === 'zeroentropyai'` branch pattern Voyage uses. New `gateway.rerank()` native HTTP path (no AI-SDK reranking abstraction): resolves the configured reranker model via `getRerankerModel()`, posts to `${recipe.base_url}/models/rerank` with bearer auth, returns `RerankResult[]` sorted by relevance score. `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'` so callers can fail-open without an HTTP call. `_rerankTransport` test seam mirrors `_embedTransport`. New `gateway.embedQuery(text)` companion threads `inputType: 'query'` through `dimsProviderOptions()` (now 4-arg). `getRerankerModel()` accessor + `isAvailable('reranker')` branch added. `configureGateway` + `reconfigureGatewayWithEngine` thread `reranker_model` through the same path as embedding/expansion/chat. `applyResolveAuth` + `defaultResolveAuth` widen touchpoint param to include `'reranker'`. **v0.34.1.0 (#875):** new `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 pre-existing Voyage `/multimodalembeddings` path is unchanged; the 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` config — no more cryptic `vector dimension mismatch` at INSERT time. Pinned by 11 cases in `test/openai-compat-multimodal.test.ts`. **v0.28.7 (#680):** module-scoped `_embedTransport` defaulting to AI SDK `embedMany`, with `__setEmbedTransportForTests(fn)` test seam so tests drive the public `embed()` function with a stubbed transport instead of probing private helpers. `splitByTokenBudget` and `isTokenLimitError` are now exported `@internal` — pure functions reused directly 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 toward the ceiling after `SHRINK_HEAL_AFTER=10` consecutive successes. `configureGateway()` walks every registered recipe at construction time and emits a once-per-process stderr warning for any embedding touchpoint missing `max_batch_tokens` (excluding the canonical OpenAI fast-path recipe). `resetGateway()` clears `_shrinkState`, the warned-set, and restores the real transport. ASCII flow diagram embedded in the `embed()` JSDoc covers the routing decision, recursion + halving, and shrinkState lifecycle. **v0.28.11 (#719):** `embedMultimodal()` reads `cfg.embedding_multimodal_model` first (falls back to `cfg.embedding_model` for single-model setups). After the existing recipe-level `supports_multimodal` fast-fail, validates the resolved model against `touchpoint.multimodal_models` when declared — closes the Voyage-text-only-model-into-multimodal-endpoint footgun before any HTTP call (Codex F1 from PR review). New `getMultimodalModel()` accessor mirrors `getEmbeddingModel` / `getChatModel` so doctor and integration tests can read the gateway state. **v0.33.1.1 (#962, Codex P3 follow-up):** new exported `VoyageResponseTooLargeError` tagged class at the top of the file. `voyageCompatFetch`'s two OOM-defense caps (Layer 1 Content-Length check at `:595`, Layer 2 per-embedding base64 cap at `:619`) now throw `VoyageResponseTooLargeError` instead of a generic `Error`. The inbound response-rewriter's surrounding try/catch (which intentionally swallows parse failures so misshaped Voyage responses fall through to the SDK's JSON parser) checks `instanceof VoyageResponseTooLargeError` and rethrows. Pre-fix, the Layer 2 throw was silently swallowed and the oversized response returned to the AI SDK anyway — Layer 2 was theatrical. Source-shape regression assertion in `test/voyage-response-cap.test.ts` pins the `instanceof ⇒ throw err` line.
|
||
- `src/core/ai/recipes/zeroentropyai.ts` (v0.35.0.0) — 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'` (NOT the misspelled `'openai-compat'` the original plan draft had — pinned by F1 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 F2 regression). `chars_per_token: 1` + `safety_factor: 0.5` match Voyage's dense-content hedge.
|
||
- `src/core/rerank-audit.ts` (v0.35.0.0) — 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`** (CDX2-F22 in plan review): 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 is 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` (v0.36.3.0) — 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 `ResolvedColumn` descriptor (frozen `{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 — a user who repointed the `embedding` builtin at Voyage doesn't accidentally serve OpenAI-shaped cache rows. `validateResolvedColumn(descriptor)` re-validates hand-rolled descriptors that bypass the registry (internal-SDK passthrough path) so the SQL-injection escape hatch through the descriptor field is closed. Consumed by `hybridSearch` (resolves once at the boundary, threads a `ResolvedColumn` into per-engine `searchVector` instead of a raw string), `gateway.embedQuery(text, {embeddingModel, dimensions})` (resolved column's provider drives the query-time embed call), `cosineReScore` (engine pulls vectors from the active column, not the hardcoded `embedding`), and the `query` MCP op (per-call `embedding_column` param). 538 lines of pure resolver, 511 unit cases in `test/search/embedding-column.test.ts` covering the 16 codex-flagged corners (prototype-pollution, descriptor passthrough, env-only Postgres install, empty-brain coverage gate, cache-space comparison).
|
||
- `src/core/search/rerank.ts` (v0.35.0.0) — 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, and 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` which means "fall through to mode bundle" (CDX2-F16). Test seam: `opts.rerankerFn` lets tests stub `gateway.rerank` without touching the network.
|
||
- `src/core/ai/recipes/voyage.ts` — Voyage AI openai-compatible recipe. **v0.28.7 (#680):** 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). Closes the v0.27 backfill loop where ~26% of the corpus stayed un-embedded because tiktoken-grounded budgeting silently undercounted Voyage's actual token usage. **v0.28.11 (#719):** 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. **v0.33.1.1 (#962, fixup):** recipe docstring at `:7-16` tightened to name the seven hosted flexible-dim models that accept `output_dimension` explicitly (`voyage-4-large`, `voyage-4`, `voyage-4-lite`, `voyage-3-large`, `voyage-3.5`, `voyage-3.5-lite`, `voyage-code-3`) and call out that `voyage-4-nano` is the open-weight variant listed separately by Voyage as fixed 1024-dim — does NOT accept the parameter. The "all v4 variants are flexible" misread is what caused the original PR to include nano in `VOYAGE_OUTPUT_DIMENSION_MODELS`; the negative regression assertion in `test/ai/gateway.test.ts` (`dimsProviderOptions` returns `undefined` for `voyage-4-nano`) pins the contract.
|
||
- `src/core/ai/recipes/anthropic.ts` — Anthropic recipe (chat + expansion touchpoints). **v0.31.12:** chat and expansion `models:` lists drop the v0.31.6 phantom `claude-sonnet-4-6-20250929` date suffix — canonical id is `claude-sonnet-4-6`. The wrong-direction alias `claude-sonnet-4-6 → claude-sonnet-4-6-20250929` is removed; 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` set by v0.31.6 installs). Recipe-shape regression pinned by `test/anthropic-model-ids.test.ts` (6 cases, verbatim cherry-pick of PR #830 plus the reverse-alias rescue case).
|
||
- `src/core/anthropic-pricing.ts` — Single source of truth for Anthropic model pricing (per-MTok input/output). **v0.31.12:** Opus 4.7 corrected from `$15/$75` to `$5/$25` (the old number was from Opus 4 generation, never refreshed when 4.7 shipped); Opus 4.6 also corrected. Consumed by `src/core/budget-meter.ts` and `src/core/cross-modal-eval/runner.ts` — the cross-modal estimator now reads `ANTHROPIC_PRICING` for Anthropic models instead of duplicating the table, killing the v0.31.6 drift bug class.
|
||
- `src/core/model-config.ts` — Model-string resolution (the seam every internal LLM call walks through). **v0.31.12:** 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`. Resolution chain is now 8 steps: cliFlag → deprecated key → config key → `models.default` → `models.tier.<tier>` → env var → `TIER_DEFAULTS[tier]` → caller fallback. Two new exports — `isAnthropicProvider(modelString)` checks `provider:model` prefix OR `claude-` bare-id pattern, and `enforceSubagentAnthropic()` is the layer-2 runtime guard: when `tier === 'subagent'` resolves to a non-Anthropic provider, it emits a once-per-`(source, model)` stderr warn AND falls back to `TIER_DEFAULTS.subagent` instead of letting the Anthropic Messages API tool-loop attempt to run on OpenAI/Gemini. `_resetDeprecationWarningsForTest()` now also clears `_subagentTierWarningsEmitted` so tests re-emit.
|
||
- `src/core/ai/model-resolver.ts` — Recipe-touchpoint validator. **v0.31.12:** `assertTouchpoint(recipe, touchpoint, modelId, extendedModels?)` gains an optional 4th `extendedModels: ReadonlySet<string>` argument. When the modelId is in that set, the native-recipe allowlist throw is bypassed — the user explicitly opted into this model via config so we let provider rejection surface 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` — typos in source code still fail fast. Replaces the earlier plan to soften the validator wholesale (Codex F4/F5 in plan review flagged that as too broad — it would have removed the fail-fast contract for chat + expand + embed all three).
|
||
- `src/core/ai/gateway.ts` extension (v0.31.12) — new module-scoped `_extendedModels: Map<providerId, Set<modelId>>` registry feeds `assertTouchpoint`'s 4th-arg path. New `reconfigureGatewayWithEngine(engine)` async function is called from `cli.ts` after `engine.connect()` (and 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 expansion + chat both. `DEFAULT_CHAT_MODEL` corrected to `anthropic:claude-sonnet-4-6` (was the v0.31.6 phantom `-20250929`). New `__setChatTransportForTests` seam mirrors `__setEmbedTransportForTests` so tests drive `chat()` with a stubbed transport.
|
||
- `src/core/minions/queue.ts` extension (v0.31.12) — `MinionQueue.add()` now rejects `subagent` jobs whose `data.model` resolves through `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 (Codex F1+F2 in plan review). Layers 2 + 3 live in `src/core/model-config.ts` (`enforceSubagentAnthropic` runtime fallback) and `src/commands/doctor.ts` (`subagent_provider` check). Pinned by 3 cases in `test/agent-cli.test.ts`.
|
||
- `src/commands/models.ts` (v0.31.12) — `gbrain models [--json]` read-only routing dashboard: prints tier defaults (`utility`/`reasoning`/`deep`/`subagent`), the resolved value for each (re-walking the resolution chain to attribute properly), every per-task override (11 `PER_TASK_KEYS` entries — `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 (defaults + user overrides), and a source-of-truth column showing `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}` — the structural fix for the v0.31.6 silent-no-op bug class. Wired into `cli.ts` dispatch table + `CLI_ONLY` set. **v0.33.1.1 (#962, Codex P3 follow-up):** doctor gains a zero-token `embedding_config` probe that runs FIRST, before any chat/expansion probes spend money. `probeEmbeddingConfig()` reads `getEmbeddingModel()` + `getEmbeddingDimensions()` from the gateway, parses the model id, and (for Voyage flexible-dim models) checks `isValidVoyageOutputDim(dims)` against `VOYAGE_VALID_OUTPUT_DIMS`. New `ProbeStatus` variant `'config'` and optional `fix?: string` field on `ProbeResult` — surfaced in both human output (paste-ready `gbrain config set ...` line under the bad probe) and JSON output. New touchpoint label `'embedding_config'` joins `'chat'` and `'expansion'` in the probe-row taxonomy. Closes the Voyage flexible-dim bug class at config time, not first-embed.
|
||
- `src/commands/doctor.ts` extension (v0.31.12) — new `subagent_provider` check (layer 3 of 3 — Codex F13). Warns when `models.tier.subagent` is explicitly set to a non-Anthropic provider (fail-loud since the user clearly meant it — message names the bad value and prints the paste-ready fix command `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 status when subagent tier resolves to Anthropic. Tests cover all three paths in `test/doctor.test.ts`.
|
||
- `src/core/check-resolvable.ts` — Resolver validation: reachability, MECE overlap, DRY checks, structured fix objects. v0.14.1: `CROSS_CUTTING_PATTERNS.conventions` is an array (notability gate accepts both `conventions/quality.md` and `_brain-filing-rules.md`). New `extractDelegationTargets()` parses `> **Convention:**`, `> **Filing rule:**`, and inline backtick references. DRY suppression is proximity-based via `DRY_PROXIMITY_LINES = 40`.
|
||
- `src/core/repo-root.ts` — Shared `findRepoRoot(startDir?)` (v0.16.4): walks up from `startDir` (default `process.cwd()`) looking for `skills/RESOLVER.md`. Zero-dependency module imported by both `doctor.ts` and `check-resolvable.ts`. Parameterized `startDir` makes tests hermetic. **v0.31.7:** read-path / write-path split. `autoDetectSkillsDir` (shared, read+write-safe) gains tier-0 `$GBRAIN_SKILLS_DIR` explicit operator override (Docker mounts, CI, monorepo subdirs) ahead of the existing 4-tier chain. New `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`) deliberately stay on the shared function so `gbrain skillpack install` from `~` cannot silently retarget the bundled gbrain repo's `skills/` instead of the user's actual workspace. Two new `SkillsDirSource` variants: `'env_explicit'`, `'install_path'`. New `AUTO_DETECT_HINT_READ_ONLY` documents the extra tier. The D6 `--fix` safety gate in `doctor.ts` + `check-resolvable.ts` refuses auto-repair when `detected.source === 'install_path'` so `gbrain doctor --fix` from `~` cannot silently rewrite the bundled install tree.
|
||
- `src/commands/check-resolvable.ts` — Standalone CLI wrapper (v0.16.4) over `checkResolvable()`. Exports `parseFlags`, `resolveSkillsDir`, `DEFERRED`, `runCheckResolvable`. Exit rule: **1 on any issue (warnings OR errors)**, stricter than doctor's `ok` flag — honors README:259. Stable JSON envelope `{ok, skillsDir, report, autoFix, deferred, error, message}` — same shape on success and error paths. `--fix` path 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 — no silent false-pass. **v0.19:** AGENTS.md workspaces now resolve natively (see `src/core/resolver-filenames.ts`) — gbrain inspects the 107-skill OpenClaw deployment whether the routing file is `RESOLVER.md` or `AGENTS.md`. `DEFERRED[]` is empty — Checks 5 + 6 shipped as real code, not issue URLs. **v0.31.7:** the resolver lookup switched from first-match-wins to 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). Lifted reachable skills on the reference OpenClaw layout from 37/224 to 200/224 — the deployment ships a thin `skills/RESOLVER.md` (~40 entries from skillpack) plus a fat `../AGENTS.md` (200+ entries, the real dispatcher), and the previous code only saw the first one. The CLI also switched to `autoDetectSkillsDirReadOnly` so `cd ~ && gbrain check-resolvable` finds the bundled skills via the install-path fallback. `--fix` carries the same D6 safety gate as `gbrain doctor --fix`: refuses to write when `detected.source === 'install_path'`.
|
||
- `src/core/resolver-filenames.ts` (v0.19) — 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}.ts` (v0.19) — `gbrain skillify scaffold <name>` creates all stubs for a new skill in one command: 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.ts` (v0.19) — `gbrain skillpack-check` agent-readable health report. Exit 0/1/2 for CI pipeline gating; JSON for debugging. Wraps `check-resolvable --json`, `doctor --json`, and migration ledger into one payload so agents can decide whether a human action is required.
|
||
- `src/commands/book-mirror.ts` (v0.25.1) — `gbrain book-mirror --chapters-dir <path> --slug <slug> [flags]`. Flagship of the v0.25.1 skills wave. 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`. Codex HIGH-1 fix applied: trust narrowing happens at the tool-allowlist layer (subagents can't call put_page) instead of allowedSlugPrefixes — untrusted EPUB content cannot 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 handling: assembles with completed chapters and a `## Failed chapters` section listing retries. Test surface: `test/book-mirror.test.ts` (9 cases — CLI registration + source invariants).
|
||
- `src/commands/skillpack.ts` + `src/core/skillpack/{bundle,scaffold,reference,migrate-fence,scrub-legacy,harvest,harvest-lint,copy,apply-hunks,diff-text,installer}.ts` (v0.19 → v0.36) — **v0.36 contract change**: managed-block install model retired. `install` and `uninstall` removed (clean break, no alias; both exit non-zero with a hint pointing at the replacement command). New surface: `scaffold` (one-time additive copy via shared `copyArtifacts` helper in `copy.ts`; refuses to overwrite existing files; partial-state fills missing paired sources declared in SKILL.md frontmatter `sources:`), `reference` (read-only diff lens with agent-readable framing line + `--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 `validateUploadPath`-style gate + default-on privacy linter in `harvest-lint.ts` against `~/.gbrain/harvest-private-patterns.txt` plus built-in `\bWintermute\b` + email + Slack-channel patterns; rollback on match). Paired-source declarations moved from `openclaw.plugin.json` to each SKILL.md's frontmatter `sources:` array (D2; validated by `loadSkillSources` in `bundle.ts`). `autoDetectSkillsDir` (in `src/core/repo-root.ts`) gains a `cwd_walk_up` tier ahead of `~/.openclaw/workspace` (D3; non-OpenClaw hosts like `~/git/wintermute` auto-detect; R5 regression preserves `$OPENCLAW_WORKSPACE` precedence). `gbrain skillpack check --strict` exits non-zero on drift (CI gate); top-level `gbrain skillpack-check` keeps exit-1-on-issues for cron compat. Companion editorial skill `skills/skillpack-harvest/SKILL.md` drives the genericization checklist before the CLI runs. Design + workflow doc: `docs/guides/skillpacks-as-scaffolding.md`. ~600 LOC of managed-block machinery deleted; ~400 LOC of new modules + ~1000 LOC of new 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 for now — `gbrain skillpack diff` still uses `diffSkill` from there; slated for v0.37 cleanup. **Historical (v0.19-v0.35.1):** managed-block model with `<!-- gbrain:skillpack:begin -->`/`end -->` fence, `cumulative-slugs="..."` receipt, content-hash gates, lockfile; `install --all` prune; `uninstall` with D8 receipt gate + D11 atomic-refusal content-hash pre-scan. Replaced wholesale in v0.36.
|
||
- `src/core/archive-crawler-config.ts` (v0.25.1) — D12 + codex HIGH-4 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). Tests in `test/archive-crawler-config.test.ts` (19 cases).
|
||
- `test/helpers/cli-pty-runner.ts` (v0.25.1) — generic real-PTY harness ported from gstack and trimmed to ~470 lines. Uses 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` (v0.19) — 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.ts` (v0.19) — `gbrain 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). The `--llm` flag is accepted as a placeholder for a future LLM tie-break layer; in v0.24.0 it emits a stderr notice and runs structural only. False positives surface before users hit them. **v0.31.7:** switched to `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 — previously `routing-eval` read only the first resolver file it found. The v0.25.1 wave skills' RESOLVER.md rows were also synced to include the full frontmatter `triggers:` arrays (was only the first trigger), so the structural matcher actually sees the realistic phrasings; ambiguous-fixture annotations cover deliberate skill chains like `enrich → article-enrichment`.
|
||
- `src/core/filing-audit.ts` + `skills/_brain-filing-rules.json` (v0.19) — Check 6 of `check-resolvable`. Parses new `writes_pages:` / `writes_to:` frontmatter on skills and audits their filing claims against the filing-rules JSON. Warning-only in v0.19, upgrades to error in v0.20.
|
||
- `src/core/dry-fix.ts` — `gbrain 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()` returns 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.
|
||
- `src/core/backoff.ts` — Adaptive load-aware throttling: CPU/memory checks, exponential backoff, active hours multiplier
|
||
- `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/core/data-research.ts` — Recipe validation, field extraction (MRR/ARR regex), dedup, tracker parsing, HTML stripping
|
||
- `src/commands/embed.ts` — `gbrain embed [--stale|--all] [--slugs ...]`. v0.22.1 (#409, contributed by @atrevino47): `--stale` path now starts with `engine.countStaleChunks()` (single SELECT count(*) WHERE embedding IS NULL, ~50 bytes wire). On a fully-embedded brain that's a 1-line short-circuit — 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 via OpenAI, re-upserts via `upsertChunks`. Replaces the prior page-walk that pulled every chunk's embedding column over the wire and discarded most.
|
||
- `src/commands/extract.ts` — `gbrain extract links|timeline|all [--source fs|db]`: batch link/timeline extraction. fs walks markdown files, db walks pages from the engine (mutation-immune snapshot iteration; use this for live brains with no local checkout). As of v0.12.1 there is no in-memory dedup pre-load — candidates are buffered 100 at a time and flushed via `addLinksBatch` / `addTimelineEntriesBatch`; `ON CONFLICT DO NOTHING` enforces uniqueness at the DB layer, and the `created` counter returns real rows inserted (truthful on re-runs). v0.22.1 (#417): `ExtractOpts.slugs?: string[]` enables incremental extract — when set, `extractForSlugs()` reads ONLY those slugs' files (single combined links+timeline pass) instead of the full directory walk. CLI `gbrain extract` keeps full-walk behavior; the cycle path threads sync's `pagesAffected` through. `walkMarkdownFiles(brainDir)` still runs at line 455 to build `allSlugs` for link resolution — see `TODOS.md` for replacing it with `engine.getAllSlugs()`.
|
||
- `src/commands/graph-query.ts` — `gbrain graph-query <slug> [--type T] [--depth N] [--direction in|out|both]`: typed-edge relationship traversal (renders indented tree)
|
||
- `src/core/link-extraction.ts` — shared library for the v0.12.0 graph layer. extractEntityRefs (canonical, replaces backlinks.ts duplicate) matches both `[Name](people/slug)` markdown links and Obsidian `[[people/slug|Name]]` wikilinks as of v0.12.3. 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.
|
||
- `src/core/zombie-reap.ts` (v0.28.1) — 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` (with Windows platform guard — SIGCHLD doesn't exist on Windows). Cross-file leak guard via `_uninstallSigchldHandlerForTests()` for tests. 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). v0.14.1 #219: `add()` plumbs `max_stalled` through with a `[1, 100]` clamp; omitted values let the schema DEFAULT (5) kick in. v0.19.0: `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). v0.19.1: `maxWaiting` coalesce path now 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.
|
||
- `src/core/minions/worker.ts` — MinionWorker class (handler registry, lock renewal, graceful shutdown, timeout safety net). v0.14.0 abort-path fix: aborted jobs now call `failJob` with reason (`timeout`/`cancel`/`lock-lost`/`shutdown`) instead of returning silently. `shutdownAbort` (instance field) fires on process SIGTERM/SIGINT and propagates to `ctx.shutdownSignal` — shell handler listens to it; non-shell handlers don't. v0.22.1 (#403): per-job timeout fires `abort.abort(new Error('timeout'))` then a 30-second grace-then-evict safety net force-evicts the job from `inFlight` and marks it dead in DB if the handler ignores the abort signal — frees the slot even when a handler wedges (the 98-waiting-0-active prod incident driver). **v0.28.1 engine-ownership invariant:** `start()` no longer calls `engine.disconnect()` on shutdown — that was a leaky abstraction (the worker disconnected an engine it didn't own). The CLI handler in `src/commands/jobs.ts case 'work'` now owns engine lifecycle via try/finally with loud error logging on disconnect failure. Pinned by `test/worker-shutdown-disconnect.test.ts` asserting the inverse (`disconnectSpy).not.toHaveBeenCalled()`). **v0.34.3.0:** RSS watchdog metric switched to non-file-backed pages on Linux. New exports `parseRssFromProcStatus(status)` (pure parser, exported for unit tests) 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` injected into `WorkerOpts` is now `getAccurateRss` instead of `process.memoryUsage().rss`. Closes the prod incident where VmRSS inflated to 7GB on a 96K-page brain (file-backed git packfile mmaps) while heap stayed at ~100MB; the watchdog was firing every autopilot cycle. M1 parser fix uses field-presence regex checks so `RssAnon: 0 + RssShmem: 512` (shmem-only worker case) parses correctly instead of falling through to VmRSS. Pinned by `test/worker-rss.test.ts` (11 cases).
|
||
- `src/core/minions/supervisor.ts` — MinionSupervisor process manager. Spawns `gbrain jobs work` as a child, restarts on crash with exponential backoff, periodic health check. v0.22.1 (#406): `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 the counter. Worker exit classifier emits `likely_cause` field on `worker_exited` events: `oom_or_external_kill` (SIGKILL), `graceful_shutdown` (SIGTERM), `runtime_error` (code 1), `clean_exit` (code 0), `unknown`. **v0.28.1:** 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 that the in-process SIGCHLD reaper can't reach). Exposes `isTiniDetected` read-only accessor for tests. **v0.34.3.0:** spawn-and-respawn loop extracted into the shared `ChildWorkerSupervisor` core (see entry below). MinionSupervisor now composes the inner class via `runSuperviseLoop()` → `new ChildWorkerSupervisor({...})` and maps `ChildSupervisorEvent` shapes back through the existing `emit()` SupervisorEvent channel — JSONL audit consumers see byte-compatible output across the rename. PID lock, signal handlers, health check, and `process.exit` on max-crashes stay in MinionSupervisor (standalone-daemon concerns). The pre-shipped reset-to-0-on-code=0 hunk that originally fixed the prod crash-counter incident is gone; the same fix lives in the shared core under the D1 amendment (code=0 leaves `crashCount` untouched, so a worker alternating real crashes + watchdog drains still trips `max_crashes`). D2 `cleanRestartBudget` (default 10 restarts per 60s) caps the macOS/non-Linux-fallback tight-loop by emitting `health_warn { reason: 'clean_restart_budget_exceeded' }` plus backoff after the threshold trips. `shutdown()` drains via `childSupervisor.killChild('SIGTERM')` + `awaitChildExit(35_000)` instead of reaching into `this.child` directly. Pinned by `test/supervisor.test.ts` (16 cases; existing tests that previously relied on clean-exit-as-crash semantics now use exit-1 workers since clean exits no longer count) and `test/supervisor-tini.test.ts`.
|
||
- `src/core/minions/child-worker-supervisor.ts` (v0.34.3.0) — shared spawn-and-respawn core extracted from `MinionSupervisor` so it can be reused by both `MinionSupervisor` (standalone `gbrain jobs supervisor` daemon) and `src/commands/autopilot.ts` (autopilot daemon). Pre-v0.34.3.0 the two consumers maintained parallel spawn loops that drifted into the same bug class — Codex caught it during plan-eng-review on PR #1003. Pure class: NO PID file, NO signal handlers, NO `process.exit`, NO health check. Lifecycle events fire via injected `onEvent: (ChildSupervisorEvent) => void` callback so each composer routes to its own log/audit channel. **D1 exit classifier:** `code === 0` leaves `crashCount` UNCHANGED (preserves flap detection across mixed exit sequences — a worker that alternates `exit 1 / exit 0 / exit 1 / exit 0` correctly trips `max_crashes` after 10 real crashes regardless of intervening clean exits). `code != 0` follows the existing `runDuration > stableRunResetMs ? 1 : ++crashCount` rule. **D2 clean-restart budget:** sliding window tracks 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) before the next spawn. Caps the worst-case tight-loop on macOS / restricted containers / kernel <4.5 where the worker's RSS watchdog falls back to VmRSS. Public read-only accessors `childAlive`, `inBackoff`, `crashCount` for composer health checks; `killChild(signal)` + `awaitChildExit(timeoutMs)` for shutdown paths. `awaitChildExit` short-circuits when `child.exitCode !== null || child.signalCode !== null` (regression caught in pre-landing /review: pre-fix, fast-SIGTERM responders caused a 35-second shutdown hang because the late `once('exit', ...)` listener never fired). Test hooks: `_backoffFloorMs` skips the real backoff curve, `_now` injects a fake clock. Pinned by `test/child-worker-supervisor.test.ts` (7 cases: D1 classifier with code=0 not counted, interleaved exits still trip max_crashes, stable-run + clean-exit interaction across faked 6-minute run, D2 budget triggers backoff + health_warn, budget config is per-instance, awaitChildExit short-circuit, event-shape regression). Plan that produced the design lives at `~/.claude/plans/this-is-a-real-sleepy-sketch.md`.
|
||
- `src/core/minions/spawn-helpers.ts` (v0.28.1) — pure `detectTini()` + `buildSpawnInvocation()` helpers consumed by both `supervisor.ts` and `autopilot.ts`. Resolves the DRY violation between the two spawn sites and makes the 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 (the env-snapshot bug fix). `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 cases) and `test/supervisor-tini.test.ts` (4 cases).
|
||
- `src/core/minions/types.ts` — `MinionJobInput` + `MinionJobStatus` + handler context types. `MinionJobInput.max_stalled` (new in v0.14.1) 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.ts` — `shell` 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 + (v0.36.5.0) `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`). **v0.36.5.0:** `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_url` → `GBRAIN_DATABASE_URL`; everything else uppercased). Names persist in `minion_jobs.data` (and the shell-audit JSONL); values never do. The canonical validator `validateShellJobParams` (sibling file `shell-validate.ts`) runs **pre-enqueue** in both submit surfaces — `gbrain jobs submit shell` (jobs.ts:271) AND `submit_job` op for `name='shell'` (operations.ts:2085). The handler-entry re-validation here is defense-in-depth. Closes the codex F-CDX-1 load-bearing bug class where validation in the handler 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` (v0.36.5.0, NEW) — three small helpers, no closed enum. `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_url` → `GBRAIN_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. An earlier closed-enum design (hardcoded `INHERITABLE` record with shadow-keys per name) was abandoned because the agent and worker share a uid — refusing to let the agent inherit arbitrary config keys defends nothing in that trust model.
|
||
- `src/core/minions/handlers/shell-validate.ts` (v0.36.5.0, NEW) — `validateShellJobParams(data, opts?)` shared pre-enqueue validator. Throws `UnrecoverableError` with paste-ready operator hints on every failure path. Three rules: (1) existing 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. Also accepts optional `redact_secrets?: boolean` for output-side scrubbing. The validator deliberately does NOT police WHICH secrets the agent passes — single-uid trust model. Test seam: `opts.config` lets unit tests drive the validator hermetically without mocking the module. The defense-in-depth re-call at `shell.ts` handler entry catches pre-existing rows submitted before v0.36.5.0.
|
||
- `src/core/minions/handlers/shell-redact.ts` (v0.36.5.0, NEW) — opt-in output-side scrubbing for shell-job stdout/stderr. Pure `redactSecretsInText(text, secrets)` function: string-mode `replaceAll` so regex metacharacters in values stay literal. When the caller passes `redact_secrets: true` (or `--redact-secrets` on the CLI), the handler builds a Map of inherit-name → resolved-value and post-processes both tails before throw/return, so the persisted `result.stdout_tail` / `result.stderr_tail` / `error_text` carry `<REDACTED:name>` instead of the value. Only `inherit:`-resolved values are scrubbed; caller-supplied `env:` values stay through (those are the agent's "fine in the row" channel). Heuristic — defeats the common-case `echo "$GBRAIN_DATABASE_URL"` echo, not adversarial encode-then-print. Default `false` for back-compat.
|
||
- `src/core/config.ts:ensureGitignore` (v0.36.5.0) — 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 next `gbrain upgrade`. Never clobbers a user-customized `.gitignore` (checks file exists + content non-empty before writing). Honest scope, named in CHANGELOG: 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` (v0.36.5.0) — filesystem check walking up from `gbrainPath()` toward `$HOME` looking for either 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` (gbrain 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()` helper with `shell-audit.ts`). `writeSupervisorEvent(emission, supervisorPid)` appends one line per supervisor 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 path for `gbrain doctor`. **v0.35.5.0:** new exports `isCrashExit(event)`, `summarizeCrashes(events)`, `CrashSummary` type, and `CLEAN_EXIT_CAUSES` denylist (`'clean_exit' | 'graceful_shutdown'`). Single regression point — both `gbrain doctor` (Lane D supervisor check at `doctor.ts:1011-1043`) and `gbrain jobs supervisor status` (`jobs.ts:803-826`) import from here so the two CLI surfaces cannot drift. `isCrashExit` classifies a single `worker_exited` event against the denylist: `clean_exit` / `graceful_shutdown` are NON-crashes; everything else (`runtime_error`, `oom_or_external_kill`, `unknown`, AND any future `likely_cause` value added upstream in `child-worker-supervisor.ts`) is a crash. Pre-v0.34 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}` so dashboards bind to named buckets — the `legacy` bucket catches BOTH pre-v0.34 fallback entries AND future unrecognized `likely_cause` values, fail-loud instead of silent underreport. Denylist-over-allowlist was a codex outside-voice catch during `/plan-eng-review` — the bug being fixed (read sites counting every `worker_exited` as a crash, inflating to 120+/day on healthy brains after v0.34.3.0 watchdog drains) was itself an allowlist-of-event-names. Pinned by `test/supervisor-audit.test.ts` (14 cases: 9-case `isCrashExit` branch matrix including denylist regression guard for unrecognized future causes + non-exit-event defensive case, 5-case `summarizeCrashes` aggregator including unrecognized-cause routing to legacy + null-code edge case) and 4 source-grep wiring assertions in `test/doctor.test.ts` guarding both surfaces against drift.
|
||
- `src/core/minions/backpressure-audit.ts` (v0.19.1) — sibling of shell-audit.ts for `maxWaiting` coalesce events. JSONL at `~/.gbrain/audit/backpressure-YYYY-Www.jsonl`. Fires one line per coalesce with `(queue, name, waiting_count, max_waiting, returned_job_id, ts)`. Closes the silent-drop vector the v0.19.0 maxWaiting guard introduced.
|
||
- `src/core/minions/handlers/subagent.ts` (v0.15) — 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. **v0.30.2:** 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 before dead-lettering. Catches both initial-prompt overflow and turn-N tool-loop accumulation that the chunker in `synthesize.ts` can't bound ahead of time.
|
||
- `src/core/minions/handlers/subagent-aggregator.ts` (v0.15) — `subagent_aggregator` handler. Claims AFTER all children resolve (queue changes guarantee every terminal child posts a `child_done` inbox message with outcome). Reads inbox via `ctx.readInbox()`, builds deterministic mixed-outcome markdown summary. No LLM call in v0.15.
|
||
- `src/core/minions/handlers/subagent-audit.ts` (v0.15) — JSONL audit + heartbeat writer at `~/.gbrain/audit/subagent-jobs-YYYY-Www.jsonl`. Events: `submission` (one line 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 path for `gbrain agent logs`.
|
||
- `src/core/minions/rate-leases.ts` (v0.15) — 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` (v0.15) — 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` (v0.15) — 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.ts` (v0.15) — `GBRAIN_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` (v0.15, extended v0.23, v0.29, v0.35.3.0) — derives subagent tool registry from `src/core/operations.ts`. 13-name allow-list as of v0.29 (was 11). By default `put_page` schema is namespace-wrapped per subagent (`^wiki/agents/<subagentId>/.+`). **v0.23 trusted-workspace path:** when `BuildBrainToolsOpts.allowedSlugPrefixes` is set, the put_page schema instead 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. **v0.29:** `get_recent_salience` + `find_anomalies` added to the allow-list. `get_recent_transcripts` deliberately NOT added — all subagent calls run with `ctx.remote === true`, and the v0.29 trust gate rejects remote callers, so adding it would always reject (footgun). The cycle synthesize phase already calls `discoverTranscripts` directly. **v0.35.3.0:** `paramsToInputSchema()` now consumes `paramDefToSchema` from `src/mcp/tool-defs.ts` instead of its own inline destructure. Required-aggregation at the tool-def level stays here (out of scope for the shared helper, which is per-param). Closes the third drift site in the ParamDef→JSON Schema bug class.
|
||
- `src/mcp/tool-defs.ts` (v0.15, v0.35.3.0) — extracted `buildToolDefs(ops)` helper. MCP server + subagent tool registry both call it; byte-for-byte equivalence pinned by `test/mcp-tool-defs.test.ts`. **v0.35.3.0:** exports the new recursive `paramDefToSchema(p: ParamDef)` helper — single source of truth for ParamDef→JSON Schema mapping. Three consumers now share one mapper: `buildToolDefs` (stdio MCP), `src/commands/serve-http.ts:837` (HTTP MCP `tools/list`), and `src/core/minions/tools/brain-allowlist.ts:84` (subagent tool registry). Pre-v0.35.3, three inline destructures had drifted across the surface — the live HTTP MCP path dropped `items` on every array param after a v0.32 review caught only the stdio side. Recursive on `items` so nested array-of-arrays preserves inner shape on the wire. Key ordering (type, description, enum, default, items) is intentional — matches the pre-v0.35.3 inline mappers so JSON.stringify output stays byte-stable. `test/mcp-tool-defs.test.ts` adds a `findArrayWithoutItems` walker that fails the suite with a property path on any future `type: 'array'` lacking `items.type`.
|
||
- `src/core/minions/attachments.ts` — Attachment validation (path traversal, null byte, oversize, base64, duplicate detection)
|
||
- `src/commands/agent.ts` (v0.16) — `gbrain 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.ts` (v0.16) — `gbrain 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.ts` — `gbrain jobs` CLI subcommands + `gbrain jobs work` daemon. **v0.28.1:** `case 'work'` now wraps `worker.start()` in try/finally and owns engine lifecycle — calls `engine.disconnect()` on shutdown with loud error logging on failure. Replaces the prior call inside `MinionWorker.start()` (which violated engine ownership: the worker disconnected an engine it didn't own, and clobbered the module-level singleton on PostgresEngine via the now-fixed idempotency bug). Pool slots now free immediately on shutdown instead of waiting for TCP keepalive (~minutes). v0.13.1 surfaces the full `MinionJobInput` retry/backoff/timeout/idempotency surface as first-class CLI flags on `jobs submit`: `--max-stalled`, `--backoff-type fixed|exponential`, `--backoff-delay`, `--backoff-jitter`, `--timeout-ms`, `--idempotency-key`. `jobs smoke --sigkill-rescue` is the opt-in regression guard for #219. v0.16 wires `registerBuiltinHandlers` to always register `subagent` + `subagent_aggregator` (no env flag — `ANTHROPIC_API_KEY` is the natural cost gate, trust is via `PROTECTED_JOB_NAMES`) and loads `GBRAIN_PLUGIN_PATH` plugins at worker startup with a loud startup-line per plugin. `shell` handler still gated by `GBRAIN_ALLOW_SHELL_JOBS=1` (RCE surface, separate concern). v0.22.10 (#521): the `autopilot-cycle` handler now forwards `job.data.phases` to `runCycle` (was previously discarded — caller-supplied phase selection silently became a full cycle). Phases are validated against `ALL_PHASES` from `src/core/cycle.ts`; invalid names are filtered out and an empty/missing array falls back to the default 6-phase cycle. v0.22.13 (PR #490 CODEX-1+CODEX-4): `sync` handler now resolves `sourceId` at entry by looking up `sources.local_path` (mirrors `cycle.ts:480`'s autopilot fix from PR #475) so multi-source brains read the per-source `last_commit` anchor instead of the global config key. Concurrency routed through the shared `autoConcurrency()` policy in `src/core/sync-concurrency.ts` instead of the prior hardcoded `4`; PGLite stays serial. `noEmbed` default is `true` (embed is a separate job — submit `gbrain embed --stale` after sync, or rely on the autopilot cycle's embed phase). **v0.35.5.0:** `gbrain jobs supervisor status` at `jobs.ts:803-826` now consumes `summarizeCrashes()` from `src/core/minions/handlers/supervisor-audit.ts` for cross-surface parity with `gbrain doctor`. JSON output adds `crashes_by_cause: {runtime_error, oom_or_external_kill, unknown, legacy}` + `clean_exits_24h` fields so dashboards bind to named buckets; human output gains a per-cause line under `Crashes (24h)` plus a `Clean exits (24h)` line. Pre-fix the read site at `jobs.ts:805` counted every `worker_exited` event as a crash regardless of `likely_cause` — the same bug class the v0.35.5.0 doctor fix closes. Pinned by the 4 source-grep wiring assertions in `test/doctor.test.ts` that require the per-cause breakdown substrings (`crashes_by_cause`, `clean_exits_24h=`) to appear in BOTH `doctor.ts` and `jobs.ts`.
|
||
- `src/commands/features.ts` — `gbrain features --json --auto-fix`: usage scan + feature adoption salesman
|
||
- `src/commands/autopilot.ts` — `gbrain autopilot --install`: self-maintaining brain daemon (sync+extract+embed). **v0.28.1:** consumes `detectTini()` from `src/core/minions/spawn-helpers.ts` and resolves it once at startup instead of per worker respawn (was paying an `execFileSync` cost on every restart). **v0.34.3.0:** inline spawn-and-respawn loop replaced with a `ChildWorkerSupervisor` instance. Drops `crashCount`, `lastWorkerStartTime`, `STABLE_RUN_RESET_MS`, `startWorker`, and the inline `child.on('exit')` block — all consolidated into the shared core. `--max-rss 2048` and `maxCrashes: 5` preserved from the legacy loop. `onMaxCrashesExceeded` now routes through autopilot's own `shutdown('max_crashes')` so the autopilot lockfile gets cleaned up (pre-refactor the inline loop called `process.exit(1)` directly and bypassed cleanup). `shutdown()` drains via `childSupervisor.killChild('SIGTERM')` + `awaitChildExit(35_000)` instead of `workerProc.kill()`. Pinned by `test/autopilot-supervisor-wiring.test.ts` (6 static-shape regression guards: composes ChildWorkerSupervisor not the legacy inline names, `--max-rss 2048` in argv, `maxCrashes: 5` literal, shutdown-via-callback wiring, no workerProc reference). Closes the parallel-supervisor bug class Codex flagged during plan-eng-review.
|
||
- `src/mcp/server.ts` — MCP stdio server (generated from operations). v0.22.7: tool-call handler delegates to `dispatchToolCall` from `src/mcp/dispatch.ts` so stdio + HTTP transports share one validation, context-build, and error-format path. **v0.34.1.0 (#870):** stdin `'end'` / `'close'` shutdown hooks are skipped when `process.env.MCP_STDIO === '1'`. Gateway-piped stdio MCP wrappers (OpenClaw's `bundle-mcp`, similar) pipe the JSON-RPC handshake then close their stdin half; pre-fix this killed the server before the first tool call landed. Signal handlers (SIGTERM / SIGINT / SIGHUP) and the parent-process watchdog still cover legitimate disconnects. `src/commands/serve.ts` exposes `ServeOptions.mcpStdio?: boolean` as a test seam so the runtime guard is exercisable without process.env mutation. Pinned by `test/serve-stdio-lifecycle.test.ts`.
|
||
- `src/mcp/dispatch.ts` (v0.22.7) — Shared tool-call dispatch consumed by both stdio (`server.ts`) and HTTP transports. Exports `dispatchToolCall(engine, name, params, opts)`, `buildOperationContext(engine, params, opts)`, and `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 to `remote: true` (untrusted); local CLI callers pass `remote: false`. Closed F1/F2/F3 drift bugs in the original v0.22.5 HTTP transport. **v0.26.9 (F8):** adds `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 as a sorted array for debug visibility; 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 via repeated probes. Operators on a personal laptop who want raw payload visibility opt back in with `gbrain serve --http --log-full-params` (loud stderr warning at startup). Canonical helper — new logging code paths route through it rather than `JSON.stringify(params)`.
|
||
- `src/mcp/rate-limit.ts` (v0.22.7) — 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 actually 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` (v0.26.0) — 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] [--log-full-params]`. Supersedes the v0.22.7 `src/mcp/http-transport.ts` simple bearer-auth path. Combines MCP SDK's `mcpAuthRouter` (authorize / token / register / revoke endpoints), a custom `client_credentials` handler (SDK's token endpoint throws `UnsupportedGrantTypeError` for CC; the custom handler runs BEFORE the router and falls through for `auth_code` / `refresh_token`), `requireBearerAuth` middleware for `/mcp` with scope enforcement before op dispatch, `localOnly` rejection, 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 endpoint broadcasts every MCP request to connected admin browsers. `cookie-parser` middleware wired (Express 5 has no built-in). Startup logging prints port, engine, configured issuer URL (honors `--public-url`), registered-client count, DCR status, and admin bootstrap token. **v0.26.9 hardening pass:** F7 sets `remote: true` explicitly on the `/mcp` request handler's OperationContext literal (closes the HTTP shell-job RCE — without this, `submit_job`'s protected-name guard at `operations.ts:1391` saw a falsy undefined and skipped, letting a `read+write`-scoped OAuth token submit `shell` jobs). F8 wires `summarizeMcpParams` from `src/mcp/dispatch.ts` into both `mcp_request_log` writes and the admin SSE feed by default (raw payloads opt-in via `--log-full-params` with stderr warning). F9 sets cookie `Secure` flag when behind HTTPS or a public-URL proxy. F10 caps the magic-link nonce store with an LRU bound. F12 routes DCR disable through the `GBrainOAuthProvider` constructor's `dcrDisabled` option instead of the prior monkey-patch on the express router. F14 wraps `transport.handleRequest` in try/catch so SDK throws return a JSON-RPC 500 envelope instead of express's default HTML error page. F15 unifies OperationError + unexpected exceptions through `buildError` / `serializeError` so `/mcp` always returns the same envelope shape. **v0.28.1:** `/health` endpoint extracted into pure `probeHealth(engine)` async function with `HEALTH_TIMEOUT_MS = 3000` exported constant — drops the timeout from 5s to 3s so Fly.io's 5s health-check deadline gets 2s of headroom for TCP, response framing, and clock skew. Races `engine.getStats()` against the timeout via `Promise.race`; saturated pool returns 503 with `Health check timed out (database pool may be saturated)` instead of hanging. `clearTimeout` in finally block prevents pending-timer pile-up under high probe rates (race-leak fix from adversarial review). **v0.28.10:** `/health` is now liveness-only via the new `probeLiveness(sql, engineName, version, timeoutMs)` helper that races `sql\`SELECT 1\`` against `HEALTH_TIMEOUT_MS` and returns the same `ProbeHealthResult` tagged-union as `probeHealth` (single timer-cleanup site, single 503 envelope). Body shape: `{status, version, engine}` only — engine stats are no longer spread on the public route. Full stats moved to a new admin endpoint `/admin/api/full-stats` (sibling to `/admin/api/stats` and `/admin/api/health-indicators`) gated by the existing `requireAdmin` middleware; that route calls `probeHealth(engine, ...)` and returns the original spread-stats body. `?full=true` query param removed entirely. Closes the original DoS surface where `getStats()`'s 6× count(*) on 96K-page brains through PgBouncer exceeded `HEALTH_TIMEOUT_MS` and triggered orchestrator restart cascades (Fly.io / k8s seeing 503 → restart loop → advisory-lock pile-up on the migration lock). Outside-voice review (Codex) caught that `/admin/api/health-indicators` is NOT a full-stats endpoint (returns only `{expiring_soon, error_rate}`), and that an alternative loopback-IP gate would have depended on `app.set('trust proxy', 'loopback')` semantics holding under proxy/XFF misconfiguration; the shipped admin-cookie design avoids both. **v0.31.3 (#681):** every OAuth/admin/audit SQL call routes through `sqlQueryForEngine(engine)` from `src/core/sql-query.ts` so `gbrain serve --http` works against PGLite brains. The four `mcp_request_log.params` INSERT sites (success path, auth_failed path, scope_denied path, server-error path) all go through `executeRawJsonb(engine, ...)` so the JSONB column stores real objects, not JSON-encoded strings — closes the bug where `params->>'op'` returned the encoded string `"search"` (with quotes) instead of `search`. Migration v46 normalizes any pre-v0.31.3 string-shaped backlog rows on first start. **v0.34.1.0 (#864):** new `--bind HOST` CLI flag with default `127.0.0.1`. Personal-laptop installs no longer publish the brain to the LAN by accident. Self-hosted operators pass `--bind 0.0.0.0` (or a specific interface IP) once to accept remote connections. A stderr WARN fires when `--public-url` is set without `--bind` so the operator sees the binding before the first request (common cause of "ngrok forwards to me but the agent can't reach the upstream" misconfigurations). The startup banner prints a `Bind:` line. **v0.34.1.0 (#861):** drops the `(authInfo as AuthInfo & {sourceId?: string}).sourceId ?? env ?? 'default'` cast chain — `AuthInfo.sourceId` and `AuthInfo.allowedSources` are now the typed source of truth, populated by `oauth-provider.ts:verifyAccessToken` from the `oauth_clients` row. **v0.35.3.0:** the inline ParamDef→schema mapper at `:837-849` (HTTP MCP `tools/list` handler) is replaced with `paramDefToSchema(v)` from `src/mcp/tool-defs.ts`. Pre-fix this site silently dropped `items` on every array param so strict-mode OAuth clients (Gemini Pro structured outputs, OpenAI strict tool defs) rejected the whole tool list. Single mapper now serves stdio MCP, HTTP MCP `tools/list`, and the subagent registry.
|
||
- `src/core/sql-query.ts` (v0.31.3) — 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)` so Postgres goes via postgres.js's `unsafe(sql, params)` path and PGLite via its embedded `db.query(sql, params)`. Deliberately narrower than postgres.js's `sql` tag: no nested fragments, no `sql.json()`, no `sql.unsafe()`, no `sql.begin()`, no array binding. The narrow surface is the feature — codex finding #7 from the v0.31 plan review argued the adapter should stay scalar-only or it drifts into a partial postgres.js clone. JSONB writes go through the separate `executeRawJsonb(engine, sql, scalarParams, jsonbParams)` helper that composes positional `$N::jsonb` casts and passes JS objects through; the v0.12.0 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 and `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 pattern. Consumed by `src/commands/auth.ts`, `src/commands/serve-http.ts`, `src/core/oauth-provider.ts`, `src/commands/files.ts`, and `src/mcp/http-transport.ts` so all five sites work against PGLite and Postgres uniformly. Closes the bug where `gbrain auth` + `gbrain serve --http` were silently Postgres-only because they routed every SQL through the postgres.js singleton (community PR #681).
|
||
- `src/commands/serve.ts` (v0.31.3) — `gbrain 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)` path that releases the engine and the PGLite write-lock dir within 5 seconds. Pre-v0.31.3 the stdio MCP server held the lock indefinitely after Claude Desktop / Cursor / launchd-managed gateways disconnected, forcing a 5-minute stale-lock wait on the next start. Watchdog reparent check is `getParentPid() !== initialParentPid` (capturing the initial ppid once at install time and firing on any change); the previous `=== 1` check missed the subreaper case under launchd / systemd. Bun's `process.ppid` cache is stale across reparenting (see [oven-sh/bun#30305](https://github.com/oven-sh/bun/issues/30305)) so `getParentPid()` runs `spawnSync('ps', ['-o', 'ppid=', '-p', PID])` per tick to read the live kernel PPID. Startup probe verifies `ps` is on PATH; if not (stripped containers, busybox without procps), the watchdog skips installing AND emits a loud `[gbrain serve] watchdog disabled: ps unavailable, parent-death detection unavailable — child will rely on stdin EOF / signals only` stderr line so operators see the degraded mode at boot. Pinned by `test/serve-stdio-lifecycle.test.ts` (22 cases). Closes #413, #446. Credit @Aragorn2046 (origin features in #591) and @seungsu-kr (rebased submitter, Bun ppid workaround).
|
||
- `src/core/oauth-provider.ts` (v0.26.0) — `GBrainOAuthProvider` implementing the MCP SDK's `OAuthServerProvider` + `OAuthRegisteredClientsStore` interfaces. Backed by raw SQL (works on both PGLite and Postgres — OAuth is infrastructure, not a BrainEngine concern). Full OAuth 2.1 spec: `authorize` + `exchangeAuthorizationCode` with PKCE (for ChatGPT), `client_credentials` (for Perplexity / Claude), `refresh_token` with rotation, `revokeToken`, `registerClient` (DCR path validates redirect_uri must be `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 race). Refresh rotation also `DELETE...RETURNING` (closes §10.4 stolen-token detection bypass). `pgArray()` escapes commas/quotes/braces in elements 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 wrapped in try/catch. **v0.26.9 RFC 6749/7009 hardening pass:** F1+F2 fold `client_id` atomically into the `DELETE WHERE` clauses for both auth-code exchange and refresh rotation — pre-fix the post-hoc client compare burned the row on wrong-client paths so the legitimate client couldn't retry. F3 enforces refresh-scope-subset against the original grant on the row (RFC 6749 §6), not the client's currently-allowed scopes — fixes the case where revoking a scope from a client wouldn't shrink the agent's existing refresh tokens. F4 binds `client_id` on `revokeToken` so a client can only revoke its own tokens (RFC 7009 §2.1). F7c validates the `/token` request's `redirect_uri` against the value stored at `/authorize` (RFC 6749 §4.1.3) — empty-string treated as missing rather than wildcard match (adversarial-review fix). F5 swaps bare `catch {}` blocks in `verifyAccessToken` and `getClient` for `isUndefinedColumnError` from `src/core/utils.ts` — only SQLSTATE 42703 falls through to legacy fallback; lock timeouts and network blips throw and surface. F6 makes `sweepExpiredTokens()` actually return the count via `RETURNING 1` + array length, not a fire-and-forget zero. F12 adds `dcrDisabled` constructor option so `serve-http.ts` can disable the `/register` endpoint without monkey-patching the router. **v0.26.2:** module-private `coerceTimestamp()` boundary helper at the top of the file normalizes postgres-driver-as-string BIGINT columns to JS numbers at every read site (5 call sites: `getClient` L112+L113 for DCR `/register` RFC 7591 §3.2.1 numeric timestamps, `exchangeRefreshToken` L274 + `verifyAccessToken` L296+L303 for the SDK's `typeof === 'number'` bearerAuth check). Throws on non-finite input (NaN/Infinity) so corrupt rows fail loud at the boundary instead of riding through as `expiresAt: NaN`; returns undefined for SQL NULL so callers decide NULL semantics explicitly (refresh + access token paths treat NULL as expired). Helper intentionally NOT promoted to `src/core/utils.ts` — codex review flagged repo-wide BIGINT precision-loss risk for a generic helper. **v0.34.1.0 (#909):** `registerClient` honors `token_endpoint_auth_method: "none"` (RFC 7591 §3.2.1) — public PKCE clients (Claude Code, Cursor, every other PKCE-first MCP client) store `client_secret_hash = NULL` and the response payload omits `client_secret` entirely. Confidential clients (default `client_secret_post` and explicit `client_secret_basic`) keep their one-time-reveal shape. `getClient` correctly normalizes a NULL `client_secret_hash` to JS `undefined` so the SDK's clientAuth path accepts the public client at `/token`. **v0.34.1.0 (#861 + #876):** `verifyAccessToken` JOINs `oauth_clients.source_id` (write scope, scalar) + `oauth_clients.federated_read` (read scope, TEXT[]) and surfaces both on the returned `AuthInfo`. Pre-v60 / pre-v61 brains degrade gracefully via `isUndefinedColumnError` fallback so the upgrade chain is non-blocking on legacy DBs.
|
||
- `admin/` (v0.26.0) — 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 button), Register (modal with scope checkboxes + grant type selector), Credentials reveal (full-screen modal with Copy + Download JSON + yellow 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 (v0.22.7 wired as a first-class CLI subcommand) plus `gbrain auth register-client` (v0.26.0) and `gbrain auth revoke-client <client_id>` (v0.26.2) 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 + authorization code in a single transaction. `process.exit(1)` on no-such-client (idempotent — re-running on the same id produces the same exit-1 message). Legacy tokens stored as SHA-256 hashes in `access_tokens`; OAuth clients in `oauth_clients`. As of v0.26.0, legacy tokens grandfather to `read+write+admin` scopes on the OAuth HTTP server, so pre-v0.26 deployments keep working with no migration. **v0.31.3 (#681):** 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 brains. Pre-fix, every call hit the postgres.js singleton via `getConn()` and silently failed (or wrote to the wrong DB) when the active engine was PGLite. The takes-holders write goes through `executeRawJsonb(engine, sql, [name, hash], [{takes_holders:[...]}])` which round-trips with `jsonb_typeof = 'object'` instead of the pre-v0.31.3 quoted-string shape. **v0.34.1.0 (#876):** `register-client` accepts `--source <id>` (write authority, scalar) and `--federated-read <S1,S2,...>` (read scope, array). The output prints the resolved `Write source` and `Federated reads` for the registered client. Pre-v0.34 clients backfill to `source_id='default'` via migration v60 so existing deployments keep their v0.33 effective behavior verbatim.
|
||
- `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 filesystem walk of `skills/migrations/*.md` needed at runtime). `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 (bumped from 60s in v0.12.1 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 (2 phases: schema ALTER minion_jobs.max_stalled SET DEFAULT 3 — superseded by v0.14.3's schema-level DEFAULT 5 + UPDATE backfill; pending-host-work ping for skills/migrations/v0.14.0.md). All orchestrators are idempotent and resumable from `partial` status. As of v0.14.2 (Bug 3), the RUNNER owns all ledger writes — orchestrators return `OrchestratorResult` and `apply-migrations.ts` persists a canonical `{version, status, phases}` shape after return. Orchestrators no longer call `appendCompletedMigration` directly. `statusForVersion` prefers `complete` over `partial` (never regresses). 3 consecutive partials → wedged → `--force-retry <version>` writes a `'retry'` reset marker. v0.14.3 (fix wave) ships schema-only migrations v14 (`pages_updated_at_index`) + v15 (`minion_jobs_max_stalled_default_5` with UPDATE backfill) via the `MIGRATIONS` array in `src/core/migrate.ts` — no orchestrator phases needed.
|
||
- `src/commands/repair-jsonb.ts` — `gbrain 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 v0.12.0 double-encode bug on Postgres; PGLite no-ops. Idempotent.
|
||
- `src/commands/orphans.ts` — `gbrain orphans [--json] [--count] [--include-pseudo]`: surfaces pages with zero inbound wikilinks, grouped by domain. Auto-generated/raw/pseudo pages filtered by default. Also exposed as `find_orphans` MCP operation. Shipped in v0.12.3 (contributed by @knee5).
|
||
- `src/commands/salience.ts` (v0.29) — `gbrain 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.ts` (v0.29) — `gbrain anomalies [--since YYYY-MM-DD] [--lookback-days N] [--sigma N] [--json]`: cohort-level activity outliers. Calls `engine.findAnomalies(opts)`. Two cohort kinds in v1: tag, type. Year cohort deferred to v0.30.
|
||
- `src/commands/whoknows.ts` (v0.33) — `gbrain whoknows <topic> [--explain] [--limit N] [--json]`: expertise + relationship-proximity routing. Mirrors v0.29 salience/anomalies shape (pure `rankCandidates()` + `findExperts()` orchestrator + `runWhoknows()` CLI dispatch + thin-client routing). MCP op = `find_experts` (scope: read, localOnly: false) per ENG-D5. Ranking formula (ENG-D1 locked): `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 the new `SearchOpts.types: ['person', 'company']` (no post-filter waste). hybridSearch's internal salience+recency boosts are intentionally disabled — 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.ts` (v0.33, v0.33.1.3 thin-client wiring) — `gbrain eval whoknows <fixture.jsonl> [--json] [--skip-replay]`: two-layer eval gate (ENG-D2). 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 so CI can gate. Mirrors v0.27.x cross-modal + v0.28.1 longmemeval dispatch shape under `src/commands/eval.ts`. **v0.33.1.3:** `WhoknowsFn` callable abstraction lets the gates be 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})` via the v0.31.1 seam; local mode calls `findExperts(engine, ...)` directly. cli.ts adds a thin-client bypass before `connectEngine` for `gbrain eval whoknows`, matching the longmemeval/cross-modal no-DB pattern. Regression gate auto-skips in thin-client mode (no DB access to `eval_candidates`). Public exports `jaccardAtK`, `topKHit`, `readFixture`, `WhoknowsFn`, threshold constants are pinned by `test/eval-whoknows.test.ts` (25 cases, +2 for the null-engine signature contract).
|
||
- `test/fixtures/whoknows-eval.jsonl` (v0.33) — 10-row synthetic placeholder demonstrating the eval-fixture schema (`{query, expected_top_3_slugs, notes?}` JSONL). End users replace with their own real queries before shipping; the placeholder uses obviously-example slugs (`wiki/people/example-alice`) so production data isn't conflated with the test fixture. Drives `test/e2e/whoknows.test.ts` (which seeds a matching synthetic brain and asserts the >=80% gate) and the `whoknows_health` doctor check.
|
||
- `src/commands/transcripts.ts` (v0.29) — `gbrain 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.ts` — `gbrain 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). v0.22.8: batch-load fast path on Postgres uses a single SQL query to fix 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` so real Postgres errors are diagnosable. **v0.32.8 (PR #860):** batch projection switched from `SELECT DISTINCT ON (slug)` to `SELECT ... ORDER BY source_id, slug` so multi-source brains scan each `(source, slug)` row independently (pre-fix the DISTINCT collapsed same-slug-different-source pages into one scan, the same bug class this PR fixes). Sequential and auto-repair loops use `listAllPageRefs()` to enumerate `(slug, source_id)` pairs and thread `sourceId` to `getPage`. Batch + sequential paths now report the same page count on multi-source brains.
|
||
- `src/commands/doctor.ts` — `gbrain doctor [--json] [--fast] [--fix] [--dry-run] [--index-audit]`: health checks. v0.12.3 added `jsonb_integrity` + `markdown_body_completeness` reliability checks. v0.14.1: `--fix` delegates inlined cross-cutting rules to `> **Convention:** see [path](path).` callouts (pipes DRY violations into `src/core/dry-fix.ts`); `--fix --dry-run` previews without writing. v0.14.2: `schema_version` check fails loudly when `version=0` (migrations never ran — the #218 `bun install -g` signature) and routes users to `gbrain apply-migrations --yes`; new opt-in `--index-audit` flag (Postgres-only) reports zero-scan indexes from `pg_stat_user_indexes` (informational only, no auto-drop). v0.15.2: every DB check is wrapped in a progress phase; `markdown_body_completeness` runs under a 1s heartbeat timer so 10+ min scans are observable on 50K-page brains. v0.19.1 added `queue_health` (Postgres-only) with two subchecks: stalled-forever active jobs (started_at > 1h) and waiting-depth-per-name > threshold (default 10, override via `GBRAIN_QUEUE_WAITING_THRESHOLD`). Worker-heartbeat subcheck intentionally deferred to follow-up B7 because it needs a `minion_workers` table to produce ground-truth signal. Fix hints point at `gbrain repair-jsonb`, `gbrain sync --force`, `gbrain apply-migrations`, and `gbrain jobs get/cancel <id>`. v0.22.12 (#500): `sync_failures` check shows `[CODE=N, ...]` breakdown for both unacked entries (warn) and acked-historical entries (ok), surfacing systemic failure modes (`SLUG_MISMATCH=2685`) instead of a bare count. v0.26.7 (#612): `rls_event_trigger` check (post-install drift detector for migration v35's auto-RLS event trigger). Lives outside the `// 5. RLS` slice that the structural doctor.test.ts guards anchor on, so the existing test guards stay intact. Healthy `evtenabled` set is `('O','A')` only — `R` is replica-only and would not fire in normal sessions; `D` is disabled. Fix hint is `gbrain apply-migrations --force-retry 35`. **v0.30.2:** `queue_health` gains a fourth subcheck — surfaces dead-lettered subagent jobs with `last_error` matching the `prompt_too_long` classifier within the last 24h. Fix hint points at `gbrain dream --phase synthesize --dry-run --json` to identify the offending transcript and `gbrain jobs prune --status dead --queue default` to clean up. Postgres-only. **v0.31.7:** `runDoctor` switches to `autoDetectSkillsDirReadOnly` (from `src/core/repo-root.ts`) so `bun install -g github:garrytan/gbrain && cd ~ && gbrain doctor` finds the bundled `skills/` via the install-path fallback instead of warning "Could not find skills directory" + docking the health score. `--fix` carries a D6 safety gate: when `detected.source === 'install_path'`, the command refuses auto-repair with a stderr message pointing at `$GBRAIN_SKILLS_DIR` / `$OPENCLAW_WORKSPACE` / `--skills-dir`, because `autoFixDryViolations` writes to SKILL.md files and would otherwise silently rewrite the install tree. The `graph_coverage` check now short-circuits to `ok: 'No entity pages — graph_coverage not applicable (markdown-only brain)'` when `SELECT COUNT(*) FROM pages WHERE type IN ('entity','person','company','organization')` returns 0 (closes #530); the entity count is woven into the warn message and the WARN hint switches from the long-deprecated `gbrain link-extract && gbrain timeline-extract` (gone since v0.16) to the canonical `gbrain extract all`. Pinned by an IRON-RULE regression assertion in `test/doctor.test.ts` that bans the stale verb names from the source string. **v0.32.4:** new `sync_freshness` check (exported `checkSyncFreshness` at the same file) added to both `runDoctor` (local) and `doctorReportRemote` (thin-client). Pure staleness probe — queries `sources.last_sync_at` only, no filesystem access. Warns at 24h, fails at 72h (or never-synced). Future-`last_sync_at` warns ("clock skew or corrupted timestamp") instead of silently falling through as ok — codex outside-voice caught the negative-ageMs bug pre-merge. Env-var overrides `GBRAIN_SYNC_FRESHNESS_WARN_HOURS` / `GBRAIN_SYNC_FRESHNESS_FAIL_HOURS`; invalid values fall back to defaults with a once-per-process stderr warn (`_resolveSyncFreshnessHours`). Failure messages embed `source.id` (not `source.name`) so the printed fix command `gbrain sync --source <id>` matches what the user copy-pastes. Filesystem-vs-DB page drift detection was deliberately stripped from the v0.32.4 scope — `doctorReportRemote` runs in the HTTP MCP server (`src/commands/serve-http.ts`), and walking DB-supplied `local_path` from a remote-callable endpoint crosses a trust boundary (OAuth write scope could mutate `sources.local_path`). Drift detection will resurface in a separate PR routed through `multi_source_drift`'s existing guard infrastructure (`GBRAIN_DRIFT_LIMIT` / `GBRAIN_DRIFT_TIMEOUT_MS`) with slug normalization tests and a meta-file allow-list. Pinned by 12 cases in `test/doctor.test.ts` ("v0.32.4 — sync_freshness check" describe block): empty sources, never-synced fail, >72h fail, exact 72h boundary, 24h-72h warn, exact 24h boundary, <24h ok, future-timestamp warn, mixed sources (highest severity wins), `executeRaw` throws → outer-catch warn, `GBRAIN_SYNC_FRESHNESS_FAIL_HOURS=6` override fires at 7h, source.id-in-message regression. **v0.36.3.0:** new `embedding_column_registry` check probes each declared column via Postgres `format_type(atttypid, atttypmod)` so a registry entry claiming 1024d Voyage against an actual 1536d OpenAI column surfaces with a paste-ready `gbrain config set embedding_columns '{...}'` ALTER hint instead of mysterious "vector dimension mismatch" errors at search time. On Postgres the check also probes HNSW index presence (`pg_indexes` lookup keyed by column name) and warns when missing (search will still work via seq scan but won't hit the index). The active default column's population coverage is computed via `COUNT(*) FILTER (WHERE <col> IS NOT NULL) / COUNT(*)` and warns below 90% — except empty brains (chunk_count = 0) where the gate short-circuits to `ok` so fresh `gbrain init` runs don't see "Active column 'embedding' is 0.0% populated" (CDX-5 codex fix). PGLite parity via the same SQL through `executeRaw` — registry validation happens on both engines. **v0.35.5.0:** the Lane D supervisor check at `doctor.ts:1011-1043` now consumes `summarizeCrashes(events)` from `src/core/minions/handlers/supervisor-audit.ts` instead of the pre-fix `events.filter(e => e.event === 'worker_exited').length`. The warn threshold drops from `>3` to `>=1` (any real crash is signal now that the counter is calibrated against clean exits). The ok message gains `clean_exits_24h=N`; the warn message gains `runtime=A oom=B unknown=C legacy=D` per-cause breakdown so an operator triages OOM vs runtime-error vs unknown-future-cause at a glance without grep'ing the JSONL audit. Closes the "Supervisor crashes: 120x/24h, was 62x — nearly doubled" alarm class that bit users on healthy brains after v0.34.3.0's RSS-watchdog work added more code=0 worker drains — both `doctor` and `gbrain jobs supervisor status` were counting every `worker_exited` event as a crash regardless of cause. Cross-surface parity is the regression guard: 4 source-grep wiring assertions in `test/doctor.test.ts` ban the ad-hoc filter pattern, pin the `>=1` threshold, and require the per-cause breakdown substrings (`runtime=`, `oom=`, `unknown=`, `legacy=`, `clean_exits_24h=`, `crashes_by_cause`) to appear in BOTH `doctor.ts` and `jobs.ts`.
|
||
- `src/core/migrate.ts` — schema-migration runner. Owns the `MIGRATIONS` array (source of truth for schema DDL). **v40 (v0.29):** `pages_emotional_weight` adds `pages.emotional_weight REAL NOT NULL DEFAULT 0.0`. Column-only (no index). On Postgres 11+ and PGLite, `ADD COLUMN` with a constant DEFAULT is metadata-only — instant on tables of any size. v0.14.2 extended the `Migration` interface with `sqlFor?: { postgres?, pglite? }` (engine-specific SQL overrides `sql`) and `transaction?: boolean` (set to false for `CREATE INDEX CONCURRENTLY`, which Postgres refuses inside a transaction; ignored on PGLite since it has no concurrent writers). Migration v14 (fix wave) uses a handler branching on `engine.kind` to run CONCURRENTLY on Postgres (with a pre-drop of any invalid remnant via `pg_index.indisvalid`) and plain `CREATE INDEX` on PGLite. v15 bumps `minion_jobs.max_stalled` default 1→5 and backfills existing non-terminal rows. v0.22.6.1: migration v24 (`rls_backfill_missing_tables`) uses `sqlFor: { pglite: '' }` to no-op on PGLite — PGLite has no RLS engine and is single-tenant by definition, and the v24 ALTERs target subagent tables that don't exist in pglite-schema.ts. Closes #395 (contributed by @jdcastro2). **v30 (v0.23):** creates `dream_verdicts (file_path TEXT, content_hash TEXT, worth_processing BOOL, reasons JSONB, judged_at TIMESTAMPTZ, PK(file_path, content_hash))`. RLS-enabled when running as a BYPASSRLS role. The synthesize phase reads/writes this table to avoid re-judging on backfill re-runs. **v35 (v0.26.7):** auto-RLS event trigger + one-time backfill. `auto_rls_on_create_table` fires on `ddl_command_end` for `WHEN TAG IN ('CREATE TABLE','CREATE TABLE AS','SELECT INTO')` and runs `ALTER TABLE … ENABLE ROW LEVEL SECURITY` on every new `public.*` table — no FORCE (matches v24/v29/schema.sql posture so non-BYPASSRLS apps can still read their own tables). The same migration backfills RLS on every existing `public.*` base table whose comment doesn't match the doctor regex (`^GBRAIN:RLS_EXEMPT\s+reason=\S.{3,}`). Per-table failure aborts the offending CREATE TABLE (event triggers fire inside the DDL transaction); no EXCEPTION wrap — that would convert loud rollback into silent permissive default. PGLite no-op via `sqlFor.pglite: ''`. Breaking change: operators with intentionally-RLS-off public tables must add the GBRAIN:RLS_EXEMPT comment BEFORE upgrade or the backfill will flip them on. **v46 (v0.31.3):** `mcp_request_log_params_jsonb_normalize` rewrites pre-v0.31.3 rows where `mcp_request_log.params` was stored as a JSON-encoded string (`jsonb_typeof = 'string'`) up to a real JSONB object via `UPDATE ... SET params = params::text::jsonb WHERE jsonb_typeof(params) = 'string'`. Single statement, idempotent — second-run finds no string-shaped rows and is a no-op. Closes the bug where `/admin/api/requests` returned a quoted string instead of the parsed object. **v0.36.3.0 (v68):** `eval_candidates_embedding_column` adds `eval_candidates.embedding_column TEXT NULL`. Per-row provenance for `gbrain eval replay`: captured rows record which column the live query ran against so replay reproduces the same retrieval space (Voyage rows replay against Voyage; OpenAI rows against OpenAI). NULL-tolerant — pre-v0.36 rows fall back to the current default during replay rather than failing. Column-only migration, metadata-only on both engines. **v0.34.1.0 (#861 + #876, v60-v65):** six-migration chain wires source-scoping into the OAuth client table. v60 (`oauth_clients_source_id_fk`) adds `oauth_clients.source_id TEXT` with NULL→`'default'` backfill and an FK to `sources(id) ON DELETE SET NULL`. v61 (`oauth_clients_federated_read_column`) adds `federated_read TEXT[] NOT NULL DEFAULT '{}'`. v62 (`oauth_clients_federated_read_backfill`) explicit-CASE backfills so `source_id IS NULL` produces `'{}'` not an array-containing-NULL. v63 (`oauth_clients_federated_read_validate`) is the fail-loud check that every row's source_id is in its federated_read array post-backfill. v64 (`oauth_clients_source_id_fk_restrict`) flips the FK to `ON DELETE RESTRICT` now that federated_read provides the alternative scope-loss path — source delete is refused if any client references it. v65 (`oauth_clients_federated_read_gin_index`) is the GIN index for the array-containment queries the read paths run. PGLite parity via `sqlFor.pglite` where needed.
|
||
- `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)` helper 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. Introduced in v0.15.2.
|
||
- `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 the 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` (v0.22.13) — generic `tryAcquireDbLock(engine, lockId, ttlMinutes)` over the existing `gbrain_cycle_locks` table. Parameterized lock id so different scopes can nest cleanly: `gbrain-cycle` for the broad cycle (held by `cycle.ts`) and `gbrain-sync` (`SYNC_LOCK_ID` constant) for `performSync`'s narrower writer window. Same UPSERT-with-TTL semantics as the prior cycle-only helper, just generalized. Survives PgBouncer transaction pooling (unlike session-scoped `pg_try_advisory_lock`); crashed holders auto-release once their TTL expires.
|
||
- `src/core/sync-concurrency.ts` (v0.22.13) — 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)` (Q1: explicit `--workers` bypasses the >50-file floor), and `parseWorkers(s)` (rejects `'0'`, `'-3'`, `'foo'`, `'1.5'`, trailing chars — replaces the prior parseInt-with-no-validation in both `sync.ts` and `import.ts`). Used by `performSync`, `performFullSync`, `runImport`, and the Minion `sync` handler so the three sites can no longer drift.
|
||
- `src/commands/sync.ts` — `gbrain sync` CLI + the `performSync` / `performFullSync` library entrypoints (consumed by the autopilot cycle and the Minion sync handler). v0.22.13 (PR #490): `performSync` wraps its body in a `gbrain-sync` writer lock so two concurrent syncs (manual + autopilot, two terminals, two Conductor workspaces) cannot both write `last_commit` and let the last writer win. Head-drift gate after the import phase re-checks `git rev-parse HEAD`; if HEAD moved (someone ran `git checkout` / `git pull` mid-sync), the bookmark refuses to advance. Vanished files now record a failedFiles entry instead of silent-skip — the silent-skip-then-advance pathology that survived prior hardening passes is dead. Worker engines wrap in try/finally so disconnect always fires (panic-path leak fix). Both PGLite-detection sites use `engine.kind === 'pglite'`. CLI accepts `--workers N` (alias `--concurrency N`), validated via `parseWorkers`. Explicit `--workers` bypasses the auto-path file-count floor; auto path defers to `autoConcurrency()`. Banner moved to stderr. **v0.34.2.0:** the inline `.sort()` over add/mod paths is replaced with `sortNewestFirst(addsAndMods)` from `src/core/sort-newest-first.ts`, so the newest-first descending-lex policy lives in one helper shared with `gbrain import` instead of drifting across two files.
|
||
- `src/commands/import.ts` — `gbrain import` CLI + `runImport` library entrypoint. v0.34.2.0 replaces the prior positional-index checkpoint (`processedIndex: N` into a sorted file list) with 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. Three bug classes died: parallel-import-with-slow-worker drops the slow file on crash-resume (closed — the slow file isn't in `completed` until its own `processFile` resolves), failed-file-bumps-counter-past-itself (closed — failures don't add to `completed`), and v0.33.x sort-flip-drops-newest-N-on-cross-version-resume (closed — 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, so a long failure tail doesn't churn the JSON. Pinned by `test/import-checkpoint.test.ts` (18 unit cases over the helpers) + `test/import-resume.test.ts` (5 integration cases under PGLite, including the SLUG_MISMATCH retry regression codex caught during plan-eng-review).
|
||
- `src/core/import-checkpoint.ts` (v0.34.2.0) — `loadCheckpoint(brainDir)`, `saveCheckpoint(brainDir, completed)`, `resumeFilter(files, completed, brainDir)`, `clearCheckpoint()`, plus the `ImportCheckpoint` type. Path-set checkpoint format (`{schema_version, brainDir, completed: string[]}`) replaces the v0.33.x positional `{processedIndex: N}` format. 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 (you ran import against a different brain), and the old positional format (logged to stderr before being discarded). `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 test isolation via `withEnv({GBRAIN_HOME: tmpdir})` works without monkey-patching the fs layer. Best-effort persistence — `saveCheckpoint` logs warnings on write errors but never throws, so import keeps making progress even if disk is full.
|
||
- `src/core/sort-newest-first.ts` (v0.34.2.0) — 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 in this helper instead of touching two CLI commands. Pinned by `test/sort-newest-first.test.ts` (5 hermetic cases: descending order, mixed prefixes, empty input, single-element input, in-place-mutation contract).
|
||
- `src/core/cycle.ts` — v0.17 brain maintenance cycle primitive (extended to **9 phases in v0.29**). `runCycle(engine: BrainEngine | null, opts: CycleOpts): Promise<CycleReport>` composes phases in semantically-driven order: **lint → backlinks → sync → synthesize → extract → patterns → recompute_emotional_weight → embed → orphans**. v0.29 adds the `recompute_emotional_weight` phase between patterns and embed; it sees the union of `syncPagesAffected` + `synthesizeWrittenSlugs` for incremental mode, or all pages when neither anchor is set (full backfill via `gbrain dream --phase recompute_emotional_weight`). v0.29 also extends `CycleReport.totals` with `pages_emotional_weight_recomputed` (additive, schema_version stays "1"). v0.23's `synthesize` phase runs after sync (cross-references see fresh brain) and before extract (auto-link materializes its writes); `patterns` runs after extract so it reads a fresh graph (codex finding #7 — subagent put_page sets `ctx.remote=true` and skips auto-link/timeline by default; extract is the canonical materialization). Three callers: `gbrain dream` CLI, `gbrain autopilot` daemon's inline path, and the Minions `autopilot-cycle` handler. Coordination via `gbrain_cycle_locks` DB table + `~/.gbrain/cycle.lock` file lock with PID-liveness for PGLite. `CycleReport.schema_version: "1"` is stable; totals additively grew in v0.23 (`transcripts_processed`, `synth_pages_written`, `patterns_written`). `yieldBetweenPhases` runs between phases. **v0.23 added `yieldDuringPhase`** for in-phase keepalive — synthesize/patterns call it during long waits to renew the cycle-lock TTL. Engine nullable; lock-skip on read-only phase selections. v0.22.1 (#403): `CycleOpts.signal?: AbortSignal` propagates the worker's abort signal; `checkAborted()` fires between every phase. v0.22.1 (#417): `runPhaseSync` returns `pagesAffected` via `SyncPhaseResult`; `runCycle` captures it and threads to `runPhaseExtract` as the 4th arg. v0.22.1 (Codex F2): `runPhaseSync` takes `willRunExtractPhase: boolean` and sets `noExtract: phases.includes('extract')` so `gbrain dream --phase sync` doesn't silently lose extraction. v0.22.5 (#475): `resolveSourceForDir(engine, brainDir)` threads `sourceId` to `performSync()` so sync reads the per-source `sources.last_commit` anchor instead of the drift-prone global `config.sync.last_commit` key.
|
||
- `src/core/cycle/synthesize.ts` (v0.23) — Synthesize phase: conversation-transcript-to-brain pipeline. Reads from `dream.synthesize.session_corpus_dir`, runs 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` — codex finding #2) 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>`. Auto-commit deferred to v1.1 (codex #5). `--dry-run` runs Haiku, skips Sonnet (codex #8). Subagent never gets fs-write access. **v0.23.2:** `renderPageToMarkdown` (now exported) stamps `dream_generated: true` and `dream_cycle_date` into every reverse-write's frontmatter; `writeSummaryPage` does the same on the dream-cycle summary index. The marker is the explicit identity surface checked by `isDreamOutput` in `transcript-discovery.ts` — replaces the v0.23.1 content-prefix heuristic that could miss real output (`serializeMarkdown` doesn't embed slugs in body) and false-positive on user transcripts citing brain pages. `judgeSignificance` and `JudgeClient` are exported; `judgeSignificance` accepts a `verdictModel` parameter (default `claude-haiku-4-5-20251001`) loaded from `dream.synthesize.verdict_model` via `loadSynthConfig`. **v0.30.2:** model-aware chunker `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 computed from `MODEL_CONTEXT_TOKENS[resolvedModel] × 0.9 × 3.5 chars/token`; non-Anthropic ids fall back to a 180K-token safe default with a once-per-process stderr warning. Operator overrides: `dream.synthesize.max_prompt_tokens` (floor 100K, wins when set) 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 (D8 lookup), so existing brains skip with `already_synthesized_legacy_single_chunk` instead of re-spending Sonnet on upgrade. `collectChildPutPageSlugs` raw-fetches every (job_id, slug) pair (not `SELECT DISTINCT`) and rewrites bare-hash6 slugs to `<hash6>-c<idx>` for chunked children (D6 — orchestrator-side, zero Sonnet trust). Cap-hit skips don't write to `dream_verdicts`, so raising the cap on next run re-attempts cleanly. D7 scope: bounds INITIAL prompt size only; tool-loop turn-N accumulation is caught by the v0.30.2 terminal-error classification in `subagent.ts`, not bounded ahead of time.
|
||
- `src/core/cycle/patterns.ts` (v0.23) — 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` (v0.32.2, extended v0.35.6.0) — extract_facts cycle phase. v0.32.2 contract: fence is canonical; per-page wipe (`deleteFactsForPage`) + reinsert from `parseFactsFence` + `extractFactsFromFenceText` + `engine.insertFacts`. Empty-fence guard refuses when v0.31 legacy rows (`row_num IS NULL AND entity_slug IS NOT NULL`) pend the v0_32_2 backfill (status: warn, hint: `gbrain apply-migrations --yes`). **v0.35.6.0** adds a phantom-redirect pre-pass that 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 was 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 (round-14 scenario-B fix: phantom had only-on-disk fence, no DB facts). `ExtractFactsResult` gains six phantom fields: `phantomsScanned`, `phantomsRedirected`, `phantomsAmbiguous`, `phantomsSkippedDrift`, `phantomsLockBusy`, `phantomsMorePending`. Three of those bubble to `CycleReport.totals` (`phantoms_redirected`, `phantoms_ambiguous`, `phantoms_skipped_drift`).
|
||
- `src/core/entities/resolve.ts` (v0.30+, extended v0.35.6.0) — 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>-%` using correlated-subquery `connection_count` for tiebreaker) → deterministic `slugify` fallback. **v0.35.6.0** exports two new helpers for the phantom-redirect pass: `resolvePhantomCanonical(engine, sourceId, phantomSlug)` — variant that SKIPS the exact-slug step (codex #1: phantom slug `'alice'` exact-matches itself, would make the redirect handler a no-op); returns the canonical only when result is non-null AND contains `/`. `findPrefixCandidates(engine, sourceId, token)` — standalone SQL query returning ALL candidates across `PREFIX_EXPANSION_DIRS` (currently hardcoded `['people', 'companies']`) using `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` because that path returns per-dir top-1 and suppresses ambiguity by design (codex #11). Pinned by `test/phantom-redirect.test.ts` resolvePhantomCanonical describe (3 cases) + findPrefixCandidates describe (6 cases including multi-dir ambiguity and the `people/aliceberg`-doesn't-match-`alice` false-positive guard).
|
||
- `src/core/cycle/phantom-redirect.ts` (v0.35.6.0) — Phantom-redirect orchestrator. Exports `runPhantomRedirectPass(engine, brainDir, sourceId, dryRun): Promise<PhantomPassResult>` (the per-cycle wrapper that acquires `gbrain-sync` writer lock once for the entire pass, 30s bounded retry, walks up-to-`GBRAIN_PHANTOM_REDIRECT_LIMIT` unprefixed phantoms) + `tryRedirectPhantom(engine, page, sourceId, brainDir, dryRun): Promise<RedirectResult>` (single-phantom orchestrator) + `stripFenceAndFrontmatterAndLeadingH1` (pure helper for the body-shape gate — strips facts fence including the preceding `## Facts` heading and the leading H1; zero residue = phantom). Handler order: body-shape gate → `resolvePhantomCanonical` (codex #1: bypasses exact-self-match) → `findPrefixCandidates` ambiguity check (codex #11: standalone query, not per-dir top-1) → `fenceDbDrift` bi-directional check (rounds 27/29/30) → dry-run early exit → materialize canonical via `serializeMarkdown` if DB-only (codex #6) → 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 (codex #7) → `engine.migrateFactsToCanonical` (codex #3/#4/#12 lossless preservation) → `engine.rewriteLinks` (DB FK rewrite; wiki-link text rewrite is a documented follow-up per codex #5) → `engine.softDeletePage` + `engine.deleteFactsForPage(phantom)` + `fs.unlinkSync(phantomPath)` (rounds 19/20). `RedirectResult.canonical` populated on outcome `'redirected'` (incl. dry-run preview) so the caller can populate `touched_canonicals`. Idempotent on re-run: phantom soft-deleted → predicate fails (`deleted_at IS NULL` filter); migrate UPDATE matches no rows; dedup-guard prevents double-append.
|
||
- `src/core/facts/phantom-audit.ts` (v0.35.6.0) — 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` because the consumer + lifecycle are distinct (stub-guard logs PREVENTIVE blocks; phantom-audit logs CLEANUP decisions, will be read by a future T9 doctor `phantoms_pending` check).
|
||
- `src/core/cycle/emotional-weight.ts` (v0.29) — 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; users override via config key `emotional_weight.high_tags` (JSON array). `userHolder` overridable via `emotional_weight.user_holder`.
|
||
- `src/core/cycle/anomaly.ts` (v0.29) — 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` (v0.29) — Cycle phase orchestrator. Two SQL round-trips total: `engine.batchLoadEmotionalInputs(slugs?)` → `computeEmotionalWeight` (per-row pure function) → `engine.setEmotionalWeightBatch(rows)`. Reads config keys `emotional_weight.high_tags` (JSON array, falls back to default seed list on parse error) and `emotional_weight.user_holder`. Empty `affectedSlugs` array short-circuits with zero-work success. dry-run mode 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.ts` (v0.29) — `listRecentTranscripts(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 keys (same as `discoverTranscripts`); walks for `.txt` files within `days`; applies `isDreamOutput` guard from `transcript-discovery.ts` (skips dream-generated files); returns `{path, date, mtime, length, summary}[]` sorted newest-first. Summary mode (default true) returns 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 library is a trusted library function used by both the gated op and the local CLI.
|
||
- `src/core/operations-descriptions.ts` (v0.29) — 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 the redirect-edited `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` (v0.23) — Pure filesystem walk for synthesize. `discoverTranscripts(opts)` filters `.txt` files by date range, min_chars, and word-boundary regex `excludePatterns` (Q-3: `medical` matches "medical advice" but NOT "comedical"; power users may pass full regex). `readSingleTranscript(path)` is the `gbrain dream --input <file>` ad-hoc path. **v0.23.2 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 `discoverTranscripts` and `readSingleTranscript` skip matching files and emit a `[dream] skipped <basename>: dream_generated marker` stderr log (no more 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`. Replaces v0.23.1's `DREAM_OUTPUT_SLUGS` content-prefix list.
|
||
- `src/commands/dream.ts` — v0.17 `gbrain dream` CLI; ~80-line thin alias over `runCycle`. brainDir resolution requires explicit `--dir` OR `sync.repo_path` config. Flags: `--dry-run`, `--json`, `--phase <name>`, `--pull`, `--dir <path>`. **v0.23 added** `--input <file>` (ad-hoc transcript, implies `--phase synthesize`), `--date YYYY-MM-DD`, `--from <d> --to <d>` (backfill range). Conflict detection: `--input` + `--date` exits 2. ISO date validation. `--dry-run` runs Haiku significance verdict but skips Sonnet synthesis (codex finding #8 — NOT zero LLM calls). Exit code 1 on status=failed. **v0.23.2 added** `--unsafe-bypass-dream-guard` (long-form intentional, plumbed through `runCycle.synthBypassDreamGuard` → `SynthesizePhaseOpts.bypassDreamGuard` → `discoverTranscripts({bypassGuard})` and `readSingleTranscript({bypassGuard})`). Loud stderr warning fires at synthesize-phase entry when set. Never auto-applied for `--input` so any caller can't silently re-trigger the loop bug.
|
||
- `src/commands/friction.ts` + `src/core/friction.ts` (v0.23) — `gbrain friction {log,render,list,summary}` reporter. Append-only JSONL under `$GBRAIN_HOME/friction/<run-id>.jsonl`. Schema is a flat extension of `StructuredAgentError` (D20). 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/` (v0.23) — `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`). Phases for scripted mode: 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. v1 ships with the OpenClaw runner only (`src/core/claw-test/runners/openclaw.ts`, invokes `openclaw agent --local --agent <name> --message <brief>`); hermes runner deferred to v1.1. Transcript capture (`transcript-capture.ts`) uses `fs.createWriteStream` with `'drain'`-event backpressure — D17 fix for the 256KB-burst child-stall scenario. v0.18 upgrade scenario seeded via `seed-pglite.ts` SQL replay.
|
||
- `skills/_friction-protocol.md` (v0.23) — 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. Stable from v0.15.2, additive only.
|
||
- `src/core/markdown.ts` — Frontmatter parsing + body splitter. `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 the 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 (v0.15.1 #219 regression guard — must be DEFAULT 5 to preserve SIGKILL-rescue). Wired into `bun test`.
|
||
- `scripts/check-source-id-projection.sh` (v0.32.8, PR #860) — 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. After v0.32.8 `Page.source_id` is required at the type level; a projection that drops the column produces `Page` rows with `source_id: undefined` while TypeScript's `: string` lies about it. Codex's outside-voice review caught two pre-existing projections (`getPage`, `putPage RETURNING`) that lacked the column. Wired into `bun run verify` + `bun run check:all`.
|
||
- `docker-compose.ci.yml` + `scripts/ci-local.sh` (v0.23.1) — 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 (matches GH Actions structure), 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 (avoids 5432 manual `gbrain-test-pg` and 5433 sibling-project conflict); override with `GBRAIN_CI_PG_PORT=NNNN`. Stronger gate than current PR CI's 2-file Tier 1 set — closes the "push-and-wait" feedback loop pre-push.
|
||
- `scripts/select-e2e.ts` + `scripts/e2e-test-map.ts` (v0.23.1) — 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 by design: EMPTY → all 29 files (clean branch shouldn't run nothing), DOC_ONLY (every path matches the README/CLAUDE/AGENTS/CHANGELOG/TODOS allowlist) → empty stdout, SRC → escape-hatch paths (schema, package.json, skills/) trigger all; otherwise the hand-tuned `E2E_TEST_MAP` glob → tests narrows; an unmapped src/ change still emits ALL files, never silently nothing. Pure-function exports (`selectTests`, `classify`, `matchGlob`) so it's trivial to test and fork. `bun run ci:select-e2e` prints the current selection on stdout, pipe-friendly. `test/select-e2e.test.ts` covers all 4 branches plus 3 codex regression guards (skills/, untracked files, unmapped src/) — 24 cases.
|
||
- `scripts/run-e2e.sh` (v0.23.1 update) — Sequential E2E runner. Now accepts an optional argv-driven file list (used by `ci:local:diff` to pipe in selector output) 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 var lets forks regenerate with their own URL base. `FULL_SIZE_BUDGET` (600KB) caps the inline bundle; generator WARNs if exceeded. Committed output is not analogous to `schema-embedded.ts` (no runtime consumer); we commit 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. v0.10.3 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; 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](https://github.com/garrytan/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 (v0.20.4 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 from bare `"gbrain jobs"` 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` (v0.26.5) — three-layer protection against accidental data loss in gbrain. `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 the column shape introduced in migration v34 (`sources.archived BOOLEAN`, `archived_at TIMESTAMPTZ`, `archive_expires_at TIMESTAMPTZ`). v0.26.5 added the page-level analog through `BrainEngine.softDeletePage` / `restorePage` / `purgeDeletedPages` plus `pages.deleted_at TIMESTAMPTZ` and a partial purge index. The MCP `delete_page` op rewires to `softDeletePage`; new 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 new 9th `purge` phase calls `purgeExpiredSources` + `engine.purgeDeletedPages(72)` so the 72h TTL is real, not honor-system.
|
||
- `src/commands/pages.ts` (v0.26.5) — `gbrain 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` (v0.36.4.0) — 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`) each 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. Pinned by `test/op-checkpoint.test.ts` (~15 cases including per-op fingerprint scoping, codex outside-voice review #10-#16). `import-checkpoint.ts` from v0.34.2.0 was NOT migrated to this primitive in v0.36.4.0 — both checkpoint systems coexist without conflict; the migration requires async-propagating 4 sync call sites in `src/commands/import.ts` and rewriting 18 tests, deferred to a follow-up wave.
|
||
- `src/core/brain-score-recommendations.ts` (v0.36.4.0) — 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` (D14: references stable ids, not check names — so plan order is reproducible across runs). `classifyChecks(report)` triages every doctor check into `remediable | human_only | blocked` (D13: three-state, not boolean — `human_only` covers RLS warnings and other human-judgment-required 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` for synthesize / patterns / consolidate and `embedding-pricing.ts` for embed jobs. Pinned by `test/brain-score-recommendations.test.ts` (~27 cases) including D6 #5 determinism, D9 content-hash idempotency, D12 DB-backed checkpoint provenance, and D13 three-state triage.
|
||
- `src/commands/doctor.ts` extension (v0.36.4.0) — new `--remediation-plan` and `--remediate` CLI surfaces. `--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]` actually submits each plan step as a Minion job, in dependency order, re-checking score between every step. `--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 (prevents synthesize loops from burning $100 of Anthropic credits while you're at lunch). JSON envelope adds a `Check.remediation` field (additive, schema_version unchanged). Pinned by tests in `test/doctor.test.ts`.
|
||
- `src/commands/jobs.ts` extension (v0.36.4.0) — registers 11 new 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` remains the single source of truth for phase semantics. Same fix wave: the standalone `sync` handler now passes `noExtract: true` to match `runPhaseSync`'s contract — pre-fix, doctor's remediation plan emitting `[sync, extract]` double-extracted (codex #5).
|
||
- `src/core/minions/protected-names.ts` extension (v0.36.4.0) — `PROTECTED_JOB_NAMES` extended with `synthesize`, `patterns`, `consolidate`. These phases internally submit `subagent` children with `allowProtectedSubmit=true` and can therefore spend Anthropic credits; treating them as routine "data-quality maintenance" was a misread caught by codex outside-voice (#6). 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 (v0.36.4.0) — targeted-submit loop replaces 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 (targeted); `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 codex flagged (#17). Pre-fix autopilot ran a full 6-phase cycle every 5 minutes regardless of brain state; healthy brains burned synthesize+patterns+embed cycles for zero work.
|
||
- `src/core/cycle.ts` extension (v0.36.4.0) — `purge` phase (the cycle's 9th phase, introduced in v0.26.5 for soft-delete TTLs) extended to GC stale `op_checkpoints` rows older than 7 days. Non-fatal on pre-v67 brains (DROP-target-table check before DELETE).
|
||
- `src/commands/embed.ts` extension (v0.36.4.0) — wires `--background` as the reference integration for the new `maybeBackground()` helper. `gbrain embed --stale --background` submits as a Minion job and prints `job_id=N` to stdout, exits 0. Composable in shell pipelines: `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 4-line pattern in a follow-up wave (T7 deferred).
|
||
- `src/core/cli-options.ts` extension (v0.36.4.0) — new `maybeBackground(opName, fingerprintArgs, runDirect)` helper. Same semantics in TTY and cron (D9 — 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
|
||
|
||
### BrainBench — in a sibling repo (v0.20+)
|
||
|
||
BrainBench — the public benchmark for personal-knowledge agent stacks — lives in
|
||
[github.com/garrytan/gbrain-evals](https://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.
|
||
|
||
- `src/core/cycle/base-phase.ts` — abstract `BaseCyclePhase` class.
|
||
Enforces `sourceScopeOpts(ctx)` threading at the type level; closes
|
||
the v0.34.1 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 `take_proposals` queue. Idempotency
|
||
cache on `(source_id, page_slug, content_hash, prompt_version)`
|
||
composite unique index. F2 fence-dedup: existing canonical takes
|
||
passed to extractor as context. v0.36.1.0 ships a stub prompt; tuned
|
||
prompt arrives via the T19 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 (D17). Conservative thresholds:
|
||
>=0.95 single OR >=0.85 ensemble 3/3 unanimous. T5 ensemble
|
||
(`aggregateEnsemble`) reuses v0.27.x cross-modal substrate; fires on
|
||
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
|
||
(D24), mode parameter (`pattern_statement` | `nudge` |
|
||
`forecast_blurb` | `dashboard_caption` | `morning_pulse`). 2 regens
|
||
then hand-written 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` — D18 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` — E7 real-time pattern surfacing.
|
||
`evaluateAndFireNudge(opts)` is the full pipeline: 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 for v0.36.1.0; multi-channel deferred.
|
||
- `src/core/calibration/take-forecast.ts` — E5 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` — E4 outcome-driven
|
||
learnings coupling. `writeIncorrectResolution(opts)` shells out to
|
||
`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` — D23 server-rendered SVG for
|
||
the admin SPA Calibration tab. Pure functions: data → SVG string.
|
||
Inlines design tokens; XSS-safe via `escapeXml()`. Four chart
|
||
renderers: `renderBrierTrend`, `renderDomainBars`,
|
||
`renderAbandonedThreadsCard`, `renderPatternStatementsCard`. SPA
|
||
renders via `<TrustedSVG>` wrapper behind `requireAdmin`.
|
||
- `src/core/calibration/undo-wave.ts` — D18 CDX-3 resolution. `undoWave`
|
||
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` — D19 A/B harness. `runAbTrial`
|
||
calls thinkRunner twice (baseline + with-calibration), records
|
||
preference to `think_ab_results`. `buildAbReport` aggregates over
|
||
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. v0.36
|
||
ship state: opt-in via the wiring layer; auto-on in v0.37+.
|
||
- `src/core/eval-contradictions/calibration-join.ts` — E3 cross-
|
||
reference. `tagFindingWithCalibration(finding, profile)` returns
|
||
bias-tag context for contradictions that match active patterns.
|
||
Returns null when profile missing (R2 regression — output
|
||
byte-identical to v0.32.6).
|
||
- `src/core/think/prompt.ts` extension — E1 anti-bias rewrite.
|
||
`withCalibration` option on `buildThinkSystemPrompt` adds anti-bias
|
||
rules. New `buildCalibrationBlock()` emits the `<calibration>` XML.
|
||
`buildThinkUserMessage` has TWO shapes: default (question first) for
|
||
R1 regression, with-calibration (retrieval → calibration → question
|
||
per D22) when opt-in. Wired into `runThink` via
|
||
`opts.withCalibration` + `opts.calibrationHolder`.
|
||
- `src/commands/calibration.ts` — CLI: `gbrain calibration` (read +
|
||
print), `--regenerate`, `--undo-wave <ver>` (T17), `ab-report` (T18).
|
||
MCP op `get_calibration_profile` (scope: read) backs the same data
|
||
path. Source-scoped via `sourceScopeOpts(ctx)`.
|
||
- `src/commands/serve-http.ts` extension — three new admin routes:
|
||
`/admin/api/calibration/profile`, `/admin/api/calibration/charts/:type`
|
||
(image/svg+xml; type in {brier-trend, domain-bars,
|
||
pattern-statements, abandoned-threads}), and
|
||
`/admin/api/calibration/pattern/:id` (TD3 drill-down).
|
||
- `src/commands/takes.ts` extension — `gbrain takes revisit <slug>`
|
||
(TD4 / D30) opens $EDITOR on the source page with a
|
||
`<!-- gbrain:revisit -->` cursor marker.
|
||
- `src/commands/doctor.ts` extension — 4 new checks: `abandoned_threads`,
|
||
`calibration_freshness`, `grade_confidence_drift` (CDX-11 mitigation
|
||
surface; math arrives v0.37+), `voice_gate_health`.
|
||
- `admin/src/pages/Calibration.tsx` — Calibration tab. Single-column
|
||
Linear-calm-clarity layout matching the approved variant-B mockup.
|
||
`<TrustedSVG>` wrapper handles `dangerouslySetInnerHTML` for the
|
||
server-rendered SVG.
|
||
- `admin/src/index.css` extension — `--text-muted: #555 → #777` (TD2,
|
||
WCAG AA contrast bump from 4.0 to ~5.5 on the #0a0a0f bg).
|
||
- `test/fixtures/calibration/extract-takes-corpus/` — synthetic prompt-
|
||
tuning corpus. v0.36.1.0 ships 5 representative pages; full 50-page
|
||
+ 10-page holdout generated by `gbrain calibration build-corpus`
|
||
(v0.37+ subcommand). All anonymized per CLAUDE.md placeholder list.
|
||
- `scripts/check-synthetic-corpus-privacy.sh` — CDX-14 mitigation. 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 test file. Pins all 5 IRON-RULE regressions in one place
|
||
for future bisects.
|
||
- `DESIGN.md` — repo-root design system. Formalizes the de facto admin
|
||
tokens that landed v0.26.0. Calibration target for future
|
||
`/plan-design-review` and `/design-review`.
|
||
|
||
## Thin-client routing (v0.31.1, Issue #734)
|
||
|
||
`gbrain init --mcp-only` (v0.29.2) sets up a thin-client install: no local
|
||
brain content, just an OAuth client pointing at a remote `gbrain serve --http`.
|
||
v0.29.2/v0.30.0 only refused 9 obvious local-only commands; the other ~25
|
||
silently fell through to `connectEngine()` and opened the empty local PGLite,
|
||
returning "No results." against a populated remote brain. v0.31.1 fixes the
|
||
silent-empty-results bug class for every operation surface.
|
||
|
||
Key files:
|
||
|
||
- `src/cli.ts` — Routing seam INSIDE the existing op-dispatch path (CDX-1: no
|
||
parallel `src/core/thin-client/` module; routing is a ~80-line conditional
|
||
in `runThinClientRouted`). Detects `isThinClient(cfg)` BEFORE `connectEngine`
|
||
so thin-client installs never open the empty PGLite. localOnly ops on
|
||
thin-client refuse via `refuseThinClient` (with pinpoint hint table
|
||
`THIN_CLIENT_REFUSE_HINTS`). Banner via `printIdentityBannerBestEffort`
|
||
before each routed call (suppressed by `--quiet`, `GBRAIN_NO_BANNER=1`,
|
||
non-TTY default). Exhaustive TS `never` switch on `RemoteMcpError.reason`
|
||
for canned, actionable error messages. ENG-2 renderer parity: local-engine
|
||
path runs `JSON.parse(JSON.stringify(result))` so renderers see the same
|
||
shape on both paths (kills Date/bigint/Buffer drift class).
|
||
- `src/core/mcp-client.ts` — `callRemoteTool(config, toolName, args, opts)`.
|
||
Hardened in v0.31.1 (CDX-4): all transport errors normalized to
|
||
`RemoteMcpError` via the `toRemoteMcpError` funnel. New `CallRemoteToolOptions
|
||
{timeoutMs, signal}`; `buildAbortController` composes external signal with
|
||
timeout. New `RemoteMcpErrorReason` stable union, `RemoteMcpErrorDetail.kind`
|
||
('timeout' | 'aborted' | 'unreachable') sub-tag, `RemoteMcpErrorDetail.code`
|
||
field carrying server-supplied error codes (e.g. `missing_scope`).
|
||
`extractToolErrorCode` parses JSON envelopes first, falls back to substring
|
||
detection for legacy server messages. `unpackToolResult<T>(res)` unchanged
|
||
(parses tool-call JSON content). `_clearMcpClientTokenCache()` test escape.
|
||
- `src/core/cli-options.ts` — `parseGlobalFlags` adds `--timeout=Ns` (accepts
|
||
`30s`, `2m`, `500ms`, plain ms). Default `null` = per-command default (30s
|
||
for most ops, 180s for `think`). `parseTimeout(s)` exported helper.
|
||
- `src/core/doctor-remote.ts` — `gbrain remote doctor` adds the
|
||
`oauth_client_scopes_probe` check (CDX-5). Probes the read tier via
|
||
`get_brain_identity` and admin tier via `get_health`; reports per-tier
|
||
status with pinpoint remediation when admin is missing. `buildScopeCheck`
|
||
+ `ScopeProbeResult` exported for test access. Skippable via
|
||
`GBRAIN_DOCTOR_SKIP_SCOPE_PROBE=1` for fixtures that mock /mcp at JSON-RPC
|
||
initialize level only (MCP SDK Client hangs on shape mismatch).
|
||
- `src/core/operations.ts` — `get_brain_identity` op (read scope, no params,
|
||
banner-only): cheap counter packet `{version, engine, page_count,
|
||
chunk_count, last_sync_iso}` for the thin-client identity banner. Reuses
|
||
`engine.getStats()`; banner's 60s client-side TTL bounds frequency to
|
||
≤1/60s per CLI process (well below the Fly.io health-check cadence that
|
||
motivated the original `getStats` cost warning).
|
||
- `src/commands/{salience,anomalies,graph-query,think}.ts` — Per-command
|
||
thin-client routing branches. These commands bypass the operation-layer
|
||
dispatch in cli.ts (call `engine.foo()` directly), so each gets its own
|
||
`if (isThinClient(cfg)) { callRemoteTool(...) }` branch that maps CLI flags
|
||
to op params. `think` is a special case: the server's `think` op
|
||
intentionally disables `--save`/`--take` for remote callers
|
||
(operations.ts:1103-1135 trust-boundary gate); thin-client `think` warns
|
||
loudly when those flags are set.
|
||
|
||
## Commands
|
||
|
||
Run `gbrain --help` or `gbrain --tools-json` for full command reference.
|
||
|
||
Key commands added in v0.7:
|
||
- `gbrain init` — defaults to PGLite (no Supabase needed), scans repo size, suggests Supabase for 1000+ files
|
||
- `gbrain migrate --to supabase` / `gbrain migrate --to pglite` — bidirectional engine migration
|
||
|
||
Key commands added for Minions (job queue):
|
||
- `gbrain jobs submit <name> [--params JSON] [--follow] [--dry-run]` — submit a background job. v0.13.1 adds first-class flags for every `MinionJobInput` tuning knob: `--max-stalled N`, `--backoff-type fixed|exponential`, `--backoff-delay Nms`, `--backoff-jitter 0..1`, `--timeout-ms N`, `--idempotency-key K`.
|
||
- `gbrain jobs list [--status S] [--queue Q]` — list jobs with filters
|
||
- `gbrain jobs get <id>` — job details with attempt history
|
||
- `gbrain jobs cancel/retry/delete <id>` — manage job lifecycle
|
||
- `gbrain jobs prune [--older-than 30d]` — clean old completed/dead jobs
|
||
- `gbrain jobs stats` — job health dashboard
|
||
- `gbrain jobs smoke [--sigkill-rescue]` — health smoke test. `--sigkill-rescue` is the v0.13.1 regression guard for #219: simulates a killed worker and asserts the stalled job is requeued instead of dead-lettered on first stall.
|
||
- `gbrain jobs work [--queue Q] [--concurrency N]` — start worker daemon (Postgres only)
|
||
|
||
Key commands added in v0.36.4.0 (brain-health-100 wave):
|
||
- `gbrain doctor --remediation-plan [--target-score N] [--json]` — preview the dependency-ordered plan that would drive the brain to target. JSON envelope is stable: each `Remediation` carries `id`, `idempotency_key` (content-hash for cron-safe retries), `severity`, `est_seconds`, `est_usd_cost`, and `depends_on` (referencing other ids). Empty `recommendations` array when the brain is already at target.
|
||
- `gbrain doctor --remediate [--yes] [--target-score N] [--max-usd N]` — actually submit the plan. Walks dependency order, submits one Minion job per step, re-checks score between steps, refuses to spend past `--max-usd` (defaults: target=90, max-usd=infinite — but cron callers should always pass `--max-usd`). Bails when target exceeds `maxReachableScore()` for the brain (empty / under-configured brains) with a clear list of what's missing.
|
||
- `gbrain embed --stale --background` — submit the embed sweep as a Minion job; print `job_id=N` to stdout; exit. Composable in shell pipelines. Add `--background --follow` to attach to the job's stderr stream (same UX as a direct call).
|
||
- Eleven new Minion job types submittable via `gbrain jobs submit <name>`: `reindex`, `repair-jsonb`, `orphans`, `integrity`, `purge`, `synthesize` (PROTECTED), `patterns` (PROTECTED), `consolidate` (PROTECTED), `extract_facts`, `resolve_symbol_edges`, `recompute_emotional_weight`. PROTECTED ones reject MCP submission and require `--allow-protected` from a trusted local caller (CLI, autopilot, `doctor --remediate`).
|
||
- `gbrain autopilot` (existing daemon) is now health-aware. Tick cost on a healthy brain drops from "full 6-phase cycle every 5 minutes" to "one SQL count, then sleep". Degraded brains get targeted handlers (`[sync]`, `[embed]`, `[backlinks]`) instead of the full cycle when the plan is small; large plans still get `autopilot-cycle`. The "60-minute full-cycle floor" runs the full phase set on a healthy brain at least every hour so phase-coupling invariants (lint-first, synthesize-before-patterns, embed-after-consolidate) keep getting exercised.
|
||
|
||
Key commands added in v0.32.7 (CJK fix wave):
|
||
- `gbrain reindex --markdown [--limit N] [--dry-run] [--json] [--no-embed] [--repo PATH]` — operator-facing markdown re-chunk sweep. Walks pages with `chunker_version < MARKDOWN_CHUNKER_VERSION` (currently 2) and re-imports each with `forceRechunk: true` so the new chunker shape actually applies. Run automatically by `gbrain upgrade`'s post-upgrade hook; available manually for triage.
|
||
- `gbrain doctor` learns a new `slug_fallback_audit` check: surfaces info-severity entries from `~/.gbrain/audit/slug-fallback-YYYY-Www.jsonl` (last 7 days) as an `ok` count when CJK / emoji / exotic-script filenames imported via the frontmatter-slug fallback path.
|
||
- `gbrain search "<CJK substring>"` on PGLite brains now uses an `ILIKE`-based fallback with bigram-frequency-count ranking when the query contains Han / Hiragana / Katakana / Hangul Syllables. ASCII queries continue through `websearch_to_tsquery('english')` unchanged. Postgres-side CJK FTS still requires an extension (pgroonga / zhparser) — see v0.33+ TODO.
|
||
- `gbrain upgrade` post-upgrade flow now prints a cost estimate before re-embedding: `[chunker-bump] Will re-embed ~N markdown pages via <provider:model>, est. ~$X.XX, ~Ymin. Press Ctrl-C within 10s to abort.` Sourced from real SQL counts + char totals; TTY-only wait (non-TTY auto-proceeds for CI / cron). Env overrides: `GBRAIN_NO_REEMBED=1` bails out entirely with a doctor-warning marker; `GBRAIN_REEMBED_GRACE_SECONDS=0` skips the wait.
|
||
|
||
Key commands added in v0.33.1.1 (Voyage 2048-dim correctness wave):
|
||
- `gbrain models doctor` learns a new zero-token `embedding_config` probe that runs FIRST, before any chat/expansion probes spend money. Catches Voyage flexible-dim misconfigs at config time, not first-embed: `embedding_model: voyage:voyage-4-large` with `embedding_dimensions` outside `{256, 512, 1024, 2048}` (most commonly: `embedding_dimensions` left unset, falling back to the OpenAI default 1536 which Voyage rejects with an opaque HTTP 400). Surfaces a paste-ready `gbrain config set embedding_dimensions <256|512|1024|2048>` fix in both human and JSON output. New probe status `'config'` joins `{ok, model_not_found, auth, rate_limit, network, unknown}`; new touchpoint label `'embedding_config'` joins `'chat'` and `'expansion'`.
|
||
- Voyage 2048-dim brains now actually embed at 2048 dims. `embedding_model: voyage:voyage-4-large` + `embedding_dimensions: 2048` routes through the SDK-supported `dimensions` field, which `voyageCompatFetch` translates to Voyage's `output_dimension` on the wire. Same fix covers `voyage-3-large`, `voyage-3.5`, `voyage-3.5-lite`, `voyage-4`, `voyage-4-lite`, `voyage-code-3`. `voyage-4-nano` (open-weight, fixed 1024-dim) intentionally NOT in the flexible-dim allowlist — sending `output_dimension` to nano's endpoint produces an error.
|
||
- Runtime validator: `dimsProviderOptions()` throws `AIConfigError` at the embed boundary with a paste-ready fix hint when a Voyage flexible-dim model is configured with an invalid dim — fail-loud even if you skipped `gbrain models doctor`.
|
||
- `VoyageResponseTooLargeError` (new tagged class exported from `src/core/ai/gateway.ts`): the 256 MB per-response cap inside `voyageCompatFetch` was previously throwing a generic `Error` that the surrounding parse-error try/catch silently swallowed, returning the oversized response to the AI SDK anyway. Now thrown at both cap sites (Content-Length Layer 1, per-embedding base64 Layer 2) and rethrown from the catch via `instanceof` check — the cap is now actually effective.
|
||
|
||
Key commands added in v0.31.12 (model tier system + routing CLI):
|
||
- `gbrain models [--json]` — read-only routing dashboard. Prints the four tier defaults (`utility`/`reasoning`/`deep`/`subagent`), the resolved value for each (after re-walking `models.default` → `models.tier.<tier>` → env → `TIER_DEFAULTS`), every per-task override (`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 (defaults + user overrides), and a source-of-truth column (`default` / `config: <key>` / `env: <VAR>`).
|
||
- `gbrain models doctor [--skip=<provider>] [--json]` — 1-token reachability probe against each configured chat + expansion model. Classifies failures into `{model_not_found, auth, rate_limit, network, unknown}`. The structural fix for the bug class that motivated v0.31.12 (v0.31.6's `claude-sonnet-4-6-20250929` chat default 404'd silently on every install).
|
||
- Power-user model routing via config keys:
|
||
- `gbrain config set models.default opus` — route every internal call (chat, expansion, synthesis, classification) through Opus 4.7. Subagent loop still falls back to `claude-sonnet-4-6` automatically (Anthropic-only by construction).
|
||
- `gbrain config set models.tier.<tier> <model>` — override one tier independently (`utility` / `reasoning` / `deep` / `subagent`).
|
||
- `gbrain config set models.aliases.frontier anthropic:claude-opus-4-7` — define an alias, then `gbrain config set models.default frontier`.
|
||
- Per-task keys (e.g. `gbrain config set models.dream.synthesize <model>`) still beat tier overrides because they are more specific.
|
||
- New `subagent_provider` check in `gbrain doctor` surfaces config drift if `models.tier.subagent` or `models.default` would route the Anthropic Messages API tool-loop to a non-Anthropic provider.
|
||
- The skill at `skills/conventions/model-routing.md` was rewritten to cover both the new tier system AND the existing subagent spawn routing in one canonical doc (power-user recipes, three-layer enforcement explanation, override priority chain).
|
||
|
||
Key commands added in v0.28.1 (LongMemEval in the box):
|
||
- `gbrain eval longmemeval <dataset.jsonl>` — run the public LongMemEval benchmark against gbrain hybrid retrieval. Flags: `--limit N`, `--model M`, `--retrieval-only`, `--keyword-only`, `--expansion`, `--top-k K`, `--output FILE`. One in-memory PGLite per benchmark run; `TRUNCATE` between questions over runtime-enumerated `pg_tables` (schema-migration-safe); `~/.gbrain` never opened. `--expansion` defaults OFF (deterministic, no per-query Haiku). Default model resolves through `resolveModel()` 6-tier chain with new `models.eval.longmemeval` config key. `gbrain eval longmemeval --help` works without a configured brain (hermeticity gate).
|
||
- Sanitization parity with takes: `INJECTION_PATTERNS` exported from `src/core/think/sanitize.ts`. The benchmark harness re-uses the same pattern set so adding a new injection pattern automatically covers takes AND benchmarks.
|
||
- Hand the resulting JSONL to LongMemEval's published `evaluate_qa.py` to score (not bundled — needs OpenAI gpt-4o per their spec). Dataset: https://huggingface.co/datasets/xiaowu0162/longmemeval.
|
||
|
||
Key commands added in v0.26.5 (destructive-guard, end-to-end):
|
||
- `gbrain sources archive <id>` — soft-delete a source. Hides from search via the new `sources.archived` column + cascading visibility filter. Preserves data for 72h. (PR #595 cherry-pick.)
|
||
- `gbrain sources restore <id> [--no-federate]` — un-archive a soft-deleted source. Re-federates by default.
|
||
- `gbrain sources archived [--json]` — list soft-deleted sources with their TTL.
|
||
- `gbrain sources purge [<id>] [--confirm-destructive]` — permanent delete; with no id, purges all sources whose TTL expired.
|
||
- `gbrain sources remove <id> [--confirm-destructive] [--dry-run]` — `--yes` alone no longer enough on populated sources. Boxed impact preview before destruction.
|
||
- `gbrain pages purge-deleted [--older-than HOURS|Nd] [--dry-run] [--json]` — operator escape hatch for page-level soft-delete cleanup. Mirror of `gbrain sources purge`. The autopilot cycle's new `purge` phase calls the same library function automatically every run.
|
||
- MCP `delete_page` op semantically shifts from hard-delete to soft-delete. New ops: `restore_page` (`scope: write`), `purge_deleted_pages` (`scope: admin`, `localOnly: true`).
|
||
- `get_page` and `list_pages` extended with `include_deleted: boolean` (default false).
|
||
- New autopilot cycle phase `purge` (9th, runs after `orphans`). `gbrain dream --phase purge` runs only the purge sweep.
|
||
- Index strategy note: the partial index `pages_deleted_at_purge_idx ON pages (deleted_at) WHERE deleted_at IS NOT NULL` supports the autopilot purge query. Search filters (`WHERE deleted_at IS NULL`) do NOT need their own index — soft-deleted cardinality stays low and Postgres won't use the partial index for the negative predicate. Don't add a regular `(deleted_at)` index without measuring.
|
||
- Schema migration v34 (`destructive_guard_columns`) adds `pages.deleted_at` + the partial purge index; promotes `archived` from `sources.config` JSONB to real columns; backfills any pre-v0.26.5 JSONB shape.
|
||
|
||
Key commands added in v0.25.0:
|
||
- `gbrain eval export [--since DUR] [--limit N] [--tool query|search]` — stream captured `eval_candidates` rows as NDJSON to stdout. Every line starts with `"schema_version": 1` per the stable contract in `docs/eval-capture.md`. EPIPE-safe, progress heartbeats on stderr, deterministic ordering. Primary consumer is the sibling `gbrain-evals` repo for BrainBench-Real replay.
|
||
- `gbrain eval prune --older-than DUR [--dry-run]` — explicit retention cleanup for `eval_candidates`. Requires `--older-than` (never deletes without a window). Duration strings: 30d, 7d, 1h, 90m, 3600s.
|
||
- `gbrain eval replay --against FILE.ndjson [--limit N] [--top-regressions K] [--json] [--verbose]` — contributor-facing dev loop. Reads a captured NDJSON snapshot, re-runs each `query` / `search` op against the current brain, computes mean set-Jaccard@k between captured + current `retrieved_slugs`, top-1 stability rate, and latency Δ. JSON mode (`schema_version: 1`) for CI gating; human mode prints a regression table sorted worst-first. Closes the gap between "data captured" and "data used to gate a PR." See `docs/eval-bench.md` for the workflow.
|
||
- `gbrain eval cross-modal --task "..." --output <path> [--cycles N] [--slot-a-model ID] [--slot-b-model ID] [--slot-c-model ID] [--receipt-dir DIR] [--json]` (v0.27.x) — multi-model quality gate. Three different-provider frontier models score the OUTPUT against the TASK on 5 documented dimensions. Pass criterion: every dim mean >=7 AND no model scored any dim <5. Exit codes: 0 PASS, 1 FAIL, 2 INCONCLUSIVE (<2/3 models returned parseable scores). Default cycles=3 in TTY, **cycles=1 in non-TTY** (limits accidental scripted bulk spend). Default slots: `openai:gpt-4o` / `anthropic:claude-opus-4-7` / `google:gemini-1.5-pro` — refresh alongside model-family bumps. Receipts land at `~/.gbrain/.gbrain/eval-receipts/<slug>-<sha8-of-output>.json` (gbrainPath honors GBRAIN_HOME). Bypasses `connectEngine()` via the cli.ts no-DB branch — runs cleanly before `gbrain init`. Reuses `src/core/ai/gateway.ts:chat()` for config/auth (no parallel provider stack). Cost-estimate prints to stderr before each cycle (T11=B partial cost guardrail; full `--budget-usd N` is a follow-up TODO).
|
||
- `gbrain doctor` gains an `eval_capture` check: reads `eval_capture_failures` for the last 24h, groups by reason, warns when non-zero. Cross-process visibility (doctor runs in a separate process from MCP). Pre-v31 brains get `Skipped (table unavailable)` — non-fatal.
|
||
- Config addition: `eval: { capture?: boolean, scrub_pii?: boolean }` in `~/.gbrain/config.json`. **File-plane only** — `gbrain config set` writes the DB plane and does NOT control capture.
|
||
- **`GBRAIN_CONTRIBUTOR_MODE=1` env var** is the contributor-facing toggle. Capture is **off by default** as of v0.25.0; production users get a quiet brain. Resolution order: explicit `eval.capture` config wins both directions, then env var, then off. Documented in README.md, CONTRIBUTING.md, and `docs/eval-bench.md`.
|
||
|
||
Key commands added in v0.12.2:
|
||
- `gbrain repair-jsonb [--dry-run] [--json]` — repair double-encoded JSONB rows left over from v0.12.0-and-earlier Postgres writes. Idempotent; PGLite no-ops. The `v0_12_2` migration runs this automatically on `gbrain upgrade`.
|
||
|
||
Key commands added in v0.12.3:
|
||
- `gbrain orphans [--json] [--count] [--include-pseudo]` — surface pages with zero inbound wikilinks, grouped by domain. Auto-generated/raw/pseudo pages filtered by default. Also exposed as `find_orphans` MCP operation. The natural consumer of the v0.12.0 knowledge graph layer: once edges are captured, find the gaps.
|
||
- `gbrain doctor` gains two new reliability detection checks: `jsonb_integrity` (v0.12.0 Postgres double-encode damage) and `markdown_body_completeness` (pages truncated by the old splitBody bug). Detection only; fix hints point at `gbrain repair-jsonb` and `gbrain sync --force`.
|
||
|
||
Key commands added in v0.14.2:
|
||
- `gbrain sync --skip-failed` — acknowledge the current set of failed-parse files recorded in `~/.gbrain/sync-failures.jsonl` so the sync bookmark advances past them. Doctor's `sync_failures` check shows previously-skipped as "all acknowledged" instead of warning.
|
||
- `gbrain sync --retry-failed` — re-walk the unacknowledged failures and re-attempt parsing. If the files now succeed, they clear from the set and the bookmark advances naturally.
|
||
- `gbrain apply-migrations --force-retry <version>` — reset a wedged migration (3 consecutive partials with no completion) by appending a `'retry'` marker. Next `apply-migrations --yes` treats the version as fresh. `complete` status never regresses to `partial` either before or after a retry marker.
|
||
- `GBRAIN_POOL_SIZE` env var — honored by both the singleton pool (`src/core/db.ts`) and the parallel-import worker pool (`src/commands/import.ts`). Default is 10; lower to 2 for Supabase transaction pooler to avoid MaxClients crashes during `gbrain upgrade` subprocess spawns. Read at call time via `resolvePoolSize()`.
|
||
- `gbrain doctor` gains two new checks: `sync_failures` (surfaces unacknowledged parse failures with exact paths + fix hints) and `brain_score` (renders the 5-component breakdown when score < 100: embed coverage / 35, link density / 25, timeline coverage / 15, orphans / 15, dead links / 10 — sum equals total).
|
||
|
||
Key commands added in v0.26.0 (OAuth 2.1 + HTTP server + admin dashboard):
|
||
- `gbrain serve --http [--port 3131] [--token-ttl 3600] [--enable-dcr] [--log-full-params]` — HTTP MCP server with OAuth 2.1, admin dashboard at `/admin`, SSE activity feed at `/admin/events`, health check at `/health`. Prints admin bootstrap token on first start. Alongside (not replacing) stdio `gbrain serve`. As of v0.26.9, `mcp_request_log.params` and the SSE feed default to a redacted summary (`{redacted, kind, declared_keys, unknown_key_count, approx_bytes}`); pass `--log-full-params` to log raw payloads on a personal laptop with a startup warning.
|
||
- **OAuth client registration** — three paths:
|
||
1. CLI: `gbrain auth register-client <name> --grant-types <types> --scopes <scopes>` (wired into `src/commands/auth.ts` as a thin wrapper over `GBrainOAuthProvider.registerClientManual`). Default grant types: `client_credentials`. Default scopes: `read`.
|
||
2. Admin dashboard: Register client modal → credential reveal with Copy + Download JSON.
|
||
3. SDK: `oauthProvider.registerClientManual(name, grantTypes, scopes, redirectUris)` for programmatic wrappers.
|
||
`--enable-dcr` on `serve --http` opens the `/register` endpoint for RFC 7591 self-service registration (off by default).
|
||
- `gbrain auth create|list|revoke|test` — legacy bearer tokens still work and grandfather to `read+write+admin` scopes on the OAuth server. `auth` is wired as a first-class `gbrain` subcommand in v0.26.0 (previously only invokable via `bun run src/commands/auth.ts`). No migration required to keep pre-v0.26 clients working.
|
||
|
||
Key commands added in v0.14.3 (fix wave):
|
||
- `gbrain doctor --index-audit` — opt-in Postgres-only check reporting zero-scan indexes from `pg_stat_user_indexes`. Informational only; never auto-drops.
|
||
- `gbrain doctor` schema_version check fails loudly when `version=0` — catches `bun install -g github:...` postinstall failures (#218) and routes users to `gbrain apply-migrations --yes`.
|
||
- `gbrain jobs submit` gains `--max-stalled`, `--backoff-type`, `--backoff-delay`, `--backoff-jitter`, `--timeout-ms`, `--idempotency-key` — exposing existing `MinionJobInput` fields as first-class CLI flags.
|
||
- `gbrain jobs smoke --sigkill-rescue` — opt-in regression smoke case simulating a killed worker; asserts the v0.14.3 schema default (`max_stalled=5`) actually rescues on first stall.
|
||
|
||
Key commands added in v0.22.13 (PR #490):
|
||
- `gbrain sync --workers N` (alias `--concurrency N`) — parallelize the import phase using per-worker Postgres engines (small pool of 2 each) with an atomic queue index. Auto-concurrency: defaults to 4 workers when the diff exceeds 100 files. Smaller diffs stay serial. Explicit `--workers` always wins (even on a 30-file diff). PGLite forces serial regardless. Validation rejects `0`, negatives, non-integers loud (replaces the prior silent fall-through to auto-concurrency).
|
||
- `gbrain import --workers N` — same `parseWorkers()` validation as sync; same try/finally worker-engine cleanup. Behavior surface unchanged.
|
||
|
||
Key commands added in v0.22.16 (claw-test friction loop):
|
||
- `gbrain claw-test [--scenario fresh-install|upgrade-from-v0.18] [--keep-tempdir]` — scripted-mode CI gate that runs the full canonical first-day flow against a fresh tempdir. Asserts every expected `--progress-json` phase fired and doctor's `status === 'ok'`. ~30s, no API keys.
|
||
- `gbrain claw-test --live --agent openclaw` — friction-discovery mode. Spawns real openclaw, hands it `BRIEF.md`, captures stdin/stdout/stderr to `<run>/transcript.jsonl`, lets the agent log friction via the friction CLI. Run on demand; ~5–10 min and ~$1–2 in tokens.
|
||
- `gbrain claw-test --list-agents` — reports which agent runners are registered + their detection state (binary path or unavailable reason).
|
||
- `gbrain friction log --severity {confused|error|blocker|nit} --phase <name> --message <text> [--hint ...] [--kind {friction|delight}] [--run-id ...]` — append a friction or delight entry to the active run JSONL.
|
||
- `gbrain friction render --run-id <id> [--json] [--transcripts] [--no-redact]` — markdown report grouped by severity + phase; `--redact` is the default for md output (strips `$HOME`/`$CWD` placeholders so reports paste safely in PRs/issues).
|
||
- `gbrain friction list [--json]` — recent run-ids with friction/delight counts; interrupted runs marked `(interrupted)`.
|
||
- `gbrain friction summary --run-id <id> [--json]` — two-column friction + delight summary.
|
||
- `GBRAIN_HOME` env override is now honored uniformly across every gbrain write site (config, audit, friction, sync-failures, import checkpoint, integrity log, integrations heartbeat, migration rollback, etc.) — `gbrainPath(...)` from `src/core/config.ts` is the canonical helper. Read-side host-fingerprint detection (`~/.claude`/`~/.openclaw` etc.) intentionally NOT confined in v1; that's a v1.1 follow-up.
|
||
|
||
## Testing
|
||
|
||
### Test command tiers (v0.26.4 — parallel fast loop)
|
||
|
||
Five tiers of test commands, each with a clear scope:
|
||
|
||
| Command | What it runs | Wallclock | When to use |
|
||
|---|---|---|---|
|
||
| `bun run test` | Parallel unit-test fast loop. 8-shard fan-out via `scripts/run-unit-parallel.sh`, then a serial pass over `*.serial.test.ts`. Excludes `*.slow.test.ts` and `test/e2e/*`. No pre-checks, no typecheck. | ~85s on a Mac dev box (3650+ tests) | Inner edit loop. Default. |
|
||
| `bun run verify` | CI's authoritative pre-test gate set: `check:privacy && check:jsonb && check:progress && check:wasm && bun run typecheck`. The 4 checks `.github/workflows/test.yml` runs on shard 1 + typecheck. Single source of truth — CI literally calls `bun run verify`. | ~12s (wasm-compile dominates) | Before pushing; before `/ship`. |
|
||
| `bun run test:full` | `verify && bun run test && bun run test:slow && [smart e2e]`. The local equivalent of "everything CI runs." Smart e2e: runs e2e only when `DATABASE_URL` is set; else loud skip notice to stderr. | ~3-5min depending on slow + e2e | Pre-merge sanity, before opening a PR. |
|
||
| `bun run test:slow` | Just the `*.slow.test.ts` set (intentional cold-path correctness checks). | seconds-to-minutes | When touching slow-path code. |
|
||
| `bun run test:serial` | Just the `*.serial.test.ts` set (cross-file-contention quarantine; runs at `--max-concurrency=1`). | ~1s per quarantined file | Debugging a specific quarantined file. |
|
||
| `bun run test:e2e` | Real Postgres E2E. Requires Docker + `DATABASE_URL`. Sequential (template-DB parallelization is a v0.27+ TODO). | ~5-10min | Pre-ship; nightly. |
|
||
| `bun run check:all` | All 7 historical pre-checks (privacy + jsonb + progress + no-legacy-getconnection + trailing-newline + wasm + exports-count). Superset of `verify`. | ~10s | Local-only sweep. The 4 not in `verify` are nice-to-haves. |
|
||
|
||
### CI vs local: intentionally divergent file sets
|
||
|
||
- **CI matrix** (`.github/workflows/test.yml`) runs `scripts/test-shard.sh` 4-way, which uses FNV-1a hash bucketing and INCLUDES `*.slow.test.ts`. As of v0.31.4.1, CI EXCLUDES `*.serial.test.ts` from the hash buckets and runs them on shard 1 via `bun run test:serial` at `--max-concurrency=1`. Before that, serial files were hashed in alongside parallel files, which broke the `mock.module` quarantine (top-level mocks in serial files leaked into the parallel files they shared a shard process with — most visibly, `eval-takes-quality-runner.serial.test.ts` stubbed `gateway.ts` and broke every `gateway.embedMultimodal` test in `voyage-multimodal.test.ts` on shard 2). CI is the ground truth for "did everything pass."
|
||
- **Local fast loop** (`scripts/run-unit-shard.sh` via the parallel wrapper) uses round-robin-by-index sharding and EXCLUDES `*.slow.test.ts` AND `*.serial.test.ts`. Local trades coverage for inner-loop speed; CI catches what local skips.
|
||
|
||
This divergence is intentional. Don't try to make them equal — the two scripts deliberately solve different problems. The regression test at `test/scripts/run-unit-shard.test.ts` pins what the local fast loop should and shouldn't include.
|
||
|
||
### Failure-first logging
|
||
|
||
When `bun run test` finds any failure, the wrapper:
|
||
|
||
1. Writes failure blocks (each prefixed with `--- shard N: <test name> ---`) to `.context/test-failures.log` (workspace-local, gitignored). On systems without a writable `.context/`, falls back to `/tmp/gbrain-test-failures.log`.
|
||
2. Prints a loud stderr banner with the absolute log path, plus the last 30 lines of the failure log inlined. Banner survives `| head` / `| tail` / agent-side log truncation.
|
||
3. Writes a one-line-per-shard summary to `.context/test-summary.txt` (`shard N/M: pass=X fail=Y skip=Z rc=W`).
|
||
4. Exits non-zero. Empty failure log + non-zero exit = infrastructure problem (wedged shard, killed child); the banner says so.
|
||
|
||
If a shard wedges (per-shard `GBRAIN_TEST_SHARD_TIMEOUT` cap, default 600s), the wrapper writes `--- shard N: WEDGED after ${SHARD_TIMEOUT}s ---` to the failure log, includes the last 50 lines of the shard log, and proceeds with other shards' results.
|
||
|
||
### File taxonomy
|
||
|
||
- `*.test.ts` → fast loop (parallel 8-shard fan-out).
|
||
- `*.slow.test.ts` → run via `bun run test:slow` only (intentional cold-path tests; would dominate the fast loop's wallclock).
|
||
- `*.serial.test.ts` → run via `bun run test:serial` after the parallel pass completes; uses `--max-concurrency=1`. Quarantine for tests that share file-wide state and race when run alongside other files in the same `bun test` process. Currently: `test/brain-registry.serial.test.ts`, `test/reconcile-links.serial.test.ts`, `test/core/cycle.serial.test.ts`, `test/embed.serial.test.ts` (the latter two added in v0.26.7 — they use `mock.module(...)` which leaks across files in the shard process). **Do not put the parallelism back on a serial file unless you've fixed the contention root cause** (it just re-introduces the flake).
|
||
- `test/e2e/*.test.ts` → real-Postgres E2E. Skipped when `DATABASE_URL` is unset.
|
||
|
||
The intra-file parallelism project (turn `bun test` into `bun test --concurrent` after sweeping shared-state contention sites) is sliced across v0.26.7 (foundation), v0.26.8 (env-mutation sweep), and v0.26.9 (PGLite sweep + codemod + measurement). v0.26.4 ships file-level parallelism only.
|
||
|
||
### Test-isolation lint and helpers (v0.26.7)
|
||
|
||
The cross-file flake class is enforced statically by `scripts/check-test-isolation.sh`, wired into `bun run verify` and `bun run check:all`. Rules (non-serial unit files only; `*.serial.test.ts` and `test/e2e/*` are skipped):
|
||
|
||
| Rule | What it bans | Fix |
|
||
|---|---|---|
|
||
| **R1** | `process.env.X = ...`, bracket assignment, `delete process.env.X`, `Object.assign(process.env, ...)`, `Reflect.set(process.env, ...)` | Use `withEnv()` from `test/helpers/with-env.ts`, OR rename file to `*.serial.test.ts` |
|
||
| **R2** | `mock.module(...)` anywhere in the file | Rename file to `*.serial.test.ts` (no DI on production code for testability) |
|
||
| **R3** | `new PGLiteEngine(` outside ~50 lines after a `beforeAll(` line | Use the canonical block (below) inside `beforeAll(` |
|
||
| **R4** | Files creating `new PGLiteEngine(` without `engine.disconnect(` inside an `afterAll(` block | Add `afterAll(() => engine.disconnect())` |
|
||
|
||
Files that violated these rules at the v0.26.7 baseline are listed in `scripts/check-test-isolation.allowlist`. **The allow-list MUST shrink over time** — never add new entries. v0.26.8 (env sweep) and v0.26.9 (PGLite sweep) remove entries as files get fixed.
|
||
|
||
#### Canonical PGLite block (R3 + R4 compliant)
|
||
|
||
Every test file that needs a PGLite engine should use this exact pattern:
|
||
|
||
```ts
|
||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
||
|
||
let engine: PGLiteEngine;
|
||
|
||
beforeAll(async () => {
|
||
engine = new PGLiteEngine();
|
||
await engine.connect({});
|
||
await engine.initSchema();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await engine.disconnect();
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
await resetPgliteState(engine);
|
||
});
|
||
```
|
||
|
||
Why this exact shape: `beforeAll` creates a single engine per file (PGLite WASM cold-start + initSchema is ~20s); `beforeEach` truncates user data via `resetPgliteState` ("two orders of magnitude faster" than fresh-engine-per-test); `afterAll` disconnects so the engine doesn't leak across file boundaries within a shard process.
|
||
|
||
#### `withEnv` pattern (R1 fix)
|
||
|
||
```ts
|
||
import { withEnv } from './helpers/with-env.ts';
|
||
|
||
test('reads OPENAI_API_KEY', async () => {
|
||
await withEnv({ OPENAI_API_KEY: 'sk-test' }, async () => {
|
||
expect(loadConfig().openai_key).toBe('sk-test');
|
||
});
|
||
});
|
||
|
||
// Delete a var (override is undefined):
|
||
await withEnv({ GBRAIN_HOME: undefined }, fn);
|
||
|
||
// Multiple keys:
|
||
await withEnv({ A: '1', B: '2', C: undefined }, fn);
|
||
```
|
||
|
||
`withEnv` saves the prior value of every key it touches and restores via try/finally — including when the callback throws. **It is cross-test safe but NOT intra-file concurrent-safe.** `process.env` is process-global; two `test.concurrent()` calls in the same file both touching the same key will race. Files using `withEnv` stay outside the future `test.concurrent()` codemod's eligibility filter.
|
||
|
||
#### When to quarantine instead of fix
|
||
|
||
Rename to `*.serial.test.ts` when:
|
||
- The file uses `mock.module(...)` (R2 — there's no clean fix without changing production code).
|
||
- The file is genuinely env-coupled (e.g. `gbrain-home-isolation.test.ts`, `claw-test-cli.test.ts`) — module-load env readers + ESM caching defeat dynamic-import-after-env tricks.
|
||
- The file's tests intentionally share state across `it()` boundaries.
|
||
|
||
Quarantine count cap: 10 (informational). Beyond that, push back on the design.
|
||
|
||
### Inventory (legacy)
|
||
|
||
`bun test` runs all tests. After the v0.12.1 release: ~75 unit test files + 8 E2E test files (1412 unit pass, 119 E2E when `DATABASE_URL` is set — skip gracefully otherwise). Unit tests run
|
||
without a database. E2E tests skip gracefully when `DATABASE_URL` is not set.
|
||
|
||
Unit tests: `test/markdown.test.ts` (frontmatter parsing), `test/chunkers/recursive.test.ts`
|
||
(chunking), `test/parity.test.ts` (operations contract
|
||
parity), `test/cli.test.ts` (CLI structure), `test/config.test.ts` (config redaction),
|
||
`test/files.test.ts` (MIME/hash), `test/import-file.test.ts` (import pipeline),
|
||
`test/upgrade.test.ts` (schema migrations),
|
||
`test/file-migration.test.ts` (file migration), `test/file-resolver.test.ts` (file resolution),
|
||
`test/import-resume.test.ts` (import checkpoints), `test/migrate.test.ts` (migration; v8/v9 helper-btree-index SQL structural assertions + 1000-row wall-clock fixtures that guard the O(n²)→O(n log n) fix + v0.13.1 assertions on v12/v13 SQL shape, `sqlFor` + `transaction:false` runner semantics, the `max_stalled DEFAULT 1` regression guard, and v0.22.6.1 v24 `sqlFor.pglite: ''` no-op assertion),
|
||
`test/bootstrap.test.ts` (v0.22.6.1 — bootstrap contract: no-op on fresh install, idempotent across two `initSchema()` calls, no-op on modern brain that already has every probed column, full bootstrap path on simulated pre-v0.18 brain, fresh-install regression guard, pre-v0.13 `links` shape coverage),
|
||
`test/schema-bootstrap-coverage.test.ts` (v0.22.6.1 CI guard — `REQUIRED_BOOTSTRAP_COVERAGE` lists every forward reference in PGLITE_SCHEMA_SQL; the test fails loudly if `applyForwardReferenceBootstrap` skips one. When you add a column-with-index to the embedded schema blob, you extend both arrays or this guard fails. The pattern that broke gbrain ten times in two years is now structurally prevented. **v0.35.5.0:** test now also parses `src/core/migrate.ts` source text for every `ALTER TABLE ... ADD COLUMN` (top-level `sql:`, `sqlFor.{postgres,pglite}` overrides, AND handler-body `engine.runMigration(N, \`ALTER TABLE ...\`)`), and asserts each (table, column) pair is covered by the bootstrap OR by the schema blob's CREATE TABLE bodies. Catches the column-only forward-reference class (e.g. `sources.archived` shape from v0.26.5, `oauth_clients.source_id` from v0.34.1) that the pre-existing CREATE INDEX parser couldn't see. Pre-existing parser bug fixed in same wave: `parseBaseTableColumns` now strips SQL line + block comments before identifying column names so commented-out lines no longer hide adjacent columns from coverage.),
|
||
`test/helpers/schema-diff.ts` + `test/helpers/schema-diff.test.ts` + `test/e2e/schema-drift.test.ts` (v0.26.6 #588 — cross-engine schema parity gate. Helper exports pure `snapshotSchema(query)` / `diffSnapshots(pg, pglite, opts)` / `formatDiffForFailure(diff)` / `isCleanDiff(diff)` over a four-tuple per column (`data_type`, `udt_name`, `is_nullable`, `column_default`). E2E test spins up fresh PGLite + Postgres, runs `engine.initSchema()` on each (bootstrap + schema replay + migrations), snapshots `information_schema.columns`, then diffs. 2-table allowlist (`files`, `file_migration_ledger`) — every other Postgres table must reach PGLite via PGLITE_SCHEMA_SQL or a migration's `sqlFor.pglite` branch. Sentinels for `oauth_clients`, `mcp_request_log`, `access_tokens`, `eval_candidates` give tighter blame messages. Skip-gracefully without `DATABASE_URL`. Wired into `scripts/e2e-test-map.ts` so changes to `src/schema.sql`, `src/core/pglite-schema.ts`, or `src/core/migrate.ts` trigger it. The failure message names every drift with a paste-ready hint pointing at `src/core/pglite-schema.ts`.),
|
||
`test/setup-branching.test.ts` (setup flow), `test/slug-validation.test.ts` (slug validation),
|
||
`test/storage.test.ts` (storage backends), `test/supabase-admin.test.ts` (Supabase admin),
|
||
`test/yaml-lite.test.ts` (YAML parsing), `test/check-update.test.ts` (version check + update CLI),
|
||
`test/pglite-engine.test.ts` (PGLite engine, all 40 BrainEngine methods including 11 cases for `addLinksBatch` / `addTimelineEntriesBatch`: empty batch, missing optionals, within-batch dedup via ON CONFLICT, missing-slug rows dropped by JOIN, half-existing batch, batch of 100 + v0.13.1 `connect()` error-wrap assertion (original error nested, #223 link in message, lock released)),
|
||
`test/engine-factory.test.ts` (engine factory + dynamic imports),
|
||
`test/integrations.test.ts` (recipe parsing, CLI routing, recipe validation),
|
||
`test/publish.test.ts` (content stripping, encryption, password generation, HTML output),
|
||
`test/backlinks.test.ts` (entity extraction, back-link detection, timeline entry generation),
|
||
`test/lint.test.ts` (LLM artifact detection, code fence stripping, frontmatter validation),
|
||
`test/report.test.ts` (report format, directory structure),
|
||
`test/skills-conformance.test.ts` (skill frontmatter + required sections validation),
|
||
`test/resolver.test.ts` (RESOLVER.md coverage, routing validation + v0.20.4 round-trip: every quoted RESOLVER.md trigger must match a frontmatter `triggers:` entry in the target skill, and every `name="<word>"` reference in any SKILL.md must resolve to a declared op in `src/core/operations.ts` or a Minions handler in `PROTECTED_JOB_NAMES`),
|
||
`test/search.test.ts` (RRF normalization, compiled truth boost, cosine similarity, dedup key),
|
||
`test/sql-ranking.test.ts` (v0.22.0 source-boost helpers: 39 cases covering longest-prefix-match in SQL CASE, detail=high temporal-bypass, three-meta-char LIKE escape (%, _, \\), single-quote SQL-literal doubling, env override parsing for GBRAIN_SOURCE_BOOST + GBRAIN_SEARCH_EXCLUDE, resolveBoostMap / resolveHardExcludes merge semantics),
|
||
`test/dedup.test.ts` (source-aware dedup, compiled truth guarantee, layer interactions),
|
||
`test/intent.test.ts` (query intent classification: entity/temporal/event/general),
|
||
`test/eval.test.ts` (retrieval metrics: precisionAtK, recallAtK, mrr, ndcgAtK, parseQrels),
|
||
`test/check-resolvable.test.ts` (resolver reachability, MECE overlap, gap detection, DRY checks + v0.14.1 proximity-based DRY detection + `extractDelegationTargets` coverage — 13 DRY cases),
|
||
`test/dry-fix.test.ts` (v0.14.1 auto-fix: three shape-aware expander pure-function tests, five guards — working-tree-dirty, no-git-backup, inside-code-fence, already-delegated within 40 lines, ambiguous-multi-match, block-is-callout — 28 cases),
|
||
`test/doctor-fix.test.ts` (v0.14.1 `gbrain doctor --fix` CLI integration: dry-run preview, apply path, JSON output shape — 3 cases),
|
||
`test/backoff.test.ts` (load-aware throttling, concurrency limits, active hours),
|
||
`test/fail-improve.test.ts` (deterministic/LLM cascade, JSONL logging, test generation, rotation),
|
||
`test/transcription.test.ts` (provider detection, format validation, API key errors),
|
||
`test/enrichment-service.test.ts` (entity slugification, extraction, tier escalation),
|
||
`test/data-research.test.ts` (recipe validation, MRR/ARR extraction, dedup, tracker parsing, HTML stripping),
|
||
`test/minions.test.ts` (Minions job queue v7: CRUD, state machine, backoff, stall detection, dependencies, worker lifecycle, lock management, claim mechanics, depth/child-cap, timeouts, cascade kill, idempotency, child_done inbox, attachments, removeOnComplete/Fail + v0.13.1 `max_stalled` clamp/default/plumbing coverage),
|
||
`test/extract.test.ts` (link extraction, timeline extraction, frontmatter parsing, directory type inference),
|
||
`test/extract-db.test.ts` (gbrain extract --source db: typed link inference, idempotency, --type filter, --dry-run JSON output),
|
||
`test/extract-fs.test.ts` (gbrain extract --source fs: first-run inserts + second-run reports zero, dry-run dedups candidates across files, second-run perf regression guard — the v0.12.1 N+1 dedup bug),
|
||
`test/link-extraction.test.ts` (canonical extractEntityRefs both formats, extractPageLinks dedup, inferLinkType heuristics, parseTimelineEntries date variants, isAutoLinkEnabled config),
|
||
`test/graph-query.test.ts` (direction in/out/both, type filter, indented tree output),
|
||
`test/features.test.ts` (feature scanning, brain_score calculation, CLI routing, persistence),
|
||
`test/file-upload-security.test.ts` (symlink traversal, cwd confinement, slug + filename allowlists, remote vs local trust),
|
||
`test/query-sanitization.test.ts` (prompt-injection stripping, output sanitization, structural boundary),
|
||
`test/search-limit.test.ts` (clampSearchLimit default/cap behavior across list_pages and get_ingest_log),
|
||
`test/repair-jsonb.test.ts` (v0.12.2 JSONB repair: TARGETS list, idempotency, engine-awareness),
|
||
`test/migrations-v0_12_2.test.ts` (v0.12.2 orchestrator phases: schema → repair → verify → record),
|
||
`test/markdown.test.ts` (splitBody sentinel precedence, horizontal-rule preservation, inferType wiki subtypes),
|
||
`test/orphans.test.ts` (v0.12.3 orphans command: detection, pseudo filtering, text/json/count outputs, MCP op),
|
||
`test/postgres-engine.test.ts` (v0.12.3 statement_timeout scoping: `sql.begin` + `SET LOCAL` shape, source-level grep guardrail against reintroduced bare `SET statement_timeout`),
|
||
`test/sync.test.ts` (sync logic + v0.12.3 regression guard asserting top-level `engine.transaction` is not called),
|
||
`test/sync-concurrency.test.ts` (v0.22.13 PR #490: 17 cases covering `autoConcurrency()` thresholds + PGLite-forces-serial + explicit-override clamping, `shouldRunParallel()` Q1 explicit-bypasses-floor contract, and `parseWorkers()` validation that rejects `'0'`/`'-3'`/`'foo'`/`'1.5'`/trailing chars),
|
||
`test/sync-parallel.test.ts` (v0.22.13 PR #490: PGLite-routed coverage of the bookmark gate under concurrency request, head-drift gate, vanished-file failure capture, PGLite-stays-serial, and the `gbrain-sync` writer-lock contract — 7 cases),
|
||
`test/sync-failures.test.ts` (v0.22.12: 28 cases pinning `classifyErrorCode` regex coverage for all 12 codes against literal production message strings from `markdown.ts:159-244` and `import-file.ts:199, 347, 352, 401`; `summarizeFailuresByCode` sort + pre-classified-honor; `recordSyncFailures` code-field persistence; `acknowledgeSyncFailures` AcknowledgeResult shape + backfill on pre-v0.22.12 entries),
|
||
`test/doctor.test.ts` (doctor command + v0.12.3 assertions that `jsonb_integrity` scans the four v0.12.0 write sites and `markdown_body_completeness` is present),
|
||
`test/utils.test.ts` (shared SQL utilities + `tryParseEmbedding` null-return and single-warn semantics),
|
||
`test/build-llms.test.ts` (llms.txt/llms-full.txt generator: path resolution, idempotence, spec shape, regen-drift guard, content contract, AGENTS.md install-path mirror, size-budget enforcement — 7 cases),
|
||
`test/oauth.test.ts` (v0.26.0 OAuth 2.1 provider — 27 cases: register, getClient, `client_credentials` grant exchange, `authorization_code` flow with PKCE challenge / verifier, refresh token rotation, `verifyAccessToken` with both OAuth + legacy `access_tokens` fallback, `revokeToken`, `sweepExpiredTokens`, and a contract test asserting `scope` + `localOnly` annotations are set correctly on all 30 operations; **v0.26.2** adds 5 `coerceTimestamp` unit cases (null/undefined/string/number/throw-on-NaN), NULL-`expires_at`-as-expired contract tests for both refresh + access token paths, and a cascade-delete contract test asserting `revoke-client` purges `oauth_tokens` + `oauth_codes` rows via FK CASCADE; **v0.26.9** adds 14 cases pinning the F1/F2/F3/F4/F5/F6/F7c/F12 invariants, including the F1/F4 cross-client isolation pattern (wrong-client attempt MUST reject AND rightful owner MUST still succeed atomically afterward) and the empty-string `redirect_uri` bypass guard surfaced during adversarial review),
|
||
`test/mcp-dispatch-summarize.test.ts` (v0.26.9 — 7 cases pinning F8 `summarizeMcpParams` invariants: declared-keys allow-list intersection, attacker-key-name leak guard (unknown keys counted not named), 1KB byte bucketing for size-probe defense, missing op falls through to fully-redacted shape, declared-keys sorted for deterministic output),
|
||
`test/trust-boundary-contract.test.ts` (v0.26.9 — 4 cases pinning F7b fail-closed semantics under cast bypass: `ctx.remote === undefined` treated as remote/untrusted at every flipped call site, `as any` and `Partial<>` spreads can't downgrade trust by accident),
|
||
`test/check-resolvable-cli.test.ts` (v0.19 CLI wrapper: exit codes, JSON envelope shape, AGENTS.md fallback chain),
|
||
`test/regression-v0_16_4.test.ts` (findRepoRoot regression guard — hermetic startDir parameterization),
|
||
`test/repo-root.test.ts` (v0.16.4 / v0.19 / v0.31.7 — 20 cases: `findRepoRoot` walk semantics + default-arg parity, the 4-tier `autoDetectSkillsDir` fallback chain (`$OPENCLAW_WORKSPACE` → `~/.openclaw/workspace` → repo-root → `./skills`), W1 RESOLVER.md/AGENTS.md filename precedence, D-CX-4 explicit-env-wins-over-repo-root, and 8 new v0.31.7 D3+D5 cases pinning tier-0 `$GBRAIN_SKILLS_DIR` valid/invalid/precedence-over-OPENCLAW_WORKSPACE, the install-path walk in `autoDetectSkillsDirReadOnly`, no-drift on primary success, `AUTO_DETECT_HINT` + `AUTO_DETECT_HINT_READ_ONLY` content, and the D5 regression guard asserting the shared `autoDetectSkillsDir` MUST NEVER return `'install_path'` source — that's how the read-path/write-path split stays safe),
|
||
`test/resolver-merge.test.ts` (v0.31.7 — 8 cases pinning the multi-file resolver merge: `findAllResolverFiles` empty / RESOLVER.md-only / AGENTS.md-only / both-present (RESOLVER.md first), and `checkResolvable` merge semantics across `skills/RESOLVER.md` + `../AGENTS.md` for the OpenClaw layout where the skillpack ships a thin RESOLVER.md and the real dispatcher lives at the workspace root — dedup by `skillPath` (first occurrence wins), AGENTS.md-at-workspace-root works alone, and the previously-unreachable 187/224 OpenClaw skills become reachable),
|
||
`test/filing-audit.test.ts` (v0.19 Check 6: `writes_pages` / `writes_to` frontmatter, filing-rules JSON validation),
|
||
`test/routing-eval.test.ts` (v0.19 Check 5: fixture parsing, structural routing, ambiguous_with, Haiku tie-break layer),
|
||
`test/skill-manifest.test.ts` (v0.19 skill manifest parser: drift detection, managed-block markers),
|
||
`test/skillify-scaffold.test.ts` (v0.19 `gbrain skillify scaffold` stubs: SKILL.md, script, tests, routing-eval fixtures),
|
||
`test/skillpack-install.test.ts` (v0.19 `gbrain skillpack install` managed-block install / update / no-clobber semantics),
|
||
`test/skillpack-sync-guard.test.ts` (v0.19 sync-guard: bundled skills stay byte-identical to `skills/` source),
|
||
`test/http-transport.test.ts` (v0.22.7 HTTP transport: 23 unit cases covering bearer auth + missing/no-Bearer/unknown/revoked + `/health` bypass, F1+F2 round-trip via dispatch.ts, F3 invalid_params, application/json response shape (not SSE), CORS default-deny + allowlist, body cap on Content-Length AND chunked, two-bucket rate limit (refill, exhaust+Retry-After, LRU eviction, TTL prune, pre-auth IP fires before DB), and `mcp_request_log` audit on success + auth_failed),
|
||
`test/restart-sweep.test.ts` (v0.28.3 — 27 bun:test cases for the `recipes/restart-sweep.md` inlined script: sentinel-anchored fenced-block extraction with salted tmp filenames to bypass ESM cache; constructor-time env reads (proves no module-load snapshot); idempotency layer load/save/atomic-tmp-rename/corrupt-JSON-recovery/30-day-prune; `(sessionKey, lastAlertedAt)` cooldown gate with 6h threshold (the C1 fix that survives synthesized restartTime); AGGRESSIVE-gate two-state tests; execFile argv shape proving shell metachars in `OPENCLAW_TELEGRAM_GROUP` cannot reach `/bin/sh`; real-`\n`-not-literal alert formatting; `GBRAIN_HOME` state path override),
|
||
`test/eval-longmemeval.test.ts` (v0.28.8 LongMemEval harness — 12 hermetic cases with no `DATABASE_URL` and no API keys: PGLite create + reset over runtime-enumerated `pg_tables`, infrastructure-table preservation across resets, JSONL question parsing, retrieval-only and answer-gen modes via stubbed `ThinkLLMClient`, `--limit` cutoff, `--keyword-only` vs hybrid, default `--expansion=off` behavior, perf gate (p50 < 30ms / p99 < 50ms warm reset+import+search on Apple Silicon), `--help` works without a configured brain, fixture round-trip via `test/fixtures/longmemeval-mini.jsonl`),
|
||
`test/longmemeval-sanitize.test.ts` (v0.28.8 sanitization parity: 12 cases pinning that `INJECTION_PATTERNS` from `src/core/think/sanitize.ts` is the single source of truth — adding a pattern there must cover both `<take>` framing and `<chat_session>` framing, no per-surface regex drift).
|
||
|
||
E2E tests (`test/e2e/`): Run against real Postgres+pgvector. Require `DATABASE_URL`.
|
||
- `bun run test:e2e` runs Tier 1 (mechanical, all operations, no API keys). Includes 9 dedicated cases for the postgres-engine `addLinksBatch` / `addTimelineEntriesBatch` bind path — postgres-js's `unnest()` binding is structurally different from PGLite's and gets its own coverage.
|
||
- `test/e2e/search-quality.test.ts` runs search quality E2E against PGLite (no API keys, in-memory)
|
||
- `test/e2e/graph-quality.test.ts` runs the v0.10.3 knowledge graph pipeline (auto-link via put_page, reconciliation, traversePaths) against PGLite in-memory
|
||
- `test/e2e/postgres-jsonb.test.ts` — v0.12.2 regression test. Round-trips all 5 JSONB write sites (pages.frontmatter, raw_data.data, ingest_log.pages_updated, files.metadata, page_versions.frontmatter) against real Postgres and asserts `jsonb_typeof='object'` plus `->>'key'` returns the expected scalar. The test that should have caught the original double-encode bug.
|
||
- `test/e2e/integrity-batch.test.ts` (v0.22.8) — parity tests for `scanIntegrity`'s batch-load fast path vs sequential. Four cases (dedup, hits, validate, topPages) seed a fixture and assert both paths return identical results. Dedup case uses raw SQL via `getConn().unsafe()` to seed a `(test-source-2, people/alice)` row alongside the default-source row, since `engine.putPage` doesn't take a `source_id`. Pins the codex-caught multi-source overcounting regression.
|
||
- `test/e2e/jsonb-roundtrip.test.ts` — v0.12.3 companion regression against the 4 doctor-scanned JSONB sites. Assertion-level overlap with `postgres-jsonb.test.ts` is intentional defense-in-depth: if doctor's scan surface ever drifts from the actual write surface, one of these tests catches it.
|
||
- `test/e2e/sync.test.ts` (v0.22.12 — `--skip-failed` failure-loop test, alongside the existing 13 happy-path tests): exercises the full chain — broken file → `performSync` returns `blocked_by_failures` with grouped breakdown → `performSync({skipFailed: true})` advances bookmark and returns `AcknowledgeResult` with code summary → second broken file → second cycle. Saves and restores the user's real `~/.gbrain/sync-failures.jsonl` so the test is hermetic on a developer machine. Asserts bookmark gating, JSONL state, dedup across paths, summary aggregation, and the literal doctor-rendering string format. This is the integration test that proves the v0.22.12 chain holds together — unit tests cover the pure functions in isolation, this covers the integration.
|
||
- `test/e2e/upgrade.test.ts` runs check-update E2E against real GitHub API (network required)
|
||
- `test/e2e/minions-shell-pglite.test.ts` (v0.20.4) exercises the PGLite `--follow` inline shell-job path (in-memory, no `DATABASE_URL` required) — the path the consolidated minion-orchestrator skill documents for dev use
|
||
- `test/e2e/openclaw-reference-compat.test.ts` (v0.19) — exercises `check-resolvable` + `skillpack install` against a minimal AGENTS.md workspace fixture (`test/fixtures/openclaw-reference-minimal/`), regression guard for the 107-skill OpenClaw deployment shape
|
||
- `test/e2e/search-swamp.test.ts` (v0.22.0) — reproduces the headline source-swamp case. Seeds a curated `originals/talks/article-outline-fat-code` page against two `wintermute/chat/` pages stuffed with the same multi-word phrase. Asserts the article wins keyword AND vector ranking, that `detail=high` lets the chat swamp re-surface (temporal-query workflow preserved), and that `source_id` passes through the two-stage CTE intact. PGLite in-memory.
|
||
- `test/e2e/search-exclude.test.ts` (v0.22.0) — verifies `test/` + `archive/` pages are hidden by default, that `include_slug_prefixes` opts back in, and that caller-supplied `exclude_slug_prefixes` adds to defaults. Both keyword and vector search paths covered.
|
||
- `test/e2e/engine-parity.test.ts` (v0.22.0) — Postgres ↔ PGLite top-result and result-set parity for `searchKeyword` + `searchVector`. Codex flagged that Postgres ranks pages then picks best chunk while PGLite returns chunks directly — without parity coverage the source-boost fix could pass on PGLite and fail on Postgres. Skips gracefully when `DATABASE_URL` is unset.
|
||
- `test/e2e/postgres-bootstrap.test.ts` (v0.22.6.1) — exercises `PostgresEngine.initSchema()` directly against a fresh real Postgres database. Asserts the bootstrap path is no-op on fresh installs and that SCHEMA_SQL replays cleanly through the engine path (not via the standalone `db.initSchema` from `src/core/db.ts`, which would have produced false-positive coverage). Codex caught the E2E-shape gap during plan review.
|
||
- `test/e2e/http-transport.test.ts` (v0.22.7) — 8 cases against real Postgres covering `gbrain serve --http` end-to-end: bearer auth round-trip, `last_used_at` SQL-level debounce semantics, `mcp_request_log` row insertion on success and auth_failed paths, `/health` DB-down → 503 (DB-probing health check), and the F1+F2+F3 dispatch round-trip with a real operation. Skips gracefully when `DATABASE_URL` is unset.
|
||
- `test/e2e/serve-http-oauth.test.ts` (v0.26.0, expanded v0.26.2, expanded v0.26.9) — real-Postgres E2E against `gbrain serve --http` with full OAuth 2.1. Spawns a subprocess server, registers a client via the CLI, mints `client_credentials` tokens, exercises the `/mcp` JSON-RPC pipeline. **v0.26.2 adds:** real DCR `/register` HTTP-level response-shape test (asserts `typeof body.client_id_issued_at === 'number'` over the wire — RFC 7591 §3.2.1 spec compliance, not just internal-store shape); real CLI subprocess test for `revoke-client` (registers → mints token → revokes via `execSync` → asserts token rejected at `/mcp` → asserts re-run exits 1); server fixture flips on `--enable-dcr` so `/register` is reachable. **bun execSync env-inheritance fix:** bun's `execSync` does NOT inherit env mutations done via `process.env.X = ...`, only OS-level env from before bun started. helpers.ts loads `.env.testing` and sets `DATABASE_URL` via `process.env` mutation, which is invisible to subprocesses unless `env: { ...process.env }` is passed explicitly — every subprocess call in this file passes `env: { ...process.env }` for that reason. Reference fix for the next maintainer hitting the same failure mode in sibling sync/cycle/dream/claw-test E2Es. `afterAll` cleanup is guarded on `clientId` (won't throw if `beforeAll` failed before registration); cleanup errors surface to stderr without throwing so real test failures aren't masked. Tracks DCR-registered clients alongside the manual one. **v0.26.9** adds 2 regressions for the F7 trust-boundary fix: an HTTP MCP `submit_job` for `name: "shell"` MUST reject with a permission error (proving the request handler now sets `remote: true` and `submit_job`'s protected-name guard fires), and the same guard rejects subagent submission. Closes the OAuth-token-to-RCE escalation path. Skips gracefully when `DATABASE_URL` is unset.
|
||
- `test/e2e/sync-parallel.test.ts` (v0.22.13 PR #490) — DATABASE_URL-gated. T2: 60-file Postgres sync at concurrency=4 imports all + no connection leak (probes `pg_stat_activity` before/after to confirm worker engines disconnected). P4: 120-file serial-vs-parallel benchmark prints `SYNC_PARALLEL_BENCH N files | serial=Xms | parallel(4)=Yms | speedup=Zx` for CHANGELOG quoting. Asserts parallel ≤ serial × 1.5 (CI-noise tolerant; not a strict speedup gate).
|
||
- `test/e2e/multi-source-bug-class.test.ts` (v0.32.8, PR #860) — 7-case PGLite in-memory regression suite pinning every bug site fixed in this PR: `listAllPageRefs` ordering by `(source_id, slug)` (F11), `getPage` with sourceId picks the right `(source, slug)` row (F2), `extract-takes` processes both overlapping `people/alice` rows independently, `listPages` filters correctly with `PageFilters.sourceId`, `addLinksBatch` with `from/to_source_id` targets the right rows (F4), `validateSourceId` rejects path traversal (F6), reverse-write disk layout uses `brainDir/.sources/<id>/<slug>.md` for non-default sources (F6). No DATABASE_URL needed. Wired into `scripts/e2e-test-map.ts` so changes to extract-takes / patterns / synthesize / embed / extract / migrate-engine auto-trigger this test. Companion: `test/e2e/integrity-batch.test.ts`'s "multi-source duplicate slugs scan once" case was pinning the pre-fix bug — assertion flipped in v0.32.8 to expect both batch + sequential paths report 2.
|
||
- `test/e2e/source-isolation-pglite.test.ts` (v0.34.1.0, #861) — 14-case PGLite in-memory regression suite pinning the source-isolation P0 seal at two layers. Engine layer: `searchKeyword` / `searchVector` / `searchKeywordChunks` / `listPages` / `getPage` / `traverseGraph` / `traversePaths` apply `sourceId` (scalar fast path) and `sourceIds` (array path) correctly across both engines. Op-handler layer: routes through `sourceScopeOpts(ctx)` so a `read+write`-scoped OAuth client bound to `--source dept-x` cannot see rows from neighboring sources via `search`, `query`, `list_pages`, `get_page`, or `find_experts`. Covers both `ctx.sourceId` (single-source clients) and `ctx.auth.allowedSources` (federated_read clients) precedence; federated array wins over scalar wins over nothing. No DATABASE_URL needed.
|
||
- `test/openai-compat-multimodal.test.ts` (v0.34.1.0, #875) — 11-case unit suite for the gateway's openai-compatible multimodal path: happy-path single + multi-input embedding, unauthenticated proxy mode, dimension-mismatch guard (D12; throws `AIConfigError` with model id + observed + expected pre-storage), default-dim fallback when recipe declares `default_dims`, HTTP 401 / 400 / malformed-JSON / non-array error paths, plus a regression test that the existing Voyage `/multimodalembeddings` recipe still routes through its dedicated path (not the openai-compatible one). Hermetic via the `__setEmbedTransportForTests` seam.
|
||
- `test/serve-stdio-lifecycle.test.ts` (extended v0.34.1.0, #870) — adds 3 new cases for the `MCP_STDIO=1` env guard: stdin EOF does NOT trigger shutdown when the env is set, SIGTERM still does (guard scope is correct), unset env preserves the pre-v0.34 CLI lifecycle. Exercises the `ServeOptions.mcpStdio?: boolean` test seam directly so tests don't mutate `process.env`.
|
||
- `test/oauth.test.ts` (extended v0.34.1.0, #909) — 5 new cases for the PKCE DCR public-client gate: `registerClient` with `token_endpoint_auth_method: "none"` returns no `client_secret` field on the public client, default `client_secret_post` clients still get the one-time-reveal secret, `getClient` NULL→undefined normalization so the SDK's clientAuth path accepts public clients, full PKCE `/authorize` → `/token` round-trip against a public client (no client_secret presented), and a regression test that the public-vs-confidential branch doesn't break confidential client `client_secret_post` exchange.
|
||
- Tier 2 (`skills.test.ts`) requires OpenClaw + API keys, runs nightly in CI
|
||
- If `.env.testing` doesn't exist in this directory, check sibling worktrees for one:
|
||
`find ../ -maxdepth 2 -name .env.testing -print -quit` and copy it here if found.
|
||
- **Run E2E tests without asking permission.** When you want to verify behavior,
|
||
there's a relevant E2E test, or you're shipping touching anything covered by an
|
||
E2E suite — just spin up the test DB, run the tests, and tear down. Don't ask,
|
||
don't propose it, don't defer. The lifecycle is short (~2-30s startup, sub-minute
|
||
tests, instant teardown) and the gate value is high. Skipping with "DATABASE_URL
|
||
unset" is silent regression, not caution.
|
||
|
||
### API keys and running ALL tests
|
||
|
||
ALWAYS source the user's shell profile before running tests:
|
||
|
||
```bash
|
||
source ~/.zshrc 2>/dev/null || true
|
||
```
|
||
|
||
This loads `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. Without these, Tier 2 tests
|
||
skip silently. Do NOT skip Tier 2 tests just because they require API keys — load
|
||
the keys and run them.
|
||
|
||
When asked to "run all E2E tests" or "run tests", that means ALL tiers:
|
||
- Tier 1: `bun run test:e2e` (mechanical, sync, upgrade — no API keys needed)
|
||
- Tier 2: `test/e2e/skills.test.ts` (requires OpenAI + Anthropic + openclaw CLI)
|
||
- Always spin up the test DB, source zshrc, run everything, tear down.
|
||
|
||
### E2E test DB lifecycle (ALWAYS follow this)
|
||
|
||
You are responsible for spinning up and tearing down the test Postgres container.
|
||
Do not leave containers running after tests. Do not skip E2E tests, do not ask
|
||
permission to run them — see the "run without asking" rule above.
|
||
|
||
1. **Check for `.env.testing`** — if missing, copy from sibling worktree.
|
||
Read it to get the DATABASE_URL (it has the port number).
|
||
2. **Check if the port is free:**
|
||
`docker ps --filter "publish=PORT"` — if another container is on that port,
|
||
pick a different port (try 5435, 5436, 5437) and start on that one instead.
|
||
3. **Start the test DB:**
|
||
```bash
|
||
docker run -d --name gbrain-test-pg \
|
||
-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
|
||
-e POSTGRES_DB=gbrain_test \
|
||
-p PORT:5432 pgvector/pgvector:pg16
|
||
```
|
||
Wait for ready: `docker exec gbrain-test-pg pg_isready -U postgres`
|
||
4. **Bootstrap the schema** (required — fresh containers have no `oauth_clients`,
|
||
`mcp_request_log`, `pages` etc.; tests like `serve-http-oauth.test.ts` will fail
|
||
with `relation "oauth_clients" does not exist` if you skip this):
|
||
```bash
|
||
DATABASE_URL=postgresql://postgres:postgres@localhost:PORT/gbrain_test \
|
||
bun run src/cli.ts doctor --json > /dev/null 2>&1
|
||
```
|
||
`gbrain doctor` triggers `initSchema()` on first connect, which is the canonical
|
||
way to bring a fresh DB to head. `apply-migrations --yes` alone does NOT seed
|
||
the base schema — it runs ALTER-style migrations on top of `initSchema`. Tests
|
||
that bypass the engine (raw `execSync`-spawned `auth register-client`) hit the
|
||
schema directly and need this step to have run first.
|
||
5. **Run E2E tests:**
|
||
`DATABASE_URL=postgresql://postgres:postgres@localhost:PORT/gbrain_test bun run test:e2e`
|
||
6. **Tear down immediately after tests finish (pass or fail):**
|
||
`docker stop gbrain-test-pg && docker rm gbrain-test-pg`
|
||
|
||
Never leave `gbrain-test-pg` running. If you find a stale one from a previous run,
|
||
stop and remove it before starting a new one.
|
||
|
||
## Search Mode (v0.32.3)
|
||
|
||
GBrain ships three named search modes that bundle the search-lite knobs from
|
||
PR #897 into a single config key. Pick one at install time; the rest of the
|
||
project resolves through `src/core/search/mode.ts`.
|
||
|
||
| Knob | `conservative` | `balanced` | `tokenmax` |
|
||
|-------------------------------|----------------|------------|----------------|
|
||
| `cache.enabled` | true | true | true |
|
||
| `cache.similarity_threshold` | 0.92 | 0.92 | 0.92 |
|
||
| `cache.ttl_seconds` | 3600 | 3600 | 3600 |
|
||
| `intentWeighting` | true | true | true |
|
||
| `tokenBudget` | **4000** | **12000** | **off** |
|
||
| `expansion` (LLM multi-query) | false | false | **true** |
|
||
| `searchLimit` default | 10 | 25 | 50 |
|
||
|
||
**Cost anchors (downstream agent input cost — gbrain itself is rounding error).**
|
||
The corner-to-corner spread is 25x once you pair mode with downstream model.
|
||
Chunks ~400 tokens avg. Per-query cost @ 10K queries/month (typical
|
||
single-user volume), full search payload, no cache savings:
|
||
|
||
| Mode \ Downstream | Haiku 4.5 (\$1/M) | Sonnet 4.6 (\$3/M) | Opus 4.7 (\$5/M) |
|
||
|---|---|---|---|
|
||
| conservative (~4K) | **\$40/mo** | \$120/mo | \$200/mo |
|
||
| balanced (~10K) | \$100/mo | \$300/mo | \$500/mo |
|
||
| tokenmax (~20K) | \$200/mo | \$600/mo | **\$1,000/mo** |
|
||
|
||
Scales linearly: multiply by 10 for 100K/mo (heavy power user / multi-user
|
||
fleet); divide by 10 for 1K/mo (light usage). Natural pairings span ~4x.
|
||
Mismatches (tokenmax+Haiku, conservative+Opus) waste capacity differently
|
||
— too-big payload overwhelms a cheap model; too-small payload starves an
|
||
expensive one.
|
||
|
||
tokenmax adds ~\$1.50 per 1K queries in Haiku expansion calls on top of
|
||
the matrix (\$15/mo @ 10K). Cache hits cut all numbers ~50%. **The cost
|
||
picker copy in `gbrain init` carries the same matrix verbatim** — update
|
||
both when refreshing.
|
||
|
||
**Per-query math vs real-world spend.** The matrix above is what an
|
||
isolated benchmark would measure. Real agent loops with disciplined
|
||
Anthropic prompt caching see 50-80% discount on top (cache hits skip
|
||
downstream entirely). The realistic-scale anchor in
|
||
`docs/eval/SEARCH_MODE_METHODOLOGY.md` walks the natural pairings at
|
||
single-power-user volume (~860 turns/mo): tokenmax+Opus ~\$700/mo,
|
||
balanced+Sonnet ~\$430/mo, conservative+Haiku ~\$170/mo. Setups WITHOUT
|
||
cache-aware prompt layout (frequent prefix churn) see the per-query
|
||
matrix dominate — mode + model choice matters more there.
|
||
|
||
**Resolution chain** (matches the v0.31.12 model-tier pattern at
|
||
`src/core/model-config.ts:resolveModel`):
|
||
|
||
per-call SearchOpts → per-key config (search.cache.enabled, …) →
|
||
MODE_BUNDLES[search.mode] → MODE_BUNDLES.balanced (fallback)
|
||
|
||
Mode resolution lives in **bare `hybridSearch`** (NOT just the cached wrapper)
|
||
per `[CDX-5+6]` in `~/.claude/plans/lets-take-a-look-validated-parrot.md` — so
|
||
`gbrain eval replay` and `gbrain eval longmemeval` test the same mode-affected
|
||
behavior as the production `query` op.
|
||
|
||
**Cache-key contamination hotfix `[CDX-4]`:** migration v56 added a
|
||
`knobs_hash` column to `query_cache`. The lookup filter is now
|
||
`WHERE source_id = $ AND knobs_hash = $ AND embedding similarity < $` so a
|
||
tokenmax write (expansion=on, limit=50) can't be served to a conservative
|
||
read.
|
||
|
||
**v0.36.3.0 knobs_hash v=2 → v=3.** The hash now folds the active
|
||
embedding column name + provider into the cache key, so a query routed
|
||
through `embedding_voyage` (1024d Voyage) can't be served a cache row
|
||
written against `embedding` (1536d OpenAI). Existing v=2 rows become
|
||
unreachable on first re-query (one-time miss spike on upgrade);
|
||
`mode.ts:KNOBS_HASH_VERSION` is the single source of truth.
|
||
|
||
**Three CLI surfaces:**
|
||
|
||
gbrain search modes # what is running, with per-knob attribution
|
||
gbrain search modes --reset # clear search.* overrides (mode bundle wins)
|
||
gbrain search stats [--days N] # cache hit rate, intent mix, budget drops
|
||
gbrain search tune [--apply] # data-driven recommendations
|
||
|
||
The install picker fires inside `gbrain init` AFTER `engine.initSchema()`
|
||
(non-TTY auto-selects). The upgrade banner fires once via `runPostUpgrade`
|
||
in `src/commands/upgrade.ts`, gated by `search.mode_upgrade_notice_shown`.
|
||
|
||
## Eval discipline (v0.32.3)
|
||
|
||
Every metric printed by any `gbrain eval *` or `gbrain search stats` command
|
||
resolves through `src/core/eval/metric-glossary.ts` so industry terms
|
||
(`P@k`, `nDCG@k`, `MRR`, `Jaccard@k`) carry a plain-English line in human
|
||
output and a `_meta.metric_glossary` block in JSON output (one block per
|
||
response per `[CDX-25]`, NOT sibling `_gloss` fields).
|
||
|
||
The full methodology — datasets, sample selection, pre-registered
|
||
expectations, threats to validity, paired-bootstrap + Bonferroni p-value
|
||
discipline `[CDX-14]` — lives in `docs/eval/SEARCH_MODE_METHODOLOGY.md`.
|
||
Auto-regenerated `docs/eval/METRIC_GLOSSARY.md` is CI-guarded against
|
||
drift (`scripts/check-eval-glossary-fresh.sh`).
|
||
|
||
Per-run records land at `<repo>/.gbrain-evals/eval-results.jsonl` per
|
||
`[CDX-23]`. The user's personal `~/.gbrain` brain is NEVER touched —
|
||
audit trail lives in the source repo's git history.
|
||
|
||
## Skills
|
||
|
||
Read the skill files in `skills/` before doing brain operations. GBrain ships 29 skills
|
||
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
|
||
|
||
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
|
||
briefing, migrate, setup, publish.
|
||
|
||
**Brain skills (ported from an upstream agent fork):** signal-detector, brain-ops, idea-ingest, media-ingest,
|
||
meeting-ingestion, citation-fixer, repo-architecture, skill-creator, daily-task-manager.
|
||
|
||
**Operational + identity:** daily-task-prep, cross-modal-review, cron-scheduler, reports,
|
||
testing, soul-audit, webhook-transforms, data-research, minion-orchestrator. As of
|
||
v0.20.4, `minion-orchestrator` is the single unified skill for both lanes of background
|
||
work (shell jobs via `gbrain jobs submit shell`, LLM subagents via `gbrain agent run`) ...
|
||
the prior `gbrain-jobs` skill was merged in, Preconditions are shared, and trigger
|
||
routing is narrowed to what the skill actually covers.
|
||
|
||
**Skillify loop (v0.19):** skillify (the markdown orchestration), skillpack-check
|
||
(agent-readable health report).
|
||
|
||
**Routing-table compression (v0.32.3.0):** `skills/functional-area-resolver/` —
|
||
two-layer dispatch pattern for shrinking large AGENTS.md / RESOLVER.md files
|
||
(>=12KB) without losing routing accuracy. Replaces one row per skill with one
|
||
entry per functional area, where each area declares its sub-skills in a
|
||
`(dispatcher for: ...)` clause. The static-prompt analog of hierarchical agent
|
||
routing (AnyTool [arXiv:2402.04253](https://arxiv.org/abs/2402.04253), RAG-MCP
|
||
[arXiv:2505.03275](https://arxiv.org/html/2505.03275v1), Anthropic Agent Skills
|
||
progressive disclosure). Empirically validated across Opus 4.7 / Sonnet 4.6 /
|
||
Haiku 4.5: +13 to +17pp over the verbose baseline at 48% the size (25KB → 13KB
|
||
on a real fork). The `(dispatcher for: ...)` clause is the load-bearing signal
|
||
— strip it and lenient accuracy collapses to 41.7% on Sonnet (the
|
||
`resolver-of-resolvers` ablation case). A/B eval surface lives at
|
||
`evals/functional-area-resolver/` (outside `skills/` deliberately so the
|
||
skillpack bundler doesn't ship eval infrastructure to downstream installs):
|
||
gateway-routed TypeScript harness, 20 training + 5 held-out fixtures, strict +
|
||
lenient scoring, three committed cross-model receipts in `baseline-runs/`.
|
||
Receipt header binds (model, prompt_template_hash, fixtures_hash, harness_sha,
|
||
ts) so future contributors can verify reproduction. Companion `rescore.mjs`
|
||
re-scores existing JSONL with lenient tolerance for zero API cost. Reproduce
|
||
with `cd evals/functional-area-resolver && node harness.mjs --model
|
||
{opus|sonnet|haiku}` (~$0.30–1.70 per model). Nine v0.33.x follow-up TODOs
|
||
filed for held-out corpus growth, cross-vendor verification, hierarchical
|
||
area-of-areas, embedding-based pre-router, and the run-1 vs run-2
|
||
prompt-design ablation methodology.
|
||
|
||
**Operational health (v0.19.1):** smoke-test (8 post-restart health checks with auto-fix
|
||
for Bun, CLI, DB, worker, Zod CJS, gateway, API key, brain repo; user-extensible via
|
||
`~/.gbrain/smoke-tests.d/*.sh`).
|
||
|
||
**Conventions:** `skills/conventions/` has cross-cutting rules (quality, brain-first,
|
||
model-routing, test-before-bulk, cross-modal). `skills/_brain-filing-rules.md` and
|
||
`skills/_output-rules.md` are shared references.
|
||
|
||
## Bulk-action progress reporting
|
||
|
||
All bulk commands (doctor, embed, import, export, sync, extract, migrate,
|
||
repair-jsonb, orphans, check-backlinks, lint, integrity auto, eval, files
|
||
sync, and apply-migrations) stream progress through the shared reporter
|
||
at `src/core/progress.ts`. Agents get heartbeats within 1 second of every
|
||
iteration regardless of how slow the underlying work is.
|
||
|
||
Rules:
|
||
- Progress always writes to **stderr**. Stdout stays clean for data output
|
||
(`--json` payloads, final summaries, JSON action events from `extract`).
|
||
- Non-TTY default: plain one-line-per-event human text. JSON requires the
|
||
explicit `--progress-json` flag.
|
||
- Global flags (`--quiet`, `--progress-json`, `--progress-interval=<ms>`)
|
||
are parsed by `src/core/cli-options.ts` BEFORE command dispatch.
|
||
- Phase names are machine-stable `snake_case.dot.path` (e.g.
|
||
`doctor.db_checks`, `sync.imports`). Documented in
|
||
`docs/progress-events.md`; additive changes only.
|
||
- `scripts/check-progress-to-stdout.sh` is a CI guard that fails the build
|
||
if any new code writes `\r` progress to stdout. Wired into `bun run test`.
|
||
- Minion handlers pass `job.updateProgress` as the `onProgress` callback
|
||
to core functions (DB-backed primary progress channel); stderr from
|
||
`jobs work` stays coarse for daemon liveness only.
|
||
|
||
When wiring a new bulk command: `import { createProgress } from '../core/progress.ts'`
|
||
and `import { getCliOptions, cliOptsToProgressOptions } from '../core/cli-options.ts'`.
|
||
Create a reporter with `createProgress(cliOptsToProgressOptions(getCliOptions()))`,
|
||
`start(phase, total?)` before the loop, `tick()` inside it, `finish()` after.
|
||
For single long-running queries, use `startHeartbeat(reporter, note)` with a
|
||
try/finally to guarantee cleanup. Never call `process.stdout.write('\r...')`
|
||
in bulk paths, the CI guard will fail the build.
|
||
|
||
## Capturing test output (NEVER pipe through `tail` / `head`)
|
||
|
||
**Iron rule:** when running `bun test`, `bun run test:e2e`, `bun run typecheck`,
|
||
or any other test/check command, redirect to a file FIRST, then `tail` the file
|
||
separately:
|
||
|
||
```bash
|
||
# RIGHT — full output preserved, real exit code visible
|
||
bun test > /tmp/ship_units.txt 2>&1
|
||
echo "EXIT=$?"
|
||
tail -50 /tmp/ship_units.txt
|
||
grep -E '(fail\)|✗|error:' /tmp/ship_units.txt | head -30
|
||
```
|
||
|
||
```bash
|
||
# WRONG — exit code is `tail`'s (always 0), failures truncated, ship gates fail open
|
||
bun test 2>&1 | tail -10
|
||
```
|
||
|
||
The pipe form silently breaks /ship Step T1 (test failure ownership triage) and
|
||
the test verification gate (Step 16) because:
|
||
- `$?` after a pipe is the LAST command's exit code (`tail` → 0), not bun's
|
||
- bun prints failure details before the summary line, so `tail -N` drops them
|
||
- Step T1 needs the full failure list to classify in-branch vs pre-existing
|
||
|
||
This bit us during v0.26.2 ship: `bun test 2>&1 | tail -10` reported "3911 pass / 23 fail"
|
||
but no failure details survived, forcing a 23-minute re-run to triage.
|
||
|
||
Apply the same pattern to any long-running command whose exit code matters:
|
||
`bun run typecheck`, `bun run ci:local`, migration runs, eval suites, etc.
|
||
For background tasks (`run_in_background: true`), the harness captures the exit
|
||
file separately — use it via the bg task's `<id>.exit` file, not the streamed
|
||
output.
|
||
|
||
## Build
|
||
|
||
`bun build --compile --outfile bin/gbrain src/cli.ts`
|
||
|
||
## Version locations (single source of truth: `VERSION` file)
|
||
|
||
Every release advances the version in **five files at once**. Keep these in
|
||
sync. `/ship` enforces this via Step 12's idempotency check (VERSION vs
|
||
package.json drift), but the canonical list lives here so future runs and
|
||
the auto-update agent know where to look.
|
||
|
||
**Version format is mandatory: `MAJOR.MINOR.PATCH.MICRO` (four numeric
|
||
segments, dot-separated, no leading `v`).** Every new release MUST use the
|
||
4-segment form. The `.MICRO` slot is the dot-suffix follow-up channel: when
|
||
a release ships its commit subject ahead of its VERSION bump (e.g. PR #795
|
||
landing as `v0.31.4` without bumping the file), the corrective ship lands
|
||
as `0.31.4.1` rather than churning the patch number to `0.31.5`. Suffixes
|
||
like `-fixwave` are still allowed as needed (`0.31.1.1-fixwave`), but the
|
||
four numeric segments are required first. Historical 3-segment versions
|
||
(`0.31.3`, `0.22.1`) remain valid in `git log` and migration filenames
|
||
(`skills/migrations/v0.21.0.md`); do NOT rewrite them. Going forward only.
|
||
|
||
**Required (every release must update all five):**
|
||
|
||
| File | What lives there | Format |
|
||
|---|---|---|
|
||
| `VERSION` | The single source of truth. Read first by `/ship`, the binary, and CI version-gate. | Bare 4-segment string `MAJOR.MINOR.PATCH.MICRO` (e.g. `0.31.4.1`), no leading `v`. |
|
||
| `package.json` | Bun/npm package version. `gbrain --version` reads it via the compiled binary's bundled package metadata. CI version-gate cross-checks this against `VERSION` and fails if they drift. | `"version": "0.31.4.1"` |
|
||
| `CHANGELOG.md` | Top entry header `## [0.31.4.1] - YYYY-MM-DD` plus the "To take advantage of v0.31.4.1" block. | Standard Keep-a-Changelog header. |
|
||
| `TODOS.md` | Any TODO entries that mention "follow-up from vX.Y.Z.W" use the version of the release that filed them. Update only when filing NEW follow-up TODOs. | Inline `vX.Y.Z.W` references in TODO bodies. |
|
||
| `CLAUDE.md` | The Key Files section's per-file annotations carry `vX.Y.Z.W (#NNN)` tags noting which release introduced a behavior. Update whenever a wave's annotations get folded in. | Inline `vX.Y.Z.W (#NNN, contributed by @user)` references. |
|
||
|
||
**Auto-derived (no manual edit; refreshed by their own commands):**
|
||
|
||
- `bun.lock` — root-package version is auto-pinned from `package.json`. After
|
||
bumping `package.json`, run `bun install` to refresh the lockfile.
|
||
- `llms-full.txt` / `llms.txt` — auto-generated documentation bundles. **Any
|
||
CLAUDE.md edit MUST be followed by `bun run build:llms` in the same commit
|
||
(or a follow-up commit before push).** The committed bundles are checked
|
||
against fresh generator output by `test/build-llms.test.ts`, which runs in
|
||
CI shard 1. If you edited CLAUDE.md and didn't regenerate, CI will fail.
|
||
This has bitten the wave 3 times — every CLAUDE.md edit gets a `bun run
|
||
build:llms` chaser, no exceptions. (The `verify` gate doesn't run this
|
||
test; only the full unit suite does. So `bun run typecheck` clean is NOT
|
||
enough to know you can push after a CLAUDE.md edit.)
|
||
|
||
**Historical (DO NOT bump on release):**
|
||
|
||
- `skills/migrations/v0.21.0.md` — migration files use the version they
|
||
shipped FROM as their filename. v0.21.0's migration always says v0.21.0.
|
||
- `src/commands/migrations/v0_21_0.ts` — same: migration code references
|
||
the schema version it migrates to.
|
||
- `test/migrations-v0_21_0.test.ts`, `test/migration-orchestrator-v0_21_0.test.ts`,
|
||
`test/migrate.test.ts` — migration tests reference historical migration
|
||
versions; these are correct as-is and should not move.
|
||
- `src/core/db.ts`, `src/core/migrate.ts`, `src/core/import-file.ts`,
|
||
`src/commands/reindex-code.ts` — code comments cite the release that
|
||
introduced a feature. Once written, these are historical record.
|
||
- `README.md` — references the latest published feature names by version
|
||
(e.g. "v0.21.0 Code Cathedral"); update only when the README's marketing
|
||
copy is intentionally being refreshed, NOT on every micro/patch bump.
|
||
|
||
**The /ship workflow's version idempotency check:** Step 12 reads
|
||
`VERSION` and `package.json`, classifies as FRESH / ALREADY_BUMPED /
|
||
DRIFT_STALE_PKG / DRIFT_UNEXPECTED, and refuses to proceed on
|
||
DRIFT_UNEXPECTED. This is why the two must move together.
|
||
|
||
**The CI version-gate** rejects pushes where `VERSION` and
|
||
`package.json` disagree, OR where `VERSION` is not strictly greater
|
||
than master's VERSION. If a queue collision claims your version on
|
||
master before yours lands, /ship's queue-aware allocator (Step 12)
|
||
will detect drift and re-bump on the next run.
|
||
|
||
### Mandatory version-consistency audit (run after EVERY merge or commit that touches VERSION, package.json, or CHANGELOG)
|
||
|
||
**The trio MUST agree.** Every merge from master will hit conflicts on
|
||
VERSION + package.json + CHANGELOG.md because master ships its own
|
||
version bumps. Auto-merge sometimes resolves these silently in unexpected
|
||
ways. After any merge, branch update, or version-related edit, run this
|
||
audit. It's three lines and never lies:
|
||
|
||
```bash
|
||
echo "VERSION: $(cat VERSION)"
|
||
echo "package.json: $(node -e 'process.stdout.write(require("./package.json").version)')"
|
||
grep -E "^## \[" CHANGELOG.md | head -1
|
||
```
|
||
|
||
All three MUST show the same `MAJOR.MINOR.PATCH.MICRO`. If any one
|
||
disagrees, you have not finished the merge. Fix it before pushing or
|
||
shipping. There is no situation in which "I'll fix it next push" is OK,
|
||
because:
|
||
|
||
- A green local test run with mismatched VERSION/package.json still
|
||
fails the CI version-gate.
|
||
- A green CHANGELOG entry under the wrong version header silently lies
|
||
to release-notes consumers.
|
||
- /ship's Step 12 idempotency check classifies a mismatch as
|
||
`DRIFT_UNEXPECTED` and HALTS — but only if you remember to run /ship
|
||
before pushing. Manual `git push` skips the check.
|
||
|
||
### Merge-conflict recovery procedure (memorize this)
|
||
|
||
When `git merge origin/master` reports conflicts on VERSION,
|
||
package.json, or CHANGELOG.md, resolve in this exact order:
|
||
|
||
1. **VERSION** — overwrite with the wave's version (`echo -n "X.Y.Z.W"
|
||
> VERSION`). Highest semver wins; do NOT take master's lower version.
|
||
2. **package.json** — strip the conflict markers, keep the wave's
|
||
version line. Sed pattern:
|
||
`sed -i.bak '/^<<<<<<< HEAD$/d; /^=======$/,/^>>>>>>> /d' package.json && rm package.json.bak`
|
||
(assumes ours is above the `=======`).
|
||
3. **CHANGELOG.md** — strip ALL three conflict markers; both your entry
|
||
and master's entry stay. Sed pattern:
|
||
`sed -i.bak '/^<<<<<<< HEAD$/d; /^=======$/d; /^>>>>>>> origin\/master$/d' CHANGELOG.md && rm CHANGELOG.md.bak`
|
||
Then verify your entry is the topmost `## [X.Y.Z.W]` and master's
|
||
newer-than-yours entries (if any) sit below.
|
||
4. **Run the 3-line audit above.** If it doesn't show your version on
|
||
all three lines, you missed a marker.
|
||
5. **Run `bun install`** to refresh `bun.lock` against the resolved
|
||
`package.json`. Stage and commit if it changed.
|
||
6. **Run `bun run typecheck`** before committing the merge.
|
||
7. Only THEN run `git commit` for the merge.
|
||
|
||
If the audit shows drift after step 4, do NOT proceed to step 5. Re-run
|
||
steps 1-3 against the actual file content; you missed a marker or
|
||
resolved one in the wrong direction.
|
||
|
||
**Anti-pattern to avoid:** Resolving via `git checkout --ours package.json`
|
||
and `git checkout --theirs scripts/test-shard.sh` mixed in the same
|
||
commit. The selective directional resolution is fine, but on
|
||
VERSION/package.json/CHANGELOG specifically, ALWAYS use the explicit
|
||
`echo > VERSION` + sed-strip-markers pattern above. The directional
|
||
checkout flags have bitten us when the conflict shape was unexpected
|
||
(e.g. master stripped a section we expected to keep).
|
||
|
||
### Pre-push gate (manual; tighten when you remember to)
|
||
|
||
Before any `git push` of a merge commit, run the audit one more time:
|
||
|
||
```bash
|
||
echo "VERSION: $(cat VERSION)"
|
||
echo "package.json: $(node -e 'process.stdout.write(require("./package.json").version)')"
|
||
grep -E "^## \[" CHANGELOG.md | head -1
|
||
```
|
||
|
||
If you've been editing the branch via `/ship` you can rely on Step 12's
|
||
idempotency check. If you've been editing manually (merge resolution,
|
||
conflict fix, version bump), the audit is the last line of defense
|
||
before CI yells at you.
|
||
|
||
## Pre-ship requirements
|
||
|
||
Before shipping (/ship) or reviewing (/review), always run the full test suite.
|
||
Two equivalent paths:
|
||
|
||
**Path A — local CI gate (recommended, v0.23.1+):**
|
||
- `bun run ci:local` runs the entire stack inside Docker: gitleaks (host), unit
|
||
tests with `DATABASE_URL` unset, and all 29 E2E files sequentially against a
|
||
fresh pgvector container. Stronger than PR CI's 2-file Tier 1 set; closer to
|
||
what nightly Tier 1 catches. Spins up + tears down postgres automatically via
|
||
`docker-compose.ci.yml`. Override the host port with
|
||
`GBRAIN_CI_PG_PORT=5435 bun run ci:local` if 5434 collides.
|
||
- `bun run ci:local:diff` runs only the E2E files matched by the diff selector
|
||
(`scripts/select-e2e.ts`), falling back to all 29 on unmapped src/ paths or
|
||
schema/skills/package.json changes. Fast iteration during a focused branch.
|
||
|
||
**Path B — manual lifecycle (still supported):**
|
||
- `bun test` — unit tests (no database required)
|
||
- Follow the "E2E test DB lifecycle" steps above to spin up the test DB,
|
||
run `bun run test:e2e`, then tear it down.
|
||
|
||
Both must pass. Do not ship with failing E2E tests. Do not skip E2E tests.
|
||
|
||
**Always run typecheck before pushing.** `bun test` (the bun runner)
|
||
skips TypeScript type checking — it only enforces runtime behavior.
|
||
Three ways to actually gate on types:
|
||
|
||
1. `bun run test` (npm script in `package.json`) — includes `bun run typecheck`
|
||
plus the four shell pre-checks (`check-jsonb-pattern.sh`,
|
||
`check-progress-to-stdout.sh`, `check-trailing-newline.sh`,
|
||
`check-wasm-embedded.sh`) before the runner. Use this mid-branch.
|
||
2. `bun run typecheck` — `tsc --noEmit` standalone. Fast (~5s on this repo).
|
||
3. `bun run ci:local` — the full local CI gate from Path A.
|
||
|
||
The trap is: writing a new test, running `bun test test/foo.test.ts`,
|
||
seeing it pass, pushing — and CI's separate typecheck stage rejects an
|
||
invalid type literal that the runner accepted. Caught one of these
|
||
shipping the v0.23.2 round-trip E2E (`type: 'reflection'` is not a
|
||
member of `PageType`). Run `bun run typecheck` once before push, even
|
||
when only test files changed.
|
||
|
||
## Post-ship requirements (MANDATORY)
|
||
|
||
After EVERY /ship, you MUST run /document-release. This is NOT optional. Do NOT
|
||
skip it. Do NOT say "docs look fine" without running it. The skill reads every .md
|
||
file in the project, cross-references the diff, and updates anything that drifted.
|
||
|
||
If /ship's Step 8.5 triggers document-release automatically, that counts. But if
|
||
it gets skipped for ANY reason (timeout, error, oversight), you MUST run it manually
|
||
before considering the ship complete.
|
||
|
||
Files that MUST be checked on every ship:
|
||
- README.md — does it reflect new features, commands, or setup steps?
|
||
- CLAUDE.md — does it reflect new files, test files, or architecture changes?
|
||
- CHANGELOG.md — does it cover every commit?
|
||
- TODOS.md — are completed items marked done?
|
||
- docs/ — do any guides need updating?
|
||
|
||
A ship without updated docs is an incomplete ship. Period.
|
||
|
||
## CHANGELOG + VERSION are branch-scoped
|
||
|
||
**VERSION and CHANGELOG describe what THIS branch adds vs master, not how we got
|
||
here.** Every feature branch that ships gets its own version bump and CHANGELOG
|
||
entry. The entry is product release notes for users; it is not a log of internal
|
||
decisions, review rounds, or codex findings.
|
||
|
||
**Write the CHANGELOG entry at /ship time, not during development.** Mid-branch
|
||
iterations, review rounds (CEO/Eng/Codex/DX), and implementation detours belong
|
||
in the plan file at `~/.claude/plans/`, not in the CHANGELOG. One unified entry
|
||
per branch, covering what the branch added vs the base branch.
|
||
|
||
**Never edit a CHANGELOG entry that already landed on master.** If master has
|
||
v0.18.2 and your branch adds features, bump to the next version (v0.19.0, not
|
||
editing master's v0.18.2). When merging master into your branch, master may
|
||
bring new CHANGELOG entries above yours — push your entry above master's
|
||
latest and verify:
|
||
|
||
- Does CHANGELOG have your branch's own entry separate from master's entries?
|
||
- Is VERSION higher than master's VERSION?
|
||
- Is your entry the topmost `## [X.Y.Z]` entry?
|
||
- `grep "^## \[" CHANGELOG.md` shows a contiguous version sequence?
|
||
|
||
If any answer is no, fix it before continuing.
|
||
|
||
**CHANGELOG is for users, not contributors.** Write like product release notes:
|
||
|
||
- Lead with what the user can now **do** that they couldn't before. Sell the capability.
|
||
- Plain language, not implementation details. "You can now..." not "Refactored the..."
|
||
- **Never mention internal artifacts**: plan file IDs, decision tags (D-CX-#, F-ENG-#),
|
||
review rounds, codex findings, subcontractor credits. These are invisible to users.
|
||
- Put contributor-facing changes in a separate `### For contributors` section at the bottom.
|
||
- Every entry should make someone think "oh nice, I want to try that."
|
||
|
||
**What to omit:**
|
||
- "Codex caught X that the CEO review missed" — private process detail.
|
||
- "D-CX-3 split errors/warnings" — tag is meaningless to users; name the feature instead.
|
||
- "Fix-wave PR #N supersedes #M" — supersede chains belong in PR bodies, not release notes.
|
||
- "215 new cases, 3 decisions applied, 7 reviews cleared" — these are planning-mode metrics.
|
||
|
||
**What to keep:**
|
||
- The user-facing change: what commands exist now, what flag was added, what behavior fixed.
|
||
- Numbers that mean something to the user: TTHW, commands that timed out before, detection counts.
|
||
- Upgrade instructions: `gbrain upgrade` + any manual step if needed.
|
||
- Credit to external contributors when a community PR was incorporated.
|
||
|
||
## CHANGELOG voice + release-summary format
|
||
|
||
Every version entry in `CHANGELOG.md` MUST start with a release-summary section in
|
||
the GStack/Garry voice — one viewport's worth of prose + tables that lands like a
|
||
verdict, not marketing. The itemized changelog (subsections, bullets, files) goes
|
||
BELOW that summary, separated by a `### Itemized changes` header.
|
||
|
||
The release-summary section gets read by humans, by the auto-update agent, and by
|
||
anyone deciding whether to upgrade. The itemized list is for agents that need to
|
||
know exactly what changed.
|
||
|
||
### Release-summary template
|
||
|
||
**Iron rule: lead ELI10, get precise after.** The first ~150 words of every entry
|
||
must be readable by someone who does NOT know gbrain's internals. No file paths,
|
||
no function names, no internal constants, no acronyms (no "RRF", no "knobsHash",
|
||
no "MODE_BUNDLES", no "CDX-4"), no jargon that requires reading the codebase to
|
||
parse. Lead with the user-visible behavior change, in everyday English, like
|
||
you're explaining it to a smart engineer who has never opened the repo.
|
||
|
||
THEN, once the reader knows what shipped and why they'd care, drill into the
|
||
precise details: real file paths, real function names, real config keys, real
|
||
numbers. The precision part is required (the entry is also the technical record
|
||
of what changed), but it lives AFTER the plain-English lead, never before it.
|
||
|
||
The shape:
|
||
|
||
1. **One-line bold headline.** What changed for the user, in human English. No
|
||
jargon. No internal terms. Example good: "Your search stops boosting weak
|
||
pages just because they have a lot of links pointing at them." Example bad:
|
||
"PostFusionOpts gains floorRatio; KNOBS_HASH_VERSION bumped 2→3."
|
||
2. **Plain-English opener** (~3-5 sentences). Describe the problem this fixes in
|
||
everyday terms. Pretend the reader has a brain full of meeting notes and
|
||
people pages and wants to know if this release helps them. Concrete example
|
||
beats abstract description.
|
||
3. **A "How to turn it on" or "How to use it" section** with paste-ready
|
||
commands. Real flags, real config keys. This is where precision starts.
|
||
4. **A "What you'd see in a concrete example" or "The X numbers that matter"
|
||
section** with a table. Use everyday-language column headers ("Page",
|
||
"Match quality", "Has many backlinks?") even when the underlying mechanism
|
||
is technical. The table teaches what the feature does without requiring the
|
||
reader to understand how.
|
||
5. **A "What's safe to know about" or "Things to watch" section** for caveats,
|
||
side effects, cache invalidation, mid-deploy notes. Still in plain language.
|
||
6. **A "What we caught and fixed before merging" section** if the work went
|
||
through review (CEO/eng/codex/outside-voice). Translate review findings into
|
||
plain English. "We caught a stale-cache bug" beats "knobsHash() did not
|
||
include floorRatio in the v=2 hash input."
|
||
7. **`### Itemized changes`** (precision lives here). File paths, function
|
||
names, types, constants, line numbers. This section is for engineers who
|
||
need to know exactly what moved.
|
||
|
||
Voice rules (apply throughout):
|
||
- No em dashes (use commas, periods, "...").
|
||
- No AI vocabulary (delve, robust, comprehensive, nuanced, fundamental, etc.) or
|
||
banned phrases ("here's the kicker", "the bottom line", etc.).
|
||
- Real numbers, real file names, real commands AFTER the ELI10 lead. Not "fast"
|
||
but "~30s on 30K pages." In the ELI10 lead, "fast enough that you won't
|
||
notice" or "~30 seconds even on a big brain."
|
||
- Short paragraphs, mix one-sentence punches with 2-3 sentence runs.
|
||
- Connect to user outcomes: "the agent does ~3x less reading" beats "improved
|
||
precision."
|
||
- Be direct about quality. "Well-designed" or "this is a mess." No dancing.
|
||
|
||
**The smell test:** if someone who has never opened gbrain reads the first 150
|
||
words and walks away knowing what shipped and whether they care, the entry
|
||
passes. If they need to grep the codebase to follow along, rewrite the lead.
|
||
|
||
**Canonical examples in this CHANGELOG:** v0.35.6.0 (floor-ratio gate, written
|
||
ELI10-lead-first), v0.34.4.0 (embed stale fix wave). Use those shapes when in
|
||
doubt. Avoid the shape of entries that lead with internal constants or release
|
||
mechanics; those exist in older history but should not be the model for new
|
||
work.
|
||
|
||
Source material to pull from:
|
||
- CHANGELOG.md previous entry for prior context
|
||
- Latest `gbrain-evals/docs/benchmarks/[latest].md` for headline numbers (sibling repo)
|
||
- Recent commits (`git log <prev-version>..HEAD --oneline`) for what shipped
|
||
- Don't make up numbers. If a metric isn't in a benchmark or production data, don't
|
||
include it. Say "no measurement yet" if asked.
|
||
|
||
Target length: ~250-350 words for the summary. Should render as one viewport.
|
||
|
||
### "To take advantage of v[version]" block (required, v0.13+)
|
||
|
||
After the release-summary and BEFORE `### Itemized changes`, every `## [X.Y.Z]`
|
||
entry MUST include a human-readable self-repair block under the heading
|
||
`## To take advantage of v[version]`.
|
||
|
||
Why: `gbrain upgrade` runs `gbrain post-upgrade` which runs `gbrain apply-migrations`.
|
||
This chain has a known weak link — `upgrade.ts` catches post-upgrade failures as
|
||
best-effort (so the binary still works). When that chain silently fails, users end
|
||
up with half-upgraded brains. The self-repair block gives them a paste-ready
|
||
recovery path; the v0.13+ `~/.gbrain/upgrade-errors.jsonl` trail + `gbrain doctor`
|
||
integration close the loop.
|
||
|
||
Template (adapt the verify commands per release):
|
||
|
||
```markdown
|
||
## To take advantage of v[version]
|
||
|
||
`gbrain upgrade` should do this automatically. If it didn't, or if `gbrain doctor`
|
||
warns about a partial migration:
|
||
|
||
1. **Run the orchestrator manually:**
|
||
```bash
|
||
gbrain apply-migrations --yes
|
||
```
|
||
2. **Your agent reads `skills/migrations/v[version].md` the next time you interact with it.**
|
||
[One sentence on whether headless agents need manual action, or whether the
|
||
orchestrator already handled the mechanical side.]
|
||
3. **Verify the outcome:**
|
||
```bash
|
||
[release-specific verify commands, e.g. `gbrain graph ... --depth 2`]
|
||
gbrain stats
|
||
```
|
||
4. **If any step fails or the numbers look wrong,** please file an issue:
|
||
https://github.com/garrytan/gbrain/issues with:
|
||
- output of `gbrain doctor`
|
||
- contents of `~/.gbrain/upgrade-errors.jsonl` if it exists
|
||
- which step broke
|
||
|
||
This feedback loop is how the gbrain maintainers find fragile upgrade paths. Thank you.
|
||
```
|
||
|
||
**Skip this block** for patches that are pure bug fixes with zero user-facing action
|
||
(rare). If the release has a schema migration, data backfill, or new feature the
|
||
user needs to verify, the block is required.
|
||
|
||
The v0.13.0 entry in CHANGELOG.md is the canonical example.
|
||
|
||
### Itemized changes (the existing rules)
|
||
|
||
Below the release summary, write `### Itemized changes` and continue with the
|
||
detailed subsections (Knowledge Graph Layer, Schema migrations, Security hardening,
|
||
Tests, etc.). Same rules as before:
|
||
|
||
- Lead with what the user can now DO that they couldn't before
|
||
- Frame as benefits and capabilities, not files changed or code written
|
||
- Make the user think "hell yeah, I want that"
|
||
- Bad: "Added GBRAIN_VERIFY.md installation verification runbook"
|
||
- Good: "Your agent now verifies the entire GBrain installation end-to-end, catching
|
||
silent sync failures and stale embeddings before they bite you"
|
||
- Bad: "Setup skill Phase H and Phase I added"
|
||
- Good: "New installs automatically set up live sync so your brain never falls behind"
|
||
- **Always credit community contributions.** When a CHANGELOG entry includes work from
|
||
a community PR, name the contributor with `Contributed by @username`. Contributors
|
||
did real work. Thank them publicly every time, no exceptions.
|
||
|
||
### Reference: v0.12.0 entry as canonical example
|
||
|
||
The v0.12.0 entry in CHANGELOG.md is the canonical example of the format. Match its
|
||
structure for every future version: bold headline, lead paragraph, "numbers that
|
||
matter" with BrainBench-style before/after table, "what this means" closer, then
|
||
`### Itemized changes` with the detailed sections below.
|
||
|
||
## Version migrations
|
||
|
||
Create a migration file at `skills/migrations/v[version].md` when a release
|
||
includes changes that existing users need to act on. The auto-update agent
|
||
reads these files post-upgrade (Section 17, Step 4) and executes them.
|
||
|
||
**You need a migration file when:**
|
||
- New setup step that existing installs don't have (e.g., v0.5.0 added live sync,
|
||
existing users need to set it up, not just new installs)
|
||
- New SKILLPACK section with a MUST ADD setup requirement
|
||
- Schema changes that require `gbrain init` or manual SQL
|
||
- Changed defaults that affect existing behavior
|
||
- Deprecated commands or flags that need replacement
|
||
- New verification steps that should run on existing installs
|
||
- New cron jobs or background processes that should be registered
|
||
|
||
**You do NOT need a migration file when:**
|
||
- Bug fixes with no behavior changes
|
||
- Documentation-only improvements (the agent re-reads docs automatically)
|
||
- New optional features that don't affect existing setups
|
||
- Performance improvements that are transparent
|
||
|
||
**The key test:** if an existing user upgrades and does nothing else, will their
|
||
brain work worse than before? If yes, migration file. If no, skip it.
|
||
|
||
Write migration files as agent instructions, not technical notes. Tell the agent
|
||
what to do, step by step, with exact commands. See `skills/migrations/v0.5.0.md`
|
||
for the pattern.
|
||
|
||
## Migration is canonical, not advisory
|
||
|
||
GBrain's job is to deliver a canonical, working setup to every user on upgrade.
|
||
Anything that looks like a "host-repo change" — AGENTS.md, cron manifests,
|
||
launchctl units, config files outside `~/.gbrain/` — is a GBrain migration
|
||
step, not a nudge we leave for the host-repo maintainer. Migrations edit host
|
||
files (with backups) to make the canonical setup real. Exceptions: changes
|
||
that require human judgment (content edits, renames that break semantics,
|
||
host-specific handler registration where shell-exec would be an RCE surface).
|
||
Everything mechanical ships in the migration.
|
||
|
||
**Test:** if shipping a feature requires a sentence that starts with "in
|
||
your AGENTS.md, add…" or "in your cron/jobs.json, rewrite…", the migration
|
||
orchestrator should be doing that edit, not the user.
|
||
|
||
**The exception is host-specific code.** For custom Minion handlers
|
||
(host-specific integrations like inbox sweeps or third-party API scanners), shipping them as a
|
||
data file the worker would exec is an RCE surface. Those get registered in
|
||
the host's own repo via the plugin contract (`docs/guides/plugin-handlers.md`);
|
||
the migration orchestrator emits a structured TODO to
|
||
`~/.gbrain/migrations/pending-host-work.jsonl` + the host agent walks the
|
||
TODOs using `skills/migrations/v0.11.0.md` — stays host-agnostic, still
|
||
canonical.
|
||
|
||
## Privacy rule: scrub real names from public docs
|
||
|
||
**Never reference real people, companies, funds, or private agent names in any
|
||
public-facing artifact.** Public artifacts include: `CHANGELOG.md`, `README.md`,
|
||
`docs/`, `skills/`, PR titles + bodies, commit messages, and comments in checked-in
|
||
code. Query examples, benchmark stories, and migration guides MUST use generic
|
||
placeholders.
|
||
|
||
Why: gbrain runs a personal knowledge brain containing notes on real people and
|
||
real companies (YC founders, portfolio companies, funds, investors, meeting
|
||
attendees). When a doc copies a query like `gbrain graph diana-hu --depth 2` or
|
||
names a specific agent fork like `Wintermute`, that real name gets indexed by
|
||
search engines, surfaced in cross-references, and distributed with every release.
|
||
|
||
**Name mapping** to use in examples:
|
||
- Agent forks → `your agent fork`, `a downstream agent`, or `agent-fork`
|
||
- Example person → `alice-example`, `charlie-example`, or `a-founder`
|
||
- Example company → `acme-example`, `widget-co`, or `a-company`
|
||
- Example fund → `fund-a`, `fund-b`, `fund-c`
|
||
- Example deal → `acme-seed`, `widget-series-a`
|
||
- Example meeting → `meetings/2026-04-03` (generic date is fine)
|
||
- Example user → `you` or `the user`, never a proper name
|
||
|
||
**Specific rule: never say `Wintermute` in any CHANGELOG, README, doc, PR, or
|
||
commit message.** When the temptation is to illustrate with the real fork name:
|
||
- Reader-facing copy → `your OpenClaw` (covers Wintermute, Hermes, AlphaClaw,
|
||
and any other downstream OpenClaw deployment in one term the reader already
|
||
recognizes).
|
||
- First-person / origin-story copy → `Garry's OpenClaw` (honest that this is
|
||
the production deployment driving the feature, without exposing the private
|
||
agent's name).
|
||
|
||
`Wintermute` may appear in private artifacts (scratch plans under
|
||
`~/.gstack/projects/…`, memory files, conversation transcripts, CEO-review
|
||
plans) — those aren't distributed. Anything checked into this repo or shipped
|
||
in a release must use the OpenClaw phrasing above. Sweeping a stale reference
|
||
is a small clean-up PR, not a debate.
|
||
|
||
**When in doubt, ask yourself:** "Would this query reveal private information
|
||
about the user's contacts, investments, or portfolio if it were read by a
|
||
stranger?" If yes, replace with generic placeholders.
|
||
|
||
**Illustrative API examples with household-brand companies** (Stripe, Brex, OpenAI,
|
||
GitHub, etc.) are fine — they're public entities, not contacts in anyone's brain.
|
||
Do not confuse illustrative API examples with queries that reveal real
|
||
relationships.
|
||
|
||
## Responsible-disclosure rule: don't broadcast attack surface in release notes
|
||
|
||
**When a release fixes a security gap or a user-impacting bug, describe the fix
|
||
functionally. Do not enumerate the attack surface, quantify the exposure window,
|
||
or highlight the most sensitive records by name in public-facing artifacts.**
|
||
|
||
Public-facing artifacts include: `CHANGELOG.md`, `README.md`, `docs/`, PR titles
|
||
and bodies, commit messages, GitHub issue titles and comments, release pages,
|
||
tweets, blog posts.
|
||
|
||
**Don't write:**
|
||
- "10 tables were publicly readable by the anon key for months, including X, Y, Z"
|
||
- "X and Y are the most sensitive ones"
|
||
- "N tables exposed. Fix: enable RLS on these specific tables: ..."
|
||
|
||
**Do write:**
|
||
- "Security hardening pass. Fresh installs secure by default. Existing brains
|
||
brought to the same bar automatically on upgrade."
|
||
- "If `gbrain doctor` still flags anything after upgrade, the message names each
|
||
table and gives the exact fix."
|
||
|
||
Why: anyone reading the release page before they've upgraded now has a directed
|
||
probe list for unpatched installs. The source code ships the specifics anyway
|
||
(`src/schema.sql`, `src/core/migrate.ts`, test fixtures) — reverse engineers can
|
||
get them. But the release page is a broadcast channel. Don't hand attackers a
|
||
curated list with a banner.
|
||
|
||
**The test:** if a reader with no prior context could read the release note and
|
||
walk away knowing "gbrain at version X has table Y readable by anon key until
|
||
they patch," the note is too specific. Rewrite until that's no longer possible.
|
||
|
||
**What IS fine in public artifacts:**
|
||
- The mechanism of the fix ("the check now scans every public table instead of
|
||
a hardcoded allowlist").
|
||
- User-facing operator ergonomics (the escape-hatch SQL template, the upgrade
|
||
commands, the breaking-change flag).
|
||
- Credit to contributors.
|
||
- Generic framing of severity ("security posture tightening pass") without
|
||
quantification.
|
||
|
||
**What stays in private artifacts (plan files, private memories, internal docs):**
|
||
- Specific table names, record counts, exposure duration.
|
||
- Which records stand out as highest-risk.
|
||
- Detailed before/after tables in the "numbers that matter" format.
|
||
|
||
If the CEO/Eng review of a plan produces a detailed exposure table, keep it in
|
||
the plan file under `~/.claude/plans/` or `~/.gstack/projects/`. Don't copy it
|
||
into the CHANGELOG or PR body.
|
||
|
||
Applies retroactively: if you see a prior CHANGELOG entry naming attack-surface
|
||
specifics, scrub it as a small cleanup commit, the same way a stale Wintermute
|
||
reference gets swept.
|
||
|
||
## Schema state tracking
|
||
|
||
`~/.gbrain/update-state.json` tracks which recommended schema directories the user
|
||
adopted, declined, or added custom. The auto-update agent (SKILLPACK Section 17)
|
||
reads this during upgrades to suggest new schema additions without re-suggesting
|
||
things the user already declined. The setup skill writes the initial state during
|
||
Phase C/E. Never modify a user's custom directories or re-suggest declined ones.
|
||
|
||
## GitHub Actions SHA maintenance
|
||
|
||
All GitHub Actions in `.github/workflows/` are pinned to commit SHAs. Before shipping
|
||
(`/ship`) or reviewing (`/review`), check for stale pins and update them:
|
||
|
||
```bash
|
||
for action in actions/checkout oven-sh/setup-bun actions/upload-artifact actions/download-artifact softprops/action-gh-release gitleaks/gitleaks-action; do
|
||
tag=$(grep -r "$action@" .github/workflows/ | head -1 | grep -o '#.*' | tr -d '# ')
|
||
[ -n "$tag" ] && echo "$action@$tag: $(gh api repos/$action/git/ref/tags/$tag --jq .object.sha 2>/dev/null)"
|
||
done
|
||
```
|
||
|
||
If any SHA differs from what's in the workflow files, update the pin and version comment.
|
||
|
||
## PR descriptions cover the whole branch
|
||
|
||
Pull request titles and bodies must describe **everything in the PR diff against the
|
||
base branch**, not just the most recent commit you made. When you open or update a
|
||
PR, walk the full commit range with `git log --oneline <base>..<head>` and write the
|
||
body to cover all of it. Group by feature area (schema, code, tests, docs) — not
|
||
chronologically by commit.
|
||
|
||
This matters because reviewers read the PR body to understand what's shipping. If
|
||
the body only covers your last commit, they miss everything else and can't review
|
||
properly. A 7-commit PR with a body that describes commit 7 is worse than no body
|
||
at all — it actively misleads.
|
||
|
||
When in doubt, run `gh pr view <N> --json commits --jq '[.commits[].messageHeadline]'`
|
||
to see what's actually in the PR before writing the body.
|
||
|
||
## Community PR wave process
|
||
|
||
Never merge external PRs directly into master. Instead, use the "fix wave" workflow:
|
||
|
||
1. **Categorize** — group PRs by theme (bug fixes, features, infra, docs)
|
||
2. **Deduplicate** — if two PRs fix the same thing, pick the one that changes fewer
|
||
lines. Close the other with a note pointing to the winner.
|
||
3. **Collector branch** — create a feature branch (e.g. `garrytan/fix-wave-N`), cherry-pick
|
||
or manually re-implement the best fixes from each PR. Do NOT merge PR branches directly —
|
||
read the diff, understand the fix, and write it yourself if needed.
|
||
4. **Test the wave** — verify with `bun test && bun run test:e2e` (full E2E lifecycle).
|
||
Every fix in the wave must have test coverage.
|
||
5. **Close with context** — every closed PR gets a comment explaining why and what (if
|
||
anything) supersedes it. Contributors did real work; respect that with clear communication
|
||
and thank them.
|
||
6. **Ship as one PR** — single PR to master with all attributions preserved via
|
||
`Co-Authored-By:` trailers. Include a summary of what merged and what closed.
|
||
|
||
**Community PR guardrails:**
|
||
- Always AskUserQuestion before accepting commits that touch voice, tone, or
|
||
promotional material (README intro, CHANGELOG voice, skill templates).
|
||
- Never auto-merge PRs that remove YC references or "neutralize" the founder perspective.
|
||
- Preserve contributor attribution in commit messages.
|
||
|
||
## Checking out PRs from garrytan-agents
|
||
|
||
`garrytan-agents` is the AI-authored PR account and is NOT a collaborator on
|
||
this repo. Its PRs live in a fork, so GitHub Actions triggered by
|
||
`pull_request` events on those PRs do not receive base-repo secrets. Any CI
|
||
job that needs `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or similar will fail
|
||
with empty-env auth errors, regardless of what's set on the base repo. This
|
||
is a GitHub security default, not a config bug.
|
||
|
||
When the user says "check out <PR link>" and the PR is from `garrytan-agents`
|
||
(or any other non-collaborator fork), move the branch into the base repo
|
||
before running CI:
|
||
|
||
1. `gh pr checkout <N>` — pull down the fork's branch. Note the PR number and
|
||
head branch name (`gh pr view <N> --json headRefName --jq .headRefName`).
|
||
2. `git push origin HEAD:<branch-name>` — push the same branch to the base
|
||
repo (origin points at `garrytan/gbrain`, not the fork). This is the move
|
||
that gives CI access to secrets.
|
||
3. `gh pr close <N> --comment "moving to base-repo branch for secret access"`
|
||
— close the fork PR so the queue stays clean.
|
||
4. `gh pr create --base master --head <branch-name>` — open the replacement
|
||
PR from the base-repo branch. **Preserve the original PR's title and body
|
||
verbatim** (`gh pr view <N> --json title,body`); contributor attribution
|
||
moves to a `Co-Authored-By:` trailer if needed.
|
||
|
||
Why this over alternatives: adding `garrytan-agents` as a collaborator, or
|
||
flipping the repo-wide "send secrets to fork PRs" toggle, both broaden
|
||
secret distribution to every fork PR from that account or any fork. Moving
|
||
the branch keeps secret scope tight to just the one PR being shipped.
|
||
|
||
## Skill routing
|
||
|
||
When the user's request matches an available skill, ALWAYS invoke it using the Skill
|
||
tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.
|
||
The skill has specialized workflows that produce better results than ad-hoc answers.
|
||
|
||
**NEVER hand-roll ship operations.** Do not manually run git commit + push + gh pr
|
||
create when /ship is available. /ship handles VERSION bump, CHANGELOG, document-release,
|
||
pre-landing review, test coverage audit, and adversarial review. Manually creating a PR
|
||
skips all of these. If the user says "commit and ship", "push and ship", "bisect and
|
||
ship", or any combination that ends with shipping — invoke /ship and let it handle
|
||
everything including the commits. If the branch name contains a version (e.g.
|
||
`v0.5-live-sync`), /ship should use that version for the bump.
|
||
|
||
Key routing rules:
|
||
- Product ideas, "is this worth building", brainstorming → invoke office-hours
|
||
- Bugs, errors, "why is this broken", 500 errors → invoke investigate
|
||
- Ship, deploy, push, create PR, "commit and ship", "push and ship" → invoke ship
|
||
- QA, test the site, find bugs → invoke qa
|
||
- Code review, check my diff → invoke review
|
||
- Update docs after shipping → invoke document-release
|
||
- Weekly retro → invoke retro
|
||
- Design system, brand → invoke design-consultation
|
||
- Visual audit, design polish → invoke design-review
|
||
- Architecture review → invoke plan-eng-review
|
||
- Save progress, checkpoint, resume → invoke checkpoint
|
||
- Code quality, health check → invoke health
|
||
|
||
---
|
||
|
||
## INSTALL_FOR_AGENTS.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||
|
||
# GBrain Installation Guide for AI Agents
|
||
|
||
Read this entire file, then follow the steps. Ask the user for API keys when needed.
|
||
Target: ~30 minutes to a fully working brain.
|
||
|
||
## Step 0: If you are not Claude Code
|
||
|
||
Read `AGENTS.md` at the repo root first. It's the non-Claude-agent operating
|
||
protocol (install, read order, trust boundary, common tasks). Claude Code reads
|
||
`CLAUDE.md` automatically and can skip ahead.
|
||
|
||
If you fetched this file by URL without cloning yet, the companion files live at:
|
||
- `https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md` — start here
|
||
- `https://raw.githubusercontent.com/garrytan/gbrain/master/llms.txt` — full doc map
|
||
- `https://raw.githubusercontent.com/garrytan/gbrain/master/llms-full.txt` — same map, inlined
|
||
|
||
## Step 1: Install GBrain
|
||
|
||
Default path (Bun is required — gbrain is a Bun + TypeScript runtime):
|
||
|
||
```bash
|
||
curl -fsSL https://bun.sh/install | bash
|
||
export PATH="$HOME/.bun/bin:$PATH"
|
||
bun install -g github:garrytan/gbrain
|
||
```
|
||
|
||
Verify: `gbrain --version` should print a version number. If `gbrain` is not found,
|
||
restart the shell or add the PATH export to the shell profile.
|
||
|
||
> **If `bun install -g` aborts or `gbrain doctor` reports `schema_version: 0`** (Bun
|
||
> occasionally blocks the top-level postinstall hook on global installs, so schema
|
||
> migrations don't run automatically), the CLI prints a recovery hint pointing at
|
||
> [#218](https://github.com/garrytan/gbrain/issues/218). Run `gbrain apply-migrations --yes`
|
||
> to recover. If that doesn't work, fall back to the deterministic install path:
|
||
>
|
||
> ```bash
|
||
> git clone https://github.com/garrytan/gbrain.git ~/gbrain && cd ~/gbrain
|
||
> bun install && bun link
|
||
> ```
|
||
|
||
## Step 2: API Keys
|
||
|
||
Ask the user for these. gbrain defaults to the ZeroEntropy embedding + reranker stack
|
||
(as of v0.36.2.0); OpenAI/Voyage are still supported as fallbacks via `gbrain config
|
||
set embedding_model <provider:model>`.
|
||
|
||
```bash
|
||
export ZEROENTROPY_API_KEY=ze-... # default embedding + reranker (v0.36.2.0+)
|
||
export OPENAI_API_KEY=sk-... # fallback for vector search; also used for chat models
|
||
export ANTHROPIC_API_KEY=sk-ant-... # optional, improves search quality via query expansion
|
||
```
|
||
|
||
Save to shell profile or `.env`. Keys are picked up by `gbrain config set` automatically
|
||
or can be stored in `~/.gbrain/config.json` (file plane). Without any embedding provider,
|
||
keyword search still works. Without Anthropic, search works but skips query expansion.
|
||
|
||
## Step 3: Create the Brain
|
||
|
||
```bash
|
||
gbrain init # PGLite, no server needed
|
||
gbrain doctor --json # verify all checks pass
|
||
```
|
||
|
||
The user's markdown files (notes, docs, brain repo) are SEPARATE from this tool repo.
|
||
Ask the user where their files are, or create a new brain repo:
|
||
|
||
```bash
|
||
mkdir -p ~/brain && cd ~/brain && git init
|
||
```
|
||
|
||
Read `~/gbrain/docs/GBRAIN_RECOMMENDED_SCHEMA.md` and set up the MECE directory
|
||
structure (people/, companies/, concepts/, etc.) inside the user's brain repo,
|
||
NOT inside ~/gbrain.
|
||
|
||
## Step 3.5: Confirm search mode with the user (DO NOT SKIP)
|
||
|
||
`gbrain init` auto-applied a default search mode (`tokenmax` unless your subagent
|
||
tier is Haiku-class or no OpenAI key is configured). The init output included the
|
||
cost matrix below preceded by `[AGENT]` markers. You must NOT silently accept the
|
||
default. Stop and ask the operator.
|
||
|
||
**Present this matrix verbatim:**
|
||
|
||
```
|
||
Per-query cost @ 10K queries/mo (typical single-user volume):
|
||
|
||
Haiku 4.5 Sonnet 4.6 Opus 4.7
|
||
($1/M) ($3/M) ($5/M)
|
||
conservative $40/mo $120/mo $200/mo
|
||
balanced $100/mo $300/mo $500/mo
|
||
tokenmax $200/mo $600/mo $1,000/mo
|
||
|
||
(scales linearly: ×10 for 100K/mo, ÷10 for 1K. 25x corner-to-corner spread.
|
||
Natural diagonal pairings — cheap/cheap → frontier/frontier — span ~4x.)
|
||
```
|
||
|
||
**Ask the operator (paraphrase if needed):**
|
||
|
||
> Your gbrain just installed with search mode `<auto-applied default>`. This is
|
||
> a one-time setup decision that controls retrieval payload size. Which mode
|
||
> do you want?
|
||
>
|
||
> 1) conservative — tight 4K budget, no LLM expansion, 10 chunks max.
|
||
> Best for Haiku subagents, cost-sensitive setups, high-volume loops.
|
||
>
|
||
> 2) balanced — 12K budget, no expansion, 25 chunks. Sonnet-tier sweet spot.
|
||
>
|
||
> 3) tokenmax (recommended default — preserves v0.31.x retrieval shape) —
|
||
> no budget, LLM expansion ON, 50 chunks. Best for Opus/frontier models.
|
||
>
|
||
> Cost depends on BOTH the mode AND the downstream model you run. See the
|
||
> matrix above for the 9-cell breakdown.
|
||
|
||
If the operator picks a non-default mode, run:
|
||
```bash
|
||
gbrain config set search.mode <mode>
|
||
```
|
||
|
||
If they pick tokenmax AND want to preserve the literal v0.31.x default
|
||
(limit=20 instead of tokenmax's 50), also run:
|
||
```bash
|
||
gbrain config set search.searchLimit 20
|
||
```
|
||
|
||
Verify the choice with `gbrain search modes` before continuing.
|
||
|
||
**Why this matters:** the cost spread between corners of the matrix is 25x.
|
||
An agent that silently accepts the default and starts running queries against
|
||
a user who didn't expect tokenmax-class context loads can rack up surprise
|
||
spend. Confirm before continuing.
|
||
|
||
## Step 4: Import and Index
|
||
|
||
```bash
|
||
gbrain import ~/brain/ --no-embed # import markdown files
|
||
gbrain embed --stale # generate vector embeddings
|
||
gbrain query "key themes across these documents?"
|
||
```
|
||
|
||
## Step 4.5: Wire the Knowledge Graph
|
||
|
||
If the user already had a brain repo (Step 3 imported existing markdown), backfill
|
||
the typed-link graph and structured timeline. This populates the `links` and
|
||
`timeline_entries` tables that future writes will maintain automatically.
|
||
|
||
```bash
|
||
gbrain extract links --source db --dry-run | head -20 # preview
|
||
gbrain extract links --source db # commit
|
||
gbrain extract timeline --source db # dated events
|
||
gbrain stats # verify links > 0
|
||
```
|
||
|
||
For brand-new empty brains, skip this step — auto-link populates the graph as the
|
||
agent writes pages going forward. There is nothing to backfill yet.
|
||
|
||
After this step:
|
||
- `gbrain graph-query <slug> --depth 2` works (relationship traversal)
|
||
- Search ranks well-connected entities higher (backlink boost)
|
||
- Every future `put_page` auto-creates typed links and reconciles stale ones
|
||
|
||
If a user has a very large brain (>10K pages), `extract --source db` is idempotent
|
||
and supports `--since YYYY-MM-DD` for incremental runs.
|
||
|
||
## Step 5: Load Skills
|
||
|
||
If you're running an agent platform (OpenClaw, Hermes, or any repo with a workspace),
|
||
scaffold the bundled skills into it:
|
||
|
||
```bash
|
||
cd /path/to/agent/workspace
|
||
gbrain skillpack scaffold --all # copy 43 curated skills + RESOLVER.md
|
||
```
|
||
|
||
Scaffolded skills are first-class files in your repo. Edit freely; re-running scaffold
|
||
refuses to overwrite anything that exists. Use `gbrain skillpack reference <name>` to
|
||
diff against gbrain's bundle when you want upstream improvements. (The legacy
|
||
`gbrain skillpack install` managed-block model was retired in v0.36.0.0 — run
|
||
`gbrain skillpack migrate-fence` once if upgrading from an older release.)
|
||
|
||
Whether you scaffolded or not, read `skills/RESOLVER.md` (in your workspace, or the
|
||
bundled copy at `~/gbrain/skills/RESOLVER.md` when running from the cloned repo). It's
|
||
the skill dispatcher — tells you which skill to read for any task. Save this to your
|
||
memory permanently.
|
||
|
||
The three most important skills to adopt immediately:
|
||
|
||
1. **Signal detector** (`skills/signal-detector/SKILL.md`) — fire this on EVERY
|
||
inbound message. It captures ideas and entities in parallel. The brain compounds.
|
||
|
||
2. **Brain-ops** (`skills/brain-ops/SKILL.md`) — brain-first lookup on every response.
|
||
Check the brain before any external API call.
|
||
|
||
3. **Conventions** (`skills/conventions/quality.md`) — citation format, back-linking
|
||
iron law, source attribution. These are non-negotiable quality rules.
|
||
|
||
## Step 6: Identity (optional)
|
||
|
||
Run the soul-audit skill to customize the agent's identity:
|
||
|
||
```
|
||
Read skills/soul-audit/SKILL.md and follow it.
|
||
```
|
||
|
||
This generates SOUL.md (agent identity), USER.md (user profile), ACCESS_POLICY.md
|
||
(who sees what), and HEARTBEAT.md (operational cadence) from the user's answers.
|
||
|
||
If skipped, minimal defaults are installed automatically.
|
||
|
||
## Step 7: Recurring Jobs
|
||
|
||
Set up using your platform's scheduler (OpenClaw cron, Railway cron, crontab), or skip the
|
||
platform glue entirely with `gbrain autopilot --install` (built-in self-maintaining daemon):
|
||
|
||
- **Live sync** (every 15 min): `gbrain sync --repo ~/brain && gbrain embed --stale`
|
||
— or `gbrain sync --watch` for a continuous loop.
|
||
- **Auto-update** (daily): `gbrain check-update --json` (tell user, never auto-install).
|
||
- **Dream cycle** (nightly): `gbrain dream` runs the 8-phase overnight maintenance cycle.
|
||
Entity sweep, citation fixes, memory consolidation, plus (v0.23+) overnight conversation
|
||
synthesis and cross-session pattern detection. One cron-friendly command. This is what
|
||
makes the brain compound. Do not skip it. See `docs/guides/cron-schedule.md` for the
|
||
full protocol.
|
||
- **Weekly**: `gbrain doctor --json && gbrain embed --stale`
|
||
|
||
## Step 8: Integrations
|
||
|
||
Run `gbrain integrations list`. Each recipe in `~/gbrain/recipes/` is a self-contained
|
||
installer. It tells you what credentials to ask for, how to validate, and what cron
|
||
to register. Ask the user which integrations they want (email, calendar, voice, Twitter).
|
||
|
||
Verify: `gbrain integrations doctor` (after at least one is configured)
|
||
|
||
## Step 9: Verify
|
||
|
||
Read `docs/GBRAIN_VERIFY.md` and run all 7 verification checks. Check #4 (live sync
|
||
actually works) is the most important.
|
||
|
||
## Upgrade
|
||
|
||
If you installed via `bun install -g`:
|
||
|
||
```bash
|
||
gbrain upgrade # self-updates the binary, runs schema migrations,
|
||
# and prints post-upgrade notes for the version range
|
||
```
|
||
|
||
If you installed via `git clone + bun link`:
|
||
|
||
```bash
|
||
cd ~/gbrain && git pull origin master && bun install
|
||
gbrain apply-migrations --yes # apply schema migrations (idempotent)
|
||
gbrain post-upgrade # show migration notes for the version range
|
||
```
|
||
|
||
Then read `~/gbrain/skills/migrations/v<NEW_VERSION>.md` (and any intermediate
|
||
versions you skipped) and run any backfill or verification steps it lists. Skipping
|
||
this is how features ship in the binary but stay dormant in the user's brain.
|
||
|
||
**v0.32.3 search modes (one-time upgrade prompt):** if the user's brain was
|
||
created before v0.32.3, `gbrain post-upgrade` prints a banner including the
|
||
9-cell cost matrix (mode × downstream model) preceded by `[AGENT]` markers.
|
||
**Do NOT silently move past the banner.** Present the matrix to the operator
|
||
verbatim, ask which mode they want (recommended default: `tokenmax` to preserve
|
||
v0.31.x retrieval shape), then run `gbrain config set search.mode <mode>`. See
|
||
Step 3.5 above for the full ask-the-user protocol — the upgrade path uses the
|
||
same matrix and same default.
|
||
|
||
For v0.12.0+ specifically: if your brain was created before v0.12.0, run
|
||
`gbrain extract links --source db && gbrain extract timeline --source db` to
|
||
backfill the new graph layer (see Step 4.5 above).
|
||
|
||
For v0.12.2+ specifically: if your brain is Postgres- or Supabase-backed and
|
||
predates v0.12.2, the `v0_12_2` migration runs `gbrain repair-jsonb`
|
||
automatically during `gbrain post-upgrade` to fix the double-encoded JSONB
|
||
columns. PGLite brains no-op. If wiki-style imports were truncated by the old
|
||
`splitBody` bug, run `gbrain sync --full` after upgrading to rebuild
|
||
`compiled_truth` from source markdown.
|
||
|
||
---
|
||
|
||
## skills/RESOLVER.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/skills/RESOLVER.md
|
||
|
||
# GBrain Skill Resolver
|
||
|
||
This is the dispatcher. Skills are the implementation. **Read the skill file before acting.** If two skills could match, read both. They are designed to chain (e.g., ingest then enrich for each entity).
|
||
|
||
## Always-on (every message)
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| Every inbound message (spawn parallel, don't block) | `skills/signal-detector/SKILL.md` |
|
||
| Any brain read/write/lookup/citation | `skills/brain-ops/SKILL.md` |
|
||
|
||
## Brain operations
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| "What do we know about", "tell me about", "search for", "who is", "background on", "notes on" | `skills/query/SKILL.md` |
|
||
| "Who knows who", "relationship between", "connections", "graph query" | `skills/query/SKILL.md` (use graph-query) |
|
||
| Creating/enriching a person or company page | `skills/enrich/SKILL.md` |
|
||
| Where does a new file go? Filing rules | `skills/repo-architecture/SKILL.md` |
|
||
| Fix broken citations in brain pages | `skills/citation-fixer/SKILL.md` |
|
||
| "citation audit", "check citations", "fix citations" | `skills/citation-fixer/SKILL.md` (focused fix). For broader brain health, chain into `skills/maintain/SKILL.md` |
|
||
| "Research", "track", "extract from email", "investor updates", "donations" | `skills/data-research/SKILL.md` |
|
||
| Share a brain page as a link | `skills/publish/SKILL.md` |
|
||
| "validate frontmatter", "check frontmatter", "fix frontmatter", "frontmatter audit", "brain lint" | `skills/frontmatter-guard/SKILL.md` |
|
||
| "what search mode", "is my cache hot", "tune my retrieval", "compare search modes", "clear search overrides" | `gbrain search modes/stats/tune` directly. See `skills/conventions/search-modes.md` |
|
||
| "eval results", "search benchmark", "haters-immune methodology", "regression check on retrieval" | `gbrain eval run-all` / `gbrain eval compare`. See `docs/eval/SEARCH_MODE_METHODOLOGY.md` |
|
||
|
||
## Content & media ingestion
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| User shares a link, article, tweet, or idea | `skills/idea-ingest/SKILL.md` |
|
||
| "watch this video", "process this YouTube link", "ingest this PDF", "save this podcast", "process this book", "summarize this book", "PDF book", "ingest it into my brain", "what's in this screenshot", "check out this repo" | `skills/media-ingest/SKILL.md` |
|
||
| Meeting transcript received | `skills/meeting-ingestion/SKILL.md` |
|
||
| Generic "ingest this" (auto-routes to above) | `skills/ingest/SKILL.md` |
|
||
|
||
## Thinking skills (from GStack)
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| "Brainstorm", "I have an idea", "office hours" | GStack: office-hours |
|
||
| "Review this plan", "CEO review", "poke holes" | GStack: ceo-review |
|
||
| "Debug", "fix", "broken", "investigate" | GStack: investigate |
|
||
| "Retro", "what shipped", "retrospective" | GStack: retro |
|
||
|
||
> These skills come from GStack. If GStack is installed, the agent reads them directly.
|
||
> If not, brain-only mode still works (brain skills function without thinking skills).
|
||
|
||
## Operational
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| Task add/remove/complete/defer/review | `skills/daily-task-manager/SKILL.md` |
|
||
| Morning prep, meeting context, day planning | `skills/daily-task-prep/SKILL.md` |
|
||
| Daily briefing, "what's happening today" | `skills/briefing/SKILL.md` |
|
||
| Cron scheduling, quiet hours, job staggering | `skills/cron-scheduler/SKILL.md` |
|
||
| Save or load reports | `skills/reports/SKILL.md` |
|
||
| "Create a skill", "improve this skill" | `skills/skill-creator/SKILL.md` |
|
||
| "Skillify this", "is this a skill?", "make this proper" | `skills/skillify/SKILL.md` |
|
||
| "Compress my resolver", "AGENTS.md too large", "RESOLVER.md too big", "functional area dispatcher", "shrink routing table" | `skills/functional-area-resolver/SKILL.md` |
|
||
| "Is gbrain healthy?", morning health check, skillpack-check | `skills/skillpack-check/SKILL.md` |
|
||
| "harvest this skill into gbrain", "publish this skill to gbrain", "lift this skill upstream", "share this skill with other gbrain clients", "promote my skill to gbrain" | `skills/skillpack-harvest/SKILL.md` |
|
||
| Post-restart health + auto-fix, "did the container restart break anything", smoke test | `skills/smoke-test/SKILL.md` |
|
||
| Cross-modal review, second opinion | `skills/cross-modal-review/SKILL.md` |
|
||
| "Validate skills", skill health check | `skills/testing/SKILL.md` |
|
||
| Webhook setup, external event processing | `skills/webhook-transforms/SKILL.md` |
|
||
| "Spawn agent", "background task", "parallel tasks", "steer agent", "pause/resume agent", "gbrain jobs submit", "submit a gbrain job", "submit a shell job", "shell job" | `skills/minion-orchestrator/SKILL.md` |
|
||
| "present options", "ask before proceeding", "choice gate", "user decision" | `skills/ask-user/SKILL.md` |
|
||
|
||
## Setup & migration
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| "Set up GBrain", first boot | `skills/setup/SKILL.md` |
|
||
| "Now what?", "fill my brain", "cold start", "bootstrap", "import my data", "what should I import first" | `skills/cold-start/SKILL.md` |
|
||
| "Migrate from Obsidian/Notion/Logseq" | `skills/migrate/SKILL.md` |
|
||
| Brain health check, maintenance run | `skills/maintain/SKILL.md` |
|
||
| "Extract links", "build link graph", "populate timeline" | `skills/maintain/SKILL.md` (extraction sections) |
|
||
| "Run dream", "process today's session", "synthesize my conversations", "consolidate yesterday's conversations", "what patterns did you see", "did the dream cycle run" | `skills/maintain/SKILL.md` (dream cycle section) |
|
||
| "Brain health", "what features am I missing", "brain score" | Run `gbrain features --json` |
|
||
| "Set up autopilot", "run brain maintenance", "keep brain updated" | Run `gbrain autopilot --install --repo ~/brain` |
|
||
| Agent identity, "who am I", customize agent | `skills/soul-audit/SKILL.md` |
|
||
| "Populate links", "extract links", "backfill graph" | `skills/maintain/SKILL.md` (graph population phase) |
|
||
| "Populate timeline", "extract timeline entries" | `skills/maintain/SKILL.md` (graph population phase) |
|
||
|
||
## Identity & access (always-on)
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| Non-owner sends a message | Check `ACCESS_POLICY.md` before responding |
|
||
| Agent needs to know its identity/vibe | Read `SOUL.md` |
|
||
| Agent needs user context | Read `USER.md` |
|
||
| Operational cadence (what to check and when) | Read `HEARTBEAT.md` |
|
||
|
||
## Disambiguation rules
|
||
|
||
When multiple skills could match:
|
||
1. Prefer the most specific skill (meeting-ingestion over ingest)
|
||
2. If the user mentions a URL, route by content type (link → idea-ingest, video → media-ingest)
|
||
3. If the user mentions a person/company, check if enrich or query fits better
|
||
4. Chaining is explicit in each skill's Phases section
|
||
5. When in doubt, ask the user (see `skills/ask-user/SKILL.md` for the choice-gate pattern)
|
||
|
||
## Conventions (cross-cutting)
|
||
|
||
These apply to ALL brain-writing skills:
|
||
- `skills/conventions/quality.md` — citations, back-links, notability gate
|
||
- `skills/conventions/brain-first.md` — check brain before external APIs
|
||
- `skills/conventions/brain-routing.md` — which brain (DB) and which source (repo) to target; cross-brain federation is latent-space only
|
||
- `skills/conventions/subagent-routing.md` — when to use Minions vs inline work
|
||
- `skills/ask-user/SKILL.md` — choice-gate pattern for human input at decision points
|
||
- `skills/_brain-filing-rules.md` — where files go
|
||
- `skills/_output-rules.md` — output quality standards
|
||
|
||
## Uncategorized
|
||
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| "personalized version of this book", "mirror this book", "two-column book analysis", "apply this book to my life", "how does this book apply to me" | `skills/book-mirror/SKILL.md` |
|
||
| "enrich this article", "enrich brain pages", "batch enrich", "make brain pages useful" | `skills/article-enrichment/SKILL.md` |
|
||
| "strategic reading", "read this through the lens of", "apply this to my problem", "what can I learn from this about", "extract a playbook from" | `skills/strategic-reading/SKILL.md` |
|
||
| "concept synthesis", "synthesize my concepts", "find patterns across my notes", "build my intellectual map", "trace idea evolution" | `skills/concept-synthesis/SKILL.md` |
|
||
| "perplexity research", "what's new about", "current state of", "web research", "what changed about" | `skills/perplexity-research/SKILL.md` |
|
||
| "crawl my archive", "find gold in my archive", "archive crawler", "scan my dropbox for", "mine my old files for" | `skills/archive-crawler/SKILL.md` |
|
||
| "verify this academic claim", "check this study", "academic verify", "validate citation", "is this study real" | `skills/academic-verify/SKILL.md` |
|
||
| "make pdf from brain", "brain pdf", "convert brain page to pdf", "publish this page as pdf", "export brain page" | `skills/brain-pdf/SKILL.md` |
|
||
| "voice note", "ingest this voice memo", "transcribe and file", "voice note ingest", "save this audio note" | `skills/voice-note-ingest/SKILL.md` |
|
||
|
||
---
|
||
|
||
## README.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/README.md
|
||
|
||
# GBrain
|
||
|
||
Your AI agent is smart but forgetful. GBrain gives it a brain.
|
||
|
||
Built by the President and CEO of Y Combinator to run his actual AI agents. The production brain behind his OpenClaw and Hermes deployments: **17,888 pages, 4,383 people, 723 companies**, 21 cron jobs running autonomously, built in 12 days. The agent ingests meetings, emails, tweets, voice calls, and original ideas while you sleep. It enriches every person and company it encounters. It fixes its own citations and consolidates memory overnight. You wake up smarter than when you went to bed.
|
||
|
||
The brain wires itself. Every page write extracts entity references and creates typed links (`attended`, `works_at`, `invested_in`, `founded`, `advises`) with zero LLM calls. Hybrid search. Self-wiring knowledge graph. Structured timeline. Backlink-boosted ranking. Ask "who works at Acme AI?" or "what did Bob invest in this quarter?" and get answers vector search alone can't reach. Benchmarked side-by-side: gbrain lands **P@5 49.1%, R@5 97.9%** on a 240-page Opus-generated rich-prose corpus, beating its graph-disabled variant by **+31.4 points P@5** and ripgrep-BM25 + vector-only RAG by a similar margin. Full BrainBench scorecards live in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo.
|
||
|
||
**New default in v0.36.2.0: ZeroEntropy** for both embedding (`zembed-1` at 1280d via Matryoshka) and reranker (`zerank-2`). On a real-corpus benchmark vs OpenAI and Voyage: **2.2× faster** (442ms vs OpenAI 973ms), **2.6× cheaper at regular pricing** ($0.05/M vs OpenAI $0.13), wins 11 of 20 queries head-to-head, reshuffles 60% of top-1 results when used as a second-pass reranker. Bring your own key from [zeroentropy.dev](https://dashboard.zeroentropy.dev), or stay on OpenAI/Voyage via `gbrain config set embedding_model <provider:model>` — your choice is sticky.
|
||
|
||
GBrain is those patterns, generalized. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
|
||
|
||
**New in v0.36.4.0 — Your agent drives the brain to 90/100 by itself.** One command does the loop you used to run by hand: `gbrain doctor --remediate --yes --target-score 90 --max-usd 5`. It computes a dependency-ordered plan (sync before extract, embed after consolidate), submits each step as a Minion job, re-checks score between every step, and refuses to spend past your cost cap. Cron can drive it unattended. `gbrain doctor --remediation-plan --json` previews what would run. Autopilot now does the same thing on its 5-minute tick: small problems get targeted handlers, big problems get the full cycle, a healthy brain sleeps for 60 minutes instead of grinding through synthesize+patterns+embed every tick. Eleven new things you can submit as background jobs (`reindex`, `repair-jsonb`, `orphans`, `integrity`, `purge`, plus six cycle phases); three of them (synthesize, patterns, consolidate) are PROTECTED so an MCP-connected agent can't silently burn Anthropic credits. New `--background` flag on `gbrain embed` submits the job and exits with `job_id=N` for shell composition.
|
||
|
||
**New in v0.35.7 — Temporal trajectory + founder scorecard.** Author typed metric assertions in the `## Facts` fence (`mrr=50000`, `arr=2000000`, `team_size=12`) and gbrain stores them as first-class typed columns. `gbrain eval trajectory companies/acme-example` prints the chronological history with regressions auto-flagged inline. `gbrain founder scorecard companies/acme-example` rolls up claim accuracy, consistency, growth direction, and red flags into a stable `schema_version: 1` JSON contract. New MCP op `find_trajectory` exposes the same data to agents (read scope, visibility-filtered for remote callers). The `consolidate` cycle phase now writes `valid_until` on chronologically-superseded facts AND uses semantic upsert on `(page_id, claim, since_date)` — re-running the dream cycle on stable input is now a true no-op (fixed a pre-existing duplicate-takes bug from prior versions).
|
||
|
||
> **~30 minutes to a fully working brain.** Database ready in 2 seconds (PGLite, no server). You just answer questions about API keys.
|
||
|
||
> **LLMs:** fetch [`llms.txt`](llms.txt) for the documentation map, or [`llms-full.txt`](llms-full.txt) for the same map with core docs inlined in one fetch. **Agents:** start with [`AGENTS.md`](AGENTS.md) (or [`CLAUDE.md`](CLAUDE.md) if you're Claude Code).
|
||
|
||
## Install
|
||
|
||
GBrain runs in three shapes. Pick the one that matches how you use AI agents today.
|
||
|
||
### Run with your agent platform
|
||
|
||
Already using [OpenClaw](https://github.com/garrytan/openclaw) or [Hermes](https://github.com/garrytan/hermes)? GBrain installs as a skillpack scaffold into your agent's workspace.
|
||
|
||
```bash
|
||
gbrain init --pglite
|
||
gbrain skillpack scaffold --all # or: scaffold <name> per skill
|
||
```
|
||
|
||
That's it. Your agent picks up 43 skills (signal detection, brain-ops, ingest, enrich, citation-fixer, daily-task-manager, cron-scheduler, eval framework, and 35 more). Routing lives in `skills/RESOLVER.md` — the agent reads it once per request, picks the right skill, executes. Scaffolded skills are first-class members of your agent repo — you own them, edit freely; `gbrain skillpack reference <name>` diffs your copy against gbrain's bundle when you want to pull upstream improvements. (The legacy `gbrain skillpack install` managed-block model was retired in v0.36.0.0; run `gbrain skillpack migrate-fence` once if you're upgrading from an older release.)
|
||
|
||
### CLI standalone
|
||
|
||
Use gbrain from any shell, no agent platform required.
|
||
|
||
```bash
|
||
bun install -g github:garrytan/gbrain
|
||
gbrain init --pglite # 2 seconds; no server, no Docker
|
||
gbrain doctor # verify health
|
||
```
|
||
|
||
Then point any MCP-aware client (Claude Code, Cursor, Windsurf) at it, or use it from your shell:
|
||
|
||
```bash
|
||
gbrain search "who works at acme AI?"
|
||
gbrain query "what did bob invest in this quarter?"
|
||
gbrain graph-query people/garry-tan --depth 2
|
||
```
|
||
|
||
Detailed setup paths (Postgres at scale, Supabase, thin-client mode) live in [`docs/INSTALL.md`](docs/INSTALL.md).
|
||
|
||
### MCP server (any MCP client)
|
||
|
||
```bash
|
||
gbrain serve # stdio MCP (Claude Desktop / Code / Cursor)
|
||
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard
|
||
# at /admin, SSE activity feed at /admin/events
|
||
```
|
||
|
||
Per-client guides (Claude Desktop, Code, Cursor, ChatGPT, Perplexity, Cowork) live under [`docs/mcp/`](docs/mcp/). HTTP server supports DCR-style client registration, scope-gated access (`read`/`write`/`admin`), and built-in rate limiting.
|
||
|
||
## What it does (the loop)
|
||
|
||
```
|
||
signal → search → respond → write → auto-link → sync
|
||
(every (brain-first (informed (page + (typed edges (cron
|
||
message) retrieval) by context) timeline) + backlinks) keeps fresh)
|
||
```
|
||
|
||
- **Signal detector** runs on every message your agent receives. Captures ideas, entity mentions, time-sensitive todos, names, links.
|
||
- **Brain-first lookup** before any external API call. The cheapest, fastest, most personal information source you have.
|
||
- **Auto-link** fires on every page write. No LLM calls; pure pattern matching on `[[wiki/people/bob]]` style references. New entity → new page stub → graph grows.
|
||
- **Cron-driven enrichment** runs while you sleep: dedup people pages, fix citations, score salience, find contradictions, prep tomorrow's tasks.
|
||
|
||
The whole loop is described in [`docs/architecture/topologies.md`](docs/architecture/topologies.md) with diagrams.
|
||
|
||
## Capabilities
|
||
|
||
**Hybrid search.** Vector (HNSW on pgvector) + BM25 keyword + reciprocal-rank fusion + source-tier boost + intent-aware query rewriting. Three named search modes (`conservative`, `balanced`, `tokenmax`) bundle the cost/quality knobs into a single config key. Live cost/recall comparisons in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md). Default: `balanced` with ZeroEntropy reranker on.
|
||
|
||
**Self-wiring knowledge graph.** Every `put_page` extracts entity refs from markdown/wikilinks/typed-link syntax and writes edges with zero LLM calls. Typed edges (`attended`, `works_at`, `invested_in`, `founded`, `advises`, `mentions`, …). Multi-hop traversal via `gbrain graph-query`. The graph is what produces the +31.4 P@5 lift over vector-only RAG.
|
||
|
||
**Job queue (Minions).** BullMQ-shaped, Postgres-native job queue. Durable subagents (LLM tool loops that survive crashes via two-phase pending→done persistence), shell jobs with audit, child jobs with cascading timeouts, rate leases for outbound providers, attachments via S3/Supabase storage. Replaces "spawn subagent as fire-and-forget Promise" with something that recovers from anything.
|
||
|
||
**43 curated skills.** Routing lives in [`skills/RESOLVER.md`](skills/RESOLVER.md). Covers signal capture, ingest (idea / media / meeting), enrichment, querying, brain ops, citation fixing, daily task management, cron scheduling, reports, voice, soul audit, skill creation, eval framework, and migrations. Skills are markdown files (tool-agnostic), packaged as a single skillpack the installer drops into your agent workspace.
|
||
|
||
**Eval framework.** `gbrain eval longmemeval` runs the public [LongMemEval](https://huggingface.co/datasets/xiaowu0162/longmemeval) benchmark against your hybrid retrieval. `gbrain eval export` + `gbrain eval replay` capture real queries and replay them against code changes (set `GBRAIN_CONTRIBUTOR_MODE=1`). `gbrain eval cross-modal` cross-checks an output against the task using three different-provider frontier models. Full methodology in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md).
|
||
|
||
**Brain consistency.** `gbrain eval suspected-contradictions` samples retrieval pairs, layered date pre-filter, query-conditioned LLM judge, persistent cache. Surfaces conflicts between takes + facts the agent has written. Wired into the daily dream cycle.
|
||
|
||
## Integrations
|
||
|
||
Data flowing into the brain. Each integration is a recipe — markdown + setup hints — that ships in `recipes/` and is discoverable via `gbrain integrations list`.
|
||
|
||
- **Voice**: Phone calls create brain pages via Twilio + OpenAI Realtime (or DIY STT+LLM+TTS). Setup recipe: [`recipes/twilio-voice-brain.md`](recipes/twilio-voice-brain.md).
|
||
- **Email + calendar**: webhook handlers that route to brain signals. [`docs/integrations/meeting-webhooks.md`](docs/integrations/meeting-webhooks.md).
|
||
- **Embedding providers**: 14 recipes covering OpenAI (default fallback), Voyage, ZeroEntropy (default), Google Gemini, Azure OpenAI, MiniMax, Alibaba DashScope, Zhipu, Ollama (local), llama.cpp llama-server (local), LiteLLM proxy. Pricing matrix + decision tree in [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md).
|
||
- **Credential gateway**: vault-aware secret distribution. [`docs/integrations/credential-gateway.md`](docs/integrations/credential-gateway.md).
|
||
- **MCP clients**: every major MCP client is supported. [`docs/mcp/`](docs/mcp/) per-client setup.
|
||
|
||
## Architecture
|
||
|
||
**Two engines, one contract.** PGLite (Postgres 17 via WASM, zero-config, default) for personal brains up to ~50K pages. Postgres + pgvector (Supabase or self-hosted) for shared / large / multi-machine deployments. The contract-first `BrainEngine` interface in [`src/core/engine.ts`](src/core/engine.ts) defines ~47 operations both engines implement; CLI and MCP server are generated from one source.
|
||
|
||
**Brain repo is the system of record.** Your knowledge lives in a regular git repo (your "brain repo") as markdown files. GBrain syncs the repo into Postgres for retrieval; deletes in git become soft-deletes in DB. You can publish public subsets, share team mounts, run thin-client setups pointing at a colleague's brain server. Topologies in [`docs/architecture/topologies.md`](docs/architecture/topologies.md).
|
||
|
||
**Two organizational axes (brain ⊥ source).** A *brain* is a database (your personal brain, a team mount you joined). A *source* is a repo inside that brain (wiki, gstack, an essay, a knowledge base). Routing lives in `.gbrain-source` dotfiles and resolves via a documented 6-tier precedence chain. Full diagrams in [`docs/architecture/brains-and-sources.md`](docs/architecture/brains-and-sources.md).
|
||
|
||
**Why the graph matters.** Vector search returns chunks that are semantically close. The graph returns chunks that are factually connected. Hybrid search pulls from both; auto-linking on every write keeps the graph fresh. Deep dive: [`docs/architecture/RETRIEVAL.md`](docs/architecture/RETRIEVAL.md).
|
||
|
||
## Docs
|
||
|
||
- [`docs/INSTALL.md`](docs/INSTALL.md) — every install path, end to end
|
||
- [`docs/architecture/`](docs/architecture/) — system design, topologies, retrieval theory
|
||
- [`docs/guides/`](docs/guides/) — how-to runbooks (sub-agent routing, minion deployment, skill development, brain-first lookup, idea capture, diligence ingestion)
|
||
- [`docs/integrations/`](docs/integrations/) — connecting external data sources (voice, email, calendar, embedding providers)
|
||
- [`docs/mcp/`](docs/mcp/) — per-client MCP setup (Claude Desktop, Code, Cursor, ChatGPT, Perplexity, Cowork)
|
||
- [`docs/eval/`](docs/eval/) — eval framework, metric glossary, methodology
|
||
- [`docs/ethos/`](docs/ethos/) — philosophy (thin harness, fat skills, markdown as recipes, origin story)
|
||
- [`AGENTS.md`](AGENTS.md) — entry point for non-Claude agents
|
||
- [`CLAUDE.md`](CLAUDE.md) — entry point for Claude Code (deep operating context)
|
||
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — contributor guide, test discipline, eval-capture mode
|
||
- [`SECURITY.md`](SECURITY.md) — OAuth threat model, hardening defaults
|
||
|
||
## Contributing
|
||
|
||
Run `bun run test` for the fast loop, `bun run verify` for the pre-push gate, `bun run ci:local` to run the full Docker-backed CI stack locally. Detailed test discipline in [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||
|
||
Community PRs are batched into release waves rather than merged one-by-one — see the "PR wave workflow" section in [`CLAUDE.md`](CLAUDE.md). Contributor attribution stays attached via `Co-Authored-By:` trailers. We credit every accepted contribution in [`CHANGELOG.md`](CHANGELOG.md).
|
||
|
||
If you find a bug or want a feature: open an issue first. Quick fixes (typo, doc bug, obvious regression) can go straight to a PR. Anything touching schema, retrieval ranking, MCP protocol, or the security boundary needs a design discussion in the issue first.
|
||
|
||
## License + credit
|
||
|
||
MIT. Built by Garry Tan to run his OpenClaw and Hermes deployments — the production brain behind his actual AI agents.
|
||
|
||
Origin story: [`docs/ethos/ORIGIN.md`](docs/ethos/ORIGIN.md).
|
||
|
||
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that became the v0.36.2.0 default. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
|
||
|
||
---
|
||
|
||
# Configuration
|
||
|
||
## docs/ENGINES.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ENGINES.md
|
||
|
||
# Pluggable Engine Architecture
|
||
|
||
## The idea
|
||
|
||
Every GBrain operation goes through `BrainEngine`. The engine is the contract between "what the brain can do" and "how it's stored." Swap the engine, keep everything else.
|
||
|
||
v0 shipped `PostgresEngine` backed by Supabase. v0.7 adds `PGLiteEngine` -- embedded Postgres 17.5 via WASM (@electric-sql/pglite), zero-config default. The interface is designed so a `DuckDBEngine`, `TursoEngine`, or any custom backend could slot in without touching the CLI, MCP server, skills, or any consumer code.
|
||
|
||
## Why this matters
|
||
|
||
Different users have different constraints:
|
||
|
||
| User | Needs | Best engine |
|
||
|------|-------|-------------|
|
||
| Getting started | Zero-config, no accounts, no server | PGLiteEngine (default since v0.7) |
|
||
| Power user (you) | World-class search, 7K+ pages, zero-ops | PostgresEngine + Supabase |
|
||
| Open source hacker | Single file, no server, git-friendly | PGLiteEngine |
|
||
| Team/enterprise | Multi-user, RLS, audit trail | PostgresEngine + self-hosted |
|
||
| Researcher | Analytics, bulk exports, embeddings | DuckDBEngine (someday) |
|
||
| Edge/mobile | Offline-first, sync later | PGLiteEngine + sync (someday) |
|
||
|
||
The engine interface means we don't have to choose. PGLite is the zero-friction default. Supabase is the production scale path. `gbrain migrate --to supabase/pglite` moves between them.
|
||
|
||
## The interface
|
||
|
||
```typescript
|
||
// src/core/engine.ts
|
||
|
||
export interface BrainEngine {
|
||
// Lifecycle
|
||
connect(config: EngineConfig): Promise<void>;
|
||
disconnect(): Promise<void>;
|
||
initSchema(): Promise<void>;
|
||
transaction<T>(fn: (engine: BrainEngine) => Promise<T>): Promise<T>;
|
||
|
||
// Pages CRUD
|
||
getPage(slug: string): Promise<Page | null>;
|
||
putPage(slug: string, page: PageInput): Promise<Page>;
|
||
deletePage(slug: string): Promise<void>;
|
||
listPages(filters: PageFilters): Promise<Page[]>;
|
||
|
||
// Search
|
||
searchKeyword(query: string, opts?: SearchOpts): Promise<SearchResult[]>;
|
||
searchVector(embedding: Float32Array, opts?: SearchOpts): Promise<SearchResult[]>;
|
||
|
||
// Chunks
|
||
upsertChunks(slug: string, chunks: ChunkInput[]): Promise<void>;
|
||
getChunks(slug: string): Promise<Chunk[]>;
|
||
|
||
// Links
|
||
addLink(from: string, to: string, context?: string, linkType?: string): Promise<void>;
|
||
removeLink(from: string, to: string): Promise<void>;
|
||
getLinks(slug: string): Promise<Link[]>;
|
||
getBacklinks(slug: string): Promise<Link[]>;
|
||
traverseGraph(slug: string, depth?: number): Promise<GraphNode[]>;
|
||
|
||
// Tags
|
||
addTag(slug: string, tag: string): Promise<void>;
|
||
removeTag(slug: string, tag: string): Promise<void>;
|
||
getTags(slug: string): Promise<string[]>;
|
||
|
||
// Timeline
|
||
addTimelineEntry(slug: string, entry: TimelineInput): Promise<void>;
|
||
getTimeline(slug: string, opts?: TimelineOpts): Promise<TimelineEntry[]>;
|
||
|
||
// Raw data
|
||
putRawData(slug: string, source: string, data: object): Promise<void>;
|
||
getRawData(slug: string, source?: string): Promise<RawData[]>;
|
||
|
||
// Versions
|
||
createVersion(slug: string): Promise<PageVersion>;
|
||
getVersions(slug: string): Promise<PageVersion[]>;
|
||
revertToVersion(slug: string, versionId: number): Promise<void>;
|
||
|
||
// Stats + health
|
||
getStats(): Promise<BrainStats>;
|
||
getHealth(): Promise<BrainHealth>;
|
||
|
||
// Ingest log
|
||
logIngest(entry: IngestLogInput): Promise<void>;
|
||
getIngestLog(opts?: IngestLogOpts): Promise<IngestLogEntry[]>;
|
||
|
||
// Config
|
||
getConfig(key: string): Promise<string | null>;
|
||
setConfig(key: string, value: string): Promise<void>;
|
||
|
||
// Migration + advanced (added v0.7)
|
||
runMigration(sql: string): Promise<void>;
|
||
getChunksWithEmbeddings(slug: string): Promise<ChunkWithEmbedding[]>;
|
||
}
|
||
```
|
||
|
||
### Key design choices
|
||
|
||
**Slug-based API, not ID-based.** Every method takes slugs, not numeric IDs. The engine resolves slugs to IDs internally. This keeps the interface portable... slugs are strings, IDs are database-specific.
|
||
|
||
**Embedding is NOT in the engine.** The engine stores embeddings and searches by vector, but it doesn't generate embeddings. `src/core/embedding.ts` handles that. This is intentional: embedding is an external API call (OpenAI), not a storage concern. All engines share the same embedding service.
|
||
|
||
**Chunking is NOT in the engine.** Same logic. `src/core/chunkers/` handles chunking. The engine stores and retrieves chunks. All engines share the same chunkers.
|
||
|
||
**Search returns `SearchResult[]`, not raw rows.** The engine is responsible for its own search implementation (tsvector vs FTS5, pgvector vs sqlite-vss) but must return a uniform result type. RRF fusion and dedup happen above the engine, in `src/core/search/hybrid.ts`.
|
||
|
||
**`traverseGraph` exists but is engine-specific.** Postgres uses recursive CTEs. SQLite would use a loop with depth tracking. The interface is the same: give me a slug and max depth, return the graph.
|
||
|
||
## How search works across engines
|
||
|
||
```
|
||
+-------------------+
|
||
| hybrid.ts |
|
||
| (RRF fusion + |
|
||
| dedup, shared) |
|
||
+--------+----------+
|
||
|
|
||
+------------+------------+
|
||
| |
|
||
+--------v--------+ +--------v--------+
|
||
| engine.search | | engine.search |
|
||
| Keyword() | | Vector() |
|
||
+-----------------+ +-----------------+
|
||
| |
|
||
+-----------+-----------+ +---------+---------+
|
||
| | | |
|
||
+-------v-------+ +-------v---+ +-------v---+ +----v--------+
|
||
| Postgres: | | PGLite: | | Postgres: | | PGLite: |
|
||
| tsvector + | | tsvector +| | pgvector | | pgvector |
|
||
| ts_rank + | | ts_rank | | HNSW | | HNSW |
|
||
| websearch_to_ | | (same SQL)| | cosine | | cosine |
|
||
| tsquery | | | | | | (same SQL) |
|
||
+---------------+ +-----------+ +-----------+ +-------------+
|
||
```
|
||
|
||
RRF fusion, multi-query expansion, and 4-layer dedup are engine-agnostic. They operate on `SearchResult[]` arrays. Only the raw keyword and vector searches are engine-specific.
|
||
|
||
## PostgresEngine (v0, ships)
|
||
|
||
**Dependencies:** `postgres` (porsager/postgres), `pgvector`
|
||
|
||
**Postgres-specific features used:**
|
||
- `tsvector` + `GIN` index for full-text search with `ts_rank` weighting
|
||
- `pgvector` HNSW index for cosine similarity vector search
|
||
- `pg_trgm` + `GIN` for fuzzy slug resolution
|
||
- Recursive CTEs for graph traversal
|
||
- Trigger-based search_vector (spans pages + timeline_entries)
|
||
- JSONB for frontmatter with GIN index
|
||
- Connection pooling via Supabase Supavisor (port 6543)
|
||
|
||
**Hosting:** Supabase Pro ($25/mo). Zero-ops. Managed Postgres with pgvector built in.
|
||
|
||
**Why not self-hosted for v0:** The brain should be infrastructure agents use, not something you maintain. Self-hosted Postgres with Docker is a welcome community PR, but v0 optimizes for zero ops.
|
||
|
||
## PGLiteEngine (v0.7, ships)
|
||
|
||
**Dependencies:** `@electric-sql/pglite` (v0.4.4+)
|
||
|
||
**What it is:** Embedded Postgres 17.5 compiled to WASM via ElectricSQL's PGLite. Runs in-process, no server, no Docker, no accounts. Same SQL as PostgresEngine -- not a separate dialect. All 37 BrainEngine methods implemented.
|
||
|
||
**PGLite-specific details:**
|
||
- Uses `pglite-schema.ts` for DDL (pgvector extension, pg_trgm, triggers, indexes)
|
||
- Parameterized queries throughout (shared utilities in `src/core/utils.ts`)
|
||
- `hybridSearch` keyword-only fallback when `OPENAI_API_KEY` is not set
|
||
- Data stored at `~/.gbrain/brain.db` (configurable)
|
||
- pgvector HNSW index for cosine similarity vector search (same as Postgres)
|
||
- tsvector + ts_rank for full-text search (same as Postgres)
|
||
- pg_trgm for fuzzy slug resolution (same as Postgres)
|
||
|
||
**When to use PGLite vs Postgres:**
|
||
|
||
| Factor | PGLite | PostgresEngine + Supabase |
|
||
|--------|--------|--------------------------|
|
||
| Setup | `gbrain init` (zero-config) | Account + connection string |
|
||
| Scale | Good for < 1,000 files | Production-proven at 10K+ |
|
||
| Multi-device | Single machine only | Any device via remote MCP |
|
||
| Cost | Free | Supabase Pro ($25/mo) |
|
||
| Concurrency | Single process | Connection pooling |
|
||
| Backups | Manual (file copy) | Managed by Supabase |
|
||
|
||
**Migration:** `gbrain migrate --to supabase` exports everything (pages, chunks, embeddings, links, tags, timeline) and imports into Supabase. `gbrain migrate --to pglite` goes the other direction. Bidirectional, lossless.
|
||
|
||
## Adding a new engine
|
||
|
||
1. Create `src/core/<name>-engine.ts` implementing `BrainEngine`
|
||
2. Add to engine factory in `src/core/engine-factory.ts`:
|
||
```typescript
|
||
export function createEngine(type: string): BrainEngine {
|
||
switch (type) {
|
||
case 'pglite': return new PGLiteEngine();
|
||
case 'postgres': return new PostgresEngine();
|
||
case 'myengine': return new MyEngine();
|
||
default: throw new Error(`Unknown engine: ${type}`);
|
||
}
|
||
}
|
||
```
|
||
The factory uses dynamic imports so engines are only loaded when selected.
|
||
3. Store engine type in `~/.gbrain/config.json`: `{ "engine": "myengine", ... }`
|
||
4. Add tests. The test suite should be engine-agnostic where possible... same test cases, different engine constructor.
|
||
5. Document in this file + add a design doc in `docs/`
|
||
|
||
### What you DON'T need to touch
|
||
|
||
- `src/cli.ts` (dispatches to engine, doesn't know which one)
|
||
- `src/mcp/server.ts` (same)
|
||
- `src/core/chunkers/*` (shared across engines)
|
||
- `src/core/embedding.ts` (shared across engines)
|
||
- `src/core/search/hybrid.ts`, `expansion.ts`, `dedup.ts` (shared, operate on SearchResult[])
|
||
- `skills/*` (fat markdown, engine-agnostic)
|
||
|
||
### What you DO need to implement
|
||
|
||
Every method in `BrainEngine`. The full interface. No optional methods, no feature flags. If your engine can't do vector search (e.g., a pure-text engine), implement `searchVector` to return `[]` and document the limitation.
|
||
|
||
## Capability matrix
|
||
|
||
| Capability | PostgresEngine | PGLiteEngine | Notes |
|
||
|-----------|---------------|-------------|-------|
|
||
| CRUD | Full | Full | Same SQL |
|
||
| Keyword search | tsvector + ts_rank | tsvector + ts_rank | Identical (real Postgres) |
|
||
| Vector search | pgvector HNSW | pgvector HNSW | Identical (real Postgres) |
|
||
| Fuzzy slug | pg_trgm | pg_trgm | Identical (real Postgres) |
|
||
| Graph traversal | Recursive CTE | Recursive CTE | Same SQL |
|
||
| Transactions | Full ACID | Full ACID | Both support this |
|
||
| JSONB queries | GIN index | GIN index | Identical |
|
||
| Concurrent access | Connection pooling | Single process | PGLite limitation |
|
||
| Hosting | Supabase, self-hosted, Docker | Local file | |
|
||
| Migration methods | runMigration, getChunksWithEmbeddings | Same | Added v0.7 |
|
||
|
||
## Future engine ideas
|
||
|
||
**TursoEngine.** libSQL (SQLite fork) with embedded replicas and HTTP edge access. Would give SQLite's simplicity with cloud sync. Interesting for mobile/edge use cases.
|
||
|
||
**DuckDBEngine.** Analytical workloads. Bulk exports, embedding analysis, brain-wide statistics. Not for OLTP. Could be a secondary engine for analytics alongside Postgres for operations.
|
||
|
||
**Custom/Remote.** The interface is clean enough that someone could build an engine backed by any storage: Firestore, DynamoDB, a REST API, even a flat file system. The interface doesn't assume SQL.
|
||
|
||
Note: The original SQLite engine plan (`docs/SQLITE_ENGINE.md`) was superseded by PGLite. PGLite uses the same SQL as Postgres, eliminating the need for a separate SQLite dialect with FTS5/sqlite-vss translation.
|
||
|
||
---
|
||
|
||
## docs/GBRAIN_RECOMMENDED_SCHEMA.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/GBRAIN_RECOMMENDED_SCHEMA.md
|
||
|
||
<!-- schema-version: 0.5.0 -->
|
||
<!-- source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/GBRAIN_RECOMMENDED_SCHEMA.md -->
|
||
# Brain: The LLM-Maintained Knowledge Base
|
||
|
||
A system prompt for any AI agent that wants to build and maintain a personal knowledge base. This describes the pattern, the architecture, and the operational discipline that makes it work.
|
||
|
||
Drop this into your agent's workspace as a skill or system prompt. Your agent will build the rest.
|
||
|
||
---
|
||
|
||
## What this is
|
||
|
||
A personal intelligence system where your AI agent builds and maintains an interlinked wiki of everything you know about your world — people, companies, deals, projects, meetings, ideas — as structured, cross-referenced markdown files. The agent writes and maintains all of it. You direct, curate, and think.
|
||
|
||
This is Karpathy's LLM wiki pattern, but extended from research notes into a full operational knowledge base — one that integrates with your calendar, email, meetings, social media, and contacts to stay continuously current.
|
||
|
||
The key insight: **knowledge management has failed for 30 years because maintenance falls on humans. LLM agents change the equation — they don't get bored, don't forget to update cross-references, and can touch 50 files in one pass.** Your wiki stays alive because the cost of maintenance is near zero.
|
||
|
||
## Three Founding Principles
|
||
|
||
### 1. Every Piece of Knowledge Has a Primary Home (MECE Directories)
|
||
|
||
Every piece of knowledge passes through a decision tree and lands in exactly one directory. No duplicated pages, no ambiguity about where something goes.
|
||
|
||
This is the single most important structural decision. Without it, knowledge bases rot — the same fact lives in three places with three different versions, nobody knows which is current, and the agent (or human) stops trusting the system. MECE directories with explicit resolver rules prevent this.
|
||
|
||
Every directory has a `README.md` (the resolver) that answers two questions:
|
||
1. **What goes here** — a positive definition with a concrete test
|
||
2. **What does NOT go here** — the key distinctions from neighboring directories that the agent might confuse
|
||
|
||
The brain also has a top-level `RESOLVER.md` — a numbered decision tree the agent walks when filing anything. When two directories seem to fit, disambiguation rules break the tie. When nothing fits, the item goes in `inbox/` — which is itself a signal the schema needs to evolve.
|
||
|
||
**The agent must read the resolver before creating any new page.** This is not optional.
|
||
|
||
**Important nuance: MECE applies to directories, not to reality.** Real people and entities are multi-faceted — a political founder can also be a friend, donor, media actor, and hiring candidate. The resolver picks the *primary home* for their page (people/), but the page itself uses typed backlinks and cross-references to surface all their facets. The MECE rule prevents duplicate pages, not duplicate relationships. Cross-references are how adjacency is preserved without breaking the one-page-per-entity rule.
|
||
|
||
### 2. Compiled Truth + Timeline (Two-Layer Pages)
|
||
|
||
Every brain page has two layers, separated by a horizontal rule (`---`):
|
||
|
||
**Above the line — Compiled Truth.** Always current, always rewritten when new information arrives. Starts with a one-paragraph executive summary. If you read only this, you know the state of play. Followed by structured State fields, Open Threads (active items — removed when resolved), and See Also (cross-links).
|
||
|
||
**Below the line — Timeline.** Append-only, never rewritten. Reverse-chronological evidence log. Each entry: date, source, what happened. When an open thread gets resolved, it moves here with its resolution.
|
||
|
||
If someone asks "what's the current state?" — read above the line. If someone asks "what happened?" — read below the line. The top is the current summary. The bottom is the source log.
|
||
|
||
This is the Karpathy wiki pattern's killer feature: **the synthesis is pre-computed.** Unlike RAG, where the LLM re-derives knowledge from scratch every query, your brain has already done the work. The cross-references are already there. The contradictions have already been flagged.
|
||
|
||
### 3. Enrichment Fires on Every Signal
|
||
|
||
Every time any signal touches a person or company — meeting, email, tweet, calendar event, contact sync, conversation mention — the enrichment pipeline fires. The brain grows as a side effect of normal operations, not as a separate task you remember to do.
|
||
|
||
This is what distinguishes an operational brain from Karpathy's research wiki. He describes ingesting sources you manually add. An operational brain goes further — every pipeline (meetings, email, social media, contacts) automatically triggers enrichment on every entity it touches. You never have to remember to update someone's page. The system does it because the plumbing is wired correctly.
|
||
|
||
## Wiring It Into Your Agent
|
||
|
||
The brain must be referenced in your agent's configuration (AGENTS.md or equivalent) as a hard rule, not a suggestion. Specifically:
|
||
|
||
1. **Before creating any brain page → read RESOLVER.md.** This should be in your agent's operational rules, not buried in documentation.
|
||
2. **Before answering any question about people, companies, deals, or strategy → search the brain first.** Even if the agent thinks it knows the answer. File contents are current; the agent's memory of them goes stale.
|
||
3. **The enrich skill fires on every signal.** Every ingest pathway — meeting processing, email triage, social monitoring, contact sync — should call the enrichment pipeline when it encounters a person or company. This is wiring, not discipline. If it depends on the agent remembering, it will eventually be forgotten.
|
||
4. **Corrections are the highest-value data.** If the user corrects the agent about a person, company, deal, or decision — it gets written to the brain immediately. No batching, no deferring.
|
||
|
||
The chain of authority: **Agent config (AGENTS.md) says "read RESOLVER.md" → RESOLVER.md is the decision tree → each directory README.md is the local resolver → schema.md defines page structure → the enrich skill defines the enrichment protocol.**
|
||
|
||
## Architecture
|
||
|
||
Three layers:
|
||
|
||
**Raw sources** — meeting transcripts, emails, tweets, web research, API responses, calendar events, contact data. Immutable. The agent reads from these but never modifies them. Stored in `sources/` and `.raw/` sidecar directories.
|
||
|
||
**The brain** — a directory of interlinked markdown files. People pages, company pages, deal pages, meeting pages, project pages, concept pages. The agent owns this layer entirely. It creates pages, updates them when new information arrives, maintains cross-references, and keeps everything consistent. You read it; the agent writes it.
|
||
|
||
**The schema** — a document (this one, plus `schema.md` and `RESOLVER.md`) that tells the agent how the brain is structured, what the conventions are, and what workflows to follow. This is the key configuration file — it makes your agent a disciplined knowledge maintainer rather than a generic chatbot.
|
||
|
||
## The Database + Markdown Architecture
|
||
|
||
The markdown wiki is the human-facing layer — the primary interface for humans and LLMs. But it's not the sole source of truth. A structured database layer provides the foundation, and the markdown is generated from it.
|
||
|
||
### The Four Database Primitives
|
||
|
||
**Entity registry** — canonical ID, all aliases, all external IDs (LinkedIn member ID, X user ID, email addresses, phone numbers) in one table. This is the single source of truth for "is this the same person?" When you merge two entities, it's a database operation (point both IDs at the same canonical record), not a file-merge operation with cross-reference fixups.
|
||
|
||
**Event ledger** — every signal that touches the brain is an immutable event: meeting attended, email received, tweet published, enrichment completed, user correction applied. Events have provenance: source, timestamp, confidence, raw payload reference. The timeline section of markdown pages is generated from this ledger. You never lose events because a page rewrite went wrong.
|
||
|
||
**Fact store** — structured claims with provenance. "Jane Doe is CTO of Acme" with `source=crustdata, confidence=high, observed_at=2026-04-07`. When two sources disagree (LinkedIn says CTO, company website says VP Engineering), the conflict is visible as two facts for the same field with different values. The compiled truth section above the line is generated from the fact store's latest-confident values. Contradictions become data, not bugs.
|
||
|
||
**Relationship graph** — typed edges between entities. Person→Company (role: CTO, started: 2024-01), Person→Person (relationship: co-founded company together), Company→Deal (type: Series A, date: 2025-03). Enables graph queries that markdown grep can't answer: "who do I know who's invested in AI infrastructure companies?" becomes a traversal, not a prayer.
|
||
|
||
### Why This Matters
|
||
|
||
- **Identity resolution** becomes a database operation (merge entity IDs), not a file-merge operation with manual cross-reference fixups
|
||
- **Contradictions are structural** (two facts with different values for the same field and different sources) rather than textual (hoping the LLM notices a discrepancy buried in prose)
|
||
- **Concurrency is solved** — events append to a ledger, facts upsert to a store, markdown is rebuilt. No more merge conflicts on shared files
|
||
- **Graph queries work** — "who do I know at this company?" and "what companies has this investor backed that I also know the founders of?" become database queries, not impossible grep chains
|
||
|
||
### File-Layer Conventions
|
||
|
||
The markdown layer uses conventions that map directly to the database primitives:
|
||
|
||
1. **Use frontmatter for structured metadata** — anything you'd want to query (role, company, stage, score, tags) goes in YAML frontmatter, not buried in prose. These map to the fact store.
|
||
2. **Use `.raw/` for provenance** — save every API response with source and timestamp. These map to provenance records in the fact store.
|
||
3. **Treat the timeline as an event stream** — dated, sourced, append-only. These map to the event ledger.
|
||
4. **Keep compiled truth conceptually separate from evidence** — above the line is synthesis; below the line is evidence. The synthesis is a generated view; the evidence is queryable records.
|
||
5. **Use canonical slugs consistently** — every cross-reference uses the filename slug. These are the entity IDs in the registry.
|
||
|
||
## Directory Structure
|
||
|
||
```
|
||
brain/
|
||
├── RESOLVER.md — master decision tree for filing (agent reads this first)
|
||
├── schema.md — page conventions, templates, workflows
|
||
├── index.md — content catalog with one-line summaries
|
||
├── log.md — chronological record of all ingests/updates
|
||
├── people/ — one page per human being
|
||
│ ├── README.md — resolver: what goes here, what doesn't
|
||
│ └── .raw/ — raw API responses per person (JSON sidecars)
|
||
├── companies/ — one page per organization
|
||
│ ├── README.md
|
||
│ └── .raw/
|
||
├── deals/ — financial transactions with terms and decisions
|
||
│ └── README.md
|
||
├── meetings/ — records of specific events with transcripts
|
||
│ └── README.md
|
||
├── projects/ — things being actively built (has a repo, spec, or team)
|
||
│ └── README.md
|
||
├── ideas/ — raw possibilities nobody is building yet
|
||
│ └── README.md
|
||
├── concepts/ — mental models and frameworks you'd teach
|
||
│ └── README.md
|
||
├── writing/ — prose artifacts (essays, philosophy, drafts)
|
||
│ └── README.md
|
||
├── programs/ — major life workstreams (the forest, not the trees)
|
||
│ └── README.md
|
||
├── org/ — your institution's strategy and operations
|
||
│ └── README.md
|
||
├── civic/ — political landscape, policy, government
|
||
│ └── README.md
|
||
├── media/ — public narrative, content ops, social monitoring
|
||
│ └── README.md
|
||
├── personal/ — private notes, health, personal reflections
|
||
│ └── README.md
|
||
├── household/ — domestic operations, properties, logistics
|
||
│ └── README.md
|
||
├── hiring/ — candidate pipelines and evaluations
|
||
│ └── README.md
|
||
├── sources/ — raw data imports and archived snapshots
|
||
│ └── README.md
|
||
├── prompts/ — reusable LLM prompt library
|
||
├── inbox/ — unsorted quick captures (temporary)
|
||
└── archive/ — dead pages, historical record
|
||
```
|
||
|
||
Every directory has a README.md resolver. Adapt directories to your life — add or remove domains as needed. Not everyone needs civic/ or hiring/ or household/. The invariant is: **one directory per knowledge domain, one file per entity, every directory has a resolver, and RESOLVER.md is the master decision tree that guarantees MECE filing.**
|
||
|
||
## Entity Identity and Deduplication
|
||
|
||
In a system fed by meetings, email, social media, contacts, and APIs, **entity identity is the first real failure mode.** Without a canonical identity layer, you will end up with subtle split-brain pages — "Jane Smith" from a meeting transcript and "J. Smith" from an email and "jsmith" from Twitter all creating separate pages for the same person.
|
||
|
||
### Canonical slugs
|
||
|
||
Every entity gets a canonical slug that serves as its stable ID:
|
||
- People: `first-last.md` (all lowercase, hyphens for spaces)
|
||
- Companies: `company-name.md`
|
||
- If collisions arise, disambiguate: `david-liu-crustdata.md`, `david-liu-meta.md`
|
||
|
||
The filename IS the identity. All references, cross-links, and .raw/ sidecars use this slug.
|
||
|
||
### Aliases
|
||
|
||
People have many names across sources. The frontmatter `aliases` field captures all known variants:
|
||
|
||
```yaml
|
||
aliases: ["Jenny Shao", "Jenny G. Shao", "JennyGShao", "jennifer.shao@company.com"]
|
||
```
|
||
|
||
Aliases include: misspellings from meeting transcripts, maiden names, nicknames, email addresses, social handles, and phonetic variants. When the enrich skill encounters a new name variant for a known entity, it adds the variant to aliases — it does NOT create a new page.
|
||
|
||
### Deduplication protocol
|
||
|
||
Before creating any new page, the agent must:
|
||
1. Search existing pages by name (exact and fuzzy)
|
||
2. Search aliases across all pages: `grep -rl "NAME_VARIANT" /data/brain/people/ --include="*.md"`
|
||
3. Check .raw/ sidecars for matching email addresses or social handles
|
||
4. If a match is found → UPDATE the existing page (add alias if the name variant is new)
|
||
5. If no match → CREATE a new page
|
||
|
||
### Merge protocol
|
||
|
||
When you discover two pages are the same person:
|
||
1. Pick the more complete page as the survivor
|
||
2. Merge all timeline entries from the duplicate into the survivor (chronological order)
|
||
3. Merge all aliases
|
||
4. Update all cross-references that pointed to the duplicate
|
||
5. Delete the duplicate
|
||
6. Commit with message: `merge: [duplicate] into [survivor]`
|
||
|
||
During weekly lint, actively look for potential duplicates: similar names, same company, same email across different pages.
|
||
|
||
## Key Disambiguation Rules
|
||
|
||
The most common filing confusions and how to resolve them:
|
||
|
||
- **Concept vs. Idea:** Could you *teach* it as a framework? → concept. Could you *build* it? → idea.
|
||
- **Concept vs. Personal:** Would you share it in a professional talk? → concept. Is it private reflection? → personal.
|
||
- **Idea vs. Project:** Is anyone working on it? Yes → project. No → idea. The graduation moment is when work starts.
|
||
- **Writing vs. Media:** Writing is the *artifact* (the essay). Media is the *production and distribution infrastructure* (content pipeline, social monitoring).
|
||
- **Writing vs. Concepts:** A concept page is distilled (200 words of compiled truth). An essay is developed prose (argument, narrative, story).
|
||
- **Person vs. Company:** Is it about *them as a human*? → people/. Is it about *the organization*? → companies/. Both pages link to each other.
|
||
- **Household vs. Personal:** Would a PA execute on it? → household (operational). Is it private reflection? → personal.
|
||
- **Sources vs. .raw/ sidecars:** Per-entity enrichment data → .raw/ sidecar. Bulk multi-entity imports → sources/.
|
||
|
||
When nothing fits, file in inbox/ and flag it. That's a signal the schema needs to evolve.
|
||
|
||
## Page Types and Templates
|
||
|
||
### Person
|
||
|
||
The most important page type. A great person page is a well-researched briefing — not a LinkedIn scrape.
|
||
|
||
```markdown
|
||
# Person Name
|
||
|
||
> Executive summary: who they are, why they matter, what you should
|
||
> know walking into any interaction with them.
|
||
|
||
## State
|
||
- **Role:** Current title
|
||
- **Company:** Current org
|
||
- **Relationship:** To you (friend, colleague, investor, etc.)
|
||
- **Key context:** 2-4 bullets of what matters right now
|
||
|
||
## What They Believe
|
||
Worldview, positions, first principles. The hills they die on.
|
||
Every claim must cite its source and type:
|
||
- [Belief] — observed: [tweet/meeting/article, date]
|
||
- [Belief] — self-described: [interview/bio, date]
|
||
- [Belief] — inferred: [pattern across N interactions, confidence: high/medium/low]
|
||
|
||
## What They're Building
|
||
Current projects, recent ships, product direction.
|
||
|
||
## What Motivates Them
|
||
Ambition drivers, career arc, what gets them out of bed.
|
||
Distinguish between what they say motivates them (self-described) and
|
||
what their behavior suggests (observed/inferred).
|
||
|
||
## Communication Style
|
||
How they prefer to communicate. How they handle disagreement.
|
||
What energizes them in conversation.
|
||
This section is high-value but requires careful sourcing.
|
||
Rules: only write here from direct observation (meeting behavior,
|
||
language in emails/tweets, visible patterns). Never generalize
|
||
from a single data point. Mark confidence level.
|
||
|
||
## Hobby Horses
|
||
Topics they return to obsessively. Recurring themes in their public voice.
|
||
|
||
## Assessment
|
||
- **Strengths:** What they're great at. Be specific.
|
||
- **Gaps:** Where they could grow. Be specific and fair.
|
||
- **Net read:** One-line synthesis.
|
||
- **Confidence:** high (5+ interactions) / medium (2-4) / low (1 or inferred)
|
||
- **Last assessed:** YYYY-MM-DD
|
||
|
||
## Trajectory
|
||
Ascending, plateauing, pivoting, declining? Evidence.
|
||
|
||
## Relationship
|
||
History of interactions, temperature, dynamic.
|
||
|
||
## Contact
|
||
- Email, phone, LinkedIn, X handle, location
|
||
|
||
## Network
|
||
- **Close to:** People they're frequently seen with
|
||
- **Crew:** Which cluster they belong to
|
||
|
||
## Open Threads
|
||
- Active items, pending intros, follow-ups
|
||
|
||
---
|
||
|
||
## Timeline
|
||
- **YYYY-MM-DD** | Source — What happened.
|
||
```
|
||
|
||
All sections are optional — include what you have, leave empty sections as `[No data yet]` rather than omitting them. **The structure itself is a prompt for future enrichment.** When a section says `[No data yet]`, the agent knows what to look for next time it encounters this person.
|
||
|
||
The principle: facts are table stakes. Context is the value.
|
||
|
||
### Epistemic discipline on people pages
|
||
|
||
The context sections (Beliefs, Motivations, Communication Style, Assessment) are the highest-value parts of the system but also the most prone to hallucination. An agent can over-generalize from sparse evidence or overfit to one recent interaction. Rules:
|
||
|
||
- **Every claim cites its source.** Not "she's aggressive" but "she pushed back hard on pricing in the March 15 meeting (observed)."
|
||
- **Three source types:** `observed` (you saw it happen), `self-described` (they said it about themselves), `inferred` (you're reading between lines). Label each.
|
||
- **Confidence tracks interaction count.** One meeting = low confidence. Five meetings = high. Don't write definitive assessments from thin data.
|
||
- **Recency matters.** A belief from 2 years ago may not be current. Mark dates.
|
||
- **Never generalize from a single data point.** "She seemed frustrated in one meeting" is a timeline entry. Patterns require multiple observations.
|
||
- **The user's corrections override everything.** If the user says "that's wrong about her," update immediately — that correction is the highest-confidence signal in the system.
|
||
|
||
### Company
|
||
|
||
```markdown
|
||
# Company Name
|
||
|
||
> What they do, stage, why they matter.
|
||
|
||
## State
|
||
- **What:** One-line description
|
||
- **Stage:** Seed / Series A / Growth / Public
|
||
- **Key people:** Names with links to people pages
|
||
- **Key metrics:** Revenue, headcount, funding
|
||
- **Connection:** How they relate to your world
|
||
|
||
## Open Threads
|
||
|
||
---
|
||
|
||
## Timeline
|
||
```
|
||
|
||
### Meeting
|
||
|
||
```markdown
|
||
# Meeting Title
|
||
|
||
> YOUR analysis — not a copy of the AI meeting notes.
|
||
> What matters given everything else going on.
|
||
> What was decided. What was left unsaid.
|
||
|
||
## Attendees
|
||
## Key Decisions
|
||
## Action Items
|
||
## Connections to other brain pages
|
||
|
||
---
|
||
|
||
## Full Transcript
|
||
```
|
||
|
||
### Deal, Project, Concept — same pattern. Compiled truth on top, timeline on bottom.
|
||
|
||
## The Enrichment Pipeline
|
||
|
||
**This is the most important operational pattern.** Every time your agent encounters a person or company — in a meeting, email, tweet, calendar event, contact sync — it should enrich the corresponding brain page.
|
||
|
||
Enrichment is not just "look up their LinkedIn." It's:
|
||
|
||
- **What they believe** — positions, worldview, public stances
|
||
- **What they're building** — current projects, what's shipping
|
||
- **What motivates them** — ambition, career trajectory
|
||
- **Their communication style** — how they engage, what energizes them
|
||
- **Their relationship to you** — history, context, open threads
|
||
- **Hard facts** — role, company, contact info, funding (table stakes)
|
||
|
||
Facts are table stakes. Context is the value.
|
||
|
||
### When to enrich
|
||
|
||
**Any time** a person or company signal appears:
|
||
- Someone is mentioned in a meeting transcript → enrich
|
||
- Someone emails you → enrich
|
||
- Someone interacts with you on social media → enrich
|
||
- A new contact appears → enrich
|
||
- You mention someone in conversation and their page is thin → enrich
|
||
- A company announces funding, ships a product, makes news → enrich
|
||
|
||
### Enrichment sources (in order of value)
|
||
|
||
1. **Your own interactions** — what you said about them, what they said to you (highest signal)
|
||
2. **Meeting transcripts** — richest context source
|
||
3. **Email threads** — tone, urgency, relationship dynamics
|
||
4. **Social media** — beliefs, public positioning, who they engage with
|
||
5. **Web search** — background, press, talks
|
||
6. **People APIs** — structured profile data (career history, education, skills, contact info)
|
||
7. **Company APIs** — funding, investors, valuations, headcount, financials
|
||
8. **Contact data** — email, phone, location
|
||
|
||
### Data source skills
|
||
|
||
Each external data source should be its own named skill with full API documentation, auth patterns, and usage notes. The enrich skill orchestrates them — it decides *which* sources to call based on tier, then delegates to the individual skill for *how* to call the API.
|
||
|
||
This keeps things DRY: the enrich skill owns the logic (when to enrich, what tier, what to extract), and each data source skill owns the API contract (endpoints, auth, rate limits, gotchas, validation rules).
|
||
|
||
Recommended data source skills:
|
||
|
||
- **Web search** — broad keyword search (Brave, Google, etc.). Quick background, press, funding.
|
||
- **Semantic search** — better than keyword for finding specific people, LinkedIn URLs, personal writing. (Exa, Perplexity, etc.)
|
||
- **Social search** — X/Twitter, Bluesky, etc. for public voice: beliefs, projects, engagement patterns.
|
||
- **People enrichment** — structured LinkedIn-like data: career history, education, skills, contact info. (Crustdata, Proxycurl, People Data Labs, etc.)
|
||
- **Network search** — search your professional network for warm intros and connections. (Happenstance, Clay, etc.)
|
||
- **Company intelligence** — Pitchbook-grade data: funding rounds, investors, valuations, headcount, financials. (Captain API, Crunchbase, etc.)
|
||
- **Meeting history** — search past meetings for interactions with this entity. (Circleback, Otter, Fireflies, etc.)
|
||
- **Contact data** — email, phone, location from your contacts. (Google Contacts, etc.)
|
||
|
||
The typical enrichment flow for a new person:
|
||
1. **Network search** → find LinkedIn URL, career arc, alternate names
|
||
2. **People enrichment** → deep structured data (skills, work history, education, contact info)
|
||
3. **Semantic search** → find personal sites, talks, writing that reveal beliefs and perspective
|
||
4. **Social search** → their public voice, who they engage with, hobby horses
|
||
5. **Web search** → press coverage, recent news, talks
|
||
6. **Meeting history** → past interactions with you
|
||
|
||
For a new company:
|
||
1. **Company intelligence** → funding, investors, headcount, financials
|
||
2. **Web search** → product, press, traction
|
||
3. **Social search** → company's public positioning
|
||
4. **People enrichment** → enrich founders/key team members (each triggers person enrichment)
|
||
|
||
### Enrichment tiers (don't over-enrich)
|
||
|
||
- **Tier 1 (key people):** Full pipeline — all sources. Inner circle, business partners, important collaborators.
|
||
- **Tier 2 (notable):** Web search + social + brain cross-reference. People you interact with occasionally.
|
||
- **Tier 3 (minor mentions):** Extract signal from source only, append to timeline. Everyone else worth tracking.
|
||
|
||
A thin page with real interaction data is better than a fat page stuffed with generic web results. Don't waste 10 API calls on someone with no public presence.
|
||
|
||
### Raw data sidecars
|
||
|
||
Every enrichment API response gets saved as a JSON sidecar:
|
||
|
||
```
|
||
people/jane-doe.md ← brain page (curated, readable)
|
||
people/.raw/jane-doe.json ← raw API responses
|
||
```
|
||
|
||
The JSON is keyed by source with fetch timestamps:
|
||
|
||
```json
|
||
{
|
||
"sources": {
|
||
"crustdata": { "fetched_at": "2026-04-05T...", "data": { ... } },
|
||
"web_search": { "fetched_at": "...", "data": { ... } }
|
||
}
|
||
}
|
||
```
|
||
|
||
The brain page is the distilled version. Raw data is the archive.
|
||
|
||
What goes in the brain page (distilled): location, current title, company, headline, education (one line), career arc (condensed), top skills, social handles, profile picture permalink.
|
||
|
||
What stays in .raw/ only: full work history with job descriptions, complete skill lists, company descriptions for each employer, platform-specific IDs, follower/connection counts, full API response bodies.
|
||
|
||
When re-enriching: overwrite the source key with fresh data + new timestamp. Don't append — replace.
|
||
|
||
### Validation rules
|
||
|
||
When auto-enriching from people/company APIs:
|
||
- **Low connection/follower count (e.g., <20):** Likely wrong person. Save to .raw/ with a `"validation": "low_connections"` flag. Don't auto-write to the brain page.
|
||
- **Name mismatch:** If the returned name doesn't share a last name with the entity, skip.
|
||
- **Obviously joke profiles:** Career arcs mentioning absurd titles — skip.
|
||
- **When in doubt:** Save raw data but don't update the brain page. Wrong data is worse than no data.
|
||
|
||
### Browser budget
|
||
|
||
If enrichment involves browser-based lookups (LinkedIn, authenticated pages), set a daily budget (e.g., 20 lookups/day) to avoid account flagging. Prefer API-based enrichment services for bulk work — they don't touch the user's browser session.
|
||
|
||
## Entry Criteria — Who Gets a Page
|
||
|
||
Not everyone deserves a brain page. Scale page creation to relationship importance:
|
||
|
||
**Always create a page for:**
|
||
- Anyone you've had a 1:1 or small-group meeting with
|
||
- Key colleagues, partners, and direct collaborators
|
||
- Anyone with a strong working relationship or better
|
||
- Family, close friends, inner circle
|
||
|
||
**Create if signal exists:**
|
||
- People from contacts with recent interaction
|
||
- Anyone mentioned by name in conversation with context
|
||
- Event contacts with multiple shared events
|
||
|
||
**Do NOT create:**
|
||
- Random names from mass event guest lists with no interaction
|
||
- Single-name entries with no identifying context
|
||
- Contacts with no relationship signal at all
|
||
|
||
When in doubt: does the user benefit from this entry existing? If no, skip it.
|
||
|
||
## The Skill Architecture
|
||
|
||
Skills are the modular building blocks of the system. There are three types, and understanding how they compose is critical.
|
||
|
||
### 1. Data source skills (leaf nodes)
|
||
|
||
Each external API or data source gets its own named skill. The skill owns the API contract: endpoints, authentication, rate limits, error handling, validation rules, and what the response looks like.
|
||
|
||
Examples:
|
||
- **People enrichment** (Crustdata, Proxycurl, People Data Labs) — structured LinkedIn-like data
|
||
- **Network search** (Happenstance, Clay) — search professional network, find mutual connections
|
||
- **Company intelligence** (Captain API/Pitchbook, Crunchbase) — funding, investors, financials
|
||
- **Semantic search** (Exa, Perplexity) — find LinkedIn URLs, personal sites, writing
|
||
- **Meeting history** (Circleback, Otter, Fireflies) — past meeting transcripts and notes
|
||
- **Calendar/contacts** (Google Calendar, Google Contacts via integration tools) — schedule, contact info
|
||
- **Social media** (X API, Bluesky API) — public posts, engagement, follower data
|
||
- **Workspace tools** (Gmail, Slack, Drive via integration tools) — email threads, messages, documents
|
||
|
||
Data source skills are **never called directly by the user.** They're called by orchestration skills (below).
|
||
|
||
### 2. Orchestration skills (coordinators)
|
||
|
||
These skills contain the *logic* — they decide what to do, then delegate to data source skills for how to do it.
|
||
|
||
**The enrich skill** is the most important orchestration skill. It decides:
|
||
- Is this a CREATE (new page) or UPDATE (new signal)?
|
||
- What tier is this entity? (determines which data sources to call)
|
||
- What signal types to extract from the source material?
|
||
- Which data source skills to call, in what order?
|
||
- How to write the results to the brain?
|
||
|
||
Other orchestration skills:
|
||
- **Meeting ingestion** — pulls meetings from a meeting tool, creates brain meeting pages with analysis, then calls enrich for every attendee and company discussed
|
||
- **Email triage / executive assistant** — processes inbox, handles scheduling, then calls enrich when it encounters people or companies
|
||
- **Social monitoring** — scans public social media for mentions and engagement, then calls enrich for notable accounts
|
||
|
||
### 3. Pipeline skills (end-to-end workflows)
|
||
|
||
These are the user-facing skills that chain multiple orchestration and data source skills together:
|
||
- **Morning briefing** — reads calendar + tasks + brain state + recent signals → produces a briefing
|
||
- **Person research** — given a name, runs full Tier 1 enrichment and presents the result
|
||
- **Weekly brain maintenance** — runs lint, flags stale pages, suggests enrichment targets
|
||
|
||
### How they compose
|
||
|
||
```
|
||
User says "tell me about Jane Doe"
|
||
→ Agent searches brain (grep/index)
|
||
→ Page is thin → calls enrich skill (orchestration)
|
||
→ enrich determines Tier 1
|
||
→ calls happenstance skill (data source) → gets LinkedIn URL
|
||
→ calls crustdata skill (data source) → gets full profile
|
||
→ calls exa skill (data source) → finds personal writing
|
||
→ calls web_search (built-in tool) → gets press coverage
|
||
→ calls meeting history (data source) → finds past meetings
|
||
→ writes brain page, saves .raw/ sidecar, cross-references
|
||
→ Agent presents the enriched page to user
|
||
```
|
||
|
||
```
|
||
Cron fires "meeting ingestion" every afternoon
|
||
→ meeting-ingestion skill (orchestration) pulls new meetings
|
||
→ for each meeting: creates brain meeting page
|
||
→ for each attendee: calls enrich skill (orchestration)
|
||
→ enrich calls relevant data source skills based on tier
|
||
→ for each company discussed: calls enrich skill
|
||
→ extracts tasks, commits brain repo
|
||
```
|
||
|
||
The key insight: **data source skills are stateless and reusable.** The enrich skill can call Crustdata whether the trigger was a meeting, an email, a social mention, or a direct user request. The data source skill doesn't care where the request came from.
|
||
|
||
## How Enrich Wires Into Everything
|
||
|
||
The enrich skill is the central hub. Every ingest pathway converges on it:
|
||
|
||
```
|
||
Meeting ingestion ───────┬─────────────────────────┬─── people enrichment API
|
||
Email triage ────────────┤ ├─── company intelligence API
|
||
Social monitoring ───────┤ ENRICH SKILL ├─── network search API
|
||
Contact sync ────────────┤ (orchestration) ├─── semantic search API
|
||
Manual conversation ─────┤ ├─── social search API
|
||
Calendar events ─────────┤ ├─── web search
|
||
Webhooks ────────────────┴─────────────────────────┴─── meeting history API
|
||
│
|
||
▼
|
||
BRAIN REPO
|
||
(people/, companies/,
|
||
meetings/, deals/)
|
||
```
|
||
|
||
Every arrow into the enrich skill carries a **signal** (the raw information from the source) and an **entity** (the person or company to enrich). The enrich skill:
|
||
|
||
1. **Checks brain state** — does a page exist? Is it thin?
|
||
2. **Determines tier** — Tier 1 (full pipeline), Tier 2 (web + social + cross-ref), Tier 3 (source extraction only)
|
||
3. **Extracts signal** from the source material (beliefs, motivations, trajectory, facts)
|
||
4. **Calls data source skills** based on tier (each skill is a named, documented module)
|
||
5. **Writes to brain** — CREATE (via RESOLVER.md) or UPDATE (append timeline, update compiled truth)
|
||
6. **Cross-references** — updates all linked entity pages
|
||
7. **Saves raw data** to `.raw/` sidecar
|
||
8. **Commits** to the brain repo
|
||
|
||
The critical wiring rule: **every ingest skill must call enrich.** This is not optional or aspirational. It's structural. If a new ingest pathway is added (say, a Slack monitoring skill), its implementation must include "for each person/company mentioned, call the enrich skill." If that line is missing, the brain stops compounding from that source.
|
||
|
||
## Automated Cron Jobs
|
||
|
||
The brain doesn't just grow when you're actively using it. Cron jobs make the system autonomous — the brain is maintained, the inbox is triaged, meetings are ingested, and mentions are monitored even while you sleep.
|
||
|
||
### The cron architecture
|
||
|
||
Cron jobs run as **isolated agent sessions** — they get their own context, read their own skills, and don't block the main conversation thread. They can post to specific notification channels (Telegram topics, Slack channels, Discord threads) or work silently.
|
||
|
||
Each cron job is essentially: "wake up, read a skill, do the work, post results (or stay silent if nothing happened), go back to sleep."
|
||
|
||
### Recommended cron jobs for a brain-powered system
|
||
|
||
**High frequency (every 10-30 minutes):**
|
||
- **Email monitor** — scan inbox, classify by priority, post digest to a notification channel. Handle low-risk items (scheduling, acknowledgments) directly.
|
||
- **Message monitor** — check key communication channels for unreplied messages from important contacts. Surface them with suggested responses.
|
||
|
||
**Medium frequency (every 1-3 hours):**
|
||
- **Social radar** — scan public social media for mentions of you or your organization, engagement, emerging narratives. Alert on items that need attention. Call enrich for notable new accounts engaging with you.
|
||
- **Heartbeat** — the omnibus check. Calendar lookahead, task review, email scan, brain state review. Post if something needs attention; stay silent if not.
|
||
|
||
**Daily:**
|
||
- **Morning briefing** — calendar + tasks + urgent items + overnight signals → one notification. The "here's your day" message.
|
||
- **Task prep** — archive yesterday's completed tasks, build today's list from calendar + backlog + recurring items.
|
||
- **Meeting ingestion** — pull all new meetings from your meeting tool, run full ingestion (create meeting pages, propagate to entity pages, extract tasks). This is the heaviest cron job — it touches the most brain pages.
|
||
- **Social media collection** — archive your own posts, track engagement velocity, detect deletions. Feed into media/ pages.
|
||
|
||
**Weekly:**
|
||
- **Brain lint** — run the full maintenance pass: contradictions, stale pages, orphans, missing cross-references, MECE filing violations. Post a report.
|
||
- **Enrichment sweep** — find brain pages that haven't been enriched in 90+ days, or pages with many `[No data yet]` sections. Queue them for re-enrichment.
|
||
- **Contact sync** — pull recent additions from your contacts, cross-reference with brain. Create pages for significant new contacts.
|
||
|
||
### How crons feed the brain
|
||
|
||
The key insight: **cron jobs are the autonomous enrichment engine.** Without them, the brain only grows when you're actively talking to the agent. With them:
|
||
|
||
- The email monitor encounters a person → calls enrich → brain grows
|
||
- The meeting ingestion processes a transcript → calls enrich for every attendee → brain grows
|
||
- The social radar detects a new notable account → calls enrich → brain grows
|
||
- The contact sync finds a new contact → calls enrich → brain grows
|
||
- The enrichment sweep finds stale pages → calls enrich with fresh data → brain stays current
|
||
|
||
The brain compounds 24/7 because the cron jobs are wired to call enrich. The user sleeps; the brain doesn't.
|
||
|
||
### Cron job design rules
|
||
|
||
1. **Silent when nothing happens.** If a cron finds nothing new, it should produce no output. No "nothing to report" messages. This is critical — noisy crons get disabled.
|
||
2. **Post to specific channels.** Each cron posts to its designated notification channel (e.g., email cron → Emails topic, social radar → Social Alerts topic). Don't mix signals.
|
||
3. **Spawn sub-agents for heavy work.** The cron session should stay lightweight. If meeting ingestion needs to process 5 meetings and update 30 entity pages, spawn sub-agents for the entity propagation.
|
||
4. **Idempotent and checkpoint-aware.** Every cron should track what it's already processed (in a state file like `meeting-notes-state.json`) so it doesn't redo work on the next run.
|
||
5. **Respect quiet hours.** Don't post between 11 PM and 7 AM unless something is genuinely urgent. Crons should check the time before posting.
|
||
6. **Every ingest cron must call enrich.** This is the structural rule. A cron that processes meetings but doesn't enrich attendees is a bug, not a feature.
|
||
|
||
### Example: how it all fits together
|
||
|
||
A typical afternoon in an autonomous brain system:
|
||
|
||
1. **3:00 PM** — Email monitor cron fires. Scans inbox. Finds 3 new emails: a scheduling request, a funding announcement, and a founder asking for advice.
|
||
- Handles the scheduling request directly (checks calendar, replies with available times)
|
||
- Calls enrich on the company in the funding announcement → updates company page with new round
|
||
- Posts the founder's email to notification channel for the user to handle
|
||
|
||
2. **3:15 PM** — Meeting ingestion cron fires. Finds 2 new meetings from today.
|
||
- Creates 2 brain meeting pages with analysis
|
||
- Calls enrich for 8 attendees across both meetings → updates 8 people pages
|
||
- Calls enrich for 3 companies discussed → updates 3 company pages
|
||
- Extracts 4 action items → adds to task list
|
||
|
||
3. **3:30 PM** — Social radar cron fires. Detects a journalist writing a thread about the user's organization.
|
||
- Posts alert to Social Alerts channel
|
||
- Calls enrich on the journalist → creates/updates their people page with recent activity
|
||
|
||
4. **4:00 PM** — Heartbeat fires. Calendar shows a meeting in 1 hour. Brain page for the attendee was last enriched 3 months ago.
|
||
- Triggers a fresh enrichment pass on the attendee
|
||
- Posts a prep note: "Meeting with X in 1 hour. Here's what's changed since you last met."
|
||
|
||
The user didn't ask for any of this. The brain grew by 12 pages and the user walked into their 4:00 PM meeting fully prepared — because the plumbing is wired correctly.
|
||
|
||
## Worked Examples From a Production System
|
||
|
||
These examples show how the architecture operates end-to-end. Names and specifics are genericized, but the skill chains are exact — every skill call, every file write, every cron trigger is how it actually works.
|
||
|
||
### Example 1: Meeting Ingestion — The Full Chain
|
||
|
||
A cron job fires at 3:00 PM daily with the prompt: "Read skills/meeting-ingestion/SKILL.md and process today's meetings."
|
||
|
||
**Step 1: Skill chain loads.** The meeting-ingestion skill's preamble says "Read skills/enrich/SKILL.md" — so the agent loads the enrichment protocol before touching any data. This is critical: it means the agent knows how to handle every person and company it encounters.
|
||
|
||
**Step 2: Pull new meetings.** The agent calls the meeting history data source skill (in this system, Circleback). It checks a state file (`memory/meeting-notes-state.json`) that tracks the last processed meeting ID. Finds 2 new meetings since last run.
|
||
|
||
**Step 3: Process Meeting 1 — "Product Review with Sarah Chen and Mike Torres."**
|
||
|
||
The agent creates `brain/meetings/2026-04-07-product-review.md` with:
|
||
- Its own analysis above the line (not a copy of the AI summary — reframed through what the brain already knows about the attendees and the project)
|
||
- Key decisions, action items, and connections to other brain pages
|
||
- Full transcript below the line
|
||
|
||
**Step 4: Enrich attendees.**
|
||
|
||
For **Sarah Chen** — the agent searches the brain: `grep -rl "Sarah Chen" /data/brain/people/`. Finds `people/sarah-chen.md`. Reads it. Page was last enriched 2 weeks ago and has good coverage. → **Tier 3**: extract signal from this meeting only. Appends to her timeline: "2026-04-07 | Meeting — Pushed back on timeline for launch, wants more QA. Concerned about API stability." Updates her Open Threads with the new follow-up item.
|
||
|
||
For **Mike Torres** — brain search finds `people/mike-torres.md`. Page exists but is thin: just a name, title, and one previous meeting entry. → **Tier 2**: web search + social + brain cross-reference. Agent finds his recent blog posts (feeds into What They Believe), his X activity (feeds into Hobby Horses), and cross-references him with two other brain pages that mention him. Updates compiled truth above the line.
|
||
|
||
For **"Alex from Meridian Labs"** (mentioned in the meeting but not an attendee) — brain search finds nothing. → **CREATE path**:
|
||
1. Reads RESOLVER.md: "a specific named person" → `people/`
|
||
2. Creates `people/alex-rivera.md` using the person template from schema.md
|
||
3. Runs **Tier 1 enrichment** (full pipeline): network search → finds LinkedIn URL. People enrichment API → full structured profile. Semantic search → finds a conference talk. Web search → finds press coverage of Meridian Labs' recent funding.
|
||
4. Saves raw API responses to `people/.raw/alex-rivera.json`
|
||
5. Cross-references: updates `companies/meridian-labs.md` to link to Alex's page
|
||
|
||
**Step 5: Enrich companies discussed.** Meridian Labs was discussed extensively. Agent checks `companies/meridian-labs.md` — exists but funding data is 4 months stale. Calls company intelligence API → gets fresh round data. Updates the page.
|
||
|
||
**Step 6: Extract action items.** Finds 3 action items in the transcript → appends to `ops/tasks.md`.
|
||
|
||
**Step 7: Repeat for Meeting 2.** Same flow.
|
||
|
||
**Step 8: Commit and notify.**
|
||
```bash
|
||
cd /data/brain && git add -A && git commit -m "meetings: 2026-04-07 product review, investor sync" && git push
|
||
```
|
||
Posts summary to the Meetings notification channel: "Processed 2 meetings. Created 1 new person page (Alex Rivera). Updated 4 entity pages. 5 action items extracted."
|
||
|
||
**Files touched in this run:**
|
||
```
|
||
brain/
|
||
├── meetings/
|
||
│ ├── 2026-04-07-product-review.md (CREATED)
|
||
│ └── 2026-04-07-investor-sync.md (CREATED)
|
||
├── people/
|
||
│ ├── sarah-chen.md (UPDATED — timeline + open threads)
|
||
│ ├── mike-torres.md (UPDATED — Tier 2 enrichment)
|
||
│ ├── alex-rivera.md (CREATED — Tier 1 enrichment)
|
||
│ └── .raw/
|
||
│ └── alex-rivera.json (CREATED — raw API responses)
|
||
├── companies/
|
||
│ └── meridian-labs.md (UPDATED — fresh funding data)
|
||
ops/
|
||
└── tasks.md (UPDATED — 5 new action items)
|
||
memory/
|
||
└── meeting-notes-state.json (UPDATED — checkpoint)
|
||
```
|
||
|
||
### Example 2: Email Triage — Resolver + Enrichment in Action
|
||
|
||
An email monitor cron fires at 12:00 PM. Its prompt: "Read skills/executive-assistant/SKILL.md and skills/gmail/SKILL.md. Triage the inbox."
|
||
|
||
**Step 1: Pull inbox.** The agent calls the Gmail data source skill via its workspace integration. Gets 8 new emails since last check.
|
||
|
||
**Step 2: Classify and handle.** Most emails are routine: 2 scheduling confirmations (handled directly — checks calendar, sends confirmations), 3 newsletters (archived), 1 internal FYI (noted). But one stands out:
|
||
|
||
**An email from "David Park, GP at Ridgeline Ventures"** — subject: "Series A for NovaTech — co-invest opportunity." The agent has never seen this person before.
|
||
|
||
**Step 3: Enrich the unknown sender.**
|
||
|
||
The agent calls the enrich skill. Enrich searches the brain:
|
||
```bash
|
||
grep -rl "David Park" /data/brain/people/ --include="*.md" # no results
|
||
grep -rl "Ridgeline" /data/brain/companies/ --include="*.md" # no results
|
||
grep -rl "david.park@ridgeline" /data/brain/people/ --include="*.md" # no results (alias search)
|
||
```
|
||
|
||
No match. → **CREATE path.**
|
||
|
||
1. Reads RESOLVER.md: "a specific named person" → `people/`
|
||
2. Runs **Tier 2 enrichment** (this is an unsolicited email, not a key relationship yet):
|
||
- Web search: finds David Park's profile on Ridgeline's website. GP, focuses on enterprise SaaS. Previously at two other funds.
|
||
- Social search: finds his X account. Recent posts about AI infrastructure, developer tools. Reposted an article about NovaTech last week.
|
||
- Brain cross-reference: searches for NovaTech → finds `companies/novatech.md` exists (from a meeting 2 months ago). Cross-links.
|
||
3. Creates `people/david-park.md` with what it found — role, fund, investment focus, public voice, connection to NovaTech.
|
||
4. Also checks `companies/ridgeline-ventures.md` — doesn't exist. Creates a thin page with what's known from the web search.
|
||
|
||
**Step 4: Back in the EA skill.** Now the agent has context. It classifies the email:
|
||
- Priority: Medium (co-invest opportunity, not urgent)
|
||
- Context: David Park is a GP at a fund that focuses on enterprise SaaS. NovaTech is already in the brain from a previous meeting.
|
||
- Action needed: User should review
|
||
|
||
Posts to the Emails notification channel:
|
||
> **Co-invest opportunity — NovaTech Series A**
|
||
> From: David Park, GP at Ridgeline Ventures
|
||
> He's reaching out about co-investing in NovaTech's Series A. Ridgeline focuses on enterprise SaaS.
|
||
> NovaTech is already in the brain — you met their founder in February.
|
||
> [Open in Gmail](link)
|
||
|
||
**The email monitor didn't just triage — it grew the brain by two pages** (one person, one company) and cross-linked them to an existing entity.
|
||
|
||
### Example 3: The Compound Effect — How Context Builds Before a Meeting
|
||
|
||
This example shows how a completely unknown person becomes a rich brain page across 4 autonomous cron runs over 48 hours, with zero manual intervention. The result: you walk into a meeting fully prepared.
|
||
|
||
**Hour 0 — Social radar cron (Tuesday, 3:00 PM)**
|
||
|
||
The social radar cron scans for mentions and engagement on X. It detects a reply to one of the user's posts from an account named `@lena_builds` — a thoughtful, technical response about developer tooling that got 50+ likes.
|
||
|
||
The agent calls enrich. Brain search: no match for "Lena" or "lena_builds." → **CREATE, Tier 3** (minor mention — just a social interaction, not a relationship yet).
|
||
|
||
Creates `people/lena-kovac.md` with minimal data: X handle, display name, the reply text, and a note that she seems technical. No API calls — Tier 3 is source-extraction only.
|
||
|
||
```markdown
|
||
# Lena Kovac
|
||
|
||
> Technical builder. Engaged with a post about developer tooling on X.
|
||
|
||
## State
|
||
- **X:** @lena_builds
|
||
- **Relationship:** None yet — social interaction only
|
||
- **Confidence:** low (1 interaction)
|
||
|
||
---
|
||
|
||
## Timeline
|
||
- **2026-04-07** | X reply — Replied to post about developer tools.
|
||
Thoughtful technical take on compiler-driven UX. 50+ likes.
|
||
```
|
||
|
||
**Hour 18 — Email monitor cron (Wednesday, 9:00 AM)**
|
||
|
||
The morning email sweep finds an email from `lena@kovac.dev` — subject: "Loved your talk at the devtools summit — would love to chat about what we're building."
|
||
|
||
The agent calls enrich. Searches the brain:
|
||
```bash
|
||
grep -rl "lena" /data/brain/people/ --include="*.md" # finds people/lena-kovac.md
|
||
grep -rl "kovac.dev" /data/brain/people/ --include="*.md" # no alias match yet
|
||
```
|
||
|
||
Finds the existing page. Reads it — it's thin (Tier 3, just the X reply). The email adds a new signal AND an email address. → **Upgrade to Tier 2.**
|
||
|
||
- Adds `lena@kovac.dev` to aliases in frontmatter
|
||
- Web search: finds her personal site (`kovac.dev`) — she's building a developer tools startup called Lattice. Previously at a major tech company on their compiler team.
|
||
- Social search: deeper X dive. She posts regularly about developer experience, compilers, and Rust. Has 3K followers.
|
||
- Brain cross-reference: searches for "Lattice" and "compiler" — finds a concept page about developer tooling that links to 2 companies in the same space.
|
||
- Updates `people/lena-kovac.md` with real substance: career history, what she's building, what she believes about developer tooling, her public voice.
|
||
|
||
**Hour 26 — Executive assistant cron (Wednesday, 5:00 PM)**
|
||
|
||
The afternoon EA sweep processes scheduling requests. One of the emails it triages is Lena's — she asked to chat. The user's calendar is open Thursday at 2 PM.
|
||
|
||
But the EA skill also checks: is there a calendar event already scheduled with this person? It searches the calendar — finds that Lena's email (`lena@kovac.dev`) appears in a calendar event for Thursday at 2 PM (she booked through the user's public booking link).
|
||
|
||
The EA skill sees the meeting is tomorrow. Calls enrich again. Page exists and is now Tier 2 with decent coverage, but there's a meeting tomorrow. → **Upgrade to Tier 1.**
|
||
|
||
- Network search: finds her LinkedIn URL. She has 2 mutual connections with the user.
|
||
- People enrichment API: full structured profile — Stanford CS, 4 years at a major tech company, founded Lattice 8 months ago.
|
||
- Semantic search: finds a conference talk she gave on "Why Developer Tools Are Stuck in 2015."
|
||
- Saves everything to `people/.raw/lena-kovac.json`
|
||
- Updates the brain page with full Tier 1 depth: beliefs, trajectory, what she's building, assessment, network connections.
|
||
|
||
**Hour 40 — Morning briefing cron (Thursday, 7:30 AM)**
|
||
|
||
The morning briefing cron builds the daily prep. It reads the calendar: meeting with Lena Kovac at 2 PM. It reads `people/lena-kovac.md` — which is now a rich page.
|
||
|
||
Produces a prep note in the daily briefing:
|
||
|
||
> **2:00 PM — Lena Kovac (Lattice)**
|
||
> Building a developer tools startup focused on compiler-driven UX. Stanford CS, 4 years on compilers at [major tech co]. Founded Lattice 8 months ago.
|
||
> She replied to your devtools post on X last Tuesday (the technical one about compiler-driven UX that got traction). Then emailed the next morning — "loved your talk, want to chat about what we're building."
|
||
> Her public writing argues that developer tools are stuck in a 2015 paradigm and that compiler intelligence should drive the entire editing experience. She gave a talk on this at DevTools Summit.
|
||
> 2 mutual connections. She's technical, has founder energy, and is building in a space you care about.
|
||
|
||
**The compound effect:** Lena went from unknown → thin Tier 3 page → substantive Tier 2 page → rich Tier 1 page → meeting prep note. Four cron runs over 48 hours. Zero manual enrichment requests. The user walks into the meeting knowing exactly who Lena is, what she cares about, and why she reached out — because every pipeline is wired to call enrich, and enrich knows how to escalate tier based on relationship signals.
|
||
|
||
This is the core insight of the brain system: **knowledge compounds autonomously when the plumbing is wired correctly.** Each cron job doesn't just do its own job — it feeds the enrichment pipeline, which feeds every future cron job. The meeting ingestion cron creates pages that the morning briefing cron reads. The email monitor enriches people that the social radar first detected. The whole system is a flywheel.
|
||
|
||
## Ingest Workflows
|
||
|
||
These are the specific ingest patterns. Each one calls enrich as its terminal step.
|
||
|
||
### Meeting ingestion
|
||
|
||
After every meeting (via Circleback, Otter, Fireflies, or manual notes):
|
||
|
||
1. Pull meeting notes + full transcript
|
||
2. Create a brain meeting page with **your own analysis** (not just regurgitated AI summary) — reframe through what you know about the attendees' world
|
||
3. **Propagate to entity pages** — call enrich for every person and company discussed. A meeting is NOT fully ingested until entity pages are updated.
|
||
4. Extract action items to task list
|
||
5. Commit
|
||
|
||
### Email ingestion
|
||
|
||
When processing email:
|
||
- Extract people and companies mentioned
|
||
- Call enrich with email context (tone, requests, relationship signals)
|
||
- Note scheduling, commitments, follow-ups
|
||
|
||
### Social media ingestion
|
||
|
||
When monitoring social media:
|
||
- Capture what people you track are saying publicly (beliefs, projects, opinions)
|
||
- Detect engagement patterns (who's replying to you, who's amplifying you)
|
||
- Call enrich for notable accounts → feed into "What They Believe" and "Hobby Horses" sections
|
||
|
||
### Manual ingestion
|
||
|
||
When you mention someone or something in conversation:
|
||
- Your own comments are the highest-value signal — always capture these
|
||
- "Really sharp on the technical side, could be a good advisor for the infra project" → that goes in the person's page immediately
|
||
- If the brain page is thin, trigger a full enrichment
|
||
|
||
## Navigation and Concurrency
|
||
|
||
**index.md** — content catalog. Every page listed with a one-line summary. Useful for navigation and query routing.
|
||
|
||
**log.md** — chronological record of ingests and updates. Append-only.
|
||
|
||
At scale (500+ pages), add search tooling (embeddings, BM25, or tools like gbrain). At moderate scale, grep works well.
|
||
|
||
### Write hotspots and concurrency
|
||
|
||
Once you have cron jobs, ingest jobs, and sub-agents all touching the brain repo, **index.md and log.md become merge-conflict magnets.** Every workflow wants to append to log.md and update index.md on every commit.
|
||
|
||
Practical mitigations:
|
||
- **Treat index.md as derived, not hand-maintained.** Instead of updating it in every ingest workflow, rebuild it periodically (daily or on-demand) by scanning the directory tree. This eliminates it as a write hotspot.
|
||
- **Make log.md append-safe.** Each entry is a self-contained line with a timestamp prefix. Concurrent appends to the end of the file rarely conflict. If they do, both sides are correct — just keep both lines.
|
||
- **Commit in batches, not per-page.** When an ingest job updates 10 entity pages, commit once at the end, not 10 times. This reduces conflict surface.
|
||
- **Pull before push.** Every workflow should `git pull --rebase` before pushing. With append-only log and independent entity pages, rebases almost always auto-resolve.
|
||
- **Entity pages rarely conflict.** Two workflows updating `people/jane-doe.md` at the same time is rare because they're triggered by different signals about different people. The real conflict hotspots are the shared files (index.md, log.md), which is why those should be append-only or derived.
|
||
|
||
## Maintenance (Lint)
|
||
|
||
Periodically (weekly), the agent should:
|
||
- **Deduplication scan:** Look for potential duplicate pages — similar names, same company, same email across different pages. Merge when confirmed.
|
||
- **Contradictions:** Check for conflicting facts between pages (e.g., two pages listing different roles for the same person at the same company).
|
||
- **Staleness:** Flag State sections superseded by newer Timeline entries.
|
||
- **Orphans:** Find pages with no inbound links.
|
||
- **Open Threads:** Check for items that seem resolved but weren't moved to Timeline.
|
||
- **Missing cross-references:** Entity A mentions Entity B but doesn't link to their page.
|
||
- **Missing pages:** Entities mentioned frequently but lacking their own page.
|
||
- **MECE filing:** Flag any pages that seem to be in the wrong directory.
|
||
- **Source audit:** Check people pages for unsourced claims in high-value sections (Beliefs, Motivations, Assessment). Flag claims without source type or date.
|
||
- **Alias coverage:** Check if recent meeting transcripts or emails contain name variants not yet in any page's aliases field.
|
||
|
||
## What makes this different from RAG
|
||
|
||
RAG re-derives knowledge from scratch on every query. The brain pre-computes synthesis and keeps it current. Specifically:
|
||
|
||
- **Cross-references are pre-built.** You don't need the LLM to discover that Person A works at Company B and was in Meeting C — that's already linked.
|
||
- **Contradictions are pre-flagged.** When new data conflicts with old data, the agent resolves or flags it during ingest, not at query time.
|
||
- **The compilation is persistent.** Each source ingested makes the brain richer. Nothing is thrown away or re-derived.
|
||
- **The structure itself is a prompt.** Empty sections ("What They Believe: [No data yet]") tell the agent what to look for next.
|
||
|
||
## Page Lifecycle
|
||
|
||
Brain pages can have implicit lifecycle states:
|
||
|
||
- **Active:** Current, recently updated, ongoing relationship or relevance
|
||
- **Dormant:** Not updated in 6+ months, relationship cooled, but still potentially relevant
|
||
- **Archived:** Moved to `archive/` — dead companies, ended relationships, resolved deals. Historical record only.
|
||
- **Graduated:** For ideas that became projects, or projects that became programs — the old page links to the new one
|
||
|
||
During lint passes, flag pages that haven't been updated in 6+ months for review. Some should be archived; others just need a fresh enrichment pass.
|
||
|
||
## What makes a great brain
|
||
|
||
A great brain lets you walk into any meeting, call, or decision already knowing:
|
||
1. Who this person is and what they care about (30 seconds of reading)
|
||
2. What the company's actual state is (not what they said 6 months ago)
|
||
3. What open threads exist between you (promises, follow-ups, deals)
|
||
4. What changed recently (latest timeline entries)
|
||
5. What to watch for (patterns, concerns, opportunities)
|
||
|
||
A bad brain is a pile of LinkedIn scrapes and meeting transcripts nobody reads. A good brain is compiled context that makes you more effective in every interaction.
|
||
|
||
## The Resolver
|
||
|
||
When creating or filing a new page, walk this decision tree. Every piece of knowledge has exactly one home.
|
||
|
||
### Decision Tree
|
||
|
||
**Start here: what is the primary subject?**
|
||
|
||
1. **A specific named person** → `people/`
|
||
2. **A specific organization** (company, fund, nonprofit, government body) → `companies/`
|
||
3. **A financial transaction** with terms and a decision to make → `deals/`
|
||
4. **A record of a specific meeting/call** that happened at a specific time → `meetings/`
|
||
5. **Something being actively built** (has a repo, spec, team, or active work) → `projects/`
|
||
6. **A raw possibility** that nobody is building yet → `ideas/`
|
||
7. **A reusable mental model or thesis** about how the world works → `concepts/`
|
||
8. **A piece of prose** that could be published as a standalone work → `writing/`
|
||
9. **Your institution's strategy, org, processes, internal dynamics** → `org/`
|
||
10. **Political or civic landscape** — policy, legislation, elections, government → `civic/`
|
||
11. **Public narrative or content operations** — social monitoring, content pipeline, published posts → `media/`
|
||
12. **A major life program** — an enduring domain of commitment containing multiple projects → `programs/`
|
||
13. **Domestic operations** — properties, logistics, household management → `household/`
|
||
14. **Private notes** — health, personal reflections, inner life → `personal/`
|
||
15. **A hiring pipeline** — candidate evaluations, role specs, interview notes → `hiring/`
|
||
16. **A reusable LLM prompt** — templates for getting specific outputs from models → `prompts/`
|
||
17. **A raw data import or snapshot** — bulk exports, API dumps, periodic captures → `sources/`
|
||
18. **Agent deliverables** — briefings, digests, and research produced by your agent → `agent/`
|
||
19. **Unsorted / quick capture** — you don't know where it goes yet → `inbox/`
|
||
20. **Dead / no longer relevant** — historical pages with no active references → `archive/`
|
||
|
||
### Disambiguation Rules
|
||
|
||
When two directories seem to fit, apply these tiebreakers:
|
||
|
||
- **Person vs. Company:** If the page is about *them as a human* (beliefs, relationship, trajectory), it's people/. If it's about *the organization they run*, it's companies/. Both pages link to each other.
|
||
- **Concept vs. Idea:** Could you *teach* it to someone as a framework? Concept. Could you *build* it? Idea.
|
||
- **Concept vs. Personal:** Would you share it in a professional talk? Concept. Is it private reflection? Personal.
|
||
- **Idea vs. Project:** Is anyone working on it? If yes, project. If no, idea. The graduation moment is when work starts.
|
||
- **Writing vs. Concepts:** Concepts are distilled (200 words of compiled truth). Writing is developed prose (argument, narrative, story).
|
||
- **Writing vs. Media:** Writing is the *artifact*. Media is the *production and distribution infrastructure*.
|
||
- **Org vs. Programs:** org/ is institutional knowledge *about* your organization. programs/ is about your personal role and priorities within it.
|
||
- **Civic vs. People:** Political figures get people/ pages. Their legislative agenda and political positioning as civic actors goes in civic/.
|
||
- **Household vs. Personal:** If a PA would execute on it, it's household (operational). If it's private reflection, it's personal (inner life).
|
||
- **Sources vs. .raw/ sidecars:** Per-entity enrichment data → .raw/ sidecar next to the entity. Bulk multi-entity imports → sources/.
|
||
- **Agent vs. Sources:** Sources feed *into* the brain. Agent deliverables are synthesized output that feeds *into your reading*.
|
||
|
||
### Special directories (not knowledge)
|
||
|
||
These exist in the brain repo but aren't knowledge directories:
|
||
|
||
- **templates/** — page templates for each type (structural, not content)
|
||
- **attachments/** — binary attachments (images, PDFs). Managed by your editor, not by the agent.
|
||
|
||
### MECE Check
|
||
|
||
Every piece of knowledge should pass through the decision tree above and land in exactly one directory. If you find something that genuinely doesn't fit any category, file it in inbox/ and flag it — that's a signal the schema needs to evolve.
|
||
|
||
## Getting started
|
||
|
||
1. Create the directory structure above (or let your agent create it)
|
||
2. Write a `RESOLVER.md` decision tree and a `README.md` resolver for each directory
|
||
3. Write a `schema.md` with your page conventions and templates
|
||
4. Add the brain rules to your agent's config (AGENTS.md or equivalent) as hard rules
|
||
5. Start with one meeting transcript or one person you want to track
|
||
6. Let the agent build the first few pages, review them, and iterate on the schema
|
||
7. Wire up your meeting tool to trigger ingestion
|
||
8. Wire up enrichment to fire on every new person/company signal
|
||
9. The brain compounds from there
|
||
|
||
The human's job: curate sources, direct analysis, ask good questions, and think about what it all means. The agent's job: everything else.
|
||
|
||
---
|
||
|
||
## docs/guides/live-sync.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/live-sync.md
|
||
|
||
# Live Sync: Keep the Index Current
|
||
|
||
## Goal
|
||
|
||
Every markdown change in the brain repo is searchable within minutes, automatically, with no manual intervention.
|
||
|
||
## What the User Gets
|
||
|
||
Without this: you correct a hallucination in a brain page, but the vector DB
|
||
keeps serving the old text because nobody ran `gbrain sync`. Stale search
|
||
results erode trust. The brain becomes unreliable.
|
||
|
||
With this: edits show up in search within minutes. The vector DB stays current
|
||
with the brain repo automatically. You never have to remember to run sync.
|
||
|
||
## Implementation
|
||
|
||
### Prerequisite: Session Mode Pooler
|
||
|
||
Sync uses `engine.transaction()` on every import. If `DATABASE_URL` points to
|
||
Supabase's **Transaction mode** pooler, sync will throw `.begin() is not a
|
||
function` and **silently skip most pages**. This is the number one cause of
|
||
"sync ran but nothing happened."
|
||
|
||
Fix: use the **Session mode** pooler string (port 6543, Session mode) or the
|
||
direct connection (port 5432, IPv6-only). Verify by running `gbrain sync` and
|
||
checking that the page count in `gbrain stats` matches the syncable file count
|
||
in the repo.
|
||
|
||
### The Primitives
|
||
|
||
Always chain sync + embed:
|
||
|
||
```bash
|
||
gbrain sync --repo /path/to/brain && gbrain embed --stale
|
||
```
|
||
|
||
- `gbrain sync --repo <path>` -- one-shot incremental sync. Detects changes via
|
||
`git diff`, imports only what changed. For small changesets (<= 100 files),
|
||
embeddings are generated inline during import.
|
||
- `gbrain embed --stale` -- backfill embeddings for any chunks that don't have
|
||
them. Safety net for large syncs (>100 files) or prior `--no-embed` runs.
|
||
- `gbrain sync --watch --repo <path>` -- foreground polling loop, every 60s
|
||
(configurable with `--interval N`). Embeds inline for small changesets. Exits
|
||
after 5 consecutive failures, so run under a process manager or pair with a
|
||
cron fallback.
|
||
|
||
### Approach 1: Cron Job (recommended)
|
||
|
||
Run every 5-30 minutes. Works with any cron scheduler.
|
||
|
||
```bash
|
||
gbrain sync --repo /data/brain && gbrain embed --stale
|
||
```
|
||
|
||
**OpenClaw:**
|
||
```
|
||
Name: gbrain-auto-sync
|
||
Schedule: */15 * * * *
|
||
Prompt: "Run: gbrain sync --repo /data/brain && gbrain embed --stale
|
||
Log the result. If sync fails with .begin() is not a function,
|
||
the DATABASE_URL is using Transaction mode pooler."
|
||
```
|
||
|
||
**Hermes:**
|
||
```
|
||
/cron add "*/15 * * * *" "Run gbrain sync --repo /data/brain &&
|
||
gbrain embed --stale. Log the result." --name "gbrain-auto-sync"
|
||
```
|
||
|
||
### Approach 2: Long-Lived Watcher
|
||
|
||
For near-instant sync (60s polling). Run under a process manager that
|
||
auto-restarts on exit. Pair with a cron fallback since `--watch` exits
|
||
on repeated failures.
|
||
|
||
```bash
|
||
gbrain sync --watch --repo /data/brain
|
||
```
|
||
|
||
### Approach 3: Git Hook / Webhook
|
||
|
||
Triggers sync on push events for instant sync (<5s).
|
||
|
||
- **GitHub webhook:** Set up the webhook to call
|
||
`gbrain sync --repo /data/brain && gbrain embed --stale`.
|
||
Verify `X-Hub-Signature-256` against a shared secret.
|
||
- **Git post-receive hook:** If the brain repo is on the same machine.
|
||
|
||
### What Gets Synced
|
||
|
||
Sync only indexes "syncable" markdown files. These are excluded by design:
|
||
- Hidden paths (`.git/`, `.raw/`, etc.)
|
||
- The `ops/` directory
|
||
- Meta files: `README.md`, `index.md`, `schema.md`, `log.md`
|
||
|
||
### Sync is Idempotent
|
||
|
||
Concurrent runs are safe. Two syncs on the same commit no-op because content
|
||
hashes match. If both a cron and `--watch` fire simultaneously, no conflict.
|
||
|
||
## Tricky Spots
|
||
|
||
1. **Always chain sync + embed.** Running `gbrain sync` without
|
||
`gbrain embed --stale` leaves new chunks without embeddings. They exist
|
||
in the database but are invisible to vector search. Always run both
|
||
commands together. The `&&` ensures embed only runs if sync succeeds.
|
||
|
||
2. **--watch polls, it doesn't stream.** The `--watch` flag polls every 60s
|
||
(configurable). It is not a filesystem watcher or git hook. It exits after
|
||
5 consecutive failures, so it needs a process manager (systemd, pm2) or a
|
||
cron fallback to stay alive. Don't assume it runs forever.
|
||
|
||
3. **Webhook needs the server running.** If you use a GitHub webhook for
|
||
instant sync, the receiving server must be running and reachable. If the
|
||
server is down when a push happens, that sync is missed. Pair webhooks
|
||
with a cron fallback that catches anything the webhook missed.
|
||
|
||
## How to Verify
|
||
|
||
1. **Edit a file and search for the change.** Edit a brain markdown file,
|
||
commit, and push. Wait for the next sync cycle (cron interval or `--watch`
|
||
poll). Run `gbrain search "<text from the edit>"`. The updated content
|
||
should appear in results. If it returns old content, sync failed.
|
||
|
||
2. **Compare page count to file count.** Run `gbrain stats` and count the
|
||
syncable markdown files in the brain repo. The page count in the database
|
||
should match. If they diverge, files are being silently skipped (likely
|
||
a Transaction mode pooler issue).
|
||
|
||
3. **Check embedded chunk count.** In `gbrain stats`, the embedded chunk
|
||
count should be close to the total chunk count. A large gap means
|
||
`gbrain embed --stale` isn't running after sync, leaving chunks invisible
|
||
to vector search.
|
||
|
||
---
|
||
|
||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||
|
||
---
|
||
|
||
## docs/guides/cron-schedule.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/cron-schedule.md
|
||
|
||
# Reference Cron Schedule
|
||
|
||
## Goal
|
||
|
||
A production brain runs 20+ recurring jobs that keep it alive, current, and
|
||
compounding. This guide shows the schedule, the patterns, and how to set it up.
|
||
|
||
## What the User Gets
|
||
|
||
Without this: the brain only updates when you manually ingest data. Pages go
|
||
stale, entities are thin, citations break, and the agent answers from old context.
|
||
|
||
With this: the brain maintains itself. Email, social, calendar, and meetings
|
||
flow in automatically. Thin pages get enriched overnight. Broken citations get
|
||
fixed. You wake up and the brain is smarter than when you went to sleep.
|
||
|
||
## The Schedule
|
||
|
||
| Frequency | Job | Brain Interaction | Recipe |
|
||
|-----------|-----|-------------------|--------|
|
||
| Every 30 min | Email monitoring | Search sender, update people pages | [email-to-brain](../../recipes/email-to-brain.md) |
|
||
| Every 30 min | X/Twitter collection | Create/update media pages, entity extraction | [x-to-brain](../../recipes/x-to-brain.md) |
|
||
| 3x/day (weekdays) | Meeting sync | Full ingestion + attendee propagation | [meeting-sync](../../recipes/meeting-sync.md) |
|
||
| Weekly | Calendar sync | Daily files + attendee enrichment | [calendar-to-brain](../../recipes/calendar-to-brain.md) |
|
||
| Daily AM | Morning briefing | Search calendar attendees, deal status, active threads | [briefing skill](../../skills/briefing/SKILL.md) |
|
||
| Weekly | Brain maintenance | `gbrain doctor`, embed stale, orphan detection | [maintain skill](../../skills/maintain/SKILL.md) |
|
||
| Nightly | Dream cycle | Entity sweep, enrich thin spots, fix citations | See below |
|
||
|
||
## Implementation: Setting Up Cron Jobs
|
||
|
||
```bash
|
||
# Email collector — every 30 minutes
|
||
*/30 * * * * cd /path/to/email-collector && node email-collector.mjs collect && node email-collector.mjs digest
|
||
|
||
# X/Twitter collector — every 30 minutes
|
||
*/30 * * * * cd /path/to/x-collector && node x-collector.mjs collect >> /tmp/x-collector.log 2>&1
|
||
|
||
# Meeting sync — 10 AM, 4 PM, 9 PM on weekdays
|
||
0 10,16,21 * * 1-5 cd /path/to/meeting-sync && node meeting-sync.mjs >> /tmp/meeting-sync.log 2>&1
|
||
|
||
# Calendar sync — Sundays at 10 AM
|
||
0 10 * * 0 cd /path/to/calendar-sync && node calendar-sync.mjs --start $(date -v-7d +%Y-%m-%d) --end $(date +%Y-%m-%d)
|
||
|
||
# Brain health — weekly Mondays at 6 AM
|
||
0 6 * * 1 gbrain doctor --json >> /tmp/gbrain-health.log 2>&1 && gbrain embed --stale
|
||
|
||
# Dream cycle — nightly at 2 AM
|
||
0 2 * * * /path/to/dream-cycle.sh
|
||
```
|
||
|
||
### Quiet Hours Gate (MANDATORY)
|
||
|
||
Every cron job that sends notifications MUST check quiet hours first.
|
||
See [Quiet Hours](quiet-hours.md) for the full pattern.
|
||
|
||
```bash
|
||
# In every cron script:
|
||
if ! bash scripts/quiet-hours-gate.sh; then
|
||
mkdir -p /tmp/cron-held
|
||
echo "$OUTPUT" > /tmp/cron-held/$(basename "$0" .sh).md
|
||
exit 0
|
||
fi
|
||
# Not quiet hours — send normally
|
||
```
|
||
|
||
### Travel-Aware Timezone Handling
|
||
|
||
The agent reads your calendar for flights, hotels, and out-of-office blocks to
|
||
infer your current location and timezone. All times shown in YOUR local timezone.
|
||
|
||
```
|
||
// Example: user flew to Tokyo
|
||
// 2 PM Pacific = 3 AM Tokyo = quiet hours
|
||
// Hold the notification, fold into morning briefing
|
||
|
||
get_user_timezone():
|
||
calendar = gbrain search "flight" --type calendar --recent 7d
|
||
if recent_flight:
|
||
return infer_timezone(flight.destination)
|
||
return config.default_timezone // fallback: US/Pacific
|
||
```
|
||
|
||
When you travel: cron jobs that would fire during your waking hours at home but
|
||
hit your sleeping hours at the destination get held and folded into the next
|
||
morning briefing. Zero config change needed.
|
||
|
||
## The Dream Cycle
|
||
|
||
The most important cron job. Runs while you sleep.
|
||
|
||
### What It Does
|
||
|
||
```
|
||
dream_cycle():
|
||
// Phase 1: Entity Sweep
|
||
conversations = get_todays_conversations()
|
||
for message in conversations:
|
||
entities = detect_entities(message)
|
||
for entity in entities:
|
||
page = gbrain search "{entity.name}"
|
||
if not page:
|
||
create_page(entity) // new entity, create + enrich
|
||
elif page.is_thin():
|
||
enrich_page(entity) // thin page, fill it out
|
||
else:
|
||
update_timeline(entity) // existing page, add today's mentions
|
||
|
||
// Phase 2: Fix Broken Citations
|
||
pages = gbrain list --type person --limit 100
|
||
for page in pages:
|
||
for entry in page.timeline:
|
||
if not entry.has_source_attribution():
|
||
fix_citation(entry) // add [Source: ...] where missing
|
||
if entry.has_tweet_url() and not entry.url_is_valid():
|
||
fix_url(entry) // broken tweet links
|
||
|
||
// Phase 3: Consolidate Memory
|
||
patterns = detect_patterns_across_conversations()
|
||
for pattern in patterns:
|
||
promote_to_memory(pattern) // ephemeral → durable knowledge
|
||
|
||
// Phase 4: Sync
|
||
gbrain sync --no-pull --no-embed
|
||
gbrain embed --stale
|
||
```
|
||
|
||
### Setting Up the Dream Cycle
|
||
|
||
**OpenClaw:** Ships with DREAMS.md as a default skill. Three phases (light,
|
||
deep, REM) run automatically during quiet hours.
|
||
|
||
**Hermes Agent:**
|
||
```bash
|
||
/cron add "0 2 * * *" "Dream cycle: search today's sessions for
|
||
entities I mentioned. For each person, company, or idea: check
|
||
if a brain page exists (gbrain search), create or update it if
|
||
thin. Fix any broken citations. Then consolidate: read MEMORY.md,
|
||
promote important signals, remove stale entries."
|
||
--name "nightly-dream-cycle"
|
||
```
|
||
|
||
**Claude Code / Custom agents:** Create a script:
|
||
```bash
|
||
#!/bin/bash
|
||
# dream-cycle.sh
|
||
|
||
# Check quiet hours (should be quiet — that's when we run)
|
||
echo "Dream cycle starting at $(date)"
|
||
|
||
# Phase 1: Entity sweep (spawn sub-agent)
|
||
# Read today's conversation logs, extract entities, update brain
|
||
|
||
# Phase 2: Citation hygiene
|
||
gbrain doctor --json | jq '.checks[] | select(.status=="warn")'
|
||
|
||
# Phase 3: Embed any stale content
|
||
gbrain embed --stale
|
||
|
||
echo "Dream cycle complete at $(date)"
|
||
```
|
||
|
||
## Tricky Spots
|
||
|
||
1. **The dream cycle is NOT optional.** Without it, signal leaks out of every
|
||
conversation. With it, nothing is lost. This is the difference between an
|
||
agent that forgets and one that remembers.
|
||
|
||
2. **Quiet hours gate on EVERY notification job.** If you skip it, the user
|
||
gets pinged at 3 AM. One 3 AM ping and they'll disable the whole system.
|
||
|
||
3. **Don't over-cron.** 20+ jobs sounds like a lot. Start with: email (30 min),
|
||
dream cycle (nightly), brain health (weekly). Add more as you add
|
||
integration recipes.
|
||
|
||
4. **Timezone changes are automatic.** Don't make the user reconfigure cron
|
||
when they travel. Read the calendar, infer the timezone, adjust delivery.
|
||
|
||
5. **Held messages MUST be picked up.** If quiet hours hold a notification,
|
||
the morning briefing MUST include it. Otherwise information is lost.
|
||
|
||
## How to Verify
|
||
|
||
1. **Quiet hours:** Set quiet hours to current hour. Run a notification cron.
|
||
Verify output went to `/tmp/cron-held/`, not to messaging.
|
||
2. **Dream cycle:** Run the dream cycle manually. Check that thin entity pages
|
||
got enriched and broken citations were fixed.
|
||
3. **Email collector cron:** Wait 30 minutes. Check `data/digests/` for new digest.
|
||
4. **Morning briefing:** Check that held messages appear in the briefing.
|
||
5. **Health check:** Run `gbrain doctor --json`. All checks should pass.
|
||
|
||
---
|
||
|
||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md). See also: [Quiet Hours](quiet-hours.md), [Operational Disciplines](operational-disciplines.md)*
|
||
|
||
---
|
||
|
||
## docs/guides/minions-deployment.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/minions-deployment.md
|
||
|
||
# Minions Worker Deployment Guide
|
||
|
||
Keep `gbrain jobs work` running across crashes, reboots, and Postgres
|
||
connection blips. Written for agents to execute line-by-line.
|
||
|
||
## The problem
|
||
|
||
The persistent worker can die silently from:
|
||
|
||
- Database connection drops (Supabase/Postgres maintenance or network blips).
|
||
- Lock-renewal failures → the stall detector eventually dead-letters jobs.
|
||
- Bun process crashes with no automatic restart.
|
||
- Internal event-loop death (PID alive, worker loop stopped).
|
||
|
||
When the worker dies, submitted jobs sit in `waiting` forever. The
|
||
canonical answer is `gbrain jobs supervisor` — a first-class CLI that
|
||
spawns `gbrain jobs work` as a child and auto-restarts it on crash.
|
||
|
||
## Worker supervision
|
||
|
||
### The canonical pattern
|
||
|
||
`gbrain jobs supervisor` is an auto-restarting wrapper around
|
||
`gbrain jobs work`. It writes a PID file, restarts the worker on crash
|
||
with exponential backoff (1s → 60s cap), emits lifecycle events to an
|
||
audit file, and drains gracefully on SIGTERM (35s worker-drain window
|
||
before SIGKILL). Exit codes are documented so agents can branch on them.
|
||
|
||
**Typical commands:**
|
||
|
||
```bash
|
||
# Start in the foreground (blocks; Ctrl-C to stop).
|
||
gbrain jobs supervisor --concurrency 4
|
||
|
||
# Start detached — returns {"event":"started","supervisor_pid":…} on stdout.
|
||
gbrain jobs supervisor start --detach --json
|
||
|
||
# Check liveness without reading log files.
|
||
gbrain jobs supervisor status --json
|
||
|
||
# Graceful stop (SIGTERM + drain wait + SIGKILL fallback).
|
||
gbrain jobs supervisor stop
|
||
```
|
||
|
||
**Exit codes:**
|
||
|
||
| Code | Meaning |
|
||
|---|---|
|
||
| 0 | Clean shutdown (SIGTERM/SIGINT received, worker drained) |
|
||
| 1 | Max crashes exceeded (worker kept dying) |
|
||
| 2 | Another supervisor holds the PID lock |
|
||
| 3 | PID file unwritable (permission / path error) |
|
||
|
||
An agent seeing exit=2 can safely treat it as "one is already running";
|
||
exit=1 should page a human.
|
||
|
||
### Which supervisor when?
|
||
|
||
The supervisor solves in-process crash recovery. Platform-level
|
||
supervision (systemd, Fly, Render) handles host-level failures. You
|
||
usually want both.
|
||
|
||
| Environment | Recommendation |
|
||
|---|---|
|
||
| **Container (Fly / Railway / Render / Heroku)** | `gbrain jobs supervisor` runs as PID 1. The platform restarts the container on OOM / host loss; supervisor restarts the worker on crash. See [Fly.io](#flyio) / [Render / Railway / Heroku](#render--railway--heroku). |
|
||
| **Linux VM with systemd** | Two-layer recommended: systemd supervises `gbrain jobs supervisor`, which in turn supervises `gbrain jobs work`. Buys you automatic restart on reboot (systemd) plus fast crash recovery (supervisor). See [systemd](#systemd). |
|
||
| **Dev laptop / macOS** | `gbrain jobs supervisor` in a terminal. Ctrl-C stops it. No system-level setup needed. |
|
||
|
||
### Variables used in this guide
|
||
|
||
Substitute these once before copy-pasting any snippet.
|
||
|
||
| Variable | Meaning | Typical value |
|
||
|---|---|---|
|
||
| `$GBRAIN_BIN` | Absolute path to the `gbrain` binary | `$(command -v gbrain)` — often `/usr/local/bin/gbrain` or `~/.bun/bin/gbrain` |
|
||
| `$GBRAIN_WORKER_USER` | OS user that owns the worker process | the same user that ran `gbrain init`; never `root` |
|
||
| `$GBRAIN_WORKSPACE` | `cwd` for shell jobs submitted by this deployment | absolute path, e.g. `/srv/my-brain` |
|
||
| `$GBRAIN_ENV_FILE` | Secrets file sourced by systemd / shell | `/etc/gbrain.env` (mode 600) |
|
||
|
||
### Preconditions
|
||
|
||
Run these before any deployment step.
|
||
|
||
```bash
|
||
# 1. gbrain is on PATH and resolves to an absolute location.
|
||
command -v gbrain || { echo "gbrain not on PATH. Install, then retry."; exit 1; }
|
||
|
||
# 2. DATABASE_URL points at reachable Postgres.
|
||
# (Supervisor is Postgres-only. PGLite's exclusive file lock blocks the
|
||
# separate worker process. If `config.engine === 'pglite'` the CLI rejects
|
||
# with a clear error.)
|
||
gbrain doctor --fast --json | jq '.checks[] | select(.name=="db_connectivity")'
|
||
|
||
# 3. Schema is up to date. If version=0 or status=="fail":
|
||
# gbrain apply-migrations --yes
|
||
gbrain doctor --fast --json | jq '.checks[] | select(.name=="schema_version")'
|
||
|
||
# 4. If you plan to submit `shell` jobs, pass --allow-shell-jobs to the
|
||
# supervisor (or export GBRAIN_ALLOW_SHELL_JOBS=1 before starting).
|
||
# Without the flag, the shell handler is disabled at worker startup.
|
||
```
|
||
|
||
## Agent usage (OpenClaw / Hermes / Cursor / Codex)
|
||
|
||
Three-command pattern an agent can drive without shell archaeology:
|
||
|
||
```bash
|
||
# Start (returns PIDs + pid_file on stdout as JSON, then detaches)
|
||
gbrain jobs supervisor start --detach --json
|
||
# → {"event":"started","supervisor_pid":1234,"worker_pid":1235,"pid_file":"/Users/you/.gbrain/supervisor.pid"}
|
||
|
||
# Check health (machine-parseable JSON, no log scraping)
|
||
gbrain jobs supervisor status --json
|
||
# → {"running":true,"supervisor_pid":1234,"last_start":"2026-04-23T15:30:22Z","crashes_24h":0, ...}
|
||
|
||
# Stop cleanly (SIGTERM + 35s drain + SIGKILL fallback)
|
||
gbrain jobs supervisor stop
|
||
```
|
||
|
||
Every lifecycle event (spawn, crash, backoff, health warning, max-crashes,
|
||
shutdown) is also written to `${GBRAIN_AUDIT_DIR:-~/.gbrain/audit}/supervisor-YYYY-Www.jsonl`
|
||
for historical inspection. `gbrain doctor` reads that file and surfaces
|
||
a `supervisor` check in its health report.
|
||
|
||
## Deployment: systemd
|
||
|
||
For long-running Linux VMs with shell access.
|
||
|
||
```bash
|
||
# Create the worker user if it doesn't exist.
|
||
sudo useradd --system --home "$GBRAIN_WORKSPACE" --shell /usr/sbin/nologin gbrain \
|
||
2>/dev/null || true
|
||
sudo mkdir -p "$GBRAIN_WORKSPACE" && sudo chown gbrain:gbrain "$GBRAIN_WORKSPACE"
|
||
|
||
# Install the env file (secrets stay out of the unit file).
|
||
sudo install -m 600 -o gbrain -g gbrain \
|
||
docs/guides/minions-deployment-snippets/gbrain.env.example /etc/gbrain.env
|
||
sudoedit /etc/gbrain.env
|
||
# Fill in DATABASE_URL, optional GBRAIN_ALLOW_SHELL_JOBS=1.
|
||
|
||
# Install the unit file, substituting /srv/gbrain → your workspace path.
|
||
sudo install -m 644 docs/guides/minions-deployment-snippets/systemd.service \
|
||
/etc/systemd/system/gbrain-worker.service
|
||
sudo sed -i "s|/srv/gbrain|$GBRAIN_WORKSPACE|g" \
|
||
/etc/systemd/system/gbrain-worker.service
|
||
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now gbrain-worker
|
||
sudo systemctl status gbrain-worker
|
||
journalctl -u gbrain-worker -n 50
|
||
```
|
||
|
||
The shipped unit file invokes `gbrain jobs supervisor` (not `gbrain jobs work`
|
||
directly) so you get two-layer supervision: systemd restarts the supervisor
|
||
on host reboot, supervisor restarts the worker on in-process crash.
|
||
|
||
`Restart=always` + `RestartSec=10s` handle the supervisor-level recovery.
|
||
The unit runs as unprivileged `gbrain` with `PrivateTmp`, `ProtectSystem=strict`,
|
||
and `ReadWritePaths=$GBRAIN_WORKSPACE,$HOME/.gbrain` (for the PID file and
|
||
audit log). `LimitNOFILE=65535` covers Bun + Postgres pool + concurrent
|
||
LLM subagent calls without hitting the default 1024 cap.
|
||
|
||
## Deployment: Fly.io
|
||
|
||
```bash
|
||
# Merge the [processes] block from fly.toml.partial into your fly.toml.
|
||
cat docs/guides/minions-deployment-snippets/fly.toml.partial >> fly.toml
|
||
# Review + edit as needed.
|
||
|
||
# Set secrets (Fly handles restart on crash).
|
||
fly secrets set DATABASE_URL='postgres://…' GBRAIN_ALLOW_SHELL_JOBS=1
|
||
```
|
||
|
||
The `[processes]` block runs `gbrain jobs supervisor` as PID 1. Fly
|
||
restarts the container on host failure; the supervisor restarts the
|
||
worker on in-process crash.
|
||
|
||
## Deployment: Render / Railway / Heroku
|
||
|
||
Drop [`Procfile`](./minions-deployment-snippets/Procfile) at the repo
|
||
root. The shipped Procfile calls `gbrain jobs supervisor`. Set
|
||
`DATABASE_URL` + optional `GBRAIN_ALLOW_SHELL_JOBS=1` via the platform's
|
||
env UI or CLI.
|
||
|
||
## Deployment: inline `--follow` (no persistent worker)
|
||
|
||
For short deterministic scripts on a fixed schedule where you don't need
|
||
a persistent worker between runs. Each cron run brings its own temporary
|
||
worker. `--follow` starts one on the queue and blocks until the
|
||
just-submitted job reaches a terminal state (`completed` / `failed` /
|
||
`dead` / `cancelled`). 2-3 s startup overhead per job; negligible vs job
|
||
duration for scheduled work.
|
||
|
||
```bash
|
||
GBRAIN_ALLOW_SHELL_JOBS=1 gbrain jobs submit shell \
|
||
--queue nightly-enrich \
|
||
--params "{\"cmd\":\"$GBRAIN_BIN embed --stale\",\"cwd\":\"$GBRAIN_WORKSPACE\"}" \
|
||
--follow \
|
||
--timeout-ms 600000
|
||
```
|
||
|
||
Replace `gbrain embed --stale` with whichever gbrain subcommand you're
|
||
scheduling (`sync`, `extract`, `orphans`, `doctor`, `check-backlinks`,
|
||
`lint`, `autopilot`). For strict single-job semantics on shared queues,
|
||
use a dedicated queue name like `nightly-enrich` above.
|
||
|
||
## Upgrading from an older deployment
|
||
|
||
### From `minion-watchdog.sh` (pre-v0.20)
|
||
|
||
Earlier versions of this guide shipped a 68-line bash watchdog
|
||
(`minion-watchdog.sh`). It's been replaced by `gbrain jobs supervisor`
|
||
which handles everything the script did, plus atomic PID locking,
|
||
structured audit events, queue-scoped health checks, and graceful
|
||
drain on SIGTERM.
|
||
|
||
**Migration:**
|
||
|
||
```bash
|
||
# 1. Stop and remove the old watchdog.
|
||
sudo kill $(head -n1 /tmp/gbrain-worker.pid) 2>/dev/null
|
||
sudo rm -f /usr/local/bin/minion-watchdog.sh /tmp/gbrain-worker.pid \
|
||
/tmp/gbrain-worker.log
|
||
crontab -e # delete the "*/5 * * * * /usr/local/bin/minion-watchdog.sh" line
|
||
|
||
# 2. Start the supervisor (systemd users: reinstall the unit from
|
||
# docs/guides/minions-deployment-snippets/systemd.service, which
|
||
# now calls `gbrain jobs supervisor`).
|
||
gbrain jobs supervisor start --detach --json
|
||
# Or: sudo systemctl restart gbrain-worker
|
||
|
||
# 3. Verify.
|
||
gbrain jobs supervisor status --json
|
||
gbrain doctor # 'supervisor' check should report running=true
|
||
```
|
||
|
||
### Schema / migration hygiene
|
||
|
||
Regardless of which deployment path you're upgrading from:
|
||
|
||
1. **Stop the worker before upgrading.** `gbrain jobs supervisor stop`
|
||
(or `sudo systemctl stop gbrain-worker`). Skipping this risks an
|
||
in-flight job landing partial schema.
|
||
2. **Run `gbrain upgrade`**. Then `gbrain apply-migrations --yes` if
|
||
`gbrain doctor` reports any migration as `partial` or `pending`.
|
||
3. **If you run shell jobs:** from v0.14 onward, pass
|
||
`--allow-shell-jobs` to the supervisor (or keep
|
||
`GBRAIN_ALLOW_SHELL_JOBS=1` in `/etc/gbrain.env`). Submitters don't
|
||
need the flag; only the worker does.
|
||
4. **Verify.** `gbrain doctor` should report zero `pending` or `partial`
|
||
migrations plus a healthy `supervisor` check. `gbrain jobs stats`
|
||
should show no unexplained growth in `dead` between pre- and
|
||
post-upgrade.
|
||
|
||
## Known issues
|
||
|
||
### Supabase connection drops
|
||
|
||
The worker uses a single Postgres connection. If Supabase drops it
|
||
(maintenance, connection limits, network blip), lock renewal fails
|
||
silently. The stall detector then dead-letters the job after
|
||
`max_stalled` misses.
|
||
|
||
**Current defaults that make this worse:**
|
||
|
||
- `lockDuration: 30000` (30 s) — too short for long jobs during
|
||
connection blips.
|
||
- `max_stalled: 5` (schema column default — see `src/schema.sql` and
|
||
`src/core/pglite-schema.ts`). Five missed heartbeats before dead-letter.
|
||
- `stalledInterval: 30000` (30 s) — checks too aggressively.
|
||
|
||
**Tune per-job today.** `gbrain jobs submit` accepts `--max-stalled N`,
|
||
`--backoff-type fixed|exponential`, `--backoff-delay <ms>`,
|
||
`--backoff-jitter 0..1`, and `--timeout-ms N` as first-class flags
|
||
(since v0.13.1). These write onto the job row at submit time — which is
|
||
what `handleStalled()` reads — so per-job tuning is the real knob today.
|
||
|
||
### DO NOT pass `maxStalledCount` to `MinionWorker`
|
||
|
||
It's a no-op. The stall detector reads the row's `max_stalled` column
|
||
(set at submit time), not the worker opt in `src/core/minions/worker.ts:74`.
|
||
Use `gbrain jobs submit --max-stalled N` per-job instead.
|
||
|
||
### Zombie shell children
|
||
|
||
When the Bun worker crashes hard, child processes from shell jobs can
|
||
become zombies. The supervisor's SIGTERM → 35s drain → SIGKILL window
|
||
covers the shell handler's 5 s child-kill grace (`KILL_GRACE_MS`). For
|
||
long-running shell jobs, prefer timeouts via `--timeout-ms` on submit
|
||
over relying on hard kills.
|
||
|
||
## Smoke test
|
||
|
||
```bash
|
||
# Supervisor alive?
|
||
gbrain jobs supervisor status --json | jq .running
|
||
|
||
# Aggregate queue health.
|
||
gbrain jobs stats
|
||
|
||
# Jobs currently stalled (still `active` with expired lock_until, pre-requeue).
|
||
gbrain jobs list --status active --limit 10
|
||
|
||
# Dead-lettered jobs.
|
||
gbrain jobs list --status dead --limit 10
|
||
|
||
# Shell handler registered? (check supervisor audit log or worker stderr.)
|
||
gbrain jobs supervisor status --json | jq '.worker_config.allow_shell_jobs'
|
||
```
|
||
|
||
## Uninstall
|
||
|
||
**`gbrain jobs supervisor`** (foreground or `--detach`):
|
||
|
||
```bash
|
||
gbrain jobs supervisor stop
|
||
```
|
||
|
||
**systemd:**
|
||
|
||
```bash
|
||
sudo systemctl disable --now gbrain-worker
|
||
sudo rm /etc/systemd/system/gbrain-worker.service /etc/gbrain.env
|
||
sudo systemctl daemon-reload
|
||
```
|
||
|
||
**Fly / Render / Railway:** delete the `worker` process from `fly.toml`
|
||
/ `Procfile` and redeploy. Secrets set via `fly secrets` persist until
|
||
`fly secrets unset`.
|
||
|
||
**Inline `--follow`:** remove the cron entry. Nothing else to clean up
|
||
— temporary workers exit with their jobs.
|
||
|
||
---
|
||
|
||
## docs/guides/quiet-hours.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/quiet-hours.md
|
||
|
||
# Quiet Hours and Timezone-Aware Delivery
|
||
|
||
## Goal
|
||
|
||
Hold all notifications during sleep hours, merge held messages into the morning briefing, and adjust automatically when the user travels.
|
||
|
||
## What the User Gets
|
||
|
||
Without this: 3 AM pings from cron jobs. One bad notification and the user
|
||
disables the entire system.
|
||
|
||
With this: the brain works overnight (dream cycle, collectors, enrichment)
|
||
but notifications are held until morning. Travel to Tokyo? The system adjusts
|
||
automatically from your calendar, no config change needed.
|
||
|
||
## Implementation
|
||
|
||
### Quiet Hours Gate
|
||
|
||
Every cron job that sends notifications must check quiet hours FIRST.
|
||
|
||
```
|
||
QUIET_START = 23 // 11 PM local time
|
||
QUIET_END = 8 // 8 AM local time
|
||
|
||
is_quiet(local_hour):
|
||
return local_hour >= QUIET_START OR local_hour < QUIET_END
|
||
```
|
||
|
||
**Before sending any notification:**
|
||
1. Determine user's current timezone (from config or heartbeat state)
|
||
2. Convert current UTC time to local time
|
||
3. If quiet hours: hold the message, don't send
|
||
|
||
### Held Messages
|
||
|
||
During quiet hours, output goes to a held directory instead of being sent:
|
||
|
||
```
|
||
if is_quiet():
|
||
mkdir -p /tmp/cron-held/
|
||
write("/tmp/cron-held/{job-name}.md", output)
|
||
exit // don't send
|
||
else:
|
||
send(output)
|
||
```
|
||
|
||
The morning briefing picks up held messages:
|
||
|
||
```
|
||
morning_briefing():
|
||
held_files = list("/tmp/cron-held/*.md")
|
||
if held_files:
|
||
briefing += "## Overnight Updates\n\n"
|
||
for file in held_files:
|
||
briefing += read(file)
|
||
delete(file)
|
||
```
|
||
|
||
This way nothing is lost. Overnight cron results get folded into the
|
||
first thing the user sees in the morning.
|
||
|
||
### Timezone Awareness
|
||
|
||
The agent should know what timezone the user is in. Store it in
|
||
the agent's operational state:
|
||
|
||
```json
|
||
{
|
||
"currentLocation": {
|
||
"timezone": "US/Pacific",
|
||
"city": "San Francisco"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Update the timezone when:**
|
||
- Calendar shows the user flying somewhere (check for airline/hotel events)
|
||
- User mentions being in a different city
|
||
- User's active hours shift (they're responding at 3 AM PT = they're probably traveling)
|
||
|
||
**All times shown to the user should be in their LOCAL timezone.** Never
|
||
show UTC or a timezone the user isn't in.
|
||
|
||
### Shell Implementation
|
||
|
||
```bash
|
||
#!/bin/bash
|
||
# quiet-hours-gate.sh — run before any notification
|
||
|
||
TIMEZONE="${USER_TIMEZONE:-US/Pacific}"
|
||
LOCAL_HOUR=$(TZ="$TIMEZONE" date +%H)
|
||
|
||
if [ "$LOCAL_HOUR" -ge 23 ] || [ "$LOCAL_HOUR" -lt 8 ]; then
|
||
echo "QUIET_HOURS=true"
|
||
exit 1 # don't send
|
||
fi
|
||
|
||
echo "QUIET_HOURS=false"
|
||
exit 0 # ok to send
|
||
```
|
||
|
||
**In cron job scripts:**
|
||
```bash
|
||
# Check quiet hours first
|
||
if ! bash scripts/quiet-hours-gate.sh; then
|
||
mkdir -p /tmp/cron-held
|
||
echo "$OUTPUT" > /tmp/cron-held/$(basename "$0" .sh).md
|
||
exit 0
|
||
fi
|
||
|
||
# Not quiet hours — send normally
|
||
send_notification "$OUTPUT"
|
||
```
|
||
|
||
### Configurable Hours
|
||
|
||
Some users want different quiet hours. Store the config:
|
||
|
||
```json
|
||
{
|
||
"quiet_hours": {
|
||
"start": 23,
|
||
"end": 8,
|
||
"enabled": true
|
||
}
|
||
}
|
||
```
|
||
|
||
Set `enabled: false` to disable quiet hours entirely (e.g., for 24/7 monitoring).
|
||
|
||
## Tricky Spots
|
||
|
||
1. **Gate on EVERY job.** The quiet hours check must run before every single
|
||
cron job that produces notifications. If even one job skips the gate, the
|
||
user gets a 3 AM ping and loses trust in the entire system. No exceptions.
|
||
|
||
2. **Held messages MUST be picked up.** If the morning briefing doesn't read
|
||
`/tmp/cron-held/`, overnight results vanish silently. Verify the briefing
|
||
skill reads and clears the held directory. Orphaned held files mean the
|
||
pickup integration is broken.
|
||
|
||
3. **Timezone auto-detection is fragile.** Calendar-based timezone detection
|
||
relies on the user having airline/hotel events with location data. If the
|
||
user books travel without calendar entries, the system won't detect the
|
||
move. Fall back to activity-hour analysis (responding at 3 AM PT = probably
|
||
not in PT anymore) and ask the user if uncertain.
|
||
|
||
## How to Verify
|
||
|
||
1. **Set quiet hours to the current hour.** Temporarily set `QUIET_START` to
|
||
one hour before now and `QUIET_END` to one hour after. Trigger a cron job.
|
||
Verify the output goes to `/tmp/cron-held/` instead of being sent.
|
||
|
||
2. **Check held message pickup.** After step 1, run or simulate the morning
|
||
briefing. Verify the held message appears in the "Overnight Updates"
|
||
section and the file is deleted from `/tmp/cron-held/`.
|
||
|
||
3. **Verify timezone adjustment.** Change the timezone config to a zone where
|
||
it's currently quiet hours. Trigger a notification. Verify it's held. Change
|
||
back to your real timezone during active hours. Trigger again. Verify it sends.
|
||
|
||
---
|
||
|
||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||
|
||
---
|
||
|
||
## docs/mcp/DEPLOY.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md
|
||
|
||
# Deploy GBrain Remote MCP Server
|
||
|
||
> **v0.26.0+:** `gbrain serve --http` ships full OAuth 2.1 (client credentials,
|
||
> auth code + PKCE, refresh rotation, optional DCR), an embedded React admin
|
||
> dashboard at `/admin`, scoped operations, and a live SSE activity feed.
|
||
> Pre-v0.26 legacy bearer tokens still work — `verifyAccessToken` falls back
|
||
> to the `access_tokens` table and grandfathers tokens to `read+write+admin`.
|
||
> Postgres-only for the legacy fallback (the `access_tokens` table is Postgres-only);
|
||
> OAuth tables work on both PGLite and Postgres. See [SECURITY.md](../../SECURITY.md)
|
||
> for env vars and tunable defaults.
|
||
|
||
Access your brain from any device, any AI client. GBrain ships two transports:
|
||
`gbrain serve` (stdio) for local agents, and `gbrain serve --http` (v0.26.0+)
|
||
for remote clients over OAuth 2.1.
|
||
|
||
## Three Paths
|
||
|
||
### Local stdio (zero setup)
|
||
|
||
```bash
|
||
gbrain serve
|
||
```
|
||
|
||
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
|
||
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
|
||
|
||
### Remote over OAuth 2.1 (recommended, v0.26.0+)
|
||
|
||
```bash
|
||
gbrain serve --http --port 3131
|
||
ngrok http 3131 --url your-brain.ngrok.app
|
||
gbrain serve --http --port 3131 --public-url https://your-brain.ngrok.app
|
||
```
|
||
|
||
Built-in HTTP transport with OAuth 2.1, scoped operations, an admin dashboard
|
||
at `/admin`, and a live SSE activity feed. Zero external dependencies. This is
|
||
the only path that works with ChatGPT (OAuth 2.1 + PKCE is required by the
|
||
ChatGPT MCP connector). Pass `--public-url` whenever the server is reachable
|
||
at anything other than `http://localhost:<port>` so the OAuth issuer in
|
||
discovery metadata matches what clients hit (RFC 8414 §3.3).
|
||
|
||
Supported clients:
|
||
- **ChatGPT** — requires OAuth 2.1 + PKCE. Works natively with `--http`.
|
||
- **Claude Desktop / Cowork** — OAuth 2.1 or legacy bearer tokens.
|
||
- **Perplexity** — OAuth 2.1 client credentials grant.
|
||
- **Claude Code, Cursor, Windsurf** — can use OAuth or legacy bearer.
|
||
|
||
See the [OAuth 2.1 setup](#oauth-21-setup-v100) section below.
|
||
|
||
### Remote with legacy bearer tokens (pre-v0.26 deployments) — Postgres only
|
||
|
||
```
|
||
Your AI client (Claude Desktop, Perplexity, etc.)
|
||
→ ngrok tunnel (https://YOUR-DOMAIN.ngrok.app)
|
||
→ gbrain serve --http (built-in transport with bearer auth)
|
||
→ Postgres (pooler connection or self-hosted)
|
||
```
|
||
|
||
This requires:
|
||
1. A Postgres-backed brain (the `access_tokens` table only exists on Postgres;
|
||
running `gbrain serve --http` against a PGLite install fails fast at startup)
|
||
2. A machine running `gbrain serve --http`
|
||
3. A public tunnel (ngrok, Tailscale, or cloud host)
|
||
4. A bearer token created via `gbrain auth create <name>`
|
||
|
||
Pre-v1.0 tokens are grandfathered as `read+write+admin` scopes when you upgrade
|
||
to the HTTP server, so no migration is required.
|
||
|
||
## OAuth 2.1 Setup (v0.26.0+)
|
||
|
||
### 1. Start the HTTP server
|
||
|
||
```bash
|
||
gbrain serve --http --port 3131
|
||
```
|
||
|
||
On first start, the server prints an **admin bootstrap token** to stderr:
|
||
|
||
```
|
||
Admin bootstrap token: 3a1f9c...
|
||
Open http://localhost:3131/admin and paste it to log in.
|
||
```
|
||
|
||
Save this token. Open `http://localhost:3131/admin` and paste it to access the
|
||
dashboard. The dashboard shows live activity, registered clients, request logs,
|
||
and per-client config export.
|
||
|
||
> **v0.26.9+:** `mcp_request_log.params` and the live SSE activity feed default
|
||
> to a redacted summary `{redacted, kind, declared_keys, unknown_key_count, approx_bytes}`.
|
||
> Declared param keys are kept (intersected against the operation's spec); unknown
|
||
> keys are counted but never named, and byte sizes round up to 1KB so size-probe
|
||
> attacks can't binary-search secret content. Operators on a personal laptop who
|
||
> want raw payloads back can pass `gbrain serve --http --log-full-params` (loud
|
||
> stderr warning fires at startup). Multi-tenant deployments should leave it on
|
||
> the redacted default.
|
||
|
||
### 2. Register OAuth clients
|
||
|
||
Register clients from the **`/admin` dashboard**:
|
||
|
||
1. Click **Register client**.
|
||
2. Enter a name (e.g. `perplexity`, `chatgpt`).
|
||
3. Pick scopes: `read`, `write`, `admin` (checkboxes).
|
||
4. Pick grant type: `client_credentials` for machine-to-machine (Perplexity,
|
||
Claude Desktop bearer mode) or `authorization_code` for browser-based
|
||
clients with PKCE (ChatGPT).
|
||
5. For `authorization_code` clients, paste the redirect URI.
|
||
6. Hit **Register**. The credential-reveal modal shows the `client_id` (and
|
||
`client_secret` for confidential clients) once. Copy or Download JSON
|
||
immediately — secrets are hashed on storage and never shown again.
|
||
|
||
Or from the CLI — faster for scripting:
|
||
|
||
```bash
|
||
gbrain auth register-client perplexity \
|
||
--grant-types client_credentials \
|
||
--scopes "read write"
|
||
```
|
||
|
||
**v0.34 — source-scoped clients.** Multi-source brains can scope a client's
|
||
write authority to one source and its read scope to a curated set with the
|
||
new `--source` and `--federated-read` flags:
|
||
|
||
```bash
|
||
gbrain auth register-client dept-x-agent \
|
||
--grant-types client_credentials \
|
||
--scopes "read write" \
|
||
--source dept-x \
|
||
--federated-read dept-x,shared,parent-canon
|
||
```
|
||
|
||
`--source` controls the write authority — `put_page` / `add_link` / etc only
|
||
land in `dept-x`. `--federated-read` controls the read axis independently;
|
||
queries return rows from any of the listed sources. Omit both flags for the
|
||
v0.33-compatible super-client shape. Pre-v0.34 clients are backfilled to
|
||
`source_id='default'` on `gbrain upgrade`.
|
||
|
||
Host-repo wrappers can register programmatically:
|
||
|
||
```ts
|
||
await oauthProvider.registerClientManual(
|
||
'perplexity',
|
||
['client_credentials'],
|
||
'read write',
|
||
[], // redirect_uris, empty for CC
|
||
);
|
||
```
|
||
|
||
For self-service client registration (Dynamic Client Registration, RFC 7591),
|
||
start the server with `--enable-dcr`. DCR is off by default.
|
||
|
||
### 3. Expose the server
|
||
|
||
**v0.34 — bind explicitly.** `gbrain serve --http` defaults to `127.0.0.1`.
|
||
To accept connections from the ngrok tunnel (or any non-loopback source),
|
||
restart with `--bind`:
|
||
|
||
```bash
|
||
gbrain serve --http --port 3131 --bind 0.0.0.0 --public-url https://your-brain.ngrok.app
|
||
```
|
||
|
||
When `--public-url` is set without `--bind`, a stderr WARN fires at
|
||
startup so the misconfiguration ("the tunnel is up but my agent gets
|
||
ECONNREFUSED") is loud.
|
||
|
||
```bash
|
||
brew install ngrok
|
||
ngrok config add-authtoken YOUR_TOKEN
|
||
ngrok http 3131 --url your-brain.ngrok.app
|
||
```
|
||
|
||
Your OAuth issuer URL becomes `https://your-brain.ngrok.app`. The MCP SDK's
|
||
router exposes the spec-compliant discovery endpoint at
|
||
`/.well-known/oauth-authorization-server`.
|
||
|
||
### 4. Scopes and localOnly
|
||
|
||
Every operation is tagged `read | write | admin`. Four operations are
|
||
`localOnly` and rejected over HTTP regardless of scope: `sync_brain`,
|
||
`file_upload`, `file_list`, `file_url`. Remote agents cannot reach local
|
||
filesystem surface area.
|
||
|
||
| Scope | What it allows |
|
||
|-------|---------------|
|
||
| `read` | `search`, `query`, `get_page`, `list_pages`, graph traversal |
|
||
| `write` | `put_page`, `delete_page`, `add_link`, `add_timeline_entry` |
|
||
| `admin` | Client management, token revocation, sweep, local-only ops |
|
||
|
||
## Legacy Bearer Token Setup
|
||
|
||
Keep using pre-v0.26 bearer tokens if you aren't ready to migrate. They
|
||
grandfather to `read+write+admin` scopes on the HTTP server.
|
||
|
||
### 1. Set up the tunnel
|
||
|
||
See the [ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for full setup.
|
||
Quick version:
|
||
|
||
```bash
|
||
brew install ngrok
|
||
ngrok config add-authtoken YOUR_TOKEN
|
||
ngrok http 8787 --url your-brain.ngrok.app # Hobby tier for fixed domain
|
||
```
|
||
|
||
### 2. Create access tokens
|
||
|
||
```bash
|
||
# Create a token for each client
|
||
gbrain auth create "claude-desktop"
|
||
|
||
# List all tokens
|
||
gbrain auth list
|
||
|
||
# Revoke a token
|
||
gbrain auth revoke "claude-desktop"
|
||
```
|
||
|
||
Tokens are per-client. Create one for each device/app. Revoke individually
|
||
if compromised. Tokens are stored SHA-256 hashed in your database.
|
||
|
||
### 3. Connect your AI client
|
||
|
||
- **ChatGPT:** [setup guide](CHATGPT.md) (OAuth 2.1 + PKCE, requires `gbrain serve --http`)
|
||
- **Claude Code:** [setup guide](CLAUDE_CODE.md)
|
||
- **Claude Desktop:** [setup guide](CLAUDE_DESKTOP.md) (must use GUI, not JSON config)
|
||
- **Claude Cowork:** [setup guide](CLAUDE_COWORK.md)
|
||
- **Perplexity:** [setup guide](PERPLEXITY.md)
|
||
|
||
### 4. Verify
|
||
|
||
```bash
|
||
gbrain auth test \
|
||
https://YOUR-DOMAIN.ngrok.app/mcp \
|
||
--token YOUR_TOKEN
|
||
```
|
||
|
||
## Operations
|
||
|
||
All 30 GBrain operations are available remotely, including `sync_brain` and
|
||
`file_upload` (no timeout limits with self-hosted server).
|
||
|
||
**Security note on `file_upload`:** remote MCP callers are confined to the working
|
||
directory where `gbrain serve` was launched. Symlinks, `..` traversal, and absolute
|
||
paths outside cwd are rejected. Page slugs and filenames are allowlist-validated
|
||
(alphanumeric + hyphens; no control chars, RTL overrides, or backslashes). Local
|
||
CLI callers (`gbrain file upload ...`) keep unrestricted filesystem access since
|
||
the user owns the machine.
|
||
|
||
## Deployment Options
|
||
|
||
See [ALTERNATIVES.md](ALTERNATIVES.md) for a comparison of ngrok, Tailscale
|
||
Funnel, and cloud hosts (Fly.io, Railway).
|
||
|
||
## Troubleshooting
|
||
|
||
**"missing_auth" error**
|
||
Include the Authorization header: `Authorization: Bearer YOUR_TOKEN`
|
||
|
||
**"invalid_token" error**
|
||
Run `gbrain auth list` to see active tokens.
|
||
|
||
**"service_unavailable" error**
|
||
Database connection failed. Check your Supabase dashboard for outages.
|
||
|
||
**Claude Desktop doesn't connect**
|
||
Remote servers must be added via Settings > Integrations, NOT
|
||
`claude_desktop_config.json`. See [CLAUDE_DESKTOP.md](CLAUDE_DESKTOP.md).
|
||
|
||
## Expected Latencies
|
||
|
||
| Operation | Typical Latency | Notes |
|
||
|-----------|----------------|-------|
|
||
| get_page | < 100ms | Single DB query |
|
||
| list_pages | < 200ms | DB query with filters |
|
||
| search (keyword) | 100-300ms | Full-text search |
|
||
| query (hybrid) | 1-3s | Embedding + vector + keyword + RRF |
|
||
| put_page | 100-500ms | Write + trigger search_vector update |
|
||
| get_stats | < 100ms | Aggregate query |
|
||
|
||
**Note:** `gbrain serve --http` shipped in v0.26.0 with OAuth 2.1 + admin
|
||
dashboard baked into the binary. The custom HTTP wrapper pattern (see
|
||
[voice recipe](../../recipes/twilio-voice-brain.md)) is still supported for
|
||
teams that need bespoke middleware, but for most remote deployments the
|
||
built-in server is the recommended path.
|
||
|
||
---
|
||
|
||
# Debugging
|
||
|
||
## docs/GBRAIN_VERIFY.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/GBRAIN_VERIFY.md
|
||
|
||
# GBrain Installation Verification Runbook
|
||
|
||
Run these checks after install to confirm every part of GBrain is working.
|
||
Each check includes the command, expected output, and what to do if it fails.
|
||
|
||
The most important check is #4 (live sync). "Sync ran" is not the same as
|
||
"sync worked." A sync that silently skips pages because of a pooler bug is
|
||
worse than no sync at all, because you think it's working.
|
||
|
||
---
|
||
|
||
## 1. Schema Verification
|
||
|
||
**Command:**
|
||
|
||
```bash
|
||
gbrain doctor --json
|
||
```
|
||
|
||
**Expected:** All checks return `"ok"`:
|
||
- `connection`: connected, N pages
|
||
- `pgvector`: extension installed
|
||
- `rls`: enabled on all tables
|
||
- `schema_version`: current
|
||
- `embeddings`: coverage percentage
|
||
|
||
**If it fails:** The doctor output includes specific fix instructions for each
|
||
check. See `skills/setup/SKILL.md` Error Recovery table.
|
||
|
||
---
|
||
|
||
## 2. Skillpack Loaded
|
||
|
||
**Check:** Ask the agent: "What is the brain-agent loop?"
|
||
|
||
**Expected:** The agent references GBRAIN_SKILLPACK.md Section 2 and describes
|
||
the read-write cycle: detect entities, read brain, respond with context, write
|
||
brain, sync.
|
||
|
||
**If it fails:** The agent hasn't loaded the skillpack. Run step 6 from the
|
||
install paste (read `docs/GBRAIN_SKILLPACK.md`).
|
||
|
||
---
|
||
|
||
## 3. Auto-Update Configured
|
||
|
||
**Command:**
|
||
|
||
```bash
|
||
gbrain check-update --json
|
||
```
|
||
|
||
**Expected:** Returns JSON with `current_version`, `latest_version`,
|
||
`update_available` (boolean). The cron `gbrain-update-check` is registered.
|
||
|
||
**If it fails:** Run step 7 from the install paste. See GBRAIN_SKILLPACK.md
|
||
Section 17.
|
||
|
||
---
|
||
|
||
## 4. Live Sync Actually Works
|
||
|
||
This is the most important check. Three parts.
|
||
|
||
### 4a. Coverage Check
|
||
|
||
Compare page count in the DB against syncable file count in the repo:
|
||
|
||
```bash
|
||
gbrain stats
|
||
```
|
||
|
||
Then count syncable files:
|
||
|
||
```bash
|
||
find /data/brain -name '*.md' \
|
||
-not -path '*/.*' \
|
||
-not -path '*/.raw/*' \
|
||
-not -path '*/ops/*' \
|
||
-not -name 'README.md' \
|
||
-not -name 'index.md' \
|
||
-not -name 'schema.md' \
|
||
-not -name 'log.md' \
|
||
| wc -l
|
||
```
|
||
|
||
**Expected:** Page count in `gbrain stats` should be close to the file count.
|
||
Some difference is normal (files added since last sync), but if page count is
|
||
less than half the file count, sync is silently skipping pages.
|
||
|
||
**If page count is way too low:** The #1 cause is the connection pooler bug.
|
||
Check your `DATABASE_URL`:
|
||
- If it contains `pooler.supabase.com:6543`, verify it's using **Session mode**,
|
||
not Transaction mode.
|
||
- Transaction mode breaks `engine.transaction()` and causes `.begin() is not a
|
||
function` errors.
|
||
- Fix: switch to Session mode pooler string, then run `gbrain sync --full`
|
||
to reimport everything.
|
||
|
||
### 4b. Embed Check
|
||
|
||
```bash
|
||
gbrain stats
|
||
```
|
||
|
||
**Expected:** Embedded chunk count should be close to total chunk count.
|
||
|
||
**If embedded is much lower than total:**
|
||
|
||
```bash
|
||
gbrain embed --stale
|
||
```
|
||
|
||
If `OPENAI_API_KEY` is not set, embeddings can't be generated. Keyword search
|
||
still works without embeddings, but hybrid/semantic search won't.
|
||
|
||
### 4c. End-to-End Test
|
||
|
||
This is the real test. Edit a brain page, push, wait, search.
|
||
|
||
1. Edit a page in the brain repo (e.g., correct a fact on a person's page):
|
||
|
||
```bash
|
||
# Example: fix a line in Gustaf's page
|
||
cd /data/brain
|
||
# Make a small edit to any .md file
|
||
git add -A && git commit -m "test: verify live sync" && git push
|
||
```
|
||
|
||
2. Wait for the next sync cycle (cron interval or `--watch` poll).
|
||
|
||
3. Search for the corrected text:
|
||
|
||
```bash
|
||
gbrain search "<text from the correction>"
|
||
```
|
||
|
||
**Expected:** The search returns the **corrected** text, not the old version.
|
||
|
||
**If it returns old text:** Sync failed silently. Check:
|
||
- Is the sync cron registered and running?
|
||
- Is `gbrain sync --watch` still alive (if using watch mode)?
|
||
- Run `gbrain config get sync.last_run` to see when sync last ran.
|
||
- Run `gbrain sync --repo /data/brain` manually and check for errors.
|
||
- If you see `.begin() is not a function`, fix the pooler (see 4a above).
|
||
|
||
---
|
||
|
||
## 5. Embedding Coverage
|
||
|
||
**Command:**
|
||
|
||
```bash
|
||
gbrain stats
|
||
```
|
||
|
||
**Expected:** Embedded chunk count matches (or is close to) total chunk count.
|
||
|
||
**If zero or very low:** `OPENAI_API_KEY` may be missing or invalid. Check:
|
||
|
||
```bash
|
||
echo $OPENAI_API_KEY | head -c 10
|
||
```
|
||
|
||
If blank, set the key. Then:
|
||
|
||
```bash
|
||
gbrain embed --stale
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Brain-First Lookup Protocol
|
||
|
||
**Check:** Ask the agent about a person or concept that exists in the brain.
|
||
|
||
**Expected:** The agent uses `gbrain search` or `gbrain query` FIRST, not grep
|
||
or external APIs. The response includes brain-sourced context with source
|
||
attribution.
|
||
|
||
**If it fails:** The brain-first lookup protocol isn't injected into the agent's
|
||
system context. See `skills/setup/SKILL.md` Phase D.
|
||
|
||
---
|
||
|
||
## 7. Knowledge Graph Wired
|
||
|
||
The v0.12.0 graph layer needs to be populated for existing brains. New writes are
|
||
auto-linked, but historical pages need a one-time backfill.
|
||
|
||
**Command:**
|
||
|
||
```bash
|
||
gbrain stats | grep -E 'links|timeline'
|
||
```
|
||
|
||
**Expected:** Both `links` and `timeline_entries` are non-zero (assuming the brain
|
||
has content with entity references and dated markdown).
|
||
|
||
**If it's zero on a brain with imported content:** Run the backfill.
|
||
|
||
```bash
|
||
gbrain extract links --source db --dry-run | head -5 # preview
|
||
gbrain extract links --source db # commit
|
||
gbrain extract timeline --source db
|
||
gbrain stats # confirm > 0
|
||
```
|
||
|
||
**Bonus check** — graph traversal works:
|
||
|
||
```bash
|
||
# Pick any well-connected slug from your brain
|
||
gbrain graph-query people/<some-person-slug> --depth 2
|
||
```
|
||
|
||
**Expected:** Indented tree of typed edges (`--attended-->`, `--works_at-->`, etc.).
|
||
If the slug has no inbound or outbound links, try a different one or run extract
|
||
again.
|
||
|
||
**If extract finds nothing:** Your pages may not use entity-reference syntax. The
|
||
extractor matches `[Name](people/slug)`, `[Name](../people/slug.md)`, and bare
|
||
`people/slug` references. If your brain uses a different format, the auto-link
|
||
heuristics won't find them — file an issue with a sample page.
|
||
|
||
---
|
||
|
||
## 8. JSONB Frontmatter Integrity (v0.12.2)
|
||
|
||
Postgres-backed brains created before v0.12.2 had double-encoded JSONB columns
|
||
(`frontmatter->>'key'` returned NULL, GIN indexes were inert). `gbrain upgrade`
|
||
runs `gbrain repair-jsonb` automatically via the `v0_12_2` orchestrator.
|
||
Verify the repair succeeded.
|
||
|
||
**Command:**
|
||
|
||
```bash
|
||
gbrain repair-jsonb --dry-run --json
|
||
```
|
||
|
||
**Expected:** `totalRepaired: 0` across all 5 columns (`pages.frontmatter`,
|
||
`raw_data.data`, `ingest_log.pages_updated`, `files.metadata`,
|
||
`page_versions.frontmatter`). A zero count means every row is properly-typed
|
||
JSON objects, not string-encoded JSON.
|
||
|
||
**If the count is > 0:** The repair didn't run or was interrupted. Re-run
|
||
without `--dry-run`:
|
||
|
||
```bash
|
||
gbrain repair-jsonb
|
||
```
|
||
|
||
Idempotent. PGLite brains always report 0 (unaffected by the original bug).
|
||
|
||
**Bonus check** — frontmatter-keyed queries actually resolve:
|
||
|
||
```bash
|
||
gbrain call list_pages '{"frontmatterKey": "type", "frontmatterValue": "person"}'
|
||
```
|
||
|
||
If this returns rows on a brain with person pages, the JSONB path is healthy.
|
||
|
||
---
|
||
|
||
## Quick Verification (all checks in one pass)
|
||
|
||
```bash
|
||
# 1. Schema
|
||
gbrain doctor --json
|
||
|
||
# 2. Sync recency
|
||
gbrain config get sync.last_run
|
||
|
||
# 3. Page count + embed coverage
|
||
gbrain stats
|
||
|
||
# 4. Search works
|
||
gbrain search "test query from your brain content"
|
||
|
||
# 5. Catch any unembedded chunks
|
||
gbrain embed --stale
|
||
|
||
# 6. Auto-update
|
||
gbrain check-update --json
|
||
|
||
# 7. Knowledge graph populated (links + timeline > 0)
|
||
gbrain stats | grep -E 'links|timeline'
|
||
|
||
# 8. JSONB integrity (v0.12.2 — Postgres only, PGLite always 0)
|
||
gbrain repair-jsonb --dry-run --json
|
||
```
|
||
|
||
If all eight return successfully, the installation is healthy. For the full
|
||
end-to-end sync test (4c), push a real change and verify it appears in search.
|
||
|
||
---
|
||
|
||
## docs/guides/minions-fix.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/minions-fix.md
|
||
|
||
# Minions fix — repairing a half-migrated install
|
||
|
||
**tl;dr:** on v0.11.1+ everything should self-heal. If Minions is partially
|
||
set up (no `~/.gbrain/preferences.json`, autopilot still inline, cron jobs
|
||
still on `agentTurn`), run:
|
||
|
||
```bash
|
||
gbrain apply-migrations --yes
|
||
```
|
||
|
||
It's idempotent. On v0.11.1 installs that already migrated it's a cheap
|
||
no-op.
|
||
|
||
## Context
|
||
|
||
v0.11.0 shipped the Minions schema, queue, worker, and migration skill —
|
||
but the migration skill itself never fired on upgrade. `runPostUpgrade`
|
||
printed the feature pitch and stopped. v0.11.0 was never released
|
||
publicly; v0.11.1 is the first public Minions ship and fixes the
|
||
mega-bug (migration fires automatically on `gbrain upgrade` and via
|
||
the `postinstall` hook).
|
||
|
||
If you're on a pre-v0.11.1 branch build (e.g. running the
|
||
`minions-jobs` branch before v0.11.1 tagged), Minions may be installed
|
||
but not wired: schema is v7, but no `~/.gbrain/preferences.json`,
|
||
autopilot still runs inline, cron jobs still call `agentTurn`.
|
||
|
||
This guide covers both paths: the canonical v0.11.1+ fix, and the
|
||
stopgap for pre-v0.11.1 binaries that don't have `apply-migrations`.
|
||
|
||
## Detecting the half-migrated state
|
||
|
||
```bash
|
||
gbrain doctor
|
||
```
|
||
|
||
If the install is half-migrated, you'll see:
|
||
|
||
```
|
||
[FAIL] minions_migration: MINIONS HALF-INSTALLED (partial migration: 0.11.0). Run: gbrain apply-migrations --yes
|
||
```
|
||
|
||
or
|
||
|
||
```
|
||
[FAIL] minions_config: MINIONS HALF-INSTALLED (schema v7+ but no ~/.gbrain/preferences.json). Run: gbrain apply-migrations --yes
|
||
```
|
||
|
||
For a machine-readable report (cron-friendly):
|
||
|
||
```bash
|
||
gbrain skillpack-check --quiet && echo healthy || echo needs_action
|
||
gbrain skillpack-check | jq -r '.actions[]' # prints the exact commands to run
|
||
```
|
||
|
||
## The fix (v0.11.1 or later)
|
||
|
||
```bash
|
||
gbrain apply-migrations --yes
|
||
```
|
||
|
||
Reads `~/.gbrain/migrations/completed.jsonl`, diffs against the TS
|
||
migration registry, runs whatever's pending. Seven phases:
|
||
|
||
```
|
||
A. Schema gbrain init --migrate-only
|
||
B. Smoke gbrain jobs smoke
|
||
C. Mode prompt (or --yes default pain_triggered)
|
||
D. Prefs write ~/.gbrain/preferences.json
|
||
E. Host AGENTS.md marker injection + cron rewrites for gbrain
|
||
builtins; JSONL TODOs for host-specific handlers
|
||
F. Install gbrain autopilot --install (env-aware)
|
||
G. Record append completed.jsonl status:"complete"
|
||
```
|
||
|
||
If Phase E emits TODOs for host-specific handlers (e.g. your OpenClaw's
|
||
~29 non-gbrain crons), the migration finishes with `status: "partial"`.
|
||
Your host agent walks the TODOs using `skills/migrations/v0.11.0.md` +
|
||
`docs/guides/plugin-handlers.md`, ships handler registrations in the
|
||
host repo, then re-runs `gbrain apply-migrations --yes`. Newly
|
||
registerable cron entries get rewritten and the JSONL rows mark
|
||
`status: "complete"`.
|
||
|
||
## The stopgap (pre-v0.11.1 binary, no apply-migrations yet)
|
||
|
||
If you're stuck on a branch build that doesn't have `apply-migrations`:
|
||
|
||
```bash
|
||
curl -fsSL https://raw.githubusercontent.com/garrytan/gbrain/v0.11.1/scripts/fix-v0.11.0.sh | bash
|
||
```
|
||
|
||
This bash script does what apply-migrations does from a shell environment:
|
||
|
||
1. `gbrain init --migrate-only` — schema v7.
|
||
2. `gbrain jobs smoke` — verify Minions health.
|
||
3. Prompt for `minion_mode` (defaults `pain_triggered` on non-TTY).
|
||
4. Write `~/.gbrain/preferences.json` atomically.
|
||
5. Append `~/.gbrain/migrations/completed.jsonl` with `status: "partial"`
|
||
and `apply_migrations_pending: true`. That partial record is the
|
||
signal to v0.11.1's `apply-migrations` to pick up remaining phases
|
||
after the user upgrades.
|
||
6. Detect host agent repos and PRINT rewrite instructions (never
|
||
auto-edits from a curl-piped script).
|
||
7. Print the next step: `Run: gbrain autopilot --install`.
|
||
|
||
Once v0.11.1 is installed, re-run `gbrain apply-migrations --yes` to
|
||
finish the remaining phases (host rewrites + autopilot install). The
|
||
stopgap's `status: "partial"` record is designed to resume cleanly
|
||
(it doesn't poison the permanent migration path).
|
||
|
||
## Verify the fix landed
|
||
|
||
```bash
|
||
# 1. Preferences exist and are readable
|
||
cat ~/.gbrain/preferences.json
|
||
|
||
# 2. Migration recorded
|
||
cat ~/.gbrain/migrations/completed.jsonl
|
||
|
||
# 3. Autopilot is supervising a Minions worker child
|
||
gbrain autopilot --status
|
||
ps aux | grep 'jobs work'
|
||
|
||
# 4. Jobs show up in the queue
|
||
gbrain jobs list
|
||
|
||
# 5. Any host-specific TODOs still pending
|
||
cat ~/.gbrain/migrations/pending-host-work.jsonl 2>/dev/null || echo "(none — all host work is done)"
|
||
|
||
# 6. Doctor + skillpack-check should both be clean
|
||
gbrain doctor
|
||
gbrain skillpack-check --quiet && echo ok
|
||
```
|
||
|
||
## If the fix fails
|
||
|
||
Each phase is idempotent. Re-running is safe. Common failure modes:
|
||
|
||
- **Phase B smoke fails:** the schema didn't apply. Check
|
||
`~/.gbrain/config.json` has a valid `database_url` (or `database_path`
|
||
for PGLite). Run `gbrain init --migrate-only` directly and look at
|
||
the error.
|
||
- **Phase F install fails:** your host environment doesn't match any
|
||
detected target. Pass `--target <macos|linux-systemd|ephemeral-container|linux-cron>`
|
||
explicitly.
|
||
- **Pending host work never clears:** your host agent hasn't shipped
|
||
handler registrations yet. Read
|
||
`~/.gbrain/migrations/pending-host-work.jsonl`, open
|
||
`skills/migrations/v0.11.0.md`, and follow the host-agent instruction
|
||
manual.
|
||
|
||
## Related
|
||
|
||
- `skills/migrations/v0.11.0.md` — full migration skill for host agents.
|
||
- `skills/skillpack-check/SKILL.md` — when and how to run the health check.
|
||
- `docs/guides/plugin-handlers.md` — plugin contract for host-specific
|
||
handlers.
|
||
- `skills/conventions/cron-via-minions.md` — the canonical cron rewrite
|
||
pattern.
|
||
|
||
---
|
||
|
||
## docs/integrations/reliability-repair.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/integrations/reliability-repair.md
|
||
|
||
# Reliability repair (v0.12.2)
|
||
|
||
If you ran v0.12.0 on real Postgres or Supabase, two bugs may have corrupted
|
||
data already in your brain. v0.12.1 fixed the code going forward.
|
||
v0.12.2 adds detection in `gbrain doctor` and a standalone `gbrain repair-jsonb`
|
||
command for the mechanically fixable class. PGLite users are not affected.
|
||
|
||
## What got corrupted
|
||
|
||
**JSONB double-encode.** Four write sites used
|
||
`${JSON.stringify(x)}::jsonb` with postgres.js, which stored a JSONB
|
||
*string literal* instead of an object. `frontmatter ->> 'key'` returns NULL;
|
||
GIN indexes are ineffective. Affected: `pages.frontmatter`,
|
||
`raw_data.data`, `ingest_log.pages_updated`, `files.metadata`.
|
||
|
||
**Markdown body truncation.** `splitBody()` treated `---` horizontal rules
|
||
as a body/timeline delimiter, dropping everything after the first rule.
|
||
Wiki-style pages with multiple `##`/`###` sections lost the bulk of their
|
||
content at import time.
|
||
|
||
## Detect
|
||
|
||
```
|
||
gbrain doctor
|
||
```
|
||
|
||
Reports two new checks:
|
||
|
||
- `jsonb_integrity` — counts double-encoded rows per table and points you
|
||
at `gbrain repair-jsonb`.
|
||
- `markdown_body_completeness` — heuristic for pages whose `compiled_truth`
|
||
is suspiciously short compared to `raw_data.data ->> 'content'`.
|
||
|
||
## Repair
|
||
|
||
For JSONB (mechanically fixable):
|
||
|
||
```
|
||
gbrain repair-jsonb
|
||
```
|
||
|
||
Runs `UPDATE <table> SET <col> = (<col>#>>'{}')::jsonb WHERE jsonb_typeof(<col>) = 'string'`
|
||
across every affected column. Idempotent. Second run reports 0 rows. Use
|
||
`--dry-run` to preview, `--json` for structured output. The `v0_12_2`
|
||
migration runs this automatically on `gbrain upgrade`.
|
||
|
||
For truncated markdown bodies (source-dependent):
|
||
|
||
```
|
||
gbrain sync --force
|
||
# or per-page
|
||
gbrain import <slug> --force
|
||
```
|
||
|
||
v0.12.2 cannot recover content that was already lost if you no longer have
|
||
the source markdown file. `gbrain doctor` tells you which pages look short;
|
||
you decide whether to re-import from source or accept the truncation.
|
||
|
||
## Verify
|
||
|
||
```
|
||
gbrain doctor
|
||
```
|
||
|
||
All four `jsonb_integrity` rows should read zero. `markdown_body_completeness`
|
||
should match your expectations for the corpus.
|
||
|
||
---
|
||
|
||
# Migrations
|
||
|
||
## docs/UPGRADING_DOWNSTREAM_AGENTS.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/UPGRADING_DOWNSTREAM_AGENTS.md
|
||
|
||
# Upgrading Downstream Agents
|
||
|
||
GBrain ships skills in `skills/`. Downstream agents (custom OpenClaw deployments,
|
||
agent forks of any kind) often **copy** these skill files into their own workspace and
|
||
diverge over time — adding agent-specific phases, removing irrelevant ones, tightening
|
||
language. Once that happens, gbrain can't push updates to those forks. The agent has
|
||
to apply the diffs by hand.
|
||
|
||
This doc lists the exact diffs each downstream agent needs to apply when upgrading.
|
||
Cross-reference against your fork's local skill files.
|
||
|
||
## Why this exists
|
||
|
||
`gbrain upgrade` ships the new binary. `gbrain post-upgrade [--execute --yes]` runs
|
||
the schema migrations and backfills the data. But the **skill files themselves**
|
||
that tell the agent how to behave — those are user-owned. If your `~/git/<your-agent>/workspace/skills/brain-ops/SKILL.md`
|
||
says `# Based on gbrain v0.10.0` at the top, it doesn't know about v0.12.0 features.
|
||
|
||
The agent will keep manually calling `gbrain link` after every `put_page` (now redundant —
|
||
auto-link does it), miss out on `gbrain graph-query` for relationship questions, and
|
||
not know to backfill the structured timeline.
|
||
|
||
## How to apply
|
||
|
||
1. Identify your forked skill files. Typically at `~/git/<your-agent>/workspace/skills/` or wherever your agent's skill directory lives.
|
||
2. For each skill listed below, find the matching phase/section in your fork.
|
||
3. Apply the diff (paste the new block in the indicated location).
|
||
4. Update the version banner at the top of your fork (`# Based on gbrain v0.12.0`).
|
||
5. Verify: ask the agent to write a test page and confirm the response includes
|
||
`auto_links: { created, removed, errors }`.
|
||
|
||
Total time: ~10 minutes for all four skills.
|
||
|
||
---
|
||
|
||
## 1. brain-ops/SKILL.md
|
||
|
||
**Where:** Insert a new `### Phase 2.5` section immediately after `### Phase 2: On Every Inbound Signal`.
|
||
|
||
**Why:** Phase 2.5 declares that auto-link runs automatically. Without this, the
|
||
agent's mental model says it must call `gbrain link` after every `put_page`, which
|
||
is now redundant and can cause double-add warnings.
|
||
|
||
```markdown
|
||
### Phase 2.5: Structured Graph Updates (automatic)
|
||
|
||
Every `put_page` call automatically extracts entity references and writes them
|
||
to the graph (`links` table) with inferred relationship types. Stale links
|
||
(refs no longer in the page text) are removed in the same call. This is
|
||
"auto-link" reconciliation.
|
||
|
||
- No manual `add_link` calls needed for ordinary page writes.
|
||
- Inferred link types: `attended` (meeting -> person), `works_at`, `invested_in`,
|
||
`founded`, `advises`, `source` (frontmatter), `mentions` (default).
|
||
- The `put_page` MCP response includes `auto_links: { created, removed, errors }`
|
||
so the agent can verify outcomes.
|
||
- To disable: `gbrain config set auto_link false`. Default is on.
|
||
- Timeline entries with specific dates still need explicit `gbrain timeline-add`
|
||
(or batch via `gbrain extract timeline --source db`).
|
||
```
|
||
|
||
**Also update the Iron Law section.** If your fork still says "Back-links maintained
|
||
on every brain write (Iron Law)" without qualification, append:
|
||
|
||
```markdown
|
||
**v0.12.0 update:** Auto-link satisfies the Iron Law for entity-reference links
|
||
on every `put_page`. The agent's Iron Law obligation is now: include the
|
||
entity reference in the page content (e.g., `[Alice](people/alice)`); auto-link
|
||
handles the structured row. Manual `add_link` calls are reserved for
|
||
relationships you can't express in markdown content.
|
||
```
|
||
|
||
---
|
||
|
||
## 2. meeting-ingestion/SKILL.md
|
||
|
||
**Where:** Append to the end of `### Phase 3: Attendee enrichment`.
|
||
|
||
**Why:** Eliminates redundant `gbrain link` calls per attendee (auto-link handles them
|
||
when the meeting page references attendees as `[Name](people/slug)`).
|
||
|
||
```markdown
|
||
**Note (v0.12.0):** Once the meeting page is written via `gbrain put`, the
|
||
auto-link post-hook automatically creates `attended` links from the meeting
|
||
to each attendee whose page is referenced as `[Name](people/slug)`. You don't
|
||
need to call `gbrain link` for attendees. You DO still need `gbrain timeline-add`
|
||
for dated events (auto-link only handles links, not timeline entries).
|
||
```
|
||
|
||
**Where:** In `### Phase 4: Entity propagation`, the line "Back-link from entity page
|
||
to meeting page" can be replaced with:
|
||
|
||
```markdown
|
||
4. Entity references in the meeting page body auto-create the link via auto-link.
|
||
For incoming references on the entity page (entity page → meeting page), edit
|
||
the entity page to mention the meeting and `put_page` it — auto-link handles
|
||
the rest.
|
||
```
|
||
|
||
---
|
||
|
||
## 3. signal-detector/SKILL.md
|
||
|
||
**Where:** Append to the end of `### Phase 2: Entity Detection`.
|
||
|
||
**Why:** Same logic as brain-ops — eliminates manual `gbrain link` after writing
|
||
originals/ideas pages that reference people or companies.
|
||
|
||
```markdown
|
||
**Auto-link (v0.12.0):** When you write/update an originals or ideas page that
|
||
references a person or company, the auto-link post-hook on `put_page`
|
||
automatically creates the link from the new page to that entity. You don't
|
||
need to call `gbrain link` manually. Timeline entries still need explicit calls.
|
||
```
|
||
|
||
---
|
||
|
||
## 4. enrich/SKILL.md
|
||
|
||
**Where:** Replace `### Step 7: Cross-reference` with the v0.12.0 version.
|
||
|
||
**Why:** Step 7 used to be primarily about creating links between related entity
|
||
pages. With auto-link, that's automatic. Step 7 is now about content updates,
|
||
not link creation.
|
||
|
||
Old (delete):
|
||
```markdown
|
||
### Step 7: Cross-reference
|
||
|
||
- Update company pages from person enrichment (and vice versa)
|
||
- Update related project/deal pages if relevant context surfaced
|
||
- Check index files if the brain uses them
|
||
- Add back-links manually via `gbrain link` for any new entity references
|
||
```
|
||
|
||
New (paste):
|
||
```markdown
|
||
### Step 7: Cross-reference
|
||
|
||
- Update company pages from person enrichment (and vice versa)
|
||
- Update related project/deal pages if relevant context surfaced
|
||
- Check index files if the brain uses them
|
||
|
||
**Note (v0.12.0):** Links between brain pages are auto-created on every
|
||
`put_page` call (auto-link post-hook). Step 7 focuses on content
|
||
cross-references (updating related pages' compiled truth with new signal
|
||
from this enrichment), not on creating links. Verify via the `auto_links`
|
||
field in the put_page response (`{ created, removed, errors }`).
|
||
Timeline entries still need explicit `gbrain timeline-add` calls.
|
||
```
|
||
|
||
---
|
||
|
||
## After all four diffs are applied
|
||
|
||
1. **Bump the version banner** at the top of each forked file:
|
||
```
|
||
# Based on gbrain v0.12.0 skills/<skill-name>, extended with <your-agent>-specific config
|
||
```
|
||
|
||
2. **Run the v0.12.0 backfill** (this populates the graph for your existing brain):
|
||
```bash
|
||
gbrain post-upgrade
|
||
```
|
||
The v0.12.0 release wires post-upgrade to call `apply-migrations --yes`
|
||
automatically, which runs the v0_12_0 orchestrator (schema → config check →
|
||
`extract links --source db` → `extract timeline --source db` → verify).
|
||
Idempotent; cheap when nothing is pending.
|
||
|
||
3. **Verify auto-link works:** ask the agent to write a test page that references
|
||
`[Some Person](people/some-person)`. Confirm the put_page response includes
|
||
`auto_links: { created: 1, removed: 0, errors: 0 }`.
|
||
|
||
4. **Verify graph traversal works:**
|
||
```bash
|
||
gbrain graph-query people/some-well-connected-person --depth 2
|
||
```
|
||
Should return an indented tree of typed edges.
|
||
|
||
---
|
||
|
||
## v0.12.2 hotfix (data-correctness, no skill edits)
|
||
|
||
v0.12.2 is a Postgres data-correctness hotfix. No forked skill files need to
|
||
change — the skill contracts are unchanged. But you DO need to run the migration,
|
||
and you should know about one behavior change in markdown parsing.
|
||
|
||
### 1. Run the migration (Postgres-backed brains)
|
||
|
||
```bash
|
||
gbrain upgrade
|
||
```
|
||
|
||
The `v0_12_2` orchestrator runs `gbrain repair-jsonb` automatically. It rewrites
|
||
rows where `jsonb_typeof = 'string'` across `pages.frontmatter`, `raw_data.data`,
|
||
`ingest_log.pages_updated`, `files.metadata`, and `page_versions.frontmatter`.
|
||
Idempotent, safe to re-run. PGLite brains no-op cleanly.
|
||
|
||
Verify after upgrade:
|
||
|
||
```bash
|
||
gbrain repair-jsonb --dry-run --json # expect totalRepaired: 0
|
||
```
|
||
|
||
### 2. Recover any truncated wiki articles
|
||
|
||
If your brain imported wiki-style markdown before v0.12.2, some pages were
|
||
silently truncated (any standalone `---` in body content was treated as a
|
||
timeline separator). Re-import from source:
|
||
|
||
```bash
|
||
gbrain sync --full
|
||
```
|
||
|
||
The new `splitBody` rebuilds `compiled_truth` correctly.
|
||
|
||
### 3. Know the splitBody contract going forward
|
||
|
||
`splitBody` now requires an explicit timeline sentinel. Recognized markers
|
||
(priority order):
|
||
|
||
1. `<!-- timeline -->` (preferred — what `serializeMarkdown` emits)
|
||
2. `--- timeline ---` (decorated separator)
|
||
3. `---` directly before `## Timeline` or `## History` heading (backward-compat)
|
||
|
||
A bare `---` in body text is now a markdown horizontal rule, not a timeline
|
||
separator. If your agent writes pages with a bare `---` delimiter, migrate to
|
||
`<!-- timeline -->` — the `serializeMarkdown` helper already does this.
|
||
|
||
### 4. Wiki subtypes now auto-typed
|
||
|
||
`inferType` now auto-detects five additional directory patterns as their own
|
||
page types (previously they all defaulted to `concept`):
|
||
|
||
| Path pattern | New type |
|
||
|------------------------|----------------|
|
||
| `/wiki/analysis/` | `analysis` |
|
||
| `/wiki/guides/` | `guide` |
|
||
| `/wiki/hardware/` | `hardware` |
|
||
| `/wiki/architecture/` | `architecture` |
|
||
| `/writing/` | `writing` |
|
||
|
||
If your skills or queries filter by `type=concept` and expect wiki content in
|
||
that bucket, update them to include the new types.
|
||
|
||
---
|
||
|
||
## v0.13.0 — Frontmatter Relationship Indexing
|
||
|
||
**Verdict: no action required for most skills.** v0.13 projects YAML frontmatter fields into the graph as typed edges. The ingestion API is unchanged — keep calling `put_page` with frontmatter the way you do today; the graph auto-populates behind the scenes.
|
||
|
||
Three skills get an optional new phase if you want to consume the new `auto_links.unresolved` response field. Without this, unresolvable frontmatter names silently skip (same as v0.12 behavior).
|
||
|
||
### 1. meeting-ingestion/SKILL.md (optional)
|
||
|
||
**Where:** Add a new section after "Phase 3: Write Meeting Page".
|
||
|
||
```markdown
|
||
### Phase 3.5: Check for unresolved attendees (v0.13+)
|
||
|
||
After `put_page`, inspect `response.auto_links.unresolved` — an array of frontmatter
|
||
references that did not resolve to existing pages. For meetings, this usually means
|
||
attendees you haven't created a person page for yet.
|
||
|
||
If `unresolved.length > 0`:
|
||
- Option 1 (create pages now): trigger an enrichment pass to build the missing people pages.
|
||
- Option 2 (defer): log the unresolved names to the enrichment queue for later.
|
||
- Option 3 (accept the gap): the attendee edge will not be created until a page exists.
|
||
Re-running `gbrain extract links --source db --include-frontmatter` after creating
|
||
the page fills in the missing edges.
|
||
```
|
||
|
||
### 2. enrich/SKILL.md (optional)
|
||
|
||
**Where:** Add to the enrichment trigger list.
|
||
|
||
```markdown
|
||
### Drain unresolved frontmatter names (v0.13+)
|
||
|
||
If any `put_page` response includes `auto_links.unresolved` entries, the enrichment
|
||
tier should pick up those (field, name) pairs and try to create the missing entity
|
||
pages. Example flow:
|
||
|
||
1. signal-detector captures a meeting with `attendees: [Alice Known, Unknown Person]`
|
||
2. put_page returns `auto_links.unresolved = [{field: 'attendees', name: 'Unknown Person'}]`
|
||
3. enrichment tier consumes `Unknown Person` → web search → creates `people/unknown-person.md`
|
||
4. The next put_page (or a backfill run) wires up the `attended` edge automatically
|
||
```
|
||
|
||
### 3. idea-ingest/SKILL.md (optional)
|
||
|
||
**Where:** Same pattern as meeting-ingestion — check `auto_links.unresolved` after `put_page`, route names to enrichment.
|
||
|
||
### Unchanged skills (no diffs needed)
|
||
|
||
- **brain-ops/SKILL.md** — auto-link mechanics are internal; the write path stays the same.
|
||
- **signal-detector/SKILL.md** — signal capture path unchanged.
|
||
- **query/SKILL.md** — `traverse_graph` now returns richer results automatically.
|
||
- **daily-task-manager/SKILL.md**, **briefing/SKILL.md**, **citation-fixer/SKILL.md**, **media-ingest/SKILL.md** — unchanged.
|
||
|
||
### New edge types you can filter in graph queries
|
||
|
||
v0.13 edges carry new `link_type` values. If your fork has graph-query skills that filter by type, these are now available:
|
||
|
||
- `works_at` (person → company) — from `company:`, `companies:`, or `key_people:`
|
||
- `founded` (person → company) — from `founded:`
|
||
- `invested_in` (investor → deal/company) — from `investors:` or `lead:`
|
||
- `led_round` (lead → deal) — from `lead:`
|
||
- `yc_partner` (partner → company) — from `partner:`
|
||
- `attended` (person → meeting) — from `attendees:`
|
||
- `discussed_in` (source → page) — from `sources:`
|
||
- `source` (page → source) — from `source:`
|
||
- `related_to` (page → target) — from `related:` or `see_also:`
|
||
|
||
### Migration timing
|
||
|
||
`gbrain upgrade` takes 2-5 min on a 46K-page brain (one-time). Runs out-of-process via `gbrain post-upgrade`. If your agent holds a DB connection during the upgrade, reconnect after; otherwise keep serving.
|
||
|
||
### Type normalization NOT in v0.13
|
||
|
||
Legacy rows with `link_type='attendee'` or `link_type='mention'` coexist with new `'attended'` / `'mentions'` rows. Your queries filtering on old type names keep working. A separate opt-in `gbrain normalize-types` command in v0.14 handles the rename.
|
||
## v0.14.0 shell jobs (optional adoption, no skill edits)
|
||
|
||
Adds a `shell` job type to Minions so deterministic cron scripts (API fetch, token
|
||
refresh, scrape + write) move off the LLM gateway. Zero tokens per fire. ~60%
|
||
gateway CPU headroom at typical scale. Feature is **off by default**, existing
|
||
installs keep running exactly as they did before. Nothing breaks.
|
||
|
||
To adopt, follow `skills/migrations/v0.14.0.md`. The short version:
|
||
|
||
1. Set `GBRAIN_ALLOW_SHELL_JOBS=1` on the worker process, then `gbrain jobs work`
|
||
(Postgres). On PGLite, every crontab invocation uses `--follow` for inline
|
||
execution; no persistent worker.
|
||
2. Classify each of your host's cron entries: LLM-requiring (keep on gateway) vs
|
||
deterministic (candidate for shell). Typical splits:
|
||
- **Deterministic → shell:** `ycli-token-refresh`, `x-oauth2-refresh`,
|
||
`x-garrytan-unified`, `calendar-sync-to-brain`, `github-pulse`,
|
||
`frameio-scan`, `flight-tracker`, `x-raw-json-backfill`.
|
||
- **LLM-requiring → stay:** `social-radar`, `content-ideas`, `adversary-vacuum`,
|
||
`ea-inbox-sweep`, `morning-briefing`, `brain-maintenance`.
|
||
3. For each deterministic cron, rewrite as:
|
||
```cron
|
||
3 13,16,19,22,1,4,7,10 * * * \
|
||
gbrain jobs submit shell \
|
||
--params '{"cmd":"node scripts/your-script.mjs","cwd":"/data/.openclaw/workspace"}' \
|
||
--max-attempts 3 --timeout-ms 300000
|
||
```
|
||
4. Watch `gbrain jobs get <id>` for exit_code / stdout_tail / stderr_tail on each
|
||
fire. Compare against pre-migration behavior before approving the next batch.
|
||
|
||
**No skill edits required.** The handler runs worker-side; skill files don't
|
||
change. If your host exposed custom handlers via the plugin contract (v0.11.0),
|
||
they still work the same way.
|
||
|
||
Iron rule: **never auto-rewrite the operator's crontab.** Every rewrite is
|
||
per-cron, human-approved, with a diff. If you want automation later, the
|
||
upcoming `gbrain crontab-to-minions <file>` helper is P1 in TODOS.
|
||
|
||
---
|
||
|
||
## v0.16.0: durable agent runtime
|
||
|
||
v0.15 ships `gbrain agent run` / `gbrain agent logs`, a new `subagent` handler
|
||
type in Minions, and a plugin contract for host-repo subagent defs. None of the
|
||
existing skills need surgery. The question for downstream agents is *how* to
|
||
adopt the new runtime, not how to patch around a breaking change.
|
||
|
||
### 1. Run a worker with an Anthropic key
|
||
|
||
The subagent handlers (`subagent` and `subagent_aggregator`) are always
|
||
registered on the worker. No separate opt-in flag — `ANTHROPIC_API_KEY` is
|
||
the natural cost gate (no key, the SDK call fails on the first turn), and
|
||
who-can-submit is already protected (`PROTECTED_JOB_NAMES` + trusted-submit:
|
||
MCP callers get `permission_denied`; only `gbrain agent run` can insert
|
||
these rows).
|
||
|
||
```bash
|
||
ANTHROPIC_API_KEY=sk-ant-... gbrain jobs work
|
||
```
|
||
|
||
Worker startup prints:
|
||
|
||
```
|
||
[minion worker] subagent handlers enabled
|
||
```
|
||
|
||
### 2. Ship your subagents as a plugin (OpenClaw + similar)
|
||
|
||
Move your custom subagent definitions out of your gbrain fork and into your own
|
||
repo as a plugin. Concretely:
|
||
|
||
```
|
||
~/<your-agent>/gbrain-plugin/
|
||
├── gbrain.plugin.json
|
||
└── subagents/
|
||
├── meeting-ingestion.md
|
||
├── signal-detector.md
|
||
└── daily-task-prep.md
|
||
```
|
||
|
||
`gbrain.plugin.json`:
|
||
|
||
```json
|
||
{
|
||
"name": "your-openclaw",
|
||
"version": "2026.4.20",
|
||
"plugin_version": "gbrain-plugin-v1"
|
||
}
|
||
```
|
||
|
||
Each `subagents/*.md` is a plain-text agent definition — YAML frontmatter +
|
||
body-as-system-prompt. Recognized frontmatter fields: `name`, `model`,
|
||
`max_turns`, `allowed_tools` (must subset the derived brain-tool registry).
|
||
|
||
Turn it on:
|
||
|
||
```bash
|
||
export GBRAIN_PLUGIN_PATH="$HOME/<your-agent>/gbrain-plugin"
|
||
```
|
||
|
||
Worker startup prints `[plugin-loader] loaded '<name>' v<ver> (N subagents)`
|
||
per plugin; any rejection (bad manifest, unknown tool in `allowed_tools`,
|
||
version mismatch) shows up as a loud warning at startup, not a silent dispatch-
|
||
time failure. See `docs/guides/plugin-authors.md` for the full contract.
|
||
|
||
### 3. Replace ephemeral subagent runs with durable ones
|
||
|
||
If your agent currently spawns ephemeral subagents (OpenClaw `Agent()`, ad-hoc
|
||
Anthropic API calls, etc.) for work that should survive crashes, sleeps, or
|
||
worker restarts, migrate those to `gbrain agent run`. The durability is free:
|
||
|
||
```bash
|
||
gbrain agent run "analyze my last 50 journal pages for recurring themes" \
|
||
--subagent-def analyzer --fanout-manifest manifests/journal-pages.json
|
||
```
|
||
|
||
Every turn persists to `subagent_messages`, every tool call is a two-phase
|
||
ledger, and `gbrain agent logs <job>` shows where it died + what the last
|
||
successful call returned. No more "re-run from scratch because the session
|
||
context evaporated."
|
||
|
||
### 4. `put_page` from subagents writes under an agent namespace
|
||
|
||
If you adopted the v0.15 subagent runtime, note that `put_page` calls
|
||
originating from a subagent's tool dispatch MUST target
|
||
`wiki/agents/<subagent_id>/...`. The schema shown to the model enforces this
|
||
on first try; a server-side fail-closed check rejects anything else. This
|
||
does NOT affect your skill files, CLI put_page calls, or MCP put_page —
|
||
only tool-dispatched writes from inside an LLM loop.
|
||
|
||
Aggregation output (the final "here's what all N children found" brain page)
|
||
goes via a separate trusted CLI path, not through a subagent tool call, so
|
||
it can write anywhere you want.
|
||
|
||
Iron rule: **never grant an agent write access beyond its namespace**. The
|
||
server-side check exists because dispatcher bugs happen; treat it as defense
|
||
in depth, not the primary boundary.
|
||
|
||
---
|
||
|
||
## v0.22.4 — frontmatter-guard adoption
|
||
|
||
### 1. Stop hand-rolling frontmatter validators
|
||
|
||
If your fork has scripts that call `js-yaml` directly to validate brain page
|
||
frontmatter, replace them with `gbrain frontmatter validate` calls. The CLI
|
||
covers the seven canonical error classes and ships a `--json` envelope that's
|
||
stable across releases.
|
||
|
||
```diff
|
||
- # Custom validator script
|
||
- node scripts/validate-frontmatter.mjs <path>
|
||
+ gbrain frontmatter validate <path> --json
|
||
```
|
||
|
||
For consumers that need the validator inside another script, import from
|
||
gbrain's `markdown` export instead of duplicating logic:
|
||
|
||
```ts
|
||
import { parseMarkdown } from 'gbrain/markdown';
|
||
|
||
const parsed = parseMarkdown(content, filePath, { validate: true, expectedSlug });
|
||
for (const err of parsed.errors ?? []) {
|
||
// err.code: MISSING_OPEN | MISSING_CLOSE | YAML_PARSE | SLUG_MISMATCH |
|
||
// NULL_BYTES | NESTED_QUOTES | EMPTY_FRONTMATTER
|
||
}
|
||
```
|
||
|
||
### 2. Drop any references to `lib/brain-writer.mjs`
|
||
|
||
If your fork's skills or scripts referenced an aspirational
|
||
`lib/brain-writer.mjs` (it never shipped — the spec was in PR #392 and never
|
||
landed), replace those references with the gbrain CLI. The `frontmatter-guard`
|
||
skill lives at `skills/frontmatter-guard/SKILL.md` and points at
|
||
`gbrain frontmatter validate` / `audit` / `install-hook`.
|
||
|
||
### 3. Wire the doctor subcheck into your health pipeline
|
||
|
||
`gbrain doctor` now reports `frontmatter_integrity` automatically. If your
|
||
fork has a custom health pipeline (e.g. a daily Slack post about brain
|
||
health), pull from `gbrain doctor --json` and surface the
|
||
`frontmatter_integrity` row counts.
|
||
|
||
### 4. (Optional) Install the pre-commit hook on brain repos
|
||
|
||
For sources backed by git, the v0.22.4 install-hook helper drops a
|
||
pre-commit script that blocks commits with malformed frontmatter:
|
||
|
||
```bash
|
||
gbrain frontmatter install-hook
|
||
```
|
||
|
||
Skip this if your brain isn't a git repo or if your downstream agent already
|
||
enforces validation at write time. See `docs/integrations/pre-commit.md` for
|
||
the full recipe.
|
||
|
||
### 5. Migration ergonomics — read pending-host-work.jsonl
|
||
|
||
After `gbrain apply-migrations --yes` runs the v0.22.4 audit, your agent
|
||
should read `~/.gbrain/migrations/pending-host-work.jsonl` (filter to
|
||
`migration === "0.22.4"`) and walk each entry's `command` field. Each entry
|
||
points to a per-source `gbrain frontmatter validate <source_path> --fix`
|
||
command — surface counts to the user, get explicit consent, then run.
|
||
|
||
The migration is **audit-only**. It never mutates brain content during
|
||
`apply-migrations`. Your agent runs the fix command with user consent.
|
||
|
||
---
|
||
|
||
## Future versions
|
||
|
||
When gbrain ships a new version, this doc will be updated with the diffs for that
|
||
version. Each new version appends a section; old sections stay so you can catch up
|
||
multiple versions at once.
|
||
|
||
To check what your fork is missing:
|
||
```bash
|
||
diff <(grep -A3 "Based on gbrain" ~/<your-fork>/skills/brain-ops/SKILL.md) \
|
||
<(grep "v[0-9]" ~/gbrain/skills/migrations/ | tail -3)
|
||
```
|
||
|
||
|
||
## v0.36.5.0 — Free-form secret inheritance for shell jobs calling `gbrain` CLI
|
||
|
||
**The change.** Shell-job params get a new `inherit:` field. Pass any
|
||
snake_case config-key name on it; the worker resolves the value from its
|
||
`loadConfig()` at child-spawn time and injects it into the child env. Names
|
||
land in the row; values never persist from `inherit:`. Validation runs
|
||
**pre-enqueue** in both submit paths (CLI + `submit_job` op), so a malformed
|
||
payload never lands in `minion_jobs.data`.
|
||
|
||
**Why.** Pre-v0.36.5.0, agents that wanted to call `gbrain` from shell jobs
|
||
had to either write `database_url` to `~/.gbrain/config.json` plaintext or
|
||
pass `env: { GBRAIN_DATABASE_URL: "..." }` per-job. Both left plaintext
|
||
secrets somewhere — disk or DB row. `inherit:` keeps names in the row and
|
||
resolves values at spawn time.
|
||
|
||
**What your agent can do.** `inherit:` is free-form. Pass any config-key:
|
||
|
||
```jsonc
|
||
{
|
||
"cmd": "gbrain sync --skip-failed && gbrain embed --stale",
|
||
"cwd": "/data/gbrain",
|
||
"inherit": ["database_url", "anthropic_api_key", "voyage_api_key"]
|
||
}
|
||
```
|
||
|
||
The env-key name in the child is derived by uppercasing the config-key:
|
||
`database_url` → `GBRAIN_DATABASE_URL`, `anthropic_api_key` →
|
||
`ANTHROPIC_API_KEY`, `voyage_api_key` → `VOYAGE_API_KEY`, etc. The validator
|
||
does NOT police which config keys you inherit — the agent is in the same
|
||
uid as the worker, so it's the agent's call.
|
||
|
||
**You can still use `env:`.** v0.36.5.0 does not forbid `env:{ ANYTHING }`.
|
||
If you have a reason to put a value in the row plaintext (a non-secret
|
||
correlation token, or a secret you know is OK to persist), pass it via
|
||
`env:`. Prefer `inherit:` when you want the value out of the row.
|
||
|
||
**Worker setup** (one-time, per host):
|
||
|
||
- `gbrain config set database_url postgresql://...` (or any other key you
|
||
want available for inherit)
|
||
- OR put the key in `~/.gbrain/config.json` directly
|
||
- OR set `GBRAIN_DATABASE_URL` / `DATABASE_URL` / per-provider env on the
|
||
worker process
|
||
|
||
If the worker can't resolve a requested name, the validator fail-fasts at
|
||
submit time with `gbrain config set <X>` hint. No more silent "No database
|
||
URL" failures in child stderr minutes after submission.
|
||
|
||
**Also new.** A `gbrain doctor` check `home_dir_in_worktree` warns if
|
||
`~/.gbrain/` lives inside a git worktree. A retroactive `~/.gbrain/.gitignore`
|
||
(single line `*`) is now laid down by every `saveConfig()` call AND by
|
||
`gbrain post-upgrade`, so existing users get coverage without re-running
|
||
`gbrain init`. Honest scope: the `.gitignore` covers casual `git add` but does
|
||
NOT cover already-tracked files, screenshots, backups, or `git add -f`.
|
||
|
||
**Strategy framing.** For agent-to-gbrain calls, the new canonical guide is
|
||
`docs/guides/agent-to-gbrain.md`. Two distinct surfaces: HTTP MCP via OAuth
|
||
for ops with MCP equivalents (`search`, `query`, `put_page`, etc.), and shell
|
||
job + `inherit:` for `localOnly` admin ops (`sync`, `embed`, `dream`,
|
||
`doctor`, etc.). Not a fallback hierarchy — pick by op.
|
||
|
||
**Errors to handle** (your agent submits shell jobs; surface these clearly):
|
||
|
||
| Error | What it means | Agent action |
|
||
|---|---|---|
|
||
| `shell: inherit must be an array of config-key names` | `inherit` wasn't an array. | Pass `"inherit": ["database_url", ...]`. |
|
||
| `shell: inherit entries must be non-empty strings` | Element was empty, non-string, or null. | Use snake_case config-key names. |
|
||
| `shell: inherit name "<X>" must match [a-z][a-z0-9_]*` | Name failed snake_case regex (uppercase, leading underscore, etc.). | Use the config-key verbatim — `database_url`, not `DATABASE_URL`. |
|
||
| `shell: inherit requested "<X>" but worker has no <X> configured` | Worker can't resolve the name from its `loadConfig()`. | Run `gbrain config set <X> <value>` on the worker host. |
|
||
|
||
---
|