* feat(admin): legacy API keys alongside OAuth clients in dashboard Adds API key management to the admin dashboard: Server (serve-http.ts): - GET /admin/api/api-keys — list legacy access_tokens with status - POST /admin/api/api-keys — create new bearer token - POST /admin/api/api-keys/revoke — revoke by name - Stats endpoint now includes active_api_keys count Admin UI (Agents.tsx): - Tabbed view: 'OAuth Clients' | 'API Keys' - API Keys tab: table with name, status, created, last used, revoke button - Create API Key modal with name input - Token reveal modal with copy button + warning - Badge showing active key count on tab Both auth methods (OAuth 2.1 client_credentials and legacy bearer tokens) now visible and manageable from a single admin surface. * feat(admin): remember admin token in localStorage + auto-reauth Login flow: - First login: paste token, saved to localStorage - Subsequent visits: auto-login from localStorage (no paste needed) - Shows 'Authenticating...' spinner during auto-login - If saved token is stale (server restarted), clears it and shows login form Session recovery: - If session cookie expires mid-use (server restart, 24h expiry), the API layer auto-reauths with the saved token before redirecting to login - Transparent to the user — one failed request triggers reauth + retry - Only falls back to login page if the saved token itself is invalid Security: - Token stored in localStorage (same-origin, tailnet-only deployment) - Cleared automatically when token becomes invalid - Cookie remains HttpOnly + SameSite=Strict for the actual session * feat(admin): rich request logging + agent activity tracking Server: - mcp_request_log now captures params (jsonb) and error_message (text) - Agents API returns last_used_at, total_requests, requests_today - Request log API supports agent/operation/status filtering via query params - SSE broadcast includes params and error details Agents page: - Shows 'Requests today / total' and 'Last used' (relative time) per agent - Removed Client ID column (low signal, shown in drawer) Request Log page: - New 'Params' column — shows query text, slug, or param count inline - Click any row to expand full details (params JSON, error message, timestamps) - Click agent name to filter all requests by that agent - Agent filter dropdown in header - Error messages shown in red in expanded view What this means: when Claude Code searches for 'pedro franceschi', the admin dashboard shows the search query, which agent ran it, how long it took, and whether it succeeded — all clickable. * feat(admin): magic link login — ask your agent for the URL New flow: 1. User opens /admin → sees 'This is a protected dashboard' 2. UI tells them: 'Ask your AI agent for the admin login link' 3. Agent generates: https://host:port/admin/auth/<token> 4. User clicks the link → auto-authenticates → redirects to dashboard 5. Session lasts 7 days (magic link) vs 24h (manual token paste) Server: GET /admin/auth/:token validates the bootstrap token, sets HttpOnly cookie, redirects to /admin/. Invalid tokens get a plain text error telling them to ask their agent for a fresh link. Login page: primary UX is the 'ask your agent' prompt with example. Manual token paste collapsed under a <details> disclosure. * feat(admin): config export for Claude Code, ChatGPT, Claude.ai, Cursor, Perplexity Agent drawer now shows setup instructions for 5 clients + raw JSON: - Claude Code: .mcp.json with bearer token + curl to mint - ChatGPT: Settings → Tools → MCP with OAuth discovery - Claude.ai (Cowork): Connected Apps → MCP with OAuth - Cursor: .cursor/mcp.json with OAuth config - Perplexity: Connectors with client ID/secret - JSON: raw config with all URLs (server, token, discovery) All snippets use the actual server URL (window.location.origin) instead of placeholder YOUR_SERVER. Client ID pre-filled. * feat(admin): per-client token TTL — configurable token lifetime Problem: OAuth tokens expire in 1 hour (hardcoded). Claude Code's built-in OAuth client doesn't auto-refresh, so users get 401s every hour. Fix: per-client token_ttl column on oauth_clients table. Set at registration time or updated later via the admin dashboard. Server: - oauth_clients.token_ttl column (nullable integer, seconds) - exchangeClientCredentials reads per-client TTL, falls back to server default - POST /admin/api/register-client accepts tokenTtl param - POST /admin/api/update-client-ttl for existing clients - Agents API returns token_ttl for display Admin UI: - Register modal: Token Lifetime dropdown (1h, 24h, 7d, 30d, 1y, no expiry) - Agent drawer: shows current TTL in Details section Presets: gstack-desktop and garry-claude-code set to 30-day tokens. * fix(admin): request log shows agent name instead of truncated client_id Resolves client_id → client_name via LEFT JOIN on oauth_clients (and access_tokens for legacy keys). Agent column now shows 'gstack-desktop' instead of 'd0db7692caf5…'. Clickable to filter by agent. * feat(admin): DESIGN.md + left-align everything DESIGN.md establishes the admin dashboard design system: - Left-align all text (Garry preference) - Inter + JetBrains Mono (shared DNA with GStack) - No accent color — semantic badges carry all color - Dense utilitarian ops dashboard - Component specs and anti-patterns documented CSS: login-box text-align center → left * feat(admin): unified agent view + resolved agent names in request log Agent names stored at log time (agent_name column). Agents page shows OAuth clients and API keys in one unified table. Request log shows human-readable names. Backfilled 1,114 existing entries. * feat(admin): working Revoke Agent button + e2e tests Bugs fixed: - Revoke Agent button was a no-op (no onClick handler, no API endpoint) - Legacy API key tokens got 401 at /mcp (missing expiresAt in AuthInfo) - token_ttl and deleted_at queries failed on PGLite (columns don't exist) Server: - POST /admin/api/revoke-client: soft-deletes oauth_clients + purges tokens - exchangeClientCredentials checks deleted_at (graceful if column missing) - Legacy token verify returns expiresAt (1yr future) for SDK compat UI: - Revoke button: confirm dialog → revoke → close drawer → reload table - Shows 'This agent has been revoked' for revoked agents E2E tests (2 new cases, 17 total): - revoke client via admin API invalidates all tokens (mint → use → revoke → verify rejected → mint fails) - revoke API key via admin API (create → use at /mcp → revoke → verify rejected) 52 tests, 0 failures, 213 assertions across unit + e2e. * fix(test): e2e tests clean up after themselves — no more orphan clients Problem: every test run left e2e-oauth-test, e2e-revoke-test, and e2e-revoke-key-test rows in oauth_clients and access_tokens. The CLI-based cleanup in afterAll was failing silently. Fix: - beforeAll: SQL DELETE of any e2e-* orphans from previous crashed runs - afterAll: direct SQL cleanup of oauth_tokens, oauth_clients, access_tokens, mcp_request_log — all rows matching 'e2e-%' pattern - No reliance on CLI commands for cleanup (they fail silently) Verified: 52 tests pass, 0 test rows remain after run. * feat(admin): hide revoked toggle on Agents page * fix(admin): styled error page for expired magic links Matches the login page aesthetic instead of plain text. Dark theme, GBrain logo, explains the link expired, tells user to ask their agent. * fix(admin): clean config export — auth-type-aware Claude Code instructions * fix(admin): rewrite all config exports — command language, auth-type-aware, verified syntax * fix(admin): API key rows clickable with revoke + sync all fixes from master Syncs all accumulated fixes onto the PR branch: - API key rows in agents table now open drawer with Revoke button - API keys show bearer token usage hint instead of config export tabs - Config export snippets use command language directed at the AI agent - Styled expired magic link error page - Hide revoked toggle - Test cleanup via direct SQL - All v0.26.2 upstream fixes incorporated * fix(oauth): port coerceTimestamp helper from master1055e10cTests in test/oauth.test.ts (already on this branch) import coerceTimestamp from oauth-provider.ts. The import was synced from master via PR commit 16 ("sync all fixes from master") but the production-code change to oauth-provider.ts was not. Result: bun test fails at module load with "coerceTimestamp is not exported". This commit ports the helper directly instead of merging master, avoiding VERSION/CHANGELOG/dist conflicts. Boundary helper for postgres.js BIGINT-as-string (auto-detected on Supabase pgbouncer / port 6543). Throws on non-finite so corrupt rows fail loud at the SELECT-row -> JS-number boundary. Returns undefined for SQL NULL; comparison sites treat NULL as expired (fail-closed). Refactors 4 sites: - getClient: DCR response numeric-shape compliance per RFC 7591 §3.2.1 - exchangeRefreshToken: NULL -> expired fail-closed - verifyAccessToken: single guard, narrowed return; folds in v0.26.1's inline Number(...) at the return site Originally landed on master as part of #593 (v0.26.2). Ported here so PR #586 (v0.26.3) can build standalone without a master merge. * feat(schema): migration v33 — admin dashboard columns Adds the 5 columns + new index referenced by PR #586 admin dashboard work that landed without a corresponding schema migration: oauth_clients.token_ttl INTEGER -- per-client OAuth TTL override oauth_clients.deleted_at TIMESTAMPTZ -- soft-delete for revoke mcp_request_log.agent_name TEXT -- resolved client_name for log mcp_request_log.params JSONB -- captured request params mcp_request_log.error_message TEXT -- captured error text on failure idx_mcp_log_agent_time INDEX -- supports new agent filter Without v33 on existing brains: - /admin/api/agents 503s (SELECT references token_ttl + deleted_at) - POST /admin/api/revoke-client throws 500 (UPDATE deleted_at) - POST /admin/api/update-client-ttl throws 500 (UPDATE token_ttl) - mcp_request_log INSERTs silently swallow column-doesn't-exist errors, request log appears empty to the operator All ALTERs use ADD COLUMN IF NOT EXISTS so re-running the migration is a no-op on a brain that already has v33. Includes inline UPDATE backfill of agent_name on existing rows via COALESCE on oauth_clients.client_name → access_tokens.name → token_name. Updates: - src/core/migrate.ts: v33 migration entry - src/schema.sql: source-of-truth schema for fresh installs - src/core/pglite-schema.ts: PGLite mirror - src/core/schema-embedded.ts: regenerated via bun run build:schema - test/migrate.test.ts: 5 SQL-shape assertions pinning the v33 contract * refactor(serve-http): parameterize request-log filter; kill dead vars Three issues in the prior /admin/api/requests handler: 1. sql.unsafe() with manual single-quote escape on user input: conditions.push(`token_name = '${agent.replace(/'/g, "''")}'`); Works under standard_conforming_strings=on (PG default since 9.1) but pattern is a footgun — any future contributor adding a filter without escaping breaks the dam. Backslashes are not escaped. Mitigated by requireAdmin but defense-in-depth says don't ship the pattern. 2. Dead variables (lines 348-357 of the prior code): `query`, `params`, `paramIdx` were built up with $N placeholders and then never used when the function fell through to sql.unsafe with manually-escaped strings. Confusing leftovers from an earlier parameterization attempt. 3. Unused `values: unknown[] = []` in the conditions block. Fix: replace the entire dynamic-WHERE construction with postgres.js tagged-template fragments. Each filter expands to either `AND col = ${val}` (true parameter binding via the postgres-js driver) or an empty fragment. `WHERE 1=1` lets us always have a WHERE clause and unconditionally append AND-prefixed fragments. No string interpolation, no manual escaping, no sql.unsafe. Net change: -27 lines (from 30 lines of broken/dead code to 17 lines of clean parameterized fragments). * perf(oauth): thread client_name through AuthInfo; drop per-request lookup PR #586's serve-http.ts /mcp handler did one extra DB roundtrip per authenticated request to resolve client_id → client_name for logging: let agentName = authInfo.clientId; try { const [client] = await sql`SELECT client_name FROM oauth_clients WHERE client_id = ${authInfo.clientId}`; if (client) agentName = client.client_name; } catch { /* best effort */ } On a busy brain (Perplexity Computer doing inline research, Claude Code searching) that is ~50–100ms extra per /mcp request — wasted on a static lookup that doesn't change between requests. Codex's review reframed the planned cache+invalidation approach: the right fix is to fold the name resolution into verifyAccessToken's existing oauth_tokens SELECT via a LEFT JOIN on oauth_clients. One query that was already running, returns the name as a bonus column, no module- scope cache to maintain, no invalidation contract for future contributors to remember. Changes: - AuthInfo (src/core/operations.ts): add optional clientName field with doc explaining why it's threaded here. - verifyAccessToken (src/core/oauth-provider.ts): SELECT becomes SELECT t.client_id, t.scopes, t.expires_at, t.resource, c.client_name FROM oauth_tokens t LEFT JOIN oauth_clients c ON c.client_id = t.client_id WHERE t.token_hash = ${tokenHash} AND t.token_type = 'access' Returns clientName in AuthInfo. - Legacy access_tokens path: clientName = name (single identifier). - serve-http.ts /mcp handler: read authInfo.clientName directly, fall back to clientId. Per-request lookup removed. Net change: -8 LOC. Eliminates the per-request DB roundtrip while keeping the same behavior surface. * security(serve-http): timingSafeEqual on admin token hash compare Both /admin/login (POST, JSON body) and /admin/auth/:token (GET, magic link) compared the sha256 of the operator-supplied token against the known bootstrapHash via JS string `===`, which short-circuits at the first mismatched character. The inputs are SHA-256 outputs so the practical timing leak only reveals hash bits (not raw token bits, since SHA-256 isn't invertible) — but defense-in-depth on the highest- privileged URLs the server exposes is the right call. New helper safeHexEqual(a, b): - Length-equal check first (both are 64-char hex) - Buffer.from(hex, 'hex') decodes each side to 32 bytes - crypto.timingSafeEqual returns the constant-time compare result Also tightens the POST handler's input validation: requires token to be a string before passing to createHash (prior code only checked truthiness, would have crashed on object-typed bodies even with express.json's parser). Used at both magic-link and password-style admin auth sites. * security(serve-http): rate-limit /admin/auth/:token at 10/min/IP Defense-in-depth on the magic-link endpoint. A misconfigured client looping on /admin/auth/:bad would otherwise consume CPU on sha256 + the inline HTML 401 response without bound. Brute-forcing the 64-char hex bootstrap token is computationally infeasible regardless, so this is about denial-of-service, not auth bypass. Reuses the existing express-rate-limit dep already wiring /token's client-credentials limiter. New adminAuthRateLimiter shares the same configuration shape (standardHeaders, legacyHeaders) for consistency. windowMs: 60_000 (1 minute) max: 10 message: plain string ("Too many magic-link attempts. Wait a minute before trying again.") instead of JSON envelope, matching the endpoint's HTML response style. * security(admin): kill JS-state token; single-use magic links; sign out everywhere Resolves D11 + D12 from the codex-pushback review. Closes the actual trust boundary instead of the persistence layer (sessionStorage was security theater per codex finding #7). # Single-use magic links (D11=C) The bootstrap token is no longer the magic-link path component. New flow: agent has bootstrap token (read from server stderr) -> POST /admin/api/issue-magic-link Authorization: Bearer <bootstrap> -> server returns one-time nonce URL -> operator clicks /admin/auth/<nonce> -> server consumes nonce, sets cookie, redirects to dashboard Server state (in-memory): - magicLinkNonces: Map<nonce, expiresAt> (5-minute TTL) - consumedNonces: Set<nonce> (LRU cap 1000 to bound memory) - pruneExpiredNonces() best-effort GC on each issue/redeem Each redemption marks the nonce consumed. Second click on the same URL gets the styled 401 page. Leaked URL grants exactly one extra session before dying. The bootstrap token never appears in a URL — no leakage via browser history, proxy access logs, or Referer headers. # Kill JS-state bootstrap token (D12=B) admin/src/pages/Login.tsx + admin/src/api.ts: - All localStorage reads/writes removed - Auto-reauth-via-saved-token logic deleted - Token only lives in form state during submit, cleared after - 401 redirects straight to login — no cache to retry against The HttpOnly cookie is the only session credential after successful authentication. Closing the tab ends the session. Reopening shows the login page. Operator asks the agent for a fresh magic link (or pastes the bootstrap token from the server terminal). # Sign out everywhere POST /admin/api/sign-out-everywhere (admin-cookie-required) calls adminSessions.clear() and returns {revoked_sessions: count}. Every browser/tab fails its next request, gets 401, redirects to login. Bootstrap token unaffected — still valid for new magic-link mints. UI: button in the sidebar footer with a confirm() guard ("Sign out every active admin session, including other browsers and tabs?"). # Notes admin/dist is gitignored on this branch (master's v0.26.2 removed that line; the merge to master will reconcile). After /ship's merge step, rebuild admin/dist with `cd admin && bun run build` to capture the new sign-out button + simplified login page. * fix(admin): rename loadApiKeys() to loadAgents() in Agents.tsx onCreated The Create API Key flow's onCreated callback called loadApiKeys() but no such function exists in this file. The unified /admin/api/agents endpoint (added in PR commit 14) returns BOTH OAuth clients AND legacy API keys, so loadAgents() is the right call. User-visible bug: clicking "+ API Key" -> filling in the name -> clicking Create would mint the key on the server but throw ReferenceError: loadApiKeys is not defined in the React onCreated callback. The token-reveal modal would still appear (because setShowApiKeyToken runs before the loadApiKeys call), but the agents table wouldn't refresh, leaving the new key invisible until manual page reload. Five Claude review passes missed this. Codex caught it in one pass. 1-line fix. * fix(admin): empty-state placeholder when filtered Agents result is empty Pre-fix: the empty-state guard checked the unfiltered agents array. If every agent was revoked AND the "Hide revoked" toggle was on (default), the table rendered a header row with zero body rows and no placeholder — looked like a broken / empty / loading state. Two cases to render distinctly: 1. agents.length === 0 (truly no agents) "No agents registered. Register your first agent to get started." 2. visibleAgents.length === 0 BUT agents.length > 0 (all agents are revoked, hideRevoked filter hides them all) "All agents are revoked. Uncheck "Hide revoked" to view them." Refactored the table render into an IIFE so the filter expression is computed once and shared between the empty-state guard and the row map. Drops the prior inline `agents.filter(...).map(...)` pattern. (F2.2 from the eng review pass #2.) * fix(admin): restore Claude Code + Cursor tabs for API-key agents Wintermute's commit 16 (3d5d0f87) wrapped the entire Config Export section in {isOAuth && (...)}, hiding ALL tabs for api_key agents and replacing them with a single line of plain instruction. That dropped the working auth-type-aware Claude Code + Cursor snippets (added by his own commit 15) along with the genuinely OAuth-only ChatGPT / Claude.ai / Perplexity ones. Codex review pass D5 settled on option C: per-tab branching. Two clients (Claude Code, Cursor) accept raw bearer tokens in their MCP config, so their snippets render normally for api_key agents (commit 15's auth-type-aware branching does the right thing). Three clients (ChatGPT, Claude.ai, Perplexity) only speak OAuth 2.0 client_credentials and reject raw bearer; for api_key agents they render an explanatory message naming the client and pointing the operator at registering an OAuth client instead. JSON tab continues to render its raw structured metadata unconditionally. Layout: removed the `{isOAuth && (...)}` outer wrap; tab list now always visible. The body of each tab is selected via an IIFE that checks (auth_type === 'api_key' && tab in oauthOnlyTabs). Net change: +24 lines (the warning panel + IIFE branch logic). * feat(admin): read -s prompt OAuth Claude Code snippet + 2-step curl fallback Wintermute's commit 15 inlined client_secret into a long compound `claude mcp add --header "Authorization: Bearer $(curl -d '... client_secret=PASTE_HERE')"` line. When the operator replaces PASTE with their real secret, that secret lands in ~/.zsh_history and appears in `ps` output for the lifetime of the curl process. D13=C from the eng review: ship both shapes. Default (read -s prompt-based, ~17 lines): - read -rs prompts for the secret without echo, stores in $GBRAIN_CS scoped to the shell session - curl uses --data-urlencode "client_secret=$GBRAIN_CS" — variable substitution at exec time, so the secret enters the curl process's argv at the moment of the call, but the shell history records literally `--data-urlencode "client_secret=$GBRAIN_CS"`, not the value - unset GBRAIN_CS afterwards to scrub the env Fallback (2-step curl + paste, for shells without read -s): - one curl command to mint the token (PASTE_YOUR_CLIENT_SECRET_HERE in the body — secret hits history but in one short isolated line that's easy to scrub) - second `claude mcp add` command with PASTE_TOKEN_FROM_ABOVE — the bearer token, not the long-lived client secret - bash + zsh history-deletion hint at the bottom Both shapes preserve the agent-facing voice ("The user wants to connect GBrain MCP to your context. Here's how.") and the token-TTL rendering ("will last 30 days") that commit 15 added. Net change: +25 lines in the configSnippets['claude-code'] OAuth branch. API-key branch unchanged (single paste, no secret). * chore(ci): gate admin React build via scripts/check-admin-build.sh Codex review pass #6 finding #3 caught loadApiKeys() referenced but undefined in Agents.tsx — a real shipping bug that 5 Claude review passes missed. Root cause: the bash test pipeline never compiled the React admin app, so missing-symbol errors only surfaced during a deliberate `cd admin && bun run build`. This commit threads the admin build into the standard test gate. Any future TypeScript error or missing symbol in admin/src/ now fails `bun run test` alongside the other shell guards (privacy, jsonb, progress-stdout, etc.) and the typecheck step. Behavior: - scripts/check-admin-build.sh runs `bun install --silent` (idempotent, ~50ms on no-op) then `bun run build` in admin/. - Vite's build runs `tsc -b && vite build` so type errors fail the pipeline, not just bundling errors. - GBRAIN_SKIP_ADMIN_BUILD=1 escape hatch for fast inner-loop test runs that don't touch admin/. Production CI MUST NOT set this. - Skips silently if admin/ doesn't exist (handles slim-clone scenarios). Wired into both: - "test" script: full pipeline now includes admin build before bun test - "check:admin-build" script: invoke standalone for debugging * test(e2e): v0.26.3 coverage — column round-trip, injection probe, TTL, magic-link Folds together the planned fix-up commits #8-#11 since they all live in the same E2E file and share the spawned-server harness. Each test block is independently bisect-readable. # Test 1: mcp_request_log new column round-trip (pins migration v33) Wipes log rows for the e2e-oauth-test client, makes a successful tools/list call + a failed tools/call (nonexistent tool name), then asserts: - rows persisted (count >= 2) — proves the INSERT wasn't silently swallowed by the "best effort" try/catch on a column-doesn't-exist error - agent_name column resolves to 'e2e-oauth-test' on every row (proves the JOIN in verifyAccessToken or the v33 backfill path) - params column persisted as JSONB on tools/call - error_message column populated on the status='error' row Without migration v33, every assertion fails — the column doesn't exist so the INSERT throws, gets swallowed, and rows.length === 0. # Test 2: request-log filter injection probe Sends `?agent=alice'%20OR%201%3D1` to /admin/api/requests. Pre-fix, the sql.unsafe path would have crashed the server with malformed SQL on the way to the auth check (or worse, returned all rows under broken escaping). Post-fix (parameterized fragments), the unauthenticated request hits 401 without ever touching SQL. Asserts: - 401 (not 500) on the injection input - server still responsive on /health afterwards (didn't crash) # Test 3: per-client token_ttl flow Registers e2e-test-ttl, sets oauth_clients.token_ttl, mints a token, asserts response's expires_in matches. Cycles through three states: - token_ttl = 86400 → expires_in = 86400 (24h custom override) - token_ttl = 7200 → expires_in = 7200 (2h different custom) - token_ttl = NULL → expires_in = 3600 (server default fallback) Pins the per-client TTL feature added in PR #586 commit 6 (e7989e97). # Test 4: magic-link styled 401 page + single-use semantic (a) Invalid nonce returns Content-Type: text/html with a body that contains "expired" and "GBrain" — pins the styled error page from PR commit 13 (f8f5cfe8). (b) Single-use semantic: extract bootstrap token from server stderr (best-effort; skips gracefully if not extractable), POST to /admin/api/issue-magic-link to mint a one-time nonce URL, click once (gets 302 + cookie), click again (gets styled 401). Pins the D11=C single-use rotation logic. # Test 5: agent_name resolution path Makes an OAuth request and asserts mcp_request_log.agent_name resolves to the OAuth client_name (not the truncated client_id). Pins the JOIN introduced in fix-up #4 + the v33 backfill path. # Test 6: register-client missing-name returns 400 (basic input validation) Hits /admin/api/register-client without auth — must 401 (not crash 500). # Other changes - Renamed describe header from `(v0.26.1 + v0.26.2)` to `(v0.26.1 + v0.26.2 + v0.26.3)` — F6.5. - All postgres.js sql tag bindings on `clientId` / `clientSecret` use the `!` non-null assertion since these are typed `string | undefined` in the test fixture but always assigned before each test block runs. - Result casts go through `as unknown as ...` per postgres.js's RowList typing (the lib's structural type doesn't unify with bare interface arrays). * chore: privacy sweep + integrity.ts on getconnection allow-list Two pre-existing CI failures uncovered while running `bun run test` on this branch — unrelated to v0.26.3 substance but blocking the pipeline. # Privacy sweep (src/core/mounts-cache.ts) Two references to the private agent fork name in code comments, violating CLAUDE.md privacy rule ("never reference real people, companies, funds, or private agent names in any public-facing artifact"). Both authored in v0.26.0 commit3c032d79e. - line 6 (docblock): "Host agents (Wintermute / OpenClaw / any Claude Code install) read" -> "Host agents (your OpenClaw / any Claude Code install) read" - line 324 (RESOLVER preamble emitter): "Host agents (Wintermute/OpenClaw/Claude Code) should prefer this file over" -> "Host agents (your OpenClaw / Claude Code) should prefer this file over" Per the documented substitution: "your OpenClaw" for reader-facing copy covers any downstream OpenClaw deployment (Wintermute, Hermes, AlphaClaw, etc.) without leaking the private name into search engines or release artifacts. # integrity.ts on the getconnection allow-list `scripts/check-no-legacy-getconnection.sh` flags `db.getConnection()` calls outside `src/core/db.ts` to enforce the multi-brain routing contract. `src/commands/integrity.ts:355` (scanIntegrityBatch) was introduced in v0.22.16 commit8468ba25a— the check ran clean at the time because the file wasn't on the allow-list yet, but PR #586's test pipeline catches it. Adds the file to ALLOWED with a "PR 1 cleanup" note matching the existing entries' pattern. The proper fix (refactor to accept engine from OperationContext) is out of v0.26.3 scope and tracked alongside the other PR 1 entries. * chore: bump v0.26.2 -> v0.26.3 + CHANGELOG VERSION + package.json already at 0.26.3 from the initial bump on this branch (see commit history). This commit lands the rewritten CHANGELOG entry covering everything that actually shipped in v0.26.3 — well past the original "legacy API keys" framing. What lands in v0.26.3: # Headline (admin trust model) Bootstrap token never persists in browser JS state (no localStorage, no sessionStorage). Magic-link URLs use single-use server-issued nonces — bootstrap token never appears in a URL. Cookie sessions are HttpOnly + SameSite=Strict. "Sign out everywhere" button revokes every active admin session in one click. # Schema Migration v33 adds 5 columns referenced by PR #586's admin-dashboard work that landed without a corresponding migration. Without v33, existing brains 503 on /admin/api/agents and silently empty their request log. Backfill of agent_name from oauth_clients.client_name -> access_tokens.name -> token_name baked into the migration. # Performance verifyAccessToken JOINs oauth_clients in its existing token SELECT and returns clientName on AuthInfo. Removes the per-MCP-request DB roundtrip that was happening on every authenticated /mcp call. # Security - crypto.timingSafeEqual on admin token hash compare - /admin/auth/:nonce rate-limited at 10/min/IP - Single-use nonces with 5-minute TTL - Request-log filter parameterized via postgres.js tagged-template fragments (sql.unsafe + manual escape removed) - Per-client OAuth token TTL (1h, 24h, 7d, 30d, 1y, no expiry) - Ported coerceTimestamp helper from master v0.26.2 (BIGINT-as-string fix) # UI - API keys + OAuth clients in one unified Agents table - Auth-type-aware Config Export tabs - Claude Code OAuth: read -s prompt-based snippet (default) + 2-step curl fallback (D13=C) - Cursor: OAuth discovery URL OR raw bearer based on auth type - ChatGPT/Cowork/Perplexity: "OAuth client required" CTA on api_key agents - Hide-revoked toggle + empty-state placeholder for filtered-empty - Bug fix: loadApiKeys -> loadAgents (codex caught what 5 review passes missed; Create-API-Key flow was broken) # Tests + CI - New E2E coverage: column round-trip, injection probe, per-client TTL, magic-link single-use, styled 401, agent_name resolution - Admin React build is now a CI gate (catches missing-symbol bugs before E2E) - check-no-legacy-getconnection allowlist updated for integrity.ts Branch shape: 16 author commits + 13 fix-up commits = 29 commits on PR. Commit-by-commit bisect-friendly. Plan + codex review pass artifacts at ~/.claude/plans/check-this-out-and-breezy-forest.md. --------- Co-authored-by: Wintermute <wintermute@garrytan.com> Co-authored-by: Garry Tan <garrytan@gmail.com>
GBrain
Your AI agent is smart but forgetful. GBrain gives it a brain.
Built by the President and CEO of Y Combinator to run his actual AI agents. The production brain powering his OpenClaw and Hermes deployments: 17,888 pages, 4,383 people, 723 companies, 21 cron jobs running autonomously, built in 12 days. The agent ingests meetings, emails, tweets, voice calls, and original ideas while you sleep. It enriches every person and company it encounters. It fixes its own citations and consolidates memory overnight. You wake up and the brain is smarter than when you went to bed.
The brain wires itself. Every page write extracts entity references and creates typed links (attended, works_at, invested_in, founded, advises) with zero LLM calls. Hybrid search. Self-wiring knowledge graph. Structured timeline. Backlink-boosted ranking. Ask "who works at Acme AI?" or "what did Bob invest in this quarter?" and get answers vector search alone can't reach. Benchmarked side-by-side against the category: gbrain lands P@5 49.1%, R@5 97.9% on a 240-page Opus-generated rich-prose corpus, beating its own graph-disabled variant by +31.4 points P@5 and ripgrep-BM25 + vector-only RAG by a similar margin. The graph layer plus v0.12 extract quality together carry the gap. Full BrainBench scorecards + corpus live in the sibling gbrain-evals repo.
GBrain is those patterns, generalized. 34 skills. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
New in v0.25.0 — BrainBench-Real (session capture, contributor opt-in): with GBRAIN_CONTRIBUTOR_MODE=1 set in your shell, every real query + search call through MCP, CLI, or the subagent tool-bridge gets captured (PII-scrubbed) into an eval_candidates table. Snapshot with gbrain eval export, replay against your code change with gbrain eval replay. Three numbers come back: mean Jaccard@k between captured and current retrieved slugs, top-1 stability, and latency Δ. Off by default for production users — no surprise data accumulation. Walkthrough: docs/eval-bench.md. NDJSON wire format: docs/eval-capture.md.
~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.txtfor the documentation map, orllms-full.txtfor the same map with core docs inlined in one fetch. Agents: start withAGENTS.md(orCLAUDE.mdif you're Claude Code).
Install
On an agent platform (recommended)
GBrain is designed to be installed and operated by an AI agent. If you don't have one running yet:
- OpenClaw ... Deploy AlphaClaw on Render (one click, 8GB+ RAM)
- Hermes Agent ... Deploy on Railway (one click)
Paste this into your agent:
Retrieve and follow the instructions at:
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
That's it. The agent clones the repo, installs GBrain, sets up the brain, loads 34 skills, and configures recurring jobs. You answer a few questions about API keys. ~30 minutes.
If your agent doesn't auto-read AGENTS.md, point it at that file first:
https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md is the non-Claude
agent operating protocol (install, read order, trust boundary, common tasks). For
the full doc map, use llms.txt at the same URL root.
Standalone CLI (no agent)
git clone https://github.com/garrytan/gbrain.git && cd gbrain && bun install && bun link
gbrain init # local brain, ready in 2 seconds
gbrain import ~/notes/ # index your markdown
gbrain query "what themes show up across my notes?"
Do NOT use bun install -g github:garrytan/gbrain. Bun blocks the top-level
postinstall hook on global installs, so schema migrations never run and the CLI
aborts with Aborted() the first time it opens PGLite. Use git clone + bun install && bun link as shown above. See #218.
3 results (hybrid search, 0.12s):
1. concepts/do-things-that-dont-scale (score: 0.94)
PG's argument that unscalable effort teaches you what users want.
[Source: paulgraham.com, 2013-07-01]
2. originals/founder-mode-observation (score: 0.87)
Deep involvement isn't micromanagement if it expands the team's thinking.
3. concepts/build-something-people-want (score: 0.81)
The YC motto. Connected to 12 other brain pages.
MCP server (Claude Code, Cursor, Windsurf)
GBrain exposes 30+ MCP tools via stdio:
{
"mcpServers": {
"gbrain": { "command": "gbrain", "args": ["serve"] }
}
}
Add to ~/.claude/server.json (Claude Code), Settings > MCP Servers (Cursor), or your client's MCP config.
Remote MCP with OAuth 2.1 (ChatGPT, Claude Desktop, Cowork, Perplexity)
gbrain serve --http starts a production-grade OAuth 2.1 server with an embedded admin dashboard. Zero external infrastructure. Every major AI client connects, every request is scoped, every action is logged.
# Start the HTTP server (prints admin bootstrap token on first start)
gbrain serve --http --port 3131
# Open the admin dashboard, paste the bootstrap token, register a client
open http://localhost:3131/admin
# Expose publicly (set --public-url so the OAuth issuer matches)
ngrok http 3131 --url your-brain.ngrok.app
gbrain serve --http --port 3131 --public-url https://your-brain.ngrok.app
# ChatGPT and other OAuth-aware clients can also connect:
claude mcp add gbrain -t http https://your-brain.ngrok.app/mcp -H "Authorization: Bearer TOKEN"
Register OAuth clients from the /admin dashboard — click Register client,
pick scopes, save the credentials shown once in the reveal modal. Programmatic
registration via oauthProvider.registerClientManual(...) and the
gbrain auth register-client CLI are also available.
- OAuth 2.1 via the MCP SDK — client credentials (machine-to-machine: Perplexity, Claude), authorization code + PKCE (browser-based: ChatGPT), refresh token rotation, revocation, protected resource metadata. Optional Dynamic Client Registration behind
--enable-dcr(DCR redirect_uris must behttps://or loopback per RFC 6749 §3.1.2.1). - Scoped operations — 30 operations tagged
read | write | admin.sync_brainandfile_uploadarelocalOnly, rejected over HTTP. - React admin dashboard — 7 screens baked into the binary (~65KB gzip). Live SSE activity feed, agents table, credential reveal, filterable request log, per-client config export.
- Legacy bearer tokens still work — pre-v0.26
gbrain auth createtokens continue to authenticate asread+write+admin. v0.22.7's simplersrc/mcp/http-transport.tspath stays compiled in for backward compat callers; v0.26+ deployments use the OAuth-awareserve-http.ts.
Per-client guides: docs/mcp/. Hardening defaults, env vars, and threat model: SECURITY.md.
Using gbrain with GStack
If your engineering agent runs on GStack, point it at gbrain for code lookup instead of grep+read. Cathedral II (v0.21.0) ships call-graph edges and two-pass retrieval — /investigate, /review, /plan-eng-review, and /office-hours all benefit when the agent walks the symbol graph instead of scanning files line by line.
The five magical-moment commands:
gbrain code-callers searchKeyword # who calls this symbol?
gbrain code-callees searchKeyword # what does this symbol call?
gbrain code-def BrainEngine # where is X defined?
gbrain code-refs BrainEngine # all reference sites
gbrain query "how does N+1 handling work" --near-symbol BrainEngine.searchKeyword --walk-depth 2
All five auto-emit JSON on non-TTY (gh-CLI convention) so a GStack subagent shelling out via bash gets a clean parseable response. Run gbrain sources add <repo> --strategy code to index a repo, then your agent's brain-first lookup covers code, not just markdown. (Cathedral II release notes)
The 34 Skills
GBrain ships 34 skills organized by skills/RESOLVER.md (or your OpenClaw's AGENTS.md — both filenames are supported as of v0.19). The resolver tells your agent which skill to read for any task. v0.25.1 added 9 research-flavored skills (book-mirror flagship plus 8 pairings); see the new "Research and synthesis" section below.
Skill files are code. They're the most powerful way to get knowledge work done. A skill file is a fat markdown document that encodes an entire workflow: when to fire, what to check, how to chain with other skills, what quality bar to enforce. The agent reads the skill and executes it. Skills can also call deterministic TypeScript code bundled in GBrain (search, import, embed, sync) for the parts that shouldn't be left to LLM judgment. Thin harness, fat skills: the intelligence lives in the skills, not the runtime.
Always-on
| Skill | What it does |
|---|---|
| signal-detector | Fires on every message. Spawns a cheap model in parallel to capture original thinking and entity mentions. The brain compounds on autopilot. |
| brain-ops | Brain-first lookup before any external API. The read-enrich-write loop that makes every response smarter. |
Content ingestion
| Skill | What it does |
|---|---|
| ingest | Thin router. Detects input type and delegates to the right ingestion skill. |
| idea-ingest | Links, articles, tweets become brain pages with analysis, author people pages, and cross-linking. |
| media-ingest | Video, audio, PDF, books, screenshots, GitHub repos. Transcripts, entity extraction, backlink propagation. |
| meeting-ingestion | Transcripts become brain pages. Every attendee gets enriched. Every company gets a timeline entry. |
| voice-note-ingest | Voice notes captured verbatim — exact phrasing preserved, never paraphrased. Routes to originals/concepts/people/companies/ideas/personal/voice-notes based on content. |
| article-enrichment | Raw article dumps become structured pages with executive summary, verbatim quotes, key insights, and why-it-matters. |
Research and synthesis (v0.25.1)
| Skill | What it does |
|---|---|
| book-mirror | Flagship. Hand the agent a book, get a personalized two-column chapter-by-chapter analysis. Left column preserves the chapter's actual content; right column maps every idea to your life using your words from the brain. ~$6 for a 20-chapter book at Opus. Pairs with gbrain book-mirror CLI for the trusted runtime. |
| strategic-reading | Read a book / article / case study through ONE specific problem-lens. Output: applied playbook with do / avoid / watch-for and short / medium / long-term recommendations. |
| concept-synthesis | Deduplicate thousands of concept stubs into a tiered intellectual map (T1 Canon to T4 Riff). Trace how ideas evolved across years of notes. |
| perplexity-research | Brain-augmented web research. Sends brain context to Perplexity so the search focuses on what's NEW vs already-known. Output: Executive Summary + Key New Developments + Confirming Signals + Contradictions or Updates + Recommended Brain Updates + Citations. |
| archive-crawler | Universal archivist for personal file archives (Dropbox / Backblaze / Gmail-takeout / hard-drive dumps). REFUSES to run unless archive-crawler.scan_paths: is set in gbrain.yml. Safe-by-default safety fence. |
| academic-verify | Trace a research claim through publication → methodology → raw data → independent replication. Routes through perplexity-research; produces a verdict (verified / partial / unverifiable / misattributed / retracted). |
| brain-pdf | Render any brain page to publication-quality PDF via the gstack make-pdf binary. Strips frontmatter, sanitizes emoji, applies running headers. |
Brain operations
| Skill | What it does |
|---|---|
| enrich | Tiered enrichment (Tier 1/2/3). Creates and updates person/company pages with compiled truth and timelines. |
| query | 3-layer search with synthesis and citations. Says "the brain doesn't have info on X" instead of hallucinating. |
| maintain | Periodic health: stale pages, orphans, dead links, citation audit, back-link enforcement, tag consistency. v0.23 adds the dream cycle's synthesize + patterns phases ... overnight conversation transcripts become reflections, originals, and 25-year patterns. |
| citation-fixer | Scans pages for missing or malformed citations. Fixes format to match the standard. |
| repo-architecture | Where new brain files go. Decision protocol: primary subject determines directory, not format. |
| publish | Share brain pages as password-protected HTML. Zero LLM calls. |
| data-research | Structured data research with parameterized YAML recipes. Extract investor updates, expenses, company metrics from email. |
Operational
| Skill | What it does |
|---|---|
| daily-task-manager | Task lifecycle with priority levels (P0-P3). Stored as searchable brain pages. |
| daily-task-prep | Morning prep: calendar lookahead with brain context per attendee, open threads, task review. |
| cron-scheduler | Schedule staggering (5-min offsets), quiet hours (timezone-aware with wake-up override), idempotency. |
| reports | Timestamped reports with keyword routing. "What's the latest briefing?" finds it instantly. |
| cross-modal-review | Quality gate via second model. Refusal routing: if one model refuses, silently switch. |
| webhook-transforms | External events (SMS, meetings, social mentions) converted into brain pages with entity extraction. |
| testing | Validates every skill has SKILL.md with frontmatter, manifest coverage, resolver coverage. |
| skill-creator | Create new skills following the conformance standard. MECE check against existing skills. |
| skillify | The "skillify it!" meta-skill. Orchestrates the 10-step loop so failures become durable skills: scaffold the stubs via gbrain skillify scaffold, write the real logic, gate with gbrain skillify check + gbrain check-resolvable. |
| skillpack-check | Agent-readable gbrain health report. Exit code for CI; JSON for debugging. Cron-friendly. |
| smoke-test | 8 post-restart health checks with auto-fix (Bun, CLI, DB, worker, Zod CJS, gateway, API key, brain repo). Drop-in user tests at ~/.gbrain/smoke-tests.d/*.sh. |
| minion-orchestrator | Background work in one skill. Shell jobs via gbrain jobs submit shell (operator/CLI, MCP blocks protected names) and LLM subagents via gbrain agent run. Parent-child DAGs, child_done inbox, durability across worker restarts. |
Identity and setup
| Skill | What it does |
|---|---|
| soul-audit | 6-phase interview generating SOUL.md (agent identity), USER.md (user profile), ACCESS_POLICY.md (4-tier privacy), HEARTBEAT.md (operational cadence). |
| setup | Auto-provision PGLite or Supabase. First import. GStack detection. |
| migrate | Universal migration from Obsidian, Notion, Logseq, markdown, CSV, JSON, Roam. |
| briefing | Daily briefing with meeting context, active deals, and citation tracking. |
Conventions
Cross-cutting rules in skills/conventions/:
- quality.md ... citations, back-links, notability gate, source attribution
- brain-first.md ... 5-step lookup before any external API call
- model-routing.md ... which model for which task
- test-before-bulk.md ... test 3-5 items before any batch operation
- cross-modal.yaml ... review pairs and refusal routing chain
How It Works
Signal arrives (meeting, email, tweet, link)
-> Signal detector captures ideas + entities (parallel, never blocks)
-> Brain-ops: check the brain first (gbrain search, gbrain get)
-> Respond with full context
-> Write: update brain pages with new information + citations
-> Auto-link: typed relationships extracted on every write (zero LLM calls)
-> Sync: gbrain indexes changes for next query
Every cycle adds knowledge. The agent enriches a person page after a meeting. Next time that person comes up, the agent already has context. The difference compounds daily.
The system gets smarter on its own. Entity enrichment auto-escalates: a person mentioned once gets a stub page (Tier 3). After 3 mentions across different sources, they get web + social enrichment (Tier 2). After a meeting or 8+ mentions, full pipeline (Tier 1). The brain learns who matters without being told. Deterministic classifiers improve over time via a fail-improve loop that logs every LLM fallback and generates better regex patterns from the failures. gbrain doctor shows the trajectory: "intent classifier: 87% deterministic, up from 40% in week 1."
"Prep me for my meeting with Jordan in 30 minutes" ... pulls dossier, shared history, recent activity, open threads
"What have I said about the relationship between shame and founder performance?" ... searches YOUR thinking, not the internet
Minions: your sub-agents won't drop work anymore
A durable, Postgres-native job queue built into the brain. Every long-running agent task is now a job that survives gateway restarts, streams progress, gets paused / resumed / steered mid-flight, and shows up in gbrain jobs list. Zero infra beyond your existing brain.
The production numbers that matter
Here's my personal OpenClaw deployment: one Render container. Supabase Postgres holding a 45,000-page brain. 19 cron jobs firing on schedule. Real gateway load from real daily work. The task: pull a month of my social posts from an external API and ingest them end-to-end into the brain as a structured page.
| Minions | sessions_spawn |
|
|---|---|---|
| Wall time | 753ms | >10,000ms (gateway timeout) |
| Token cost | $0.00 | ~$0.03 per run |
| Success rate | 100% | 0% (couldn't even spawn) |
| Memory/job | ~2 MB | ~80 MB |
Under that 19-cron load, sub-agent spawn couldn't clear the 10-second gateway wall. Minions landed it in under a second for zero tokens. Scaling: 19,240 posts across 36 months, single bash loop, ~15 min total, $0.00. Sub-agents: ~9 min best case, ~$1.08 in tokens, ~40% spawn failure. Lab: durability ∞ (SIGKILL mid-flight, 10/10 rescued), throughput ~10× faster, fan-out ~21× with no failure wall, memory ~400× less.
Full benchmarks live in gbrain-evals.
The routing rule
Deterministic (same input → same steps → same output) → Minions Judgment (input requires assessment or decision) → Sub-agents
Pull posts, parse JSON, write a brain page, run a sync — deterministic. $0 tokens, survives restart, millisecond runtime. Triage the inbox, assess meeting priority, decide if a cold email deserves a reply — judgment. What sub-agents are actually good at. minion_mode: pain_triggered (the default) automates the routing.
What's fixed
The six daily pains — spawn storms, agents that stop responding, forgotten dispatches, gateway crashes mid-run, runaway grandchildren, debugging soup — all belonged to the "deterministic work through a reasoning model" mistake. Minions fixes them by not making that mistake: max_children cap, timeout_ms + AbortSignal, child_done inbox, full parent_job_id/depth/transcript per job, Postgres durability with stall detection, cascade cancel via recursive CTE. Plus idempotency keys, attachment validation, removeOnComplete, and gbrain jobs smoke that proves the install in half a second.
gbrain jobs smoke # verify install
gbrain jobs submit sync --params '{}' # fire a background job
gbrain jobs stats # health dashboard
gbrain jobs supervisor --concurrency 4 # canonical: auto-restarting worker (Postgres only)
gbrain jobs work --concurrency 4 # raw worker (no crash recovery — prefer `supervisor`)
gbrain jobs supervisor keeps the worker alive across crashes with exponential backoff, atomic PID locking, structured audit events at ~/.gbrain/audit/supervisor-*.jsonl, and a start --detach / status --json / stop subcommand surface for agents. In containers it runs as PID 1; on systemd hosts it's the child of gbrain-worker.service. Full deployment guide: docs/guides/minions-deployment.md.
Read skills/minion-orchestrator/SKILL.md for parent-child DAGs, fan-in collection, steering via inbox.
Minions is not incrementally better than sub-agents for background work. It's categorically different. 753ms vs gateway timeout. $0 vs tokens. 100% vs couldn't-spawn. If your agent does deterministic work on a schedule, it runs on Minions now.
Health check and self-heal
Minions is canonical as of v0.11.1 — every gbrain upgrade runs the migration automatically (schema → smoke → prefs → host rewrites → env-aware autopilot install). If you ever want to verify manually or wire a cron into your morning briefing:
gbrain doctor # half-migrated state? prints loud banner + exits non-zero
gbrain skillpack-check --quiet # exit 0/1/2 for pipeline gating
gbrain skillpack-check | jq # full JSON: {healthy, summary, actions[], doctor, migrations}
If anything's off, actions[] tells you the exact command to run. For deeper troubleshooting: docs/guides/minions-fix.md.
Moving gateway crons to Minions (deterministic scripts, zero LLM tokens per fire): docs/guides/minions-shell-jobs.md.
Durable agents: gbrain agent (v0.15)
Your subagent runs survive crashes now. OpenClaw died mid-run? The worker re-claims on restart and replays from the last committed turn. Fan-out across 50 shards, one shard crashes — the aggregator still claims after every child reaches a terminal state and writes a mixed-outcome summary. Tool calls persist as a two-phase ledger (pending → complete | failed) so replay is safe by construction, not by hope.
# Submit a single-subagent run
gbrain agent run "summarize my last 10 journal pages"
# Fan out N prompts across N subagent children + 1 aggregator
gbrain agent run "analyze every page" \
--fanout-manifest manifests/pages.json \
--subagent-def analyzer
# Tail a running job (heartbeat per turn + full transcript on completion)
gbrain agent logs 1247 --follow --since 5m
Durability is the point: every Anthropic turn commits to subagent_messages, every tool call to subagent_tool_executions. Worker kills, OpenClaw crashes, timeouts — all resumable. Host repos (your OpenClaw, etc.) ship their own subagent definitions via GBRAIN_PLUGIN_PATH + a gbrain.plugin.json manifest: see docs/guides/plugin-authors.md. Requires ANTHROPIC_API_KEY on the worker.
Skillify: say "skillify it!" and the bug becomes structurally impossible to repeat
Your OpenClaw hit a new failure. You fix it once in conversation. You say "skillify it!" And now the fix is permanent: a SKILL.md with triggers, a deterministic script with tests, a routing fixture the agent re-evaluates daily, a filing audit that keeps the output from drifting. Ten items. Every one required. The bug can't recur.
Hermes and similar agent frameworks auto-create skills as a background behavior. Fine until you don't know what the agent shipped. Checklists decay. Tests drift. Resolver entries get stale. Six months later it's an opaque pile nobody has read, nobody has tested, and nobody is sure still works. GBrain ships the same capability except the human stays in the loop and every step is a command you can run.
The four verbs you need (v0.19)
# 1. Scaffold all 5 stub files for a new skill in one shot.
gbrain skillify scaffold webhook-verify \
--description "verify ngrok webhooks" \
--triggers "verify the webhook,check tunnel" \
--writes-pages --writes-to people/,companies/
# 2. Replace the SKILLIFY_STUB sentinels with real logic + real tests.
$EDITOR skills/webhook-verify/scripts/webhook-verify.mjs
$EDITOR test/webhook-verify.test.ts
# 3. Run the 10-item audit: SKILL.md exists, script exists, unit + E2E tests,
# LLM evals, resolver entry, trigger eval, check-resolvable gate, brain filing.
gbrain skillify check skills/webhook-verify/scripts/webhook-verify.mjs
# 4. Verify the whole tree: reachability, MECE overlap, DRY, routing gaps,
# filing audit, SKILLIFY_STUB sentinels (fails if any skill still has one).
gbrain check-resolvable # warnings advisory, errors block
gbrain check-resolvable --strict # warnings block too (CI opt-in)
Idempotent re-runs. --force regenerates stub files but NEVER duplicates a resolver row.
Scaffold completes in under 2 seconds. The real work (your rule, your script, your tests)
is what you spend time on. Everything else is boilerplate the CLI writes for you.
gbrain routing-eval — catch the routing gaps your users actually hit
Drop a routing-eval.jsonl fixture next to any skill. Each line is {intent, expected_skill, ambiguous_with?}. gbrain check-resolvable runs the structural layer by default; gbrain routing-eval runs the same structural layer as a dedicated CI verb. The --llm flag is
accepted as a placeholder for a future LLM tie-break layer; in this release it emits a stderr
notice and runs structural only. False positives (wrong skill matched), missed routes (no
skill matched), and tautological fixtures (intent copies trigger verbatim) all surface as
specific advisories with the exact file:line to fix.
Works on your OpenClaw, not just gbrain's repo
v0.19 teaches gbrain check-resolvable to accept AGENTS.md as a resolver file alongside
RESOLVER.md, at either the skills directory OR one level up (OpenClaw-native workspace-root
layout). The skill manifest auto-derives from walking skills/*/SKILL.md when manifest.json
is missing. Set OPENCLAW_WORKSPACE=~/your-openclaw/workspace and everything just works:
export OPENCLAW_WORKSPACE=~/your-openclaw/workspace
gbrain check-resolvable --verbose
# Auto-detects: AGENTS.md at workspace root, 107 skills derived from SKILL.md walk,
# 15 unreachable errors surfaced, 108 advisory warnings for overlaps and gaps.
First run on a real OpenClaw deployment found 15 unreachable skills out of 102 — about 15% of the tree was dark. The essay's "skills the agent can never reach" footgun, now visible.
gbrain skillpack install — drop 25 curated skills into your OpenClaw
The skills gbrain ships are a curated bundle. Install them into your workspace with
dependency closure (shared conventions come along), per-file diff protection (your local
edits are never clobbered without --overwrite-local), a file lock that serializes
concurrent installers, and an atomic managed-block update to your AGENTS.md so you can
see exactly what gbrain wrote.
gbrain skillpack list # 25 curated skills
gbrain skillpack install brain-ops # one skill + its shared conventions
gbrain skillpack install --all # the full bundle
gbrain skillpack install brain-ops --dry-run # preview; no writes
gbrain skillpack diff brain-ops # compare bundle vs your local copy
Re-running is safe. The managed-block markers in your AGENTS.md let skillpack install
accumulate rows across separate single-skill installs instead of overwriting each other.
A receipt comment inside the fence (<!-- gbrain:skillpack:manifest cumulative-slugs="..." -->)
tracks what gbrain has installed across runs. install --all is the only path that prunes;
per-skill install never deletes what it didn't install. If you hand-add a row inside the fence,
gbrain preserves it on reinstall and emits a stderr notice telling your agent to investigate.
Skillify is the piece that makes the skills tree survive six months of compounding work.
Read skills/skillify/SKILL.md for the full 10-item checklist
and the anti-patterns it catches.
Storage tiering: keep bulk content out of git (v0.22.11)
When your brain crosses 100K files and bulk machine-generated content (tweets, articles, transcripts) becomes the size driver, declare which directories belong in git and which live in the database only.
# gbrain.yml at the brain repo root
storage:
db_tracked:
- people/
- companies/
- deals/
db_only:
- media/x/
- media/articles/
- meetings/transcripts/
gbrain sync auto-manages your .gitignore for db_only paths. gbrain export --restore-only --repo .
repopulates missing files from the database (container restart, fresh clone, accidental rm).
gbrain storage status shows the tier breakdown.
Full guide: docs/storage-tiering.md.
Getting Data In
GBrain ships integration recipes that your agent sets up for you. Each recipe tells the agent what credentials to ask for, how to validate, and what cron to register.
| Recipe | Requires | What It Does |
|---|---|---|
| Public Tunnel | — | Fixed URL for MCP + voice (ngrok Hobby $8/mo) |
| Credential Gateway | — | Gmail + Calendar access |
| Voice-to-Brain | ngrok-tunnel | Phone calls to brain pages (Twilio + OpenAI Realtime) |
| Email-to-Brain | credential-gateway | Gmail to entity pages |
| X-to-Brain | — | Twitter timeline + mentions + deletions |
| Calendar-to-Brain | credential-gateway | Google Calendar to searchable daily pages |
| Meeting Sync | — | Circleback transcripts to brain pages with attendees |
Data research recipes extract structured data from email into tracked brain pages. Built-in recipes for investor updates (MRR, ARR, runway, headcount), expense tracking, and company metrics. Create your own with gbrain research init.
Run gbrain integrations to see status.
GBrain + GStack
GStack is the engine. GBrain is the mod.
- GStack = coding skills (ship, review, QA, investigate, office-hours, retro). 70,000+ stars, 30,000 developers per day. When your agent codes on itself, it uses GStack.
- GBrain = everything-else skills (brain ops, signal detection, ingestion, enrichment, cron, reports, identity). When your agent remembers, thinks, and operates, it uses GBrain.
hosts/gbrain.ts= the bridge. Tells GStack's coding skills to check the brain before coding.
gbrain init detects if GStack is installed and reports mod status. If GStack isn't there, it tells you how to get it.
Architecture
┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐
│ Brain Repo │ │ GBrain │ │ AI Agent │
│ (git) │ │ (retrieval) │ │ (read/write) │
│ │ │ │ │ │
│ markdown files │───>│ Postgres + │<──>│ 29 skills │
│ = source of │ │ pgvector │ │ define HOW to │
│ truth │ │ │ │ use the brain │
│ │<───│ hybrid │ │ │
│ human can │ │ search │ │ RESOLVER.md │
│ always read │ │ (vector + │ │ routes intent │
│ & edit │ │ keyword + │ │ to skill │
│ │ │ RRF) │ │ │
└──────────────────┘ └───────────────┘ └──────────────────┘
The repo is the system of record. GBrain is the retrieval layer. The agent reads and writes through both. Human always wins... edit any markdown file and gbrain sync picks up the changes.
The Knowledge Model
Every page follows the compiled truth + timeline pattern:
---
type: concept
title: Do Things That Don't Scale
tags: [startups, growth, pg-essay]
---
Paul Graham's argument that startups should do unscalable things early on.
The key insight: the unscalable effort teaches you what users actually
want, which you can't learn any other way.
---
- 2013-07-01: Published on paulgraham.com
- 2024-11-15: Referenced in batch W25 kickoff talk
Above the ---: compiled truth. Your current best understanding. Gets rewritten when new evidence changes the picture. Below: timeline. Append-only evidence trail. Never edited, only added to.
Knowledge Graph
Pages aren't just text. Every mention of a person, company, or concept becomes a typed link in a structured graph. The brain wires itself.
Write a meeting page mentioning Alice and Acme AI
-> Auto-link extracts entity refs from content (zero LLM calls)
-> Infers types: meeting page + person ref => `attended`
"CEO of X" pattern => `works_at`
"invested in" => `invested_in`
"advises", "advisor" => `advises`
"founded", "co-founded" => `founded`
-> Reconciles stale links: edits remove links no longer in content
-> Backlinks rank well-connected entities higher in search
gbrain graph-query people/alice --type attended --depth 2
# returns who Alice met with, transitively
The graph powers questions vector search can't: "who works at Acme AI?", "what has Bob invested in?", "find the connection between Alice and Carol". Backfill an existing brain in one command:
gbrain extract links --source db # wire up the existing 29K pages
gbrain extract timeline --source db # extract dated events from markdown timelines
Then ask graph questions or watch the search ranking improve. Benchmarked side-by-side against ripgrep-BM25, vector-only RAG (same embedder), and gbrain-with-graph-disabled: gbrain lands P@5 49.1%, R@5 97.9% on a 240-page Opus-generated rich-prose corpus, beating hybrid-nograph by +31.4 points P@5. Isolate the contribution: v0.11→v0.12 moved the same gbrain codebase from P@5 22.1% → 49.1% on identical inputs, so typed-link extract quality is load-bearing. Full scorecards + reproducible corpus: gbrain-evals.
Search
Hybrid search: vector + keyword + RRF fusion + multi-query expansion + 4-layer dedup.
Query
-> Intent classifier (entity? temporal? event? general?)
-> Multi-query expansion (Claude Haiku)
-> Vector search (HNSW cosine) + Keyword search (tsvector)
-> RRF fusion: score = sum(1/(60 + rank))
-> Cosine re-scoring + compiled truth boost
-> 4-layer dedup + compiled truth guarantee
-> Results
Keyword alone misses conceptual matches. Vector alone misses exact phrases. RRF gets both. Search quality is benchmarked and reproducible: gbrain eval --qrels queries.json measures P@k, Recall@k, MRR, and nDCG@k. A/B test config changes before deploying them.
Why it works: many strategies in concert
The brain isn't one trick. Every retrieval question goes through ~20 deterministic techniques layered together. No single one is magic; the win comes from stacking them so each layer covers what the others miss.
Question
│
├─ INGESTION (every put_page)
│ ├─ Recursive markdown chunking (or semantic / LLM-guided)
│ ├─ Embedding cache invalidation on edit
│ └─ Idempotent imports (content-hash dedup)
│
├─ GRAPH EXTRACTION (auto-link post-hook, zero LLM)
│ ├─ Entity-ref regex (markdown links + bare slugs)
│ ├─ Code-fence stripping (no false-positive slugs in code blocks)
│ ├─ Typed inference cascade (FOUNDED → INVESTED → ADVISES → WORKS_AT)
│ ├─ Page-role priors (partner-bio language → invested_in)
│ ├─ Within-page dedup (same target collapses to one link)
│ ├─ Stale-link reconciliation (edits remove dropped refs)
│ └─ Multi-type link constraint (same person can works_at AND advises)
│
├─ SEARCH PIPELINE (every query)
│ ├─ Intent classifier (entity / temporal / event / general — auto-routes)
│ ├─ Multi-query expansion (Haiku rephrases the question 3 ways)
│ ├─ Vector search (HNSW cosine over OpenAI embeddings)
│ ├─ Keyword search (Postgres tsvector + websearch_to_tsquery)
│ ├─ Source-aware ranking (curated dirs outrank chat/daily swamp at SQL layer)
│ ├─ Hard-exclude (test/ archive/ attachments/ .raw/ filtered before retrieval)
│ ├─ Reciprocal Rank Fusion (score = sum 1/(60+rank) across both)
│ ├─ Cosine re-scoring (re-rank chunks against actual query embedding)
│ ├─ Compiled-truth boost (assessments outrank timeline noise)
│ ├─ Backlink boost (well-connected entities rank higher)
│ └─ Source-aware dedup (one CT chunk per page guaranteed)
│
├─ GRAPH TRAVERSAL (relational queries)
│ ├─ Recursive CTE with cycle prevention (visited-array check)
│ ├─ Type-filtered edges (--type works_at, attended, etc.)
│ ├─ Direction control (in / out / both)
│ └─ Depth-capped (≤10 for remote MCP; DoS prevention)
│
└─ AGENT WORKFLOW (graph-confident hybrid)
├─ Graph-query first (high-precision typed answers)
├─ Grep fallback when graph returns nothing
└─ Graph hits ranked first in top-K (better P@K and R@K)
End-to-end on the BrainBench v1 corpus (240 rich-prose pages, before/after PR #188):
| Metric | BEFORE PR #188 | AFTER PR #188 | Δ |
|---|---|---|---|
| Precision@5 | 39.2% | 44.7% | +5.4 pts |
| Recall@5 | 83.1% | 94.6% | +11.5 pts |
| Correct in top-5 | 217 | 247 | +30 |
| Graph-only F1 (ablation) | 57.8% (grep) | 86.6% | +28.8 pts |
Plus 5 orthogonal capability checks (identity resolution, temporal queries, performance at 10K-page scale, robustness to malformed input, MCP operation contract). All pass. Full report: gbrain-evals.
The point: each technique handles a class of inputs the others miss. Vector search misses exact slug refs; keyword catches them. Keyword misses conceptual matches; vector catches them. RRF picks the best of both. Compiled-truth boost keeps assessments above timeline noise. Auto-link extraction wires the graph that lets backlink boost rank well-connected entities higher. Graph traversal answers questions search alone can't reach. The agent picks graph-first for precision and falls back to keyword for recall. All deterministic, all in concert, all measured.
Voice
Call a phone number. Your AI answers. It knows who's calling, pulls their full context from the brain, and responds like someone who actually knows your world. When the call ends, a brain page appears with the transcript, entity detection, and cross-references.
The voice recipe ships with GBrain: Voice-to-Brain. WebRTC works in a browser tab with zero setup. A real phone number is optional.
Engine Architecture
CLI / MCP Server
(thin wrappers, identical operations)
|
BrainEngine interface (pluggable)
|
+--------+--------+
| |
PGLiteEngine PostgresEngine
(default) (Supabase)
| |
~/.gbrain/ Supabase Pro ($25/mo)
brain.pglite Postgres + pgvector
embedded PG 17.5
gbrain migrate --to supabase|pglite
(bidirectional migration)
PGLite: embedded Postgres, no server, zero config. When your brain outgrows local (1000+ files, multi-device), gbrain migrate --to supabase moves everything.
File Storage
Brain repos accumulate binaries. GBrain has a three-stage migration:
gbrain files mirror <dir> # copy to cloud, local untouched
gbrain files redirect <dir> # replace local with .redirect pointers
gbrain files clean <dir> # remove pointers, cloud only
gbrain files restore <dir> # download everything back (undo)
Storage backends: S3-compatible (AWS, R2, MinIO), Supabase Storage, or local.
Commands
SETUP
gbrain init [--supabase|--url] Create brain (PGLite default)
gbrain migrate --to supabase|pglite Bidirectional engine migration
gbrain upgrade Self-update with feature discovery
PAGES
gbrain get <slug> Read a page (fuzzy slug matching)
gbrain put <slug> [< file.md] Write/update (auto-versions)
gbrain delete <slug> Delete a page
gbrain list [--type T] [--tag T] List with filters
SEARCH
gbrain search <query> Keyword search (tsvector)
gbrain query <question> Hybrid search (vector + keyword + RRF)
IMPORT
gbrain import <dir> [--no-embed] [--workers N]
Import markdown (idempotent)
gbrain sync [--repo <path>] [--workers N]
Git-to-brain incremental sync
(>100-file diffs auto-parallelize 4 workers on Postgres)
gbrain export [--dir ./out/] Export to markdown
FILES
gbrain files list|upload|sync|verify File storage operations
EMBEDDINGS
gbrain embed [<slug>|--all|--stale] Generate/refresh embeddings
LINKS + GRAPH
gbrain link|unlink|backlinks Cross-reference management
gbrain extract links|timeline|all Batch backfill from existing pages
(--source db|fs, --type, --since, --dry-run)
gbrain graph-query <slug> Typed traversal (--type T --depth N
--direction in|out|both)
JOBS (Minions)
gbrain jobs submit <name> [--params JSON] [--follow] Submit a background job
gbrain jobs list [--status S] [--queue Q] List jobs with filters
gbrain jobs get|cancel|retry|delete <id> Manage job lifecycle
gbrain jobs prune [--older-than 30d] Clean completed/dead jobs
gbrain jobs stats Job health dashboard
gbrain jobs smoke One-command health check
gbrain jobs work [--queue Q] [--concurrency N] Start worker daemon
SKILLS (v0.19)
gbrain skillify scaffold <name> Create 5 stub files + idempotent resolver row
gbrain skillify check [path] 10-item audit of a skill
gbrain skillpack list Print the 25 curated skills in the bundle
gbrain skillpack install <name> Copy one skill + its shared conventions into target
gbrain skillpack install --all Install the full curated bundle
gbrain skillpack diff <name> Per-file diff: bundle vs target workspace
gbrain check-resolvable [--strict] Resolver audit (reachability, MECE, DRY, routing, filing,
SKILLIFY_STUB). Accepts RESOLVER.md OR AGENTS.md.
gbrain routing-eval [--llm] [--json] Intent→skill routing accuracy on fixtures
ADMIN
gbrain doctor [--json] [--fast] Health checks (resolver, skills, DB, embeddings)
gbrain doctor --fix [--dry-run] Auto-fix DRY violations (delegate inlined rules to conventions)
gbrain doctor --locks List idle-in-tx backends (57014 diagnostic, Postgres only)
gbrain stats Brain statistics
gbrain serve MCP server (stdio)
gbrain serve --http [--port 3131] HTTP MCP server with OAuth 2.1 + admin dashboard
[--token-ttl 3600] [--enable-dcr]
[--public-url URL]
gbrain auth create|list|revoke|test Legacy bearer token management
gbrain auth register-client <name> Register an OAuth 2.1 client
--grant-types client_credentials,authorization_code
--scopes "read write admin"
gbrain auth revoke-client <client_id> Revoke an OAuth 2.1 client (cascade purges
active tokens + auth codes via FK CASCADE)
# OAuth 2.1 clients can also be registered from the /admin dashboard or
# programmatically via oauthProvider.registerClientManual() for host-repo wrappers.
gbrain integrations Integration recipe dashboard
gbrain sources list|add|remove|... Multi-source brain management (v0.18)
gbrain dream [--dry-run] [--phase N] 8-phase maintenance cycle (lint→backlinks→sync→synthesize
→extract→patterns→embed→orphans). v0.23 added synthesize +
patterns: transcripts → reflections + cross-session themes.
gbrain dream --input <file> Ad-hoc transcript synthesis (implies --phase synthesize)
gbrain dream --date YYYY-MM-DD Synthesize a single day; --from/--to for backfill ranges
gbrain check-backlinks check|fix Back-link enforcement
gbrain lint [--fix] LLM artifact detection
gbrain repair-jsonb [--dry-run] Repair v0.12.0 double-encoded JSONB (Postgres)
gbrain orphans [--json] [--count] Find pages with zero inbound wikilinks
gbrain transcribe <audio> Transcribe audio (Groq Whisper)
gbrain research init <name> Scaffold a data-research recipe
gbrain research list Show available recipes
Run gbrain --help for the full reference.
Origin Story
I was setting up my OpenClaw agent and started a markdown brain repo. One page per person, one page per company, compiled truth on top, timeline on the bottom. Within a week: 10,000+ files, 3,000+ people, 13 years of calendar data, 280+ meeting transcripts, 300+ captured ideas.
The agent runs while I sleep. The dream cycle scans every conversation, enriches missing entities, fixes broken citations, consolidates memory. I wake up and the brain is smarter than when I went to sleep.
The skills in this repo are those patterns, generalized. What took 11 days to build by hand ships as a mod you install in 30 minutes.
Docs
For agents:
- skills/RESOLVER.md ... Start here. The skill dispatcher.
- Individual skill files ... 28 standalone instruction sets (25 ship in the curated
gbrain skillpack installbundle) - GBRAIN_SKILLPACK.md ... Legacy reference architecture
- Getting Data In ... Integration recipes and data flow
- GBRAIN_VERIFY.md ... Installation verification
For humans:
- GBRAIN_RECOMMENDED_SCHEMA.md ... Brain repo directory structure
- Thin Harness, Fat Skills ... Architecture philosophy
- ENGINES.md ... Pluggable engine interface
Reference:
- GBRAIN_V0.md ... Full product spec
- CHANGELOG.md ... Version history
Benchmarks:
- gbrain-evals ... BrainBench, the sibling repo that holds the eval harness, corpus, scorecards, and 4-adapter comparisons. Depends on gbrain; not installed alongside gbrain.
Contributing
See CONTRIBUTING.md. Run bun test for unit tests. For the full local CI gate (gitleaks + unit + all 29 E2E files in Docker, the same checks GH Actions runs), use bun run ci:local ... or bun run ci:local:diff for the diff-aware subset during fast iteration.
If you're working on retrieval or any of the search/embedding/ranking surface, set GBRAIN_CONTRIBUTOR_MODE=1 in your shell rc and use gbrain eval replay to gate your changes against a snapshot of real captured queries — the dev loop is documented in docs/eval-bench.md. Capture is off by default for production users (no surprise data accumulation); the env var is the contributor opt-in.
PRs welcome for: new enrichment APIs, performance optimizations, additional engine backends, new skills following the conformance standard in skills/skill-creator/SKILL.md.
License
MIT
