mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* v0.42.60.0 chore(release): eleven verified community fixes — changelog + version bump Windows full-sync mass-delete fix, gateway tool-loop resume consolidation (fix-wave A), two source-isolation closes, search-cache exclude-policy keying, and six more verified community fixes. Files the take-writes fail-open source fallback and the #2112 doctor hunk as follow-up TODOs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: update reference docs for v0.42.60.0 - KEY_FILES.md: unpin stale KNOBS_HASH_VERSION number in the autocut entry (mode.ts is the single source of truth); document the full-sync reconcile path-separator normalization + mass-delete safety valve (planReconcileDeletes, GBRAIN_ALLOW_MASS_RECONCILE); describe the TTY-gated admin bootstrap token banner (--print-admin-token, env-sourced always hidden) - docs/mcp/DEPLOY.md + docs/tutorials/company-brain.md: bootstrap token is now hidden on non-TTY starts; document GBRAIN_ADMIN_BOOTSTRAP_TOKEN and --print-admin-token for headless deploys Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(docs): regenerate llms bundle after KEY_FILES/deploy-doc sync Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
4410 lines
218 KiB
Plaintext
4410 lines
218 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) — orientation + resolver: architecture, cross-cutting
|
||
invariants, the reference map, inline ship rules. It routes to on-demand detail docs:
|
||
[`./docs/architecture/KEY_FILES.md`](./docs/architecture/KEY_FILES.md) (per-file index —
|
||
read a file's entry before editing it), [`./docs/TESTING.md`](./docs/TESTING.md) (test
|
||
tiers + isolation lint + E2E lifecycle), and
|
||
[`./docs/architecture/thin-client.md`](./docs/architecture/thin-client.md) (remote-MCP seam).
|
||
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. **v0.40.2.0:**
|
||
`gbrain think` now uses this substrate automatically on temporal /
|
||
knowledge_update intent (default ON; flip `think.trajectory_enabled=false`
|
||
to opt out). Migration v82 added `facts.event_type` so non-metric event
|
||
rows (`meeting`, `job_change`, `location_change`) ride through the same
|
||
pipeline; pass `kind: 'event'` or `'all'` to `find_trajectory` to query
|
||
them.
|
||
- **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,
|
||
guards + typecheck, then 4-shard parallel unit + E2E against four pgvector
|
||
containers plus a transaction-mode PgBouncer; unit phase keeps `DATABASE_URL`
|
||
unset) 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. The full release + contributor process
|
||
(CHANGELOG voice, version-locations sync, PR conventions, community-PR-wave) lives in
|
||
[`./docs/RELEASING.md`](./docs/RELEASING.md); read it before shipping.
|
||
|
||
## 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.
|
||
|
||
## North Star
|
||
|
||
gbrain aims to be the **next Postgres for memory**: the most well-tested, widest-coverage,
|
||
best-for-the-most-at-the-least retrieval + agent memory system for company brains and
|
||
personal AI, built to serve a billion people. Every feature and every eval is judged
|
||
against this bar. "gbrain is best" is a WHOLE-SYSTEM claim — proven across the full
|
||
BrainBench suite (retrieval, longmemeval, calibration, …) — not by any single feature.
|
||
When scoping an eval, prove the FEATURE delivers value to gbrain users; do not waste it
|
||
proving that gbrain's particular algorithm beats some other algorithm (a research
|
||
bake-off, off-mission).
|
||
|
||
## 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 ~90 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`). 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.
|
||
|
||
**Cross-cutting invariants (must-never-violate, regardless of which file you touch).**
|
||
These used to be buried across the per-file index; they live here so they always load.
|
||
Per-file detail is in `docs/architecture/KEY_FILES.md`.
|
||
|
||
- **Trust is fail-closed.** `OperationContext.remote` is REQUIRED on the type. Anything not
|
||
strictly `false` is treated as remote/untrusted (`ctx.remote === false` for trusted-only
|
||
sites; `ctx.remote !== false` for untrust-unless-explicit-false). Don't default it falsy.
|
||
- **Source isolation.** Every read-side op routes through `sourceScopeOpts(ctx)`; precedence
|
||
is federated array (`ctx.auth.allowedSources`) > scalar (`ctx.sourceId`) > nothing. Don't
|
||
hand-roll source filtering — a missed thread is a cross-source data leak.
|
||
- **JSONB: never `JSON.stringify` into a `::jsonb` cast.** postgres.js double-encodes it (a jsonb
|
||
string scalar); PGLite hides the bug. This bites BOTH spellings — the template form
|
||
(`${JSON.stringify(x)}::jsonb`) AND the positional form (`executeRaw(\`…$N::jsonb\`, [JSON.stringify(x)])`,
|
||
the #2339 class that aborted every sync). Fix: pass a raw object to `engine.executeRaw` / use
|
||
`executeRawJsonb` / `sql.json()`; or for the positional path bind through `$N::text::jsonb` (binds as
|
||
text, the cast parses it). Guarded by `scripts/check-jsonb-pattern.sh` (template grep) +
|
||
`scripts/check-jsonb-params.mjs` (positional AST scanner); the real backstop is the DATABASE_URL-gated
|
||
e2e parity tests, since PGLite can't surface the bug. Full rule in `docs/ENGINES.md`.
|
||
- **Engine parity.** `src/core/postgres-engine.ts` and `src/core/pglite-engine.ts` move in
|
||
lockstep — a new method/SQL shape lands in BOTH, pinned by `test/e2e/engine-parity.test.ts`.
|
||
Forward-referenced columns/indexes go in the bootstrap probe set (guarded by
|
||
`test/schema-bootstrap-coverage.test.ts`).
|
||
- **Contract-first.** `src/core/operations.ts` is the single source; CLI + MCP are generated
|
||
from it. Every op carries `scope: 'read'|'write'|'admin'` + optional `localOnly`. HTTP
|
||
dispatch enforces scope/localOnly before the handler runs.
|
||
- **Migrations.** Schema DDL lives in the `MIGRATIONS` array in `src/core/migrate.ts`.
|
||
`CREATE INDEX CONCURRENTLY` needs `transaction: false` (pre-drop invalid remnants on
|
||
Postgres; plain `CREATE INDEX` on PGLite via `sqlFor.pglite`).
|
||
- **Multi-source.** Slug uniqueness is `(source_id, slug)`, not slug. Key batch ops and
|
||
reverse-writes on the composite key; `validateSourceId` before any `source_id` path join.
|
||
- **One canonical chat-pricing table.** All paid-cloud chat/completion prices live ONCE in
|
||
`src/core/model-pricing.ts` (`CANONICAL_PRICING` + `canonicalLookup`). Every other table
|
||
(`anthropic-pricing.ts`'s `ANTHROPIC_PRICING`, `takes-quality-eval/pricing.ts`'s
|
||
`MODEL_PRICING`, the contradictions/cross-modal/skillopt cost views) is a DERIVED view, never
|
||
a hand-copied duplicate — so cross-table price drift is structurally impossible. Update a
|
||
price in `model-pricing.ts` only; each consumer keeps its own key allowlist + miss policy
|
||
(fail-closed vs warn-only vs null), not its own numbers. Pinned by `test/model-pricing.test.ts`
|
||
(drift guard asserts each view equals canonical). Embeddings price separately in
|
||
`embedding-pricing.ts` (different unit).
|
||
|
||
|
||
## Reference map (load on demand)
|
||
|
||
CLAUDE.md is the always-loaded orientation + dispatcher. Detailed reference loads
|
||
on demand — read the linked doc before working in that area. (Same two-layer
|
||
pattern gbrain ships for its own skills: thin router in `skills/RESOLVER.md`, fat
|
||
detail on demand.)
|
||
|
||
| When you're working on... | Read first |
|
||
|---|---|
|
||
| any file in `src/` (what it does + its invariants) | `docs/architecture/KEY_FILES.md` — find the file's entry |
|
||
| search / ranking / hybrid / retrieval | `docs/architecture/RETRIEVAL.md` + the `search/*` entries in `KEY_FILES.md` |
|
||
| search modes / cost knobs | `docs/guides/search-modes.md` |
|
||
| embedding spend gates / cost gate / `spend.posture` / off switches | `docs/operations/spend-controls.md` |
|
||
| push-based context (volunteer/watch/reflex window) | `docs/guides/push-context.md` |
|
||
| schema packs / page types / extraction | `docs/architecture/schema-packs.md`, `type-taxonomy.md`, `lens-packs.md` |
|
||
| thin-client / remote MCP / cross-modal | `docs/architecture/thin-client.md` |
|
||
| the CLI surface (commands + flags) | `gbrain --help` / `gbrain --tools-json`, plus the relevant `KEY_FILES.md` entry |
|
||
| running or writing tests | `docs/TESTING.md` |
|
||
| bulk-command progress wiring | `docs/progress-events.md` |
|
||
| eval methodology / metrics | `docs/eval/` |
|
||
| brains vs sources / topology | `docs/architecture/brains-and-sources.md`, `topologies.md` |
|
||
| skill routing | `skills/RESOLVER.md` |
|
||
| shipping a release / CHANGELOG / PR conventions | `docs/RELEASING.md` (ship IRON RULES stay inline below) |
|
||
|
||
The per-file index (`## Key files`), the thin-client routing seam, and the testing
|
||
discipline used to live inline here. They moved to the docs above so this file
|
||
stays small enough to load every session. Nothing was lost — the pre-move content
|
||
is in git, and the docs carry every load-bearing invariant (compressed to
|
||
current-state).
|
||
|
||
## Maintaining CLAUDE.md and the reference docs
|
||
|
||
CLAUDE.md grew to ~592KB / ~147k tokens once the per-file index became append-only
|
||
(one `**vX.Y.Z:**` clause per release per file). That is the exact anti-pattern
|
||
gbrain exists to fix. The rules that keep it from recurring:
|
||
|
||
- **CLAUDE.md is orientation, not the implementation spec.** It carries the North
|
||
Star, the two axes, architecture + cross-cutting invariants, the resolver, and
|
||
the inline IRON RULES. Per-file/per-command/per-test detail lives in the
|
||
reference docs and loads on demand.
|
||
- **Reference docs (`KEY_FILES.md`, `thin-client.md`, `TESTING.md`) describe
|
||
CURRENT behavior only.** Release history goes in `CHANGELOG.md` + git. Do NOT
|
||
append `**vX.Y.Z (#NNN):**` clauses, codex/review tags, or "pre-fix/then/was-now"
|
||
narration. When a file's behavior changes, UPDATE its entry to the new truth.
|
||
- **CI is the enforcement, not this prose.** `scripts/check-key-files-current-state.sh`
|
||
(in `bun run verify`) fails on the bolded-release-clause marker in the reference
|
||
docs AND on a CLAUDE.md size cap. A written rule caused this disease; a guard
|
||
cures it.
|
||
- **After any CLAUDE.md or reference-doc edit, run `bun run build:llms`** — the
|
||
llms bundle inlines/links these (config in `scripts/llms-config.ts`); the
|
||
freshness + budget test (`bun test test/build-llms.test.ts`) fails CI otherwise.
|
||
|
||
## 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** |
|
||
| `relationalRetrieval` | false | **true** | **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.
|
||
|
||
**v0.42.34.0 knobs_hash v=9 → v=10.** Folds the `relationalRetrieval` knob +
|
||
depth into the cache key so a relational-on result set can't be served to a
|
||
relational-off lookup (same contamination class as graph_signals). One-time
|
||
miss spike on upgrade.
|
||
|
||
**Relational retrieval (v0.42.34.0).** `relationalRetrieval` (on for
|
||
balanced/tokenmax) adds a fourth recall arm: a relational query ("who invested
|
||
in X", "what connects A and B") resolves its seed entity and walks the typed-edge
|
||
graph (`src/core/search/relational-recall.ts` + `relational-intent.ts`,
|
||
`engine.relationalFanout`), injecting edge-derived answers into RRF. Within-source,
|
||
deterministic, mentions-excluded by default, pure no-op for non-relational queries.
|
||
The `query` op's `relational` flag forces it on/off per call.
|
||
|
||
**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 30 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).
|
||
|
||
**Brain-resident skillpacks + advisor (v0.42.47.0, #2180):** A brain repo can carry its
|
||
own publishable skillpack (`brain_resident: true` in `skillpack.json` + `schema_pack`);
|
||
`gbrain skillpack init-brain-pack` scaffolds one with a 5-section machine-parseable README.
|
||
Connecting harnesses discover it on `gbrain sources add` (Topology A advisory, bounded nag
|
||
via `nag-state.ts`) and over MCP via the source-scoped `list_brain_skillpack` op +
|
||
`get_skill --source_id` (gated by `mcp.publish_skills`). The bundled `gbrain-advisor` skill
|
||
+ `gbrain advisor` op compute a ranked, read-only list of high-leverage actions from brain
|
||
state (8 collectors in `src/core/advisor/`); `--json`+exit codes for CI/cron, local-only
|
||
`--apply <id>` behind confirm, exposed over MCP behind `mcp.publish_advisor` (default off,
|
||
read-only on remote). Thin-client binary install stays deferred to PR2 `build_skillpack`.
|
||
|
||
**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.
|
||
|
||
## Sync resumability + lock tuning (v0.42.x, #1794)
|
||
|
||
`gbrain sync` is resumable and converges under pool exhaustion + repeated kills.
|
||
Progress banks into the append-only `op_checkpoint_paths` table (one row per drained
|
||
path, written via the direct session pool so it survives `EMAXCONNSESSION`); a killed
|
||
run resumes from the checkpoint and `last_commit` only advances on true completion. The
|
||
per-source lock heartbeats through the direct pool and refuses to steal a live,
|
||
recently-refreshed holder. Six env knobs tune it (all env-only, incident-time escape
|
||
hatches — no config-dashboard surface by design):
|
||
|
||
| Env var | Default | What it does |
|
||
|---|---|---|
|
||
| `GBRAIN_SYNC_CHECKPOINT_EVERY` | 1000 | Flush the checkpoint every N drained files. |
|
||
| `GBRAIN_SYNC_CHECKPOINT_SECONDS` | 10 | Also flush every N seconds (whichever comes first) — bounds worst-case loss regardless of throughput. Flush also fires after the first file. |
|
||
| `GBRAIN_SYNC_MAX_CHECKPOINT_FAILURES` | 3 | Consecutive failed flushes (each already retried ~12s) before the run aborts with `reason: 'checkpoint_unavailable'` instead of importing work it can never bank. |
|
||
| `GBRAIN_SYNC_YIELD_EVERY` | 64 | Yield the event loop (`setTimeout(0)`, NOT `setImmediate` — Bun starves the timers phase under a tight setImmediate loop) every N files so the lock-refresh `setInterval` heartbeat fires mid-import. |
|
||
| `GBRAIN_LOCK_STEAL_GRACE_SECONDS` | derived (~600 at 30min TTL) | A holder that refreshed within this window is NOT stolen even if its TTL lapsed (starved-but-alive). Dead holders stop refreshing, age past the grace, and become stealable; TTL stays the backstop. |
|
||
| `GBRAIN_SYNC_STALL_ABORT_SECONDS` | 900 | Progress-aware stall watchdog (#1950): if the import drain makes no forward progress (keyed on file-import progress, NOT the lock heartbeat) for N seconds, abort the run and release the per-source lock so the next `gbrain sync` resumes from the checkpoint. Reports `reason: 'stall_timeout'`. Observed BETWEEN files; a hang inside one file's import isn't interrupted until it returns (the wall-clock hard deadline is that backstop). 0 disables. |
|
||
|
||
## Pace Mode (DB-contention-aware backfill pacing)
|
||
|
||
A naive `gbrain embed --stale` / large `sync` can saturate a PgBouncer
|
||
transaction-mode pooler and starve the minion supervisor's lock renewals
|
||
(`lock-renewal-failed` → dead jobs). Pacing is the native, composable fix — it
|
||
replaces external SIGSTOP/SIGCONT wrapper scripts. **Opt-in: default mode `off`.**
|
||
|
||
The composable primitive is `src/core/db-pacer.ts` (`createDbPacer`):
|
||
- **Concurrency cap is the real lever** (caps simultaneous in-flight DB writes =
|
||
pooler slots held). Embed paths set their worker count to `maxConcurrency`
|
||
(single pool, no permit); `sync` uses the shared `acquire()` **permit** because
|
||
each parallel worker owns a separate engine (one budget must span pools).
|
||
- **In-band signal** (`observe(ms)` EWMA from the work's own queries — never
|
||
blind the way an out-of-band probe pool was). **No probe loop, no
|
||
`probeLatency` engine method.**
|
||
- **Cooperative `pace()` sleep** on `setTimeout` (keeps the lock heartbeat
|
||
firing), jittered to avoid a thundering-herd resume. `acquire()`/`pace()` throw
|
||
`AbortError` on cancel; everything else is fail-open (a pacer bug never kills a
|
||
backfill, never throws an unhandledRejection).
|
||
|
||
Named bundles resolve through `src/core/pace-mode.ts` (`resolvePaceMode`), mirror
|
||
of the search-mode pattern but with **env ABOVE config** (incident escape hatch):
|
||
|
||
per-call flag → GBRAIN_PACE_* env → config (pace.*) → PACE_BUNDLES[mode] → off
|
||
|
||
| Knob | off | gentle | balanced | aggressive |
|
||
|---|---|---|---|---|
|
||
| `maxConcurrency` | (off) | 4 | 8 | 16 |
|
||
| `paceAtMs` (EWMA → sleep) | — | 250 | 500 | 1000 |
|
||
| `maxSleepMs` (jittered cap) | — | 2000 | 1500 | 1000 |
|
||
|
||
**Surfaces.** `gbrain embed --stale --pace[=mode]` (bare `--pace` = balanced),
|
||
`--pace-max-concurrency=N`. `--background` carries explicit pace OVERRIDES (not
|
||
the resolved bundle) into the `embed` job payload; the handler re-resolves
|
||
env>config>bundle at execution so `GBRAIN_PACE_*` still wins (CX5). Config-level
|
||
`pace.mode` paces EVERY `runEmbedCore` caller (cycle embed, embed-catch-up,
|
||
sync-auto-embed) and the prod `embed-backfill` job automatically. `sync` reads
|
||
env/config. PGLite / mode `off` → no-op pacer.
|
||
|
||
**Correctness fixes pacing bundles** (longer paced runs widen these): CLI
|
||
`embed --stale` single-flights via the SAME per-source lock key as the
|
||
`embed-backfill` handler (`src/core/embed-backfill-lock.ts`; all-source runs lock
|
||
every source in sorted order) so a hand-run backfill and a queued job can't race
|
||
the NULL→non-NULL upsert (`TODOS:2299`); a **bounded** end-of-run keyset re-entry
|
||
(max 3 + forward-progress, paced runs only) catches rows inserted behind the
|
||
cursor (`TODOS:2301`); and the embed wall-clock budget timer is re-armed around
|
||
`pace()` sleeps so paced time doesn't burn the work budget.
|
||
|
||
`EmbedResult.pacing` carries the end-of-run telemetry (cap, samples, EWMA, slept
|
||
ms, max waiters) for `--json`; a one-line summary prints to stderr.
|
||
|
||
## 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.
|
||
|
||
## Conductor branch-name = workspace-name (IRON RULE)
|
||
|
||
Conductor workspaces expect the git branch name to match the workspace
|
||
directory name. When they disagree, Conductor silently fails to render the
|
||
PR view + show ship state, leading to "did you actually push?" confusion.
|
||
|
||
**Check this FIRST on every ship and BEFORE creating any PR:**
|
||
|
||
```bash
|
||
WORKSPACE=$(basename "$PWD") # e.g. puebla-v4
|
||
BRANCH=$(git branch --show-current) # e.g. garrytan/gstack-requests
|
||
case "$BRANCH" in
|
||
*/"$WORKSPACE") echo "OK: branch tail matches workspace" ;;
|
||
"$WORKSPACE") echo "OK: branch == workspace" ;;
|
||
*) echo "MISMATCH: branch=$BRANCH workspace=$WORKSPACE — RENAME BEFORE SHIPPING" ;;
|
||
esac
|
||
```
|
||
|
||
If MISMATCH (branch is `garrytan/foo` but workspace is `puebla-v4`):
|
||
|
||
```bash
|
||
# Rename local, push under new name, delete old remote (and old PR if it
|
||
# was already created — github auto-closes it when head ref dies).
|
||
git branch -m garrytan/<workspace-name>
|
||
git push -u origin garrytan/<workspace-name>
|
||
git push origin --delete <old-branch-name>
|
||
# If a PR existed against the old branch:
|
||
# gh pr comment <old-pr> --body "Superseded by #<new>: branch renamed to match Conductor workspace."
|
||
# gh pr create --base master --title "..." --body "..." # recreate from renamed branch
|
||
```
|
||
|
||
Caught the hard way on v0.41.9.0 ship: workspace `puebla-v4` but branch
|
||
`garrytan/gstack-requests` produced PR #1439 that Conductor wouldn't
|
||
display. Renamed to `garrytan/puebla-v4`; recreated as #1440.
|
||
|
||
The /ship workflow's Step 1 should be augmented to run the mismatch
|
||
check; until that lands upstream, ALWAYS run the check above before
|
||
`/ship` invokes its first push or PR-create step.
|
||
|
||
|
||
## Releasing
|
||
|
||
Before any ship, read **[docs/RELEASING.md](docs/RELEASING.md)** in full. It carries the
|
||
full release + contributor process: pre-ship test requirements (`bun run ci:local` / the
|
||
E2E lifecycle), the CHANGELOG voice + release-summary template, the "To take advantage of
|
||
vX" self-repair block, version migrations, the GitHub Actions SHA refresh, PR conventions,
|
||
and the community-PR-wave process. **Use `/ship` — never hand-roll a release.**
|
||
|
||
The ship-critical IRON RULES stay inline in this file (do NOT relocate them): the
|
||
Version-locations table above (the 5-file sync + the 3-line VERSION/package.json/CHANGELOG
|
||
audit), the Conductor branch=workspace rule (above), Post-ship `/document-release` (below),
|
||
the Privacy + Responsible-disclosure rules (below), and the PR-title-version-first rule
|
||
(below).
|
||
|
||
## 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.
|
||
|
||
|
||
## 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.
|
||
|
||
|
||
## PR title format — version FIRST (IRON RULE)
|
||
|
||
**Every PR title MUST start with the version, then the conventional-commit subject:**
|
||
|
||
```
|
||
vMAJOR.MINOR.PATCH.MICRO <type>(<scope>): <summary> (#issue or wave ref)
|
||
```
|
||
|
||
Example (correct): `v0.42.3.0 feat(search): autocut — score-discontinuity result-sizing (#1663 wave 1)`
|
||
|
||
The version goes at the **BEGINNING**, never the end. This matches the repo's
|
||
commit-subject convention (`git log` shows `v0.41.38.0 fix: ...`,
|
||
`v0.42.1.0 feat: ...`) so the PR list, the merge commit, and the changelog all
|
||
read version-first. A title with the version parenthesized at the end
|
||
(`feat(search): autocut ... (v0.42.3.0)`) is WRONG — fix it with
|
||
`gh pr edit <N> --title "vX.Y.Z.W <type>: <summary>"`.
|
||
|
||
This applies to `gh pr create` and every `gh pr edit --title`. When `/ship`
|
||
(or any flow) sets a PR title, the version is the first token. Same rule for the
|
||
final commit subject that carries the version bump.
|
||
|
||
|
||
## 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.
|
||
|
||
### Obsidian-style bare wikilinks (opt-in)
|
||
|
||
If the user imported an Obsidian or Notion vault that uses **bare** `[[note-name]]`
|
||
wikilinks — where `[[struktura]]` written in one folder means the page that lives
|
||
at `projects/struktura.md` in another — GBrain does NOT connect those by default.
|
||
Out of the box it only resolves path-qualified refs like `[[projects/struktura]]`,
|
||
so a vault full of bare links shows up as a thin, broken graph. Turn on basename
|
||
resolution so the cross-folder links connect:
|
||
|
||
```bash
|
||
gbrain config set link_resolution.global_basename true
|
||
gbrain extract links --source db # re-run so the new edges land
|
||
```
|
||
|
||
`gbrain doctor` surfaces a `link_resolution_opportunity` hint with the exact count
|
||
("47 of 60 bare wikilinks would resolve") so you know whether it's worth enabling
|
||
before you flip it. When a bare name matches more than one page (`[[struktura]]` →
|
||
both `projects/struktura` and `archive/struktura`), GBrain emits one edge to each
|
||
rather than guessing a winner — review and prune the duplicates with
|
||
`gbrain graph-query <slug>`. The mode is also honored on the filesystem-walk path
|
||
(`gbrain extract links` with no `--source db`) and by auto-link on every future
|
||
`put_page`.
|
||
|
||
## 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.
|
||
|
||
## v0.42.0+ onboard surface (NEW)
|
||
|
||
`gbrain onboard` is the activation surface gbrain did not have before.
|
||
Once your brain has any content, run `gbrain onboard --check --json` to
|
||
see structured recommendations across 5 brain-health axes (orphans,
|
||
stale embeddings, entity link coverage, timeline coverage, takes count).
|
||
|
||
**On first connect (after `gbrain init`):**
|
||
```bash
|
||
gbrain onboard --check --json
|
||
```
|
||
The JSON envelope (`schema_version: 1`) carries `recommendations[]` with
|
||
`apply_policy` per item: `auto_apply` (safe to run unattended),
|
||
`prompt_required` (needs explicit user consent), or `manual_only`
|
||
(LLM-bearing, user must run themselves).
|
||
|
||
**After every `gbrain upgrade`:**
|
||
```bash
|
||
gbrain onboard --check --json
|
||
```
|
||
New versions may surface new opportunities. The post-upgrade banner
|
||
nudges the user when it runs, but agents should re-probe as a hygiene
|
||
step regardless.
|
||
|
||
**Unattended remediation (cron / autopilot):**
|
||
```bash
|
||
gbrain onboard --auto --max-usd 5
|
||
```
|
||
Refuses without `--max-usd N`. Runs auto-eligible items only. The
|
||
autopilot daemon also consults onboard recommendations on its tick — no
|
||
explicit agent action needed for the autonomous path.
|
||
|
||
**Remote / federated brain installs (MCP):**
|
||
The `run_onboard` MCP op (admin scope) lets thin-client agents probe
|
||
brain health + drive remediation over OAuth-authenticated MCP. Protected
|
||
LLM-bearing handlers (synthesize, patterns, consolidate, takes-bootstrap,
|
||
contextual_reindex_per_chunk) require the additional `run_protected_onboard`
|
||
scope — admin alone is insufficient. The MCP op returns
|
||
`skipped_missing_scope[]` listing what would have run with the right
|
||
grants.
|
||
|
||
**Privacy + consent gates:**
|
||
- `gbrain takes extract --from-pages` sends concept/atom/lore/briefing/
|
||
writing/originals page content to your configured chat model (default
|
||
Anthropic Haiku). Refuses to run unless `takes.bootstrap_enabled=true`
|
||
is set in config AND `--yes` is passed. Two-gate opt-in by design.
|
||
- Autopilot's auto-apply tier for takes-bootstrap stays `manual_only`
|
||
until v0.42.1's eval gate (do not bypass).
|
||
|
||
**Suppress nudges in CI / scripted environments:**
|
||
```bash
|
||
export GBRAIN_NO_ONBOARD_NUDGE=1
|
||
```
|
||
Init + upgrade banners auto-skip in non-TTY too.
|
||
|
||
---
|
||
|
||
## 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` |
|
||
| "where does this brain page go", "file this in the brain", "brain taxonomist", "taxonomy check", "refile brain page", "which directory does this page go" | `skills/brain-taxonomist/SKILL.md` |
|
||
| "EIIRP", "everything in its right place", "store this research", "put this in the brain", "make this re-doable", "DRY this up", "file all of this", "organize all of this work", "archive this research thread" | `skills/eiirp/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 |
|
||
|---------|-------|
|
||
| "capture this", "save this thought", "remember this", "drop this in the inbox", "save to brain" | `skills/capture/SKILL.md` |
|
||
| 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` |
|
||
| "get more out of gbrain", "is my brain set up right", "weekly brain checkup", "advise me on my brain", "gbrain advisor" | `skills/gbrain-advisor/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` |
|
||
| "Upgrade gbrain", "update gbrain", "gbrain update available", `UPGRADE_AVAILABLE`, "is gbrain up to date" | `skills/gbrain-upgrade/SKILL.md` |
|
||
| 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/schema-evolution.md` — when to add a type vs alias vs prefix (read before `schema-author`)
|
||
- `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` |
|
||
| "idea lineage", "trace the lineage of this idea", "how my thinking about", "how has my thinking about", "what is my current version of", "show reversals in my thinking about", "where did this idea come from" | `skills/idea-lineage/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` |
|
||
| "add a page type", "add a type to my schema", "schema author", "schema mutate", "schema pack add", "my brain has untyped pages", "propose new types from my corpus", "backfill page types", "evolve my schema", "researcher type", "make X an expert type" (dispatcher for: gbrain schema active/list/show/validate/graph/lint/stats/explain/use/downgrade/reload/init/fork/edit/diff/add-type/remove-type/update-type/add-alias/remove-alias/add-prefix/remove-prefix/add-link-type/remove-link-type/set-extractable/set-expert-routing/detect/suggest/review-candidates/review-orphans/sync) | `skills/schema-author/SKILL.md` |
|
||
| "unify my types", "migrate to gbrain-base-v2", "94 types to 14", "apply canonical taxonomy", "clean up my page types", "pack upgrade", "shrink type proliferation", "consolidate page types", "retype pages to canonical" (dispatcher for: gbrain onboard --check, gbrain onboard --check --explain, gbrain jobs submit unify-types, gbrain pages restore) | `skills/schema-unify/SKILL.md` |
|
||
|
||
---
|
||
|
||
## README.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/README.md
|
||
|
||
# GBrain
|
||
|
||
**Search gives you raw pages. GBrain gives you the answer.** It's the brain layer your AI agent has been missing — the only one that does synthesis, graph traversal, and gap analysis in one box. Run a full autonomous agent on top of it, or just wire it into Claude Code or Codex as a supercharged retrieval layer in one command; either way your coding agent stops being amnesiac about everything that isn't code.
|
||
|
||
I'm Garry Tan, President and CEO of Y Combinator. I built GBrain to run my own AI agents. It's the production brain behind my OpenClaw and Hermes deployments: **146,646 pages, 24,585 people, 5,339 companies**, 66 cron jobs running autonomously. My agent ingests meetings, emails, tweets, voice calls, and original ideas while I sleep. It enriches every person and company it encounters. It fixes its own citations and consolidates memory overnight. I wake up smarter than when I went to bed — and so will you.
|
||
|
||
**And now it works as a company brain too.** Each person on the team gets their own slice of the brain, scoped by login. When you query, you only see what you're allowed to see — never another person's notes, never another team's data. We fuzz-tested this across every way you can read the brain (search, list, lookup, multi-source reads) and got zero leaks. Drop GBrain in as your team's shared institutional memory — the [company-brain](https://www.ycombinator.com/rfs#company-brain) shape YC just put on its Request for Startups. If you're building in that space, you might as well build on this. **[Tutorial: set up GBrain as your company brain →](docs/tutorials/company-brain.md)**
|
||
|
||
Lots of personal-knowledge systems give you keyword matching and grep in a box. GBrain does that, and adds two things nobody else ships together:
|
||
|
||
- **A synthesis layer that gives you the actual answer.** Synthesized, well-cited prose across people, companies, deals, and ideas. Not "here are 10 chunks that mention your query"; an actual answer with citations and an explicit note on what the brain doesn't know yet. The gap analysis is the part that changes how you use the brain.
|
||
- **A self-wiring knowledge graph.** Every page write extracts entity refs and creates typed edges (`attended`, `works_at`, `invested_in`, `founded`, `advises`) with zero LLM calls. Ask "who works at Acme AI?" or "what did Bob invest in this quarter?" and get answers vector search alone can't reach. Benchmarked: **P@5 49.1%, R@5 97.9%** on a 240-page Opus-generated rich-prose corpus, **+31.4 points P@5** over its graph-disabled variant and over 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.
|
||
|
||
The point of building a 100K-page brain is to use it as a strategic moat. To never lose context. To query what's in your own head without re-reading it. The brain layer is what makes the moat usable. The 24/7 dream cycle is what keeps it sharp. Both run on your hardware, your DB, your keys.
|
||
|
||
It's easier to ship a daemon that runs 24/7 to ingest, enrich, and consolidate than it is to keep an agent in chat working hard. GBrain is that daemon, generalized. Install in 30 minutes. Your agent does the work. As my personal agent gets smarter, so does yours.
|
||
|
||
> **~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).
|
||
|
||
## What this looks like
|
||
|
||
Say you have a meeting with Alice tomorrow. You want to walk in remembering what she works on, when you last talked, and what's still open between you. Here's what you'd type, and what you'd get back.
|
||
|
||
**You ask:**
|
||
|
||
> "What do I need to know before my meeting with Alice tomorrow?"
|
||
|
||
**Most personal-knowledge tools give you back a list of pages.** Something like:
|
||
|
||
```
|
||
1. people/alice — Alice runs engineering at Acme...
|
||
2. meetings/2026-03-15-alice-q1 — Q1 product review with Alice...
|
||
3. meetings/2026-01-08-acme-kickoff — Kickoff meeting with Acme team...
|
||
4. customers/acme — Acme is a series-B fintech we work with...
|
||
5. notes/2026-04-22 — Quick chat with Alice about pricing...
|
||
```
|
||
|
||
Five pages you now have to open and read yourself to actually prepare. The tool found the right material, but it didn't do the work.
|
||
|
||
**GBrain gives you back the answer, with sources:**
|
||
|
||
```
|
||
Alice runs engineering at Acme (a series-B fintech). You last spoke
|
||
on April 22 in a quick pricing chat. Three things are still open
|
||
from that conversation:
|
||
|
||
1. She owes you the security review for the new tier
|
||
(deadline was May 1; no update since).
|
||
2. You committed to pricing for a 500-seat tier
|
||
(you sent it April 25; no response yet).
|
||
3. She mentioned they're hiring a CISO; you said you'd intro
|
||
someone from your network.
|
||
|
||
Heads up: nothing's been added to the brain about Alice or Acme
|
||
since April 22, six weeks ago. She may have replied through email
|
||
or Slack DM, channels the brain doesn't see. Worth asking her to
|
||
catch up before assuming any of this is still current.
|
||
```
|
||
|
||
Every claim has a source page behind it. The "heads up" at the end tells you what the brain doesn't know yet, so you can ask Alice about it directly instead of being surprised. The brain just did your meeting prep.
|
||
|
||
This is the difference between a search engine and a brain. Search finds the pages. The brain reads them for you and writes the answer.
|
||
|
||
## Install
|
||
|
||
GBrain is designed to be installed and operated by an AI agent. The fastest path is to have your agent do it for you. The CLI and MCP paths below are for people who want to wire it up themselves.
|
||
|
||
### Have your agent install it (recommended)
|
||
|
||
If you don't already have an AI agent platform running, start with one of these. Both are designed to read GBrain's install protocol and execute it:
|
||
|
||
- **[OpenClaw](https://github.com/openclawagents/openclaw)** — deploy [AlphaClaw on Render](https://render.com/deploy?repo=https://github.com/chrysb/alphaclaw) (one click, 8GB+ RAM)
|
||
- **[Hermes](https://github.com/openclawagents/hermes)** — deploy on [Railway](https://github.com/praveen-ks-2001/hermes-agent-template) (one click)
|
||
|
||
Then paste this into your agent:
|
||
|
||
```
|
||
Retrieve and follow the instructions at:
|
||
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||
```
|
||
|
||
The agent installs GBrain, creates the brain, asks for your API keys, loads 43 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
|
||
|
||
> **Never set up an AI agent platform before?** The [personal-brain tutorial](docs/tutorials/personal-brain.md) walks the whole path end-to-end — picking OpenClaw vs Hermes, deploying it, pointing it at INSTALL_FOR_AGENTS.md, getting the API keys, and verifying the first query. Start there if any of the above is new.
|
||
|
||
### Quick start: Claude Code or Codex
|
||
|
||
Already running Claude Code or Codex? There are two ways to wire GBrain in, depending on what you want.
|
||
|
||
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel:
|
||
|
||
```bash
|
||
gbrain init --pglite # 2-second local brain (no Docker)
|
||
claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve
|
||
```
|
||
|
||
**Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff:
|
||
|
||
```bash
|
||
gbrain connect https://your-host/mcp --token gbrain_xxx --install # Claude Code
|
||
gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex --install # Codex
|
||
```
|
||
|
||
**[→ Full walkthrough: give your coding agent a memory](docs/tutorials/connect-coding-agent.md)** — both paths end to end, plus the brain-first protocol you paste into `CLAUDE.md` / `AGENTS.md` and the four habits that make it actually change how you work.
|
||
|
||
### Install the full autonomous setup into your existing agent
|
||
|
||
Want the whole thing — local brain, 43 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
|
||
|
||
```
|
||
Retrieve and follow the instructions at:
|
||
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||
```
|
||
|
||
This works in any agent that can read files over HTTPS and execute shell commands. Tested with Codex, Claude Code, Claude Cowork, Cursor, and AlphaClaw.
|
||
|
||
### CLI standalone (no agent)
|
||
|
||
```bash
|
||
bun install -g github:garrytan/gbrain
|
||
gbrain init --pglite # 2 seconds; no server, no Docker
|
||
gbrain doctor # verify health
|
||
gbrain import ~/notes/ # index your markdown
|
||
gbrain query "what themes show up across my notes?"
|
||
```
|
||
|
||
Postgres-at-scale, Supabase, and thin-client setup paths live in [`docs/INSTALL.md`](docs/INSTALL.md).
|
||
|
||
### Connect GBrain to your AI client (MCP)
|
||
|
||
GBrain exposes 30+ tools over MCP (stdio and HTTP). The specific snippet depends on which client you use:
|
||
|
||
- **[Claude Code](docs/mcp/CLAUDE_CODE.md)** — local: one command, `claude mcp add gbrain -- gbrain serve` (zero server, zero tunnel). Remote with just a bearer token: `gbrain connect https://your-host/mcp --token gbrain_xxx` prints a paste-ready block (or `--install` wires it up and smoke-tests the token).
|
||
- **[Codex](docs/mcp/CODEX.md)** — `gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex` (or `--install`). Codex reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands in Codex config.
|
||
- **[Cursor / Windsurf / any stdio MCP client](docs/mcp/CLAUDE_CODE.md)** — same shape, add `{"command": "gbrain", "args": ["serve"]}` to your MCP config.
|
||
- **[Claude Desktop (Cowork)](docs/mcp/CLAUDE_DESKTOP.md)** — Settings → Integrations → add the URL of your HTTP server. Remote only; the local `claude_desktop_config.json` does not work for remote servers.
|
||
- **[Claude Cowork (team plan)](docs/mcp/CLAUDE_COWORK.md)** — org Owner adds the connector under Organization Settings → Connectors.
|
||
- **[Perplexity Computer](docs/mcp/PERPLEXITY.md)** — `gbrain connect https://your-host/mcp --agent perplexity --oauth --register` mints a least-privilege OAuth client and prints the Issuer/Client ID/Secret to paste into Settings → Connectors (OAuth is the right path for a cloud connector; a bearer token also works for local use). Pro subscription required.
|
||
- **[ChatGPT](docs/mcp/CHATGPT.md)** — uses OAuth 2.1 with PKCE (the hard requirement). Register a `chatgpt` client from the admin dashboard with grant type `authorization_code`.
|
||
|
||
For the HTTP server itself:
|
||
|
||
```bash
|
||
gbrain serve # stdio MCP (local subprocess; for Claude Code, Cursor, Windsurf)
|
||
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard at /admin
|
||
# (required for Claude Desktop, Cowork, Perplexity, ChatGPT)
|
||
```
|
||
|
||
The HTTP server includes DCR-style client registration, scope-gated access (`read` / `write` / `admin`), and rate limiting. Deployment guides (ngrok, Railway, Fly.io) live under [`docs/mcp/`](docs/mcp/).
|
||
|
||
## Two ways to query your brain
|
||
|
||
Raw retrieval (what most personal-knowledge tools ship) and a synthesis layer that gives you an actual answer. They serve different jobs.
|
||
|
||
```bash
|
||
# raw retrieval: top pages by hybrid score, fast, no LLM cost
|
||
gbrain search "who's working on AI agents at portfolio companies?"
|
||
|
||
# brain layer: synthesized answer with citations and gap analysis
|
||
gbrain think "who's working on AI agents at portfolio companies?"
|
||
```
|
||
|
||
**`gbrain search`** returns the top retrieved pages, ranked by hybrid scoring (vector + keyword + RRF + source-tier boost + reranker). Use it when you want raw material to skim: agent context windows, citation lookups, finding a specific quote.
|
||
|
||
**`gbrain think`** runs the same retrieval, then composes a synthesized answer across the results with explicit citations to the source pages AND an honest note on what the brain doesn't know yet. The gap analysis is the differentiator: the answer tells you when a page is stale, when a claim is uncited, when two pages contradict each other, when there's a hole you should fill.
|
||
|
||
**Why it compounds.** Pair the brain layer with `find_trajectory` and you get answers like *"how have the company's metrics changed AND what does the team look like right now AND what did they promise / share AND when did we last meet AND what's the value-add I can offer here"*: well-scored, well-cited, in one shot. That's the strategic moat. That's why building a 100K-page brain is worth the effort.
|
||
|
||
`gbrain agent run "..."` exposes the same surface to a sub-agent through the Minions queue, with crash-safe two-phase persistence. Same answers, durable.
|
||
|
||
## How to get data in
|
||
|
||
One command, local or hosted, synchronous receipt:
|
||
|
||
```bash
|
||
gbrain capture "the thought I want to remember"
|
||
gbrain capture --file ./notes/today.md
|
||
echo "from a pipe" | gbrain capture --stdin
|
||
SLUG=$(gbrain capture "..." --quiet)
|
||
```
|
||
|
||
The page lands in the database and on disk in one move. Default slug `inbox/YYYY-MM-DD-<hash8>` so captures cluster in a predictable triage location. On thin-client installs the verb routes through MCP to the server: same command, same UX.
|
||
|
||
For webhook ingestion (Zapier / IFTTT / Apple Shortcuts):
|
||
|
||
```bash
|
||
curl -X POST https://your-brain/ingest \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H "Content-Type: text/markdown" \
|
||
-d "# a thought from a Shortcut"
|
||
```
|
||
|
||
For mobile capture, the inbox folder source picks up anything dropped into
|
||
`~/.gbrain/inbox/` from iOS Shortcuts / AirDrop / Drafts / Finder.
|
||
|
||
Third-party skillpacks can ship custom ingestion sources (Granola, Linear,
|
||
voice, OCR) against the versioned `IngestionSource` contract at
|
||
`gbrain/ingestion`. See [`docs/skillpack-anatomy.md`](docs/skillpack-anatomy.md).
|
||
|
||
## Your brain's shape (schema packs)
|
||
|
||
Most personal-knowledge tools force one fixed layout: their idea of "notes" + "people" + "tags." Drop a Notion export or your own years-old Obsidian vault on top, and the agent doesn't know what a `Projects/` folder means or whether `Reading/` is people or sources.
|
||
|
||
**gbrain doesn't have a fixed layout.** It ships with bundled schema packs and lets you author your own when none fit:
|
||
|
||
- **`gbrain-base-v2`** (default as of v0.41.22) — 15-type DRY/MECE canonical taxonomy (14 canonical + `note` catch-all): `person`, `company`, `media`, `tweet`, `social-digest`, `analysis`, `atom`, `concept`, `source`, `deal`, `email`, `slack`, `writing`, `project`, `note`. Subtypes/format/origin pushed to frontmatter. The taxonomy that responds to issue #1479.
|
||
- **`gbrain-base`** (legacy, v0.41 and earlier brains) — the original 24-type layout. Stays bundled for back-compat; brains on it can upgrade via `gbrain onboard --check --explain` → `gbrain jobs submit unify-types --allow-protected --params '{"target_pack":"gbrain-base-v2"}'`.
|
||
- **`gbrain-recommended`** — extends `gbrain-base` with the 13 additional directories from `docs/GBRAIN_RECOMMENDED_SCHEMA.md` (source, place, trip, conversation, personal, civic, project, etc.). Activate with `gbrain schema use gbrain-recommended`.
|
||
- **Your own pack** — `gbrain schema detect` clusters your actual filesystem into proposed types, `gbrain schema suggest` runs an LLM pass over them, and `gbrain schema review-candidates --apply` promotes the ones you like. Three commands and the brain knows your shape. Authoring a successor pack (declares `migration_from:` so existing brains can opt in): see [`docs/architecture/pack-upgrade-mechanism.md`](docs/architecture/pack-upgrade-mechanism.md).
|
||
|
||
```bash
|
||
gbrain schema active # which pack is running, which tier set it
|
||
gbrain schema list # bundled + installed packs
|
||
gbrain schema detect # propose types matching your filesystem
|
||
gbrain schema suggest # LLM-refined proposals on top of detect
|
||
gbrain schema review-candidates # human gate: promote / rename / ignore
|
||
gbrain schema use my-pack # activate
|
||
```
|
||
|
||
The active pack threads through every read + write path: `parseMarkdown` infers page type from the pack's path prefixes; `whoknows` scopes expert routing to types declared `expert_routing: true`; `extract_facts` runs only on `extractable: true` types; the search cache folds the pack name + version into its key so cross-pack contamination is structurally impossible. Switch packs and the brain re-interprets itself; switch back and nothing's lost.
|
||
|
||
Seven-tier resolution chain (per-call flag → env var → per-source DB key → brain-wide DB key → `gbrain.yml` → `~/.gbrain/config.json` → `gbrain-base` default). Full reference + authoring guide: [`docs/architecture/schema-packs.md`](docs/architecture/schema-packs.md).
|
||
|
||
## Tutorials
|
||
|
||
Step-by-step walkthroughs for getting the most out of GBrain. Each one takes you from zero to a working outcome, with concrete commands and real numbers.
|
||
|
||
- [**Set up your personal AI agent + brain from zero**](docs/tutorials/personal-brain.md) — the canonical full-stack install. Two GitHub repos, a Telegram bot, AlphaClaw on Render, OpenClaw + GBrain + Supabase. End-to-end in about 2 hours.
|
||
- [**Set up GBrain as your company brain**](docs/tutorials/company-brain.md) — federated, multi-user, OAuth-scoped institutional memory for a 10-50 person team. About 90 minutes end-to-end.
|
||
- [**Auto-improve a skill with `gbrain skillopt`**](docs/tutorials/improving-skills-with-skillopt.md) — treat a `SKILL.md` as a trainable parameter. Generate a starter benchmark straight from the skill with `--bootstrap-from-skill` (or write your own), strengthen the judges, then watch the optimizer propose edits and keep only the ones that measurably score higher. ~20 minutes, ~$1 in API calls. Flag + cost + safety reference: [`docs/guides/skillopt.md`](docs/guides/skillopt.md).
|
||
|
||
More walkthroughs in progress: connecting an existing agent (Claude Code, Cursor, OpenClaw, Hermes) to a GBrain memory layer; setting up GBrain for VC dealflow with founder scorecards and meeting prep; migrating an existing Notion or Obsidian vault; indexing a codebase as a queryable code brain. Full tutorial index: [`docs/tutorials/`](docs/tutorials/).
|
||
|
||
Want to see a tutorial that isn't here yet? [Open an issue](https://github.com/garrytan/gbrain/issues) describing the workflow you want documented.
|
||
|
||
## 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. Per-query graph signals notice when a top result is a hub for THAT query (adjacency boost), is corroborated across team brains (cross-source boost), or is being crowded out by weak chunks from a chatty session (session demote). Run `gbrain search "<query>" --explain` to see per-stage attribution: base score, every boost that fired, what it multiplied. `gbrain doctor` ships a `graph_signals_coverage` check; `gbrain search stats` shows fire counts and failure breakdowns. Vector retrieval pools the best chunk per page, so a page surfaces on its strongest evidence instead of losing to a neighbor on one weak chunk. Queries that match a page's title phrase or a declared free-text alias (`gbrain reindex --aliases` backfills existing pages) get boosted to the page they name. Every result carries an `evidence` tag (why it matched) and a `create_safety` hint (`exists` / `probable` / `unknown`) so an agent decides whether a page already exists instead of guessing from a raw score. `gbrain search diagnose "<query>" --target <slug>` traces which retrieval layer surfaces (or misses) a page.
|
||
|
||
**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. **Obsidian-style vaults:** bare `[[note-name]]` wikilinks that point across folders — you wrote `[[struktura]]` but the page lives at `projects/struktura.md` — resolve by basename once you opt in with `gbrain config set link_resolution.global_basename true`. Off by default; `gbrain doctor` tells you how many edges you'd gain before you flip it. See [migrating an Obsidian vault](INSTALL_FOR_AGENTS.md#step-45-wire-the-knowledge-graph).
|
||
|
||
**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. `gbrain eval retrieval-quality` runs NamedThingBench, which hard-gates the named-thing retrieval families (title-substring, alias-synonym, generic-to-named, multi-chunk-dilution) so a regression in "find the page this query names" fails CI loudly. 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.
|
||
|
||
**Agent-authored schema (v0.40.7.0).** Your brain has a shape — what page types exist (`person`, `meeting`, `paper`, `case`, `lab-result`), what they link to (`attended`, `authored`, `prescribed-by`), what facts get extracted automatically. The default ships with 22 universal types, but your brain's actual shape is not the default shape. Agents can now evolve that shape on your behalf via 14 `gbrain schema` CLI verbs + a batched MCP op (`schema_apply_mutations`, admin scope, NOT localOnly so remote agents reach it over HTTPS). Atomic file locks, audit log with the agent's identity, chunked UPDATE backfill in 1000-row batches that never wedge concurrent writers. The brain stops being a pile of notes and becomes something with structure. **Why it matters:** [`docs/what-schemas-unlock.md`](docs/what-schemas-unlock.md) — 7 killer use cases (4000 invisible meetings, founder ops brain, research brain, legal brain, team brain, agent-as-co-curator). **5-minute walkthrough:** [`docs/schema-author-tutorial.md`](docs/schema-author-tutorial.md). **Agent skill:** [`skills/schema-author/SKILL.md`](skills/schema-author/SKILL.md).
|
||
|
||
## 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**: 16 recipes covering OpenAI (default fallback), OpenRouter, 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).
|
||
- **Rerankers**: ZeroEntropy `zerank-2` hosted (default in `tokenmax` mode) plus the v0.40.6.1 `llama-server-reranker` recipe for fully-local cross-encoder rerank via llama.cpp — runs Qwen3-Reranker or self-hosted ZeroEntropy weights against the same `gateway.rerank()` seam. Setup walkthrough in [`docs/ai-providers/llama-server-reranker.md`](docs/ai-providers/llama-server-reranker.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).
|
||
|
||
## Troubleshooting
|
||
|
||
**`gbrain import` fails with `expected N dimensions, not M`?** Run `gbrain doctor`. It will print the exact `gbrain config set ...` or `gbrain retrieval-upgrade` command to repair the mismatch. You should not need to delete `~/.gbrain`. Fresh `gbrain init --pglite` auto-detects your embedding provider from API keys in your environment: set `OPENAI_API_KEY` (or `ZEROENTROPY_API_KEY` / `VOYAGE_API_KEY`) before running init, or pass `--embedding-model <provider>:<model>` explicitly. With multiple keys set, init fires an interactive picker. In non-TTY contexts (CI, Docker) with no keys, init exits 1 with a paste-ready setup hint; pass `--no-embedding` to defer setup until runtime. See [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md) for the full provider matrix and [`docs/operations/headless-install.md`](docs/operations/headless-install.md) for Docker/CI sequencing.
|
||
|
||
**Hourly cron sync keeps timing out on a federated brain?** v0.41.13.0 ships
|
||
two flags + a recommended pattern. Switch your cron to a per-source loop
|
||
with shell `timeout(1)` doing the OS-level kill and gbrain self-terminating
|
||
gracefully half-a-minute earlier:
|
||
|
||
```bash
|
||
gbrain sync --break-lock --all --max-age 1800
|
||
for src in $(gbrain sources list --json | jq -r '.[].id'); do
|
||
timeout 600 gbrain sync --source "$src" --timeout 540 || true
|
||
done
|
||
```
|
||
|
||
When `--timeout` fires mid-import, `gbrain sync` exits 0 with status
|
||
`partial` and `last_commit` UNCHANGED — the next run re-walks the same
|
||
diff and `content_hash` short-circuits already-imported files. The
|
||
`--max-age 1800` first command self-heals any wedged-but-alive locks
|
||
left by a hung previous run, using the v98 `last_refreshed_at` semantic
|
||
(NOT `acquired_at`) so healthy long-running holders are safe by
|
||
construction. See the v0.41.13.0 entry in [`CHANGELOG.md`](CHANGELOG.md)
|
||
for the honest scope notes (extract + embed phases run to completion;
|
||
30-min rollout window for `--max-age` post-migration v98; full-sync
|
||
triggers deferred to v0.42+).
|
||
|
||
**Dream cycle silently losing wiki links on Supabase?** v0.41.19.0 fixes
|
||
the bug class structurally. The engine now self-retries every bulk batch
|
||
write (`addLinksBatch` / `addTimelineEntriesBatch` / `upsertChunks`) on
|
||
Supavisor pooler blips, with a 12s worst-case wait that covers the full
|
||
5-10s circuit-breaker recovery window. `gbrain doctor` surfaces incidents
|
||
via the new `batch_retry_health` check (reads the last 24h of
|
||
`~/.gbrain/audit/batch-retry-YYYY-Www.jsonl`). To tune for an unusually
|
||
slow pooler:
|
||
|
||
```bash
|
||
# Defaults: 3 retries, base 1s, max 10s, decorrelated jitter.
|
||
# Override per operator without a release:
|
||
export GBRAIN_BULK_MAX_RETRIES=5 # int >= 0; 0 disables retries
|
||
export GBRAIN_BULK_RETRY_BASE_MS=2000 # int > 0
|
||
export GBRAIN_BULK_RETRY_MAX_MS=15000 # int >= base
|
||
```
|
||
|
||
Bad values surface at `gbrain doctor` startup with a paste-ready fix
|
||
(not at first-retry mid-cycle). PGLite-only installs pay zero cost — the
|
||
retry wrap is engine-level, but PGLite has no pooler so retries never
|
||
fire in practice.
|
||
|
||
**Dream cycle losing ~150 link rows per run with `'No database
|
||
connection: connect() has not been called'` errors in the log?** v0.41.27.0
|
||
makes the retry layer self-heal on a nulled-out database singleton. A
|
||
new `reconnect` callback on `withRetry` rebuilds the connection between
|
||
attempts; `PostgresEngine.batchRetry` injects `() => this.reconnect()`
|
||
so engine-level batch writes survive a mid-cycle disconnect by something
|
||
else in the same process. Same release: `gbrain capture` no longer trails
|
||
a `'No database connection'` stderr line from a background facts:absorb
|
||
worker firing after CLI exit — the op-dispatch finally block awaits
|
||
`getFactsQueue().drainPending({timeout: 1000})` before
|
||
`engine.disconnect()`. To find which code path is still calling
|
||
disconnect mid-process, run `gbrain doctor --json | jq '.checks[] |
|
||
select(.id=="batch_retry_health")'`; the extended check now surfaces
|
||
24h disconnect-call count and the most-recent caller frame from a new
|
||
`~/.gbrain/audit/db-disconnect-YYYY-Www.jsonl` audit. (Closes #1570.)
|
||
|
||
**`gbrain brainstorm` returning `judge_failed: true` with 0 scored
|
||
ideas?** v0.41.21.0 closes the two bugs that caused it. The judge
|
||
hard-coded a 4K-token output cap; for any run past ~40 ideas the call
|
||
truncated mid-JSON and the parser threw. Same release closes a slash-
|
||
form pricing miss: `gbrain brainstorm --judge-model
|
||
anthropic/claude-sonnet-4-6 --max-cost 5` failed with
|
||
`BudgetExhausted reason=no_pricing` because every pricing site only
|
||
matched the colon form. Both shapes work now. No config change, no
|
||
schema migration — `gbrain upgrade` is the whole fix.
|
||
|
||
**`gbrain reindex --markdown` wiped your auto/dream/signal-detector
|
||
tags?** v0.41.37.0 makes tag reconciliation add-only. Re-import and
|
||
`reindex --markdown` now ADD current frontmatter tags and never delete,
|
||
so enrichment tags written to the DB (auto-tag, dream synthesize,
|
||
signal-detector) survive a re-chunk. The reindex DB-only fallback also
|
||
reconstructs the full markdown (frontmatter + body + timeline) before
|
||
re-chunking, so a page with no on-disk source keeps its frontmatter,
|
||
title, and timeline instead of getting overwritten with empty
|
||
frontmatter. Trade-off: removing a tag from a page's frontmatter no
|
||
longer removes it from the DB on the next sync (frontmatter-tag removal
|
||
needs a provenance column, deferred). (Closes #1621.)
|
||
|
||
**`gbrain sync` wedges on a large brain (no progress, high CPU)?**
|
||
v0.41.37.0 ships three things. First, name the stalling file:
|
||
|
||
```bash
|
||
GBRAIN_SYNC_TRACE=1 gbrain sync --no-pull --no-embed --yes
|
||
```
|
||
|
||
The last `[sync] begin import: <path>` line with no following completion
|
||
is the file being processed when the hang hit. Second, if you suspect a
|
||
schema-pack `inference.regex` with catastrophic backtracking, complete
|
||
the sync with the pack disabled and re-run extraction later:
|
||
|
||
```bash
|
||
gbrain sync --no-schema-pack --no-pull --no-embed --yes
|
||
```
|
||
|
||
`gbrain schema lint` now warns on the classic nested-quantifier ReDoS
|
||
shapes (`(a+)+`, `(a*)*`, …) in pack regexes, and the runtime caps
|
||
inference-regex input length (override via `GBRAIN_MAX_REGEX_INPUT_CHARS`).
|
||
Third, on a PGLite brain, stop `gbrain serve` before a large sync —
|
||
PGLite is single-writer and a live MCP server contends for the write
|
||
lock. See [`docs/architecture/serve-sync-concurrency.md`](docs/architecture/serve-sync-concurrency.md)
|
||
for the full triage. (Closes #1569.)
|
||
|
||
**`gbrain init --migrate-only` / a schema migration fails on Windows
|
||
with `getaddrinfo ENOTFOUND`?** v0.41.37.0 runs the 9 schema-bring-up
|
||
phases in-process instead of spawning a child `gbrain init
|
||
--migrate-only` per phase. The spawned child died on
|
||
Windows + bun + Supabase pooler with a DNS-resolution failure even
|
||
though the parent connected fine; running in-process removes the spawn
|
||
entirely. The v0.13.1 grandfather migration that hung 70+ minutes on an
|
||
82K-page PGLite brain is also fixed — it now runs as a chunked bulk SQL
|
||
pass (keyed on the page PK, soft-delete-filtered, source-safe) that
|
||
completes in ~1-2 seconds. (Closes #1605, #1581.)
|
||
|
||
## Docs
|
||
|
||
- [`docs/INSTALL.md`](docs/INSTALL.md) — every install path, end to end
|
||
- [`docs/what-schemas-unlock.md`](docs/what-schemas-unlock.md) — why schemas matter: 7 killer use cases, the structural argument for typed page kinds, the agent-co-curates pattern (v0.40.7.0)
|
||
- [`docs/schema-author-tutorial.md`](docs/schema-author-tutorial.md) — 5-minute walkthrough: fork the bundled pack, add a custom type, backfill existing pages, prove the wiring via `gbrain whoknows`
|
||
- [`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. I built GBrain to run my OpenClaw and Hermes deployments — the production brain behind my 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 ships as the 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 (a thin delegation to the provider-agnostic AI gateway in `src/core/ai/gateway.ts`). This is intentional: embedding is an external API call (OpenAI, Voyage, a local Ollama — whichever provider you configured), 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.
|
||
|
||
## JSONB writes: never double-encode (the #2339 trap)
|
||
|
||
Writing a JS value into a `jsonb` column has exactly two correct forms. Get this
|
||
wrong and the write succeeds on PGLite but stores a **jsonb string scalar** on
|
||
real Postgres — `col ->> 'k'` returns NULL, `jsonb_array_elements` throws, and a
|
||
`jsonb_typeof = 'array'` CHECK rejects the row (this aborted every sync in #2339).
|
||
|
||
| Form | Verdict |
|
||
|---|---|
|
||
| Template tag: `` sql`... ${sql.json(obj)}` `` (postgres-engine only) | ✅ native jsonb serialization |
|
||
| Positional raw call, raw object: `executeRawJsonb(engine, sql, scalars, [obj])` | ✅ object reaches the wire as jsonb |
|
||
| Positional raw call, stringified: `executeRaw(\`... $N::text::jsonb\`, [JSON.stringify(x)])` | ✅ binds as text, the cast parses it |
|
||
| Positional raw call, BARE cast: `executeRaw(\`... $N::jsonb\`, [JSON.stringify(x)])` | ❌ **double-encodes** under postgres.js `.unsafe()` |
|
||
| Template literal interpolation: `` `... ${JSON.stringify(x)}::jsonb` `` | ❌ double-encodes |
|
||
|
||
**Why:** postgres.js `.unsafe(sql, params)` (the path behind `executeRaw` /
|
||
`executeRawDirect`) binds a JS **string** as a text param. A bare `$N::jsonb`
|
||
cast then wraps that already-JSON string into a jsonb scalar string instead of
|
||
parsing it. Casting through `$N::text::jsonb` forces a text→jsonb parse.
|
||
**PGLite's `db.query` parses text→jsonb natively, so it hides the bug** — which is
|
||
why a regression only shows up on Postgres (and why the parity test must run there).
|
||
|
||
**Two CI guards enforce this, both wired into `scripts/check-jsonb-pattern.sh`:**
|
||
- the template-tag grep (`${JSON.stringify(x)}::jsonb`), and
|
||
- `scripts/check-jsonb-params.mjs`, an AST-lite scanner for the positional
|
||
`$N::jsonb` + `JSON.stringify` form the grep misses. Sanctioned escapes:
|
||
`$N::text::jsonb`, `$N::text[]`, `executeRawJsonb`, `sql.json`, or an inline
|
||
`jsonb-guard-ok` comment.
|
||
|
||
The real backstop is `test/e2e/op-checkpoint-jsonb-parity.test.ts` +
|
||
`test/e2e/jsonb-roundtrip.test.ts`, which round-trip writes through real Postgres
|
||
and assert `jsonb_typeof` — the assertion PGLite cannot make.
|
||
|
||
## 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/what-schemas-unlock.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/what-schemas-unlock.md
|
||
|
||
# What schemas unlock
|
||
|
||
Most note-taking apps treat every page the same. You write something, it goes in a pile, you search the pile with text matching. Tags help, but tags are flat. After a few thousand pages, the pile gets noisy and the search gets stupid.
|
||
|
||
Schemas are how gbrain stops being a pile of notes and becomes something with structure. A schema declares what KINDS of things live in your brain (`person`, `company`, `meeting`, `researcher`, `case`, `lab-result`), what they link to (`attended`, `authored`, `prescribed-by`), what facts the system should extract automatically (`mrr=50000`, `damages=5000000`), and which types route through expert search vs general search.
|
||
|
||
The default schema (`gbrain-base`) ships with 22 page types covering the universal shapes — people, companies, meetings, notes, daily, calendar events. That's enough to start. But your brain is yours, and your brain's shape is not the default shape. A research brain needs `researcher` and `paper` as first-class types. A founder brain needs `lead`, `investor`, `portco`, `deal-stage`. A lawyer brain needs `case`, `motion`, `deposition`, `precedent`. Same engine, totally different shape.
|
||
|
||
v0.40.7.0 made it possible for AGENTS to author that shape for you. Not just "the user manually edits YAML in `~/.gbrain/schema-packs/mine/pack.yaml`" but "your agent sees the corpus, proposes a type, asks for approval, applies it atomically with a full audit trail, then backfills 4000 existing pages with one chunked SQL command." That's the new thing.
|
||
|
||
This doc is the WHY. The [tutorial](schema-author-tutorial.md) is the HOW.
|
||
|
||
## Killer use cases
|
||
|
||
### 1. The 4000 invisible pages
|
||
|
||
You have 4000 markdown files under `meetings/` going back two years. The default schema doesn't have a `meeting` type, so all 4000 are typed `note` (the catchall). When you run:
|
||
|
||
```bash
|
||
gbrain whoknows "Q3 roadmap discussion"
|
||
```
|
||
|
||
You get the top 10 text matches, ranked by raw relevance. The brain has no idea these are meetings. It can't route to attendees. It can't pull dates. It can't surface "this conversation came up again with the same people three weeks later."
|
||
|
||
Add a `meeting` type:
|
||
|
||
```bash
|
||
gbrain schema add-type meeting --primitive temporal --prefix meetings/ --extractable
|
||
gbrain schema sync --apply
|
||
```
|
||
|
||
The sync backfills `page.type = 'meeting'` on all 4000 pages in 1000-row batches. Now:
|
||
|
||
- `gbrain whoknows "Q3 roadmap discussion"` routes through the meeting type, ranking by `expert_routing` signal (attendees, recency, salience) instead of raw text.
|
||
- `gbrain extract-facts` runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`.
|
||
- The downstream `think` skill can now answer "what did we decide about pricing in the last three roadmap meetings" by querying the meeting graph instead of grep'ing 4000 files.
|
||
|
||
One command. 4000 pages went from invisible to queryable. The content didn't change. The structure did.
|
||
|
||
### 2. The founder ops brain
|
||
|
||
You're a founder or investor with ~500 markdown files mixing leads, portfolio companies, deal notes, intros, and follow-ups. You've been writing freely; you have no system. Your queries are all "wait, who introduced me to that fintech founder again?" and you scroll Notion for 20 minutes.
|
||
|
||
Add the founder shape:
|
||
|
||
```bash
|
||
gbrain schema fork gbrain-base mine
|
||
gbrain schema use mine
|
||
|
||
# Types
|
||
gbrain schema add-type lead --primitive entity --prefix people/leads/ --expert
|
||
gbrain schema add-type investor --primitive entity --prefix people/investors/ --expert --extractable
|
||
gbrain schema add-type portco --primitive entity --prefix companies/portco/ --expert --extractable
|
||
gbrain schema add-type deal --primitive entity --prefix companies/deals/ --extractable
|
||
|
||
# Link verbs
|
||
gbrain schema add-link-type invested-in --page-type investor --target-type portco
|
||
gbrain schema add-link-type intro-from --page-type lead --target-type lead
|
||
gbrain schema add-link-type passed-on --page-type investor --target-type deal
|
||
gbrain schema add-link-type led-by --page-type deal --target-type investor
|
||
|
||
gbrain schema sync --apply
|
||
```
|
||
|
||
Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." `gbrain extract-facts` starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`.
|
||
|
||
The CRM you've been promising yourself you'll set up next quarter? You just shipped it in 4 commands. It's downstream of your notes, not parallel to them.
|
||
|
||
### 3. The research brain
|
||
|
||
Replace "founder" with "PhD student" and the same pattern applies with different types: `researcher`, `paper`, `lab`, `grant`, `dataset` + `authored`, `cites`, `funded-by`, `uses-dataset`.
|
||
|
||
```bash
|
||
gbrain schema add-type paper --primitive annotation --prefix research/papers/ --extractable
|
||
gbrain schema add-link-type authored --page-type researcher --target-type paper
|
||
gbrain schema add-link-type cites --page-type paper --target-type paper
|
||
gbrain schema add-link-type uses --page-type paper --target-type dataset
|
||
```
|
||
|
||
Suddenly "show me papers that cite this work AND use the same dataset" is a `gbrain graph-query` traversal, not 30 minutes in Google Scholar. The fact extraction picks up `arxiv_id=2402.04253`, `cited_by_count=140`, `published_date=2026-02-15` automatically. Your reading-list-as-markdown turns into a queryable research graph that knows who works on what and what's connected to what.
|
||
|
||
### 4. The legal brain (or any domain where claims have numbers)
|
||
|
||
Lawyers, medical providers, accountants, anyone working in a domain where the meaning of a number depends on its type. A "judgment of $5M" against a "$2M case strategy threshold" is a comparison the brain can do — but only if both numbers are typed.
|
||
|
||
```bash
|
||
gbrain schema add-type case --primitive entity --prefix legal/cases/ --extractable --expert
|
||
gbrain schema add-type motion --primitive annotation --prefix legal/motions/ --extractable
|
||
gbrain schema add-type deposition --primitive annotation --prefix legal/depositions/ --extractable
|
||
gbrain schema add-link-type filed-in --page-type motion --target-type case
|
||
gbrain schema add-link-type cites --page-type motion --target-type precedent
|
||
```
|
||
|
||
Now `## Facts` fences in your case notes can carry typed claims (`damages=5000000`, `filed_date=2026-05-23`, `judge=jane-doe`) that gbrain stores as first-class columns. `gbrain eval trajectory legal/cases/acme-v-widget` prints the case history with regressions flagged. `gbrain founder scorecard` (renamed for legal: roll up plaintiff success rate, average damages, settlement-vs-trial ratio) gives you a structured view of how your practice is performing.
|
||
|
||
This isn't possible without typed page kinds. You can write the same prose in any note-taking app. Only gbrain treats the numbers as comparable across pages of the same type.
|
||
|
||
### 5. The team brain
|
||
|
||
`gbrain mounts add` lets you stack additional brains alongside your personal one. Each mounted brain has its OWN schema pack. The eng team's brain has `incident`, `runbook`, `service`, `oncall-rotation`. The design team's brain has `component`, `experiment`, `ab-test`, `figma-link`. The legal team's brain has cases and depositions.
|
||
|
||
When you query, the schema pack governs how each source's content is routed. An eng query against the mounted eng brain knows that `incidents/2026-05-23-db-outage.md` is an `incident` page with `severity=p0`, `mttr=47min`, `on_call=alice-example` — extractable typed facts. Your personal query against the same brain still works, but the routing is sharper because the eng team has invested in their ontology.
|
||
|
||
The schema is the team's tribal knowledge made explicit. Two engineers on different teams searching the same brain get DIFFERENT routing because their personal packs declare different expert types.
|
||
|
||
### 6. The "agent co-curates your ontology" pattern (the new thing)
|
||
|
||
This is what v0.40.7.0 actually enabled, and what the closed PR #1321 was reaching for.
|
||
|
||
Your OpenClaw (or any agent connected to your brain over HTTPS MCP with admin scope) watches your ingestion stream. After a week of you dumping notes under `garrytan/companies/yc-w24/`, the agent runs `gbrain schema detect` periodically, sees that prefix accumulating, and proposes:
|
||
|
||
> You have 47 pages under `companies/yc-w24/` typed as `company` (generic). They share a structural pattern (founder names, raise amounts, batch tag). Should I add a `yc-w24-company` type with `extractable: true` and the existing aliases pointing back to `company`? I'd backfill the 47 pages and add `cohort=W24` as a typed fact extracted from each page.
|
||
|
||
You approve once. The agent calls `schema_apply_mutations` over MCP with a batch:
|
||
|
||
```json
|
||
{
|
||
"pack": "mine",
|
||
"mutations": [
|
||
{"op": "add_type", "name": "yc-w24-company", "primitive": "entity", "prefix": "companies/yc-w24/", "extractable": true, "expert_routing": true},
|
||
{"op": "add_alias", "type": "yc-w24-company", "alias": "company"}
|
||
]
|
||
}
|
||
```
|
||
|
||
All inside ONE `withPackLock` scope, atomic, audited (the agent's `client_id` captured in the audit log as `actor: mcp:<clientId8>`). Cache invalidated cross-process. Sync backfills the 47 pages. The brain learned a new category of thing without you having to think about it.
|
||
|
||
The next time you query "YC W24 companies in fintech", the brain routes through the new type. Six months later when you forget the pattern entirely, the agent reminds you it's there and offers to consolidate it with the W25 batch.
|
||
|
||
The brain learns. The agent is the curator. You approve, the agent does the work.
|
||
|
||
### 7. The before-vs-after benchmark
|
||
|
||
If you want to FEEL the difference without buying the pitch:
|
||
|
||
Pick a real corpus you have. Run `gbrain whoknows` on a topic that should match. Note the top-3 results.
|
||
|
||
Then run `gbrain schema review-orphans --limit 50 --json` and look at the untyped pages. If 10+ of them share an obvious prefix that should be a real type, add the type + sync.
|
||
|
||
Re-run the same `whoknows` query. Top-3 should shift, because the new type is now routing through expert ranking instead of being lumped into the catchall. The numerical delta IS the win. You can run a tutorial in 5 minutes; this experiment proves it matters on your actual content.
|
||
|
||
## Why this matters
|
||
|
||
Three things gbrain does that generic note systems can't:
|
||
|
||
**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. `gbrain extract-facts` only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached.
|
||
|
||
**2. Untyped content is invisible content.** If your meetings are typed as `note`, expert routing skips them, facts extraction ignores them, link inference doesn't fire. They exist on disk and they're indexed for text search, but the structural surfaces (whoknows, find_experts, recall, think) treat them as second-class. Adding a type isn't cosmetic; it's structural promotion.
|
||
|
||
**3. The schema is queryable AND mutable AND auditable.** You can ask the brain what its schema looks like (`gbrain schema graph`), evolve it through 14 atomic CLI verbs + 9 MCP ops with full lock + audit semantics, and recover from any mistake (every primitive has an inverse, plus `gbrain schema downgrade` restores the previous active pack). This isn't "vibes-based knowledge management." It's a production system with structural integrity guarantees.
|
||
|
||
## What changed in v0.40.7.0 specifically
|
||
|
||
v0.39.1.0 shipped the schema-pack engine. You could ALREADY fork the bundled pack and edit `pack.yaml` by hand. What you couldn't do was let an agent author it safely — there were no atomic file locks, no audit log, no MCP exposure, no pack-aware wiring in the query path. The cathedral was built but unreachable from the outside.
|
||
|
||
v0.40.7.0 closed those gaps:
|
||
|
||
- **`withMutation` skeleton** wraps every primitive in 8 ordered safety steps (bundled-guard → lock → read → mutate → validate → atomic write → audit → invalidate). The pack file on disk is never partial. Two concurrent agents can't race.
|
||
- **Per-pack `O_CREAT|O_EXCL` atomic lock** (not the TOCTOU `existsSync+writeFileSync` pattern from page-lock.ts — codex caught that during plan review). TTL refresh every 10s while a mutation runs; `--force` means "steal stale lock" not "skip locking."
|
||
- **Privacy-redacted audit log** at `~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl`. Type names sha8-hashed, prefixes truncated to first segment only. A leaked screenshot of the audit can't reveal sensitive taxonomy like `personal/oncology/` or `legal/depositions/`.
|
||
- **9 new MCP ops** including the batched `schema_apply_mutations` (admin scope, NOT localOnly — your OpenClaw and any remote agent author packs over normal HTTPS MCP, with `client_id` captured as `actor: mcp:<clientId8>`).
|
||
- **T1.5 wiring** finally completes for `whoknows` and `find_experts`: a custom `researcher` type marked `--expert` now actually surfaces in query results. Pre-v0.40.7 it silently never matched because the query path read hardcoded `['person', 'company']`.
|
||
- **Cross-process invalidation** via stat-mtime TTL gate inside `loadActivePack`. Operator runs `gbrain schema add-type` from a terminal; the autopilot daemon picks up the new type within 1 second without a restart.
|
||
|
||
The cumulative effect: an agent can safely co-curate your ontology with a complete forensic trail. That's the new thing.
|
||
|
||
## Where to start
|
||
|
||
- **Want to see it work in 5 minutes?** Run the [tutorial](schema-author-tutorial.md). Forks the bundled pack, adds a researcher type, proves the wiring end-to-end.
|
||
- **Want the agent recipe?** Read [`skills/schema-author/SKILL.md`](../skills/schema-author/SKILL.md). 7-phase workflow agents follow when they detect a schema-evolution opportunity.
|
||
- **Want the rules of thumb?** Read [`skills/conventions/schema-evolution.md`](../skills/conventions/schema-evolution.md). Decision tree for when to add a type vs alias vs prefix. <20 pages don't pack-codify. 100+ pages need first-class types.
|
||
- **Want the architecture?** The "Schema Cathedral v3 (v0.40.7.0)" section in `CLAUDE.md` has the 14-bullet module-by-module breakdown, each citing the design decision and codex finding that motivated it.
|
||
- **Want to set up an agent that co-curates your brain?** Run `gbrain auth register-client my-agent --scopes admin` to mint an OAuth client your remote agent can use to call `schema_apply_mutations` over MCP. The agent then runs detect → suggest → apply on its own cadence and asks you to approve substantive changes.
|
||
|
||
The killer feature isn't "schemas." Personal knowledge systems have had schemas forever. The killer feature is that your AGENT can shape them safely on your behalf, with structural integrity guarantees that match what you'd expect from a database, not a notes app.
|
||
|
||
That's what we built. Try it on a corpus you actually have and the numbers go up.
|
||
|
||
---
|
||
|
||
## docs/schema-author-tutorial.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/schema-author-tutorial.md
|
||
|
||
# Tutorial: Build your first schema pack
|
||
|
||
You'll fork the bundled `gbrain-base` pack, add a custom `researcher` page type, import a handful of placeholder researcher pages, backfill their `page.type` column with one command, then prove the wiring works by running `gbrain whoknows` and seeing your new type surface in results. End state: a forked-and-active pack on disk, ~5 pages typed as `researcher`, and a query that proves the pack-aware routing fires end-to-end.
|
||
|
||
**Want the WHY before the HOW?** Read [`what-schemas-unlock.md`](what-schemas-unlock.md) first — 7 concrete use cases (4000 invisible meetings, the founder ops brain, the research brain, the legal brain, the team brain, agent-as-co-curator) plus the structural argument for why types matter at query time. Then come back here for the 5-minute walkthrough.
|
||
|
||
The whole walkthrough takes about 5 minutes. You'll see something working by step 3.
|
||
|
||
## What you'll need
|
||
|
||
- gbrain v0.40.7.0 or later (`gbrain --version` to check)
|
||
- A brain that's been initialized (`gbrain init` already run; either PGLite or Postgres is fine)
|
||
- A terminal you can paste commands into
|
||
|
||
That's it. No API keys required for this tutorial — every step works against the bundled pack and local-only commands.
|
||
|
||
## Step 1: See what pack is active today
|
||
|
||
```bash
|
||
gbrain schema active --json
|
||
```
|
||
|
||
You'll see something like:
|
||
|
||
```json
|
||
{
|
||
"pack_name": "gbrain-base",
|
||
"version": "1.0.0",
|
||
"sha8": "...",
|
||
"page_types_count": 22,
|
||
"source_tier": "default"
|
||
}
|
||
```
|
||
|
||
`source_tier: "default"` means you haven't customized anything — you're on the bundled pack. `page_types_count: 22` is the universal starter (person, company, meeting, note, etc.).
|
||
|
||
**You can't mutate bundled packs directly.** Step 2 forks it so you have something writable.
|
||
|
||
## Step 2: Fork the bundled pack
|
||
|
||
```bash
|
||
gbrain schema fork gbrain-base mine
|
||
```
|
||
|
||
Output: `Forked 'gbrain-base' → 'mine' at ~/.gbrain/schema-packs/mine/pack.json`.
|
||
|
||
The fork is a byte-for-byte copy of `gbrain-base` living at `~/.gbrain/schema-packs/mine/pack.json`. Now you have a writable pack you can mutate.
|
||
|
||
## Step 3: Activate the fork
|
||
|
||
```bash
|
||
gbrain schema use mine
|
||
```
|
||
|
||
Output: `Pack: mine (json) ... Active.`
|
||
|
||
Run `gbrain schema active --json` again to confirm `pack_name` is now `mine` and `source_tier` is `home-config` (read from `~/.gbrain/config.json`).
|
||
|
||
**You've already accomplished something visible** — the active pack changed, and any future query will route through your fork. The next four steps add a custom type and prove it works.
|
||
|
||
## Step 4: Add a researcher type
|
||
|
||
```bash
|
||
gbrain schema add-type researcher \
|
||
--primitive entity \
|
||
--prefix people/researchers/ \
|
||
--extractable \
|
||
--expert
|
||
```
|
||
|
||
Output: `Pack: mine (json)` + `Sha8: <prev> → <new>`.
|
||
|
||
What just happened:
|
||
- The mutation went through `withMutation`'s 8-step skeleton: bundled-guard → per-pack lock → read → mutate → file-plane lint validation → atomic write → audit log → cache invalidation.
|
||
- The pack now declares `researcher` as an entity primitive bound to `people/researchers/`, marked `extractable: true` (eligible for facts extraction) and `expert_routing: true` (surfaces in `whoknows` queries).
|
||
- An audit row landed in `~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl` with your type name SHA-8-redacted and the prefix's first segment only (`people`) for privacy.
|
||
|
||
Verify the type is in the pack:
|
||
|
||
```bash
|
||
gbrain schema explain researcher
|
||
```
|
||
|
||
You'll see the resolved settings printed back.
|
||
|
||
## Step 5: Import some placeholder researcher pages
|
||
|
||
You need pages under `people/researchers/` for the next step to do anything. If your brain repo already has them, skip ahead. If not, drop 3-5 placeholder markdown files into `<your-brain-repo>/people/researchers/` and import:
|
||
|
||
```bash
|
||
mkdir -p people/researchers
|
||
cat > people/researchers/alice-example.md <<'EOF'
|
||
---
|
||
title: Alice Example
|
||
---
|
||
|
||
ML researcher at Example Lab. Works on contrastive embeddings.
|
||
EOF
|
||
|
||
cat > people/researchers/bob-example.md <<'EOF'
|
||
---
|
||
title: Bob Example
|
||
---
|
||
|
||
Vision researcher at Widget University. Recent paper on diffusion models.
|
||
EOF
|
||
|
||
cat > people/researchers/charlie-example.md <<'EOF'
|
||
---
|
||
title: Charlie Example
|
||
---
|
||
|
||
RL researcher at Acme Research. Focus on inverse reinforcement learning.
|
||
EOF
|
||
|
||
gbrain sync
|
||
```
|
||
|
||
The sync imports the new files. They'll be stored in the database but their `type` column will still be empty — the new type was added to the pack AFTER these pages already existed (the typical real-world scenario for an agent walking into an existing brain).
|
||
|
||
## Step 6: See the gap with `stats`
|
||
|
||
```bash
|
||
gbrain schema stats --json | jq '.aggregate, .dead_prefixes'
|
||
```
|
||
|
||
You'll see `untyped_pages: 3` (or however many you just imported) and `dead_prefixes: []` — your new prefix has 3 matching pages, so it's not dead.
|
||
|
||
The 3 researcher pages are "orphaned" by type even though they live in the right directory. The next step backfills them.
|
||
|
||
## Step 7: Backfill with `sync --apply`
|
||
|
||
First dry-run to see what would happen:
|
||
|
||
```bash
|
||
gbrain schema sync --json
|
||
```
|
||
|
||
You'll see something like:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"apply": false,
|
||
"per_prefix": [
|
||
{
|
||
"type": "researcher",
|
||
"prefix": "people/researchers/",
|
||
"would_apply": 3,
|
||
"sample_slugs": ["people/researchers/alice-example", "people/researchers/bob-example", "people/researchers/charlie-example"],
|
||
"applied": 0
|
||
}
|
||
],
|
||
"total_would_apply": 3,
|
||
"total_applied": 0
|
||
}
|
||
```
|
||
|
||
`would_apply: 3` is what you'd touch. `sample_slugs` is the agent's drilldown signal — if those slugs look wrong, abort. They look right, so apply:
|
||
|
||
```bash
|
||
gbrain schema sync --apply
|
||
```
|
||
|
||
You'll see per-batch progress lines on stderr and a final `total_applied: 3`. The UPDATE ran in chunks of 1000 (yours fit in one chunk) and never wedged any concurrent writer.
|
||
|
||
## Step 8: Prove the wiring works
|
||
|
||
```bash
|
||
gbrain whoknows "machine learning"
|
||
```
|
||
|
||
If your researcher pages contain ML-related content, they'll surface in the ranked results — even though they're typed `researcher`, not `person` or `company`.
|
||
|
||
**This is the load-bearing demonstration of T1.5 wiring.** Pre-v0.40.7.0, `whoknows` hardcoded `['person', 'company']` as the eligible types and would have ignored your `researcher` pages entirely. The v0.40.7.0 wiring consults the active pack's `expert_routing: true` types via `expertTypesFromPack(pack.manifest)`, so your custom type now routes through expert search.
|
||
|
||
## What you built
|
||
|
||
You now have:
|
||
- A fork of `gbrain-base` named `mine` at `~/.gbrain/schema-packs/mine/pack.json`, active in your brain via `~/.gbrain/config.json`.
|
||
- A `researcher` page type registered in the pack with `entity` primitive, `people/researchers/` prefix, `extractable: true`, `expert_routing: true`.
|
||
- 3 pages typed as `researcher` (backfilled from disk via `gbrain schema sync --apply`).
|
||
- A query path that routes through the new type: `gbrain whoknows` reads the pack and includes `researcher` in its type filter.
|
||
|
||
You also exercised the full mutation skeleton: bundled-pack guard, per-pack lock, validation gate, atomic write, audit log, cache invalidation. Every step was idempotent — re-running any of them is a no-op.
|
||
|
||
## Next steps
|
||
|
||
**Add a link verb.** A `researcher` can `author` a `paper`. To model that:
|
||
|
||
```bash
|
||
gbrain schema add-type paper --primitive annotation --prefix research/papers/ --extractable
|
||
gbrain schema add-link-type authored --page-type researcher --target-type paper
|
||
gbrain schema graph
|
||
```
|
||
|
||
The graph now shows `researcher --(authored)--> paper`.
|
||
|
||
**Add aliases for query closure.** If you want `gbrain query researcher` to also surface `person` rows (because researchers ARE people):
|
||
|
||
```bash
|
||
gbrain schema add-alias researcher person
|
||
```
|
||
|
||
Read [`skills/conventions/schema-evolution.md`](../skills/conventions/schema-evolution.md) for the decision tree on when to add types vs aliases vs prefixes. The short version: <20 pages → don't pack-codify; 20-100 → alias on existing type; 100+ → first-class type.
|
||
|
||
**Lint your pack before shipping.** The 11-rule lint surface (with the optional `--with-db` flag for DB-aware checks) catches dangling references, prefix collisions, and dead-corpus warnings:
|
||
|
||
```bash
|
||
gbrain schema lint --with-db
|
||
```
|
||
|
||
**Commit your pack to source control.** If `~/.gbrain/schema-packs/mine/` is a git repo, commit `pack.json` and push. Your pack survives across machines, and the `mutation_count_anomaly` lint rule will nudge you when you hit >50 mutations in a week (the "you should be committing this" signal).
|
||
|
||
**For agents (MCP):** the same operations are reachable over HTTPS MCP via 9 new ops. Register an admin-scope OAuth client and `schema_apply_mutations` lets a remote agent compose multi-step refactors as one atomic batch. The batched MCP op + per-pack lock + audit log are the load-bearing primitives that make remote schema authoring safe. See [`skills/schema-author/SKILL.md`](../skills/schema-author/SKILL.md) for the agent dispatcher.
|
||
|
||
**Undo a mistake.** Every mutation primitive has an inverse (`remove-type`, `remove-alias`, `remove-prefix`, `remove-link-type`, `set-extractable false`, etc.). If you fork twice and want to revert, `gbrain schema downgrade` restores the previous active pack from `~/.gbrain/schema-pack-history.jsonl`.
|
||
|
||
## Related docs
|
||
|
||
- **Reference:** `gbrain schema --help` for the full 22-verb CLI surface; CLAUDE.md's "Schema Cathedral v3 (v0.40.7.0)" section for the module-by-module architecture.
|
||
- **How-to:** [`skills/schema-author/SKILL.md`](../skills/schema-author/SKILL.md) — the agent dispatcher with the 7-phase workflow (brain → assess → propose → apply → sync → verify → commit).
|
||
- **Explanation:** [`skills/conventions/schema-evolution.md`](../skills/conventions/schema-evolution.md) — when to add a type vs alias vs prefix.
|
||
- **Plan + decisions:** the original design captured 21 decisions including the bundled-pack guard rationale (D6), the empty-filter fallback contract (D4), and the MCP non-localOnly trust posture (D2). Lives in `~/.claude/plans/system-instruction-you-are-working-recursive-thacker.md` (private).
|
||
|
||
---
|
||
|
||
## 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: a reachable direct connection
|
||
|
||
GBrain is tuned for the Supabase **Transaction pooler** (port 6543): it
|
||
auto-disables prepared statements there and routes `engine.transaction()`
|
||
(migrations, DDL, sync imports) to a derived **direct** connection
|
||
(`db.<ref>.supabase.co:5432`). That direct host is IPv6-only, so on an
|
||
IPv4-only host, reads work but sync **silently skips most pages**. This is the
|
||
number one cause of "sync ran but nothing happened."
|
||
|
||
Fix: make the direct connection reachable over IPv4. Either set
|
||
`GBRAIN_DIRECT_DATABASE_URL` to the **Session pooler** string (port 5432 on the
|
||
`pooler.supabase.com` host, IPv4), or enable Supabase's IPv4 add-on. 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 errors mention an unreachable host or timeout,
|
||
the direct connection isn't reachable over IPv4 (set
|
||
GBRAIN_DIRECT_DATABASE_URL to the Session pooler, or enable the IPv4 add-on)."
|
||
```
|
||
|
||
**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.
|
||
|
||
4. **A single un-parseable file can't wedge all indexing.** When a file fails
|
||
to import (malformed YAML frontmatter, an unquoted colon, etc.), sync holds
|
||
the bookmark and tells you exactly which file broke — a *fresh* failure
|
||
fails closed so nothing is silently dropped. But a file that fails the same
|
||
way `GBRAIN_SYNC_AUTOSKIP_AFTER` consecutive syncs (default 3, set `0` to
|
||
disable) is auto-skipped so the rest of the brain keeps indexing past it.
|
||
Skipped files don't disappear: `gbrain doctor` keeps warning until you fix
|
||
or delete them, and fixing the file clears it on the next sync. A repository
|
||
history rewrite still hard-blocks even with `--skip-failed`. Run
|
||
`gbrain sync --skip-failed` to acknowledge a known-bad set yourself.
|
||
|
||
## 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 an
|
||
unreachable direct connection on IPv4 — see the prerequisite above).
|
||
|
||
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/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/guides/scaling-skills.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md
|
||
|
||
# Scaling skills past 300 without drowning the context window
|
||
|
||
When an agent grows past 100 skills, a wall starts forming. Sessions take
|
||
longer to start. The model gets a little dumber about which skill to pick.
|
||
Tokens that should be powering reasoning are powering a skill catalog the
|
||
model reads on every turn whether it needs to or not.
|
||
|
||
This guide is the recipe for breaking through that wall without deleting
|
||
capabilities. Three tiers, one resolver, one safety net. Production-tested
|
||
on a 306-skill agent (Garry's OpenClaw, the agent behind Y Combinator's
|
||
president). The pattern works whether you run OpenClaw, Hermes, Claude Code,
|
||
Cursor, or your own MCP-aware agent.
|
||
|
||
## The problem
|
||
|
||
OpenClaw scans every skill file on disk at session start and injects them
|
||
into the system prompt as `<available_skills>` entries. The model sees a
|
||
name, description, and file path for each one. When a request matches, the
|
||
model reads the full SKILL.md and follows it.
|
||
|
||
This is great architecture at 50 skills. At 100, it's fine. At 200, it
|
||
starts to drag. At 300, the system prompt eats more than 25,000 tokens on
|
||
skill descriptions alone. Tokens that aren't going to reasoning, context,
|
||
or actual work.
|
||
|
||
The symptoms compound:
|
||
|
||
- Sessions take noticeably longer to start.
|
||
- The model has less room for conversation history.
|
||
- Skill routing gets fuzzier. With 300 descriptions competing for attention,
|
||
the model occasionally picks the wrong one.
|
||
- Cost goes up because every turn carries the full skill manifest.
|
||
|
||
The naive fix is to delete skills you don't use often. Don't do this. The
|
||
whole point of skills is that capabilities compound. A gift pipeline that
|
||
fires twice a month saves 30 minutes each time it does. A flight tracker
|
||
fires once per trip and prevents a missed Uber. Deleting low-frequency
|
||
skills optimizes for prompt size at the cost of capability. You wouldn't
|
||
delete apps from your phone because the home screen is too crowded. You'd
|
||
organize them.
|
||
|
||
## The three tiers
|
||
|
||
Not all skills need to be visible to the model at all times. Some are core.
|
||
Some are specialized. Some are dormant.
|
||
|
||
### Tier A: always loaded (~35 skills)
|
||
|
||
The skills the model needs on every single turn. Brain search, email triage,
|
||
calendar, meeting ingestion, content creation, the executive assistant.
|
||
They stay in the system prompt's `<available_skills>` manifest. The model
|
||
sees them natively and routes to them without any lookup.
|
||
|
||
### Tier B: resolver-routed (~85 skills)
|
||
|
||
Real, active skills that fire regularly but don't need to pollute every
|
||
turn. Gift pipeline, flight tracker, investor update ingestion, adversary
|
||
tracking, book mirror, civic intelligence. They live on disk. They have
|
||
full SKILL.md files. But OpenClaw doesn't inject them into the prompt.
|
||
|
||
Instead, a compact RESOLVER.md handles routing. One line per skill with
|
||
trigger phrases:
|
||
|
||
```markdown
|
||
- **gift-advisor**: gift idea | what should I bring | birthday gift | housewarming
|
||
- **flight-tracker**: track my flight | flight status | when does my flight land
|
||
- **investor-update-ingest**: investor update | portfolio update | company metrics
|
||
```
|
||
|
||
When the model sees "what should I bring to Jessica's dinner," it checks
|
||
the resolver, finds `gift-advisor`, reads the SKILL.md, and executes. Same
|
||
result. Zero wasted tokens on the other 84 turns where gifts aren't relevant.
|
||
|
||
### Tier C: dormant (~180 skills)
|
||
|
||
Built-in OpenClaw skills that aren't in active rotation (1Password, Discord,
|
||
Notion, Trello, integrations you haven't wired up yet) plus specialized
|
||
skills that almost never fire. They're explicitly disabled in the config
|
||
with `enabled: false`. They exist on disk as documentation and potential.
|
||
Flip one boolean to wake them up. Zero tokens contributed to every prompt
|
||
until then.
|
||
|
||
### The numbers
|
||
|
||
Before tiering, on Garry's 306-skill OpenClaw:
|
||
|
||
| Metric | Before |
|
||
|---|---|
|
||
| Skills in system prompt | 306 |
|
||
| Skill-description tokens per turn | ~25,000 |
|
||
| Skill routing accuracy | degrading |
|
||
| Session startup | slow |
|
||
|
||
After tiering:
|
||
|
||
| Metric | After |
|
||
|---|---|
|
||
| Skills in system prompt (Tier A) | 35 |
|
||
| Skill-description tokens per turn | ~4,000 |
|
||
| Skills still accessible (A + B + C) | 301 |
|
||
| Capability loss | zero |
|
||
| **Tokens freed per turn** | **~21,000** |
|
||
|
||
21K tokens per turn is not a small optimization. It's the difference between
|
||
the model having room to think and the model being squeezed. It's the
|
||
difference between carrying 3 pages of conversation history and carrying 15.
|
||
|
||
## What the resolver actually does
|
||
|
||
The resolver is cheaper than the manifest. That's the load-bearing insight.
|
||
|
||
OpenClaw's native skill manifest puts ~80 tokens per skill into the system
|
||
prompt (name + description + location). At 300 skills that's 24,000 tokens
|
||
spent every turn whether the model needs the catalog or not.
|
||
|
||
The resolver puts ~15 tokens per skill into a compact markdown list. At
|
||
300 skills that's 4,500 tokens. But it only fires when the model checks
|
||
it, which is only when the request doesn't match a Tier A skill. Most
|
||
turns, the resolver costs zero tokens because the Tier A match handles it.
|
||
|
||
This is the routing-table pattern but applied to the skill manifest itself.
|
||
The resolver routes to skills, but it also routes around skills, keeping
|
||
them out of the context window until they're needed.
|
||
|
||
GBrain ships with a [bundled `skills/RESOLVER.md`](../../skills/RESOLVER.md)
|
||
you can use as a reference shape. The skillpack story for distributing
|
||
your own resolvers across machines is covered in
|
||
[skillpacks as scaffolding](skillpacks-as-scaffolding.md).
|
||
|
||
## The compact list format (v0.41.7.0)
|
||
|
||
GBrain's resolver parser used to require markdown tables:
|
||
|
||
```markdown
|
||
| Trigger | Skill |
|
||
|---------|-------|
|
||
| "gift idea" | `skills/gift-advisor/SKILL.md` |
|
||
```
|
||
|
||
That's fine when you have 20 entries. It gets unwieldy at 200, and at 300
|
||
it's unreadable. OpenClaw deployments quietly evolved a compact list
|
||
format that scales better:
|
||
|
||
```markdown
|
||
- **gift-advisor**: gift idea | what should I bring | birthday gift
|
||
- **flight-tracker**: track my flight | flight status | when does my flight land
|
||
```
|
||
|
||
Before v0.41.7.0, `gbrain doctor` only spoke the table dialect. On a
|
||
306-skill compact-format resolver, the doctor reported every skill as
|
||
unreachable: **238 FAIL errors on every doctor run**. The parser was
|
||
silently treating the compact dialect as zero skills.
|
||
|
||
v0.41.7.0 ships dual-format support. The same `parseResolverEntries`
|
||
function reads both table rows and list rows in the same file, with the
|
||
v0.31.7 multi-resolver merge (skillpack `skills/RESOLVER.md` + workspace
|
||
`../AGENTS.md`) folding everything into one unified view. Run `gbrain doctor`
|
||
and the 238 FAILs collapse to 0.
|
||
|
||
### The list-format contract
|
||
|
||
A few rules to keep the parser unambiguous:
|
||
|
||
- **Skill names must be kebab-lowercase.** `gift-advisor`, `flight-tracker`,
|
||
`email-triage`. Names that start with an uppercase letter (`MyTool`,
|
||
`Note`, `Convention`) are deliberately ignored. This is what stops prose
|
||
bullets like `- **Note**: see [link]` from being mis-parsed as skill
|
||
rows in real-world AGENTS.md files.
|
||
- **The path always resolves to `skills/<name>/SKILL.md`.** An optional
|
||
`→ \`skills/path\`` (or ASCII `->`) suffix is allowed for readability,
|
||
but the parser strips it. For non-conventional paths (skills under
|
||
nested directories, references into `conventions/`, anything that
|
||
isn't `skills/<name>/SKILL.md`), use the table format.
|
||
- **Triggers separate with `|`.** Empty pieces and the literal `...`
|
||
placeholder are dropped. Each trigger becomes its own resolver entry,
|
||
all pointing at the same skill.
|
||
- **Bold or plain.** `- **name**: triggers` is preferred. `- name: triggers`
|
||
works as a fallback.
|
||
|
||
You can mix table and list rows in the same file. Useful when a brain
|
||
inherits a table-format `RESOLVER.md` from gbrain and a list-format
|
||
`../AGENTS.md` from OpenClaw.
|
||
|
||
## The doctor safety net
|
||
|
||
The danger with tiering is invisible skill loss. You disable a skill from
|
||
native scanning, forget to add it to the resolver, and now the agent can't
|
||
do something it used to do. You won't notice until the moment you need it.
|
||
|
||
`gbrain doctor` walks every skill on disk and verifies it's reachable,
|
||
either through native scanning (Tier A) or through the resolver (Tier B
|
||
and C). On Garry's setup, the first run after tiering found 63 unreachable
|
||
skills. Sixty-three capabilities that existed on disk but had no routing
|
||
path. Fixed in an hour by adding resolver entries.
|
||
|
||
Run it after every skill change:
|
||
|
||
```bash
|
||
gbrain doctor
|
||
```
|
||
|
||
For CI gates, use the JSON-emitting variant:
|
||
|
||
```bash
|
||
gbrain check-resolvable --json
|
||
gbrain check-resolvable --strict # warnings fail too
|
||
```
|
||
|
||
If a skill is unreachable, the output tells you which one and suggests
|
||
the fix. The resolver is a document. Documents are cheap to fix.
|
||
|
||
## Implementation walkthrough
|
||
|
||
Three changes. Total time about 45 minutes once you've decided which
|
||
skills go in which tier.
|
||
|
||
### 1. Audit and tier your skills
|
||
|
||
Walk through every skill. Ask: does this need to fire on every turn?
|
||
|
||
- If yes → Tier A.
|
||
- If it fires weekly or less but is real → Tier B.
|
||
- If you don't use it → Tier C.
|
||
|
||
### 2. Disable Tier B and C in your agent's config
|
||
|
||
For OpenClaw, the file is `openclaw.json`. Add an entry per disabled skill:
|
||
|
||
```json
|
||
{
|
||
"skills": {
|
||
"entries": {
|
||
"gift-advisor": { "enabled": false },
|
||
"flight-tracker": { "enabled": false },
|
||
"1password": { "enabled": false }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The exact config shape depends on which agent runtime you use. The point
|
||
is the same in all of them: tell the runtime not to inject this skill into
|
||
the system prompt. The file stays on disk; only the prompt injection stops.
|
||
|
||
### 3. Write the resolver
|
||
|
||
One line per Tier B and Tier C skill. Trigger phrases that match how you
|
||
actually ask for things:
|
||
|
||
```markdown
|
||
- **gift-advisor**: gift idea | what should I bring | birthday gift
|
||
- **flight-tracker**: track my flight | flight status | when do I land
|
||
- **investor-update-ingest**: investor update | portfolio update | company metrics
|
||
```
|
||
|
||
That's it. The model handles the rest. When a request doesn't match Tier A,
|
||
it checks the resolver, reads the matching SKILL.md, and executes.
|
||
|
||
### 4. Run `gbrain doctor` and fix any unreachable skills
|
||
|
||
The doctor sweep tells you which skills don't have a routing path. Add a
|
||
resolver entry for each one, re-run, repeat until the count is zero.
|
||
|
||
## A lesson from the first version
|
||
|
||
I initially converted my resolver from a clean list format to a table
|
||
format because the validator only spoke tables. That was wrong. When a
|
||
tool fails against valid data, the right move is to fix the tool, not
|
||
reshape the data. The list format was correct, compact, readable, easy
|
||
to maintain. The parser needed to support both shapes. v0.41.7.0 is
|
||
that fix.
|
||
|
||
The same principle applies everywhere in agent systems. Your SKILL.md is
|
||
the source of truth. Your AGENTS.md is the source of truth. Your resolver
|
||
is the source of truth. When tooling disagrees with your configuration,
|
||
the tooling is wrong. Fix the tooling.
|
||
|
||
## The scaling curve
|
||
|
||
At 50 skills, you don't need any of this. Just load everything.
|
||
|
||
At 100, you start feeling the drag but can push through.
|
||
|
||
At 200, routing accuracy drops and sessions get noticeably slower. This
|
||
is where most people stop adding skills, which means their agent stops
|
||
getting more capable. Bad trade.
|
||
|
||
At 300+, tiering is mandatory. But with tiering, there's no ceiling.
|
||
1,000 skills with 35 in the hot path and 965 in the resolver is the same
|
||
per-turn cost as 35 skills with no resolver. The cost stays flat.
|
||
Capabilities compound.
|
||
|
||
The architecture that gets you from 50 to 300 is different from the
|
||
architecture that gets you from 10 to 50. That's normal. Systems that
|
||
scale change shape. The important thing is that each tier preserves full
|
||
capability. You're organizing, not deleting.
|
||
|
||
## Related
|
||
|
||
- [Skill development cycle](skill-development.md) — the 5-step loop for
|
||
turning a repeated task into a real skill.
|
||
- [Skillpacks as scaffolding](skillpacks-as-scaffolding.md) — how to
|
||
distribute a coherent set of skills across machines and agents.
|
||
- [Sub-agent routing](sub-agent-routing.md) — when to delegate to a
|
||
sub-agent vs handle in-line, and the model routing table for each path.
|
||
|
||
GBrain: [github.com/garrytan/gbrain](https://github.com/garrytan/gbrain).
|
||
The `parseResolverEntries` parser lives at
|
||
[`src/core/check-resolvable.ts`](../../src/core/check-resolvable.ts);
|
||
the bundled resolver lives at [`skills/RESOLVER.md`](../../skills/RESOLVER.md).
|
||
|
||
---
|
||
|
||
## docs/guides/push-context.md
|
||
|
||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/push-context.md
|
||
|
||
# Push-based context (#2095, v0.42.43.0)
|
||
|
||
Retrieval used to be pull-only: the agent had to *know to ask* before the brain
|
||
contributed anything. Push-based context inverts that — the brain volunteers
|
||
relevant pages from the recent conversation, confidence-gated so push noise
|
||
never becomes worse than pull silence.
|
||
|
||
Three channels share one zero-LLM core (`src/core/context/volunteer.ts`):
|
||
|
||
| Channel | Surface | When to use |
|
||
|---|---|---|
|
||
| `reflex` | automatic, inside the context engine | default-on for plugin hosts; nothing to call |
|
||
| `op` | `gbrain volunteer-context` / MCP `volunteer_context` | agents without the plugin; one call per turn |
|
||
| `watch` | `gbrain watch` | stream a transcript in, volunteered pages stream out |
|
||
|
||
## How it decides
|
||
|
||
1. **Extract** entities across the last N turns (capitalized runs, `@handles`),
|
||
merged with recency / frequency / user-role salience. Assistant-introduced
|
||
entities and "what did she invest in?" follow-ups whose antecedent was named
|
||
in the window now resolve.
|
||
2. **Resolve** through the alias table, exact titles, and slug suffixes — each
|
||
arm carries an honest confidence: alias 0.9, exact title 0.8, slug-suffix 0.6,
|
||
+0.05 when mentioned in ≥2 turns or the newest turn.
|
||
3. **Gate** at `min_confidence` (default 0.7 — slug-suffix matches need an
|
||
explicit lower gate), suppress pages already surfaced (slug-presence only),
|
||
cap at 3 pages (hard cap 5).
|
||
|
||
## CLI
|
||
|
||
```bash
|
||
# one-shot: pipe recent turns (oldest → newest)
|
||
printf 'user: ask alice-example about the deal\nassistant: noted\nuser: what did she say?\n' \
|
||
| gbrain volunteer-context
|
||
|
||
# streaming: volunteered pages print as the transcript flows
|
||
some-transcript-feed | gbrain watch --json
|
||
|
||
# the feedback loop: how often were volunteered pages actually opened?
|
||
gbrain volunteer-context --stats
|
||
```
|
||
|
||
Stats are **approximate** by design: "used" means `pages.last_retrieved_at >
|
||
volunteered_at` — the 5-minute last-retrieved throttle causes false negatives
|
||
and unrelated reads of the same page cause false positives. Use the per-arm
|
||
precision to tune `min_confidence`, not as an exact metric.
|
||
|
||
**PGLite + `gbrain watch`:** PGLite is single-connection, and watch holds its
|
||
connection for the whole session — a concurrent `gbrain serve` or any write
|
||
path blocks until watch exits. On a PGLite brain, run watch in bursts (piped
|
||
input exits at EOF) or use the ambient reflex channel instead, which routes
|
||
through a running serve's resolve socket rather than taking the lock. Routing
|
||
watch through that same socket is a filed follow-up (TODOS.md). Postgres
|
||
brains are unaffected.
|
||
|
||
## Config
|
||
|
||
| Key | Default | What it does |
|
||
|---|---|---|
|
||
| `retrieval_reflex_window_turns` | 4 | turns the ambient reflex extracts from; 1 = legacy current-turn-only (file/env plane: `GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS`) |
|
||
| `retrieval_reflex` | true | the ambient channel's master switch |
|
||
| `retrieval_reflex_max_pointers` | 3 | pointer cap per turn |
|
||
|
||
Per-call knobs: `max_pages` + `min_confidence` on both the op and `gbrain watch`
|
||
(`--max-pages` / `--min-confidence`, plus `--window-turns` / `--source` on watch);
|
||
on the op only: `prior_context` (text whose already-surfaced slugs are suppressed),
|
||
`session_id` / `turn` attribution params (watch stamps its own per-session id and
|
||
turn numbers in the feedback log), and `days` to size the `--stats` window.
|
||
|
||
## Storage + privacy
|
||
|
||
Volunteered pages log to `context_volunteer_events` (migration v117): slug,
|
||
arm, confidence, channel, optional session/turn — the rationale is a
|
||
deterministic template string, never raw conversation text. Event writes are
|
||
best-effort (fire-and-forget, drained at CLI exit) — the log is a tuning signal,
|
||
not an audit trail. Rows are pruned after 90 days by the dream cycle's purge
|
||
phase. Synopses always strip the takes/facts fences — the same strip `get_page`
|
||
applies to untrusted callers, applied unconditionally here so private fence rows
|
||
never reach a prompt regardless of caller trust.
|
||
|
||
---
|
||
|
||
## 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 in an interactive terminal, the server prints an **admin
|
||
bootstrap token** to stderr:
|
||
|
||
```
|
||
Admin bootstrap token: 3a1f9c...
|
||
Open http://localhost:3131/admin and paste it to log in.
|
||
```
|
||
|
||
On a non-TTY start (systemd, Docker, any piped or captured logs) the generated
|
||
token is hidden so it never lands in log storage. For headless deploys either
|
||
set `GBRAIN_ADMIN_BOOTSTRAP_TOKEN` to a value you control before starting, or
|
||
run `gbrain serve --http --print-admin-token` once on a trusted terminal to
|
||
force printing.
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
# AI providers
|
||
|
||
# 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 an unreachable direct
|
||
connection on an IPv4-only host. GBrain uses the Transaction pooler (port 6543)
|
||
for reads, but routes migrations, DDL, and sync transactions to a derived direct
|
||
connection (`db.<ref>.supabase.co:5432`), which is IPv6-only.
|
||
- On an IPv4-only host, reads work but sync transactions fail and silently skip
|
||
pages.
|
||
- Fix: set `GBRAIN_DIRECT_DATABASE_URL` to the **Session pooler** string (port
|
||
5432 on the `pooler.supabase.com` host, IPv4), or enable Supabase's IPv4
|
||
add-on. 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 sync errors mention an unreachable host or connection timeout, the direct
|
||
connection isn't reachable on IPv4 (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
|