mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-16 18:02:30 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ece117449 |
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/sh
|
||||
# .agents/gbrain-launcher — MCP-server launcher for the gbrain Codex and
|
||||
# Claude Code plugins. Unix-only (macOS/Linux): needs /bin/sh, executable
|
||||
# bits, and `command -v`. Windows support is a filed follow-up.
|
||||
#
|
||||
# Resolves the gbrain binary (the plugin snapshot cannot ship it — the CLI
|
||||
# installs separately), then execs it with the argv the plugin manifest
|
||||
# pinned. Resolution order:
|
||||
# 1. $GBRAIN_BIN explicit override (must be executable)
|
||||
# 2. `gbrain` on PATH
|
||||
# 3. ~/.bun/bin/gbrain the sanctioned global-install location
|
||||
#
|
||||
# GBRAIN_SURFACE: when set and argv[0] is `serve`, replaces the value of an
|
||||
# existing `--surface <x>` pair, or appends `--surface $GBRAIN_SURFACE` if
|
||||
# the pair is absent — so a user can widen (full) or narrow (verbs) this
|
||||
# machine's plugin surface without editing the plugin snapshot.
|
||||
#
|
||||
# No auto-install by design: an MCP server start must never run a network
|
||||
# install. On a miss this exits 127 with the recovery path on stderr; the
|
||||
# bundled `setup` skill walks the install interactively.
|
||||
|
||||
set -eu
|
||||
|
||||
resolve_bin() {
|
||||
if [ -n "${GBRAIN_BIN:-}" ]; then
|
||||
if [ ! -x "$GBRAIN_BIN" ]; then
|
||||
echo "gbrain-launcher: GBRAIN_BIN='$GBRAIN_BIN' is not an executable file" >&2
|
||||
exit 127
|
||||
fi
|
||||
printf '%s' "$GBRAIN_BIN"
|
||||
return
|
||||
fi
|
||||
# ~/.bun/bin (the sanctioned global-install location) is preferred OVER a
|
||||
# bare PATH lookup: a hostile repo that prepends node_modules/.bin with a
|
||||
# fake `gbrain` must not win over the real install. GBRAIN_BIN (above) is
|
||||
# the explicit escape hatch for a gbrain living elsewhere.
|
||||
if [ -x "${HOME:-}/.bun/bin/gbrain" ]; then
|
||||
printf '%s' "$HOME/.bun/bin/gbrain"
|
||||
return
|
||||
fi
|
||||
if command -v gbrain >/dev/null 2>&1; then
|
||||
command -v gbrain
|
||||
return
|
||||
fi
|
||||
echo "gbrain-launcher: gbrain binary not found." >&2
|
||||
echo " install: bun install -g github:garrytan/gbrain#latest-stable" >&2
|
||||
echo " (the npm package named 'gbrain' is unrelated - do not npm install it)" >&2
|
||||
echo " then run the bundled 'setup' skill to initialize your brain," >&2
|
||||
echo " or set GBRAIN_BIN to an absolute gbrain binary path." >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
BIN="$(resolve_bin)"
|
||||
echo "gbrain-launcher: using $BIN" >&2
|
||||
|
||||
# Surface override — only for `serve` invocations. Rebuilds the positional
|
||||
# params in place (rotate-through-sentinel idiom; no eval, no word-splitting
|
||||
# hazards): replace the value of an existing `--surface <x>` pair, or append
|
||||
# the pair when absent.
|
||||
if [ -n "${GBRAIN_SURFACE:-}" ] && [ "${1:-}" = "serve" ]; then
|
||||
replaced=0
|
||||
expect_value=0
|
||||
set -- "$@" "__gbrain_end__"
|
||||
while [ "$1" != "__gbrain_end__" ]; do
|
||||
a="$1"
|
||||
shift
|
||||
if [ "$expect_value" = 1 ]; then
|
||||
expect_value=0
|
||||
replaced=1
|
||||
set -- "$@" "$GBRAIN_SURFACE"
|
||||
continue
|
||||
fi
|
||||
if [ "$a" = "--surface" ]; then
|
||||
expect_value=1
|
||||
fi
|
||||
set -- "$@" "$a"
|
||||
done
|
||||
shift
|
||||
if [ "$replaced" = 0 ]; then
|
||||
set -- "$@" "--surface" "$GBRAIN_SURFACE"
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$BIN" "$@"
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"interface": { "displayName": "GBrain" },
|
||||
"plugins": [
|
||||
{
|
||||
"name": "gbrain",
|
||||
"source": { "source": "local", "path": "./" },
|
||||
"policy": { "installation": "AVAILABLE", "authentication": "ON_USE" },
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"description": "GBrain — a persistent knowledge brain for your coding agent: hybrid search, synthesis, and cross-session memory.",
|
||||
"owner": { "name": "Garry Tan" },
|
||||
"plugins": [
|
||||
{
|
||||
"name": "gbrain",
|
||||
"source": "./",
|
||||
"description": "Personal knowledge brain for your coding agent — hybrid search, synthesis, and durable cross-session memory, plus a curated brain-first skill set.",
|
||||
"category": "productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"version": "0.46.11.0",
|
||||
"description": "Personal knowledge brain for your coding agent — hybrid search, synthesis, graph traversal, and durable cross-session memory over Postgres/PGLite with pgvector, plus a curated brain-first skill set.",
|
||||
"author": {
|
||||
"name": "Garry Tan",
|
||||
"url": "https://github.com/garrytan"
|
||||
},
|
||||
"homepage": "https://github.com/garrytan/gbrain",
|
||||
"repository": "https://github.com/garrytan/gbrain",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"memory",
|
||||
"knowledge-base",
|
||||
"mcp",
|
||||
"search",
|
||||
"agent",
|
||||
"brain",
|
||||
"pgvector"
|
||||
],
|
||||
"skills": "./plugin/skills/",
|
||||
"mcpServers": {
|
||||
"gbrain": {
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/.agents/gbrain-launcher",
|
||||
"args": [
|
||||
"serve",
|
||||
"--surface",
|
||||
"starter",
|
||||
"--source-guard"
|
||||
],
|
||||
"cwd": "${CLAUDE_PLUGIN_ROOT}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"gbrain": {
|
||||
"command": "./.agents/gbrain-launcher",
|
||||
"args": [
|
||||
"serve",
|
||||
"--surface",
|
||||
"starter",
|
||||
"--source-guard"
|
||||
],
|
||||
"cwd": ".",
|
||||
"env_vars": [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_BASE_URL",
|
||||
"AZURE_OPENAI_API_KEY",
|
||||
"DASHSCOPE_API_KEY",
|
||||
"DATABASE_URL",
|
||||
"DEEPGRAM_API_KEY",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"GBRAIN_BIN",
|
||||
"GBRAIN_BRAIN_ID",
|
||||
"GBRAIN_CHAT_FALLBACK_CHAIN",
|
||||
"GBRAIN_CHAT_MODEL",
|
||||
"GBRAIN_DATABASE_URL",
|
||||
"GBRAIN_EMBEDDING_DIMENSIONS",
|
||||
"GBRAIN_EMBEDDING_IMAGE_OCR",
|
||||
"GBRAIN_EMBEDDING_IMAGE_OCR_MODEL",
|
||||
"GBRAIN_EMBEDDING_MODEL",
|
||||
"GBRAIN_EMBEDDING_MULTIMODAL",
|
||||
"GBRAIN_EMBEDDING_MULTIMODAL_MODEL",
|
||||
"GBRAIN_EXPANSION_MODEL",
|
||||
"GBRAIN_HOME",
|
||||
"GBRAIN_MAX_MARKUP_RATIO",
|
||||
"GBRAIN_MCP_FORCE_SURFACE",
|
||||
"GBRAIN_NO_JUNK_PATTERNS",
|
||||
"GBRAIN_NO_SANITY",
|
||||
"GBRAIN_PAGE_BLOCK_BYTES",
|
||||
"GBRAIN_PAGE_WARN_BYTES",
|
||||
"GBRAIN_REMOTE_CLIENT_SECRET",
|
||||
"GBRAIN_RETRIEVAL_REFLEX",
|
||||
"GBRAIN_RETRIEVAL_REFLEX_WINDOW_TURNS",
|
||||
"GBRAIN_SOURCE",
|
||||
"GBRAIN_SURFACE",
|
||||
"GEMINI_API_KEY",
|
||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
"GROQ_API_KEY",
|
||||
"HOME",
|
||||
"LITELLM_API_KEY",
|
||||
"LITELLM_BASE_URL",
|
||||
"LLAMA_SERVER_API_KEY",
|
||||
"LLAMA_SERVER_BASE_URL",
|
||||
"LLAMA_SERVER_RERANKER_API_KEY",
|
||||
"LLAMA_SERVER_RERANKER_BASE_URL",
|
||||
"LMSTUDIO_BASE_URL",
|
||||
"MINIMAX_API_KEY",
|
||||
"MISTRAL_API_KEY",
|
||||
"MOONSHOT_API_KEY",
|
||||
"NVIDIA_API_KEY",
|
||||
"OLLAMA_API_KEY",
|
||||
"OLLAMA_BASE_URL",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_BASE_URL",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENROUTER_BASE_URL",
|
||||
"PATH",
|
||||
"PERPLEXITY_API_KEY",
|
||||
"PPLX_API_KEY",
|
||||
"TOGETHER_API_KEY",
|
||||
"VOYAGE_API_KEY",
|
||||
"ZEROENTROPY_API_KEY",
|
||||
"ZHIPUAI_API_KEY"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "gbrain",
|
||||
"version": "0.46.11.0",
|
||||
"description": "Personal knowledge brain for your coding agent — hybrid search, synthesis, graph traversal, and durable cross-session memory over Postgres/PGLite with pgvector, plus a curated brain-first skill set.",
|
||||
"author": {
|
||||
"name": "Garry Tan",
|
||||
"url": "https://github.com/garrytan"
|
||||
},
|
||||
"homepage": "https://github.com/garrytan/gbrain",
|
||||
"repository": "https://github.com/garrytan/gbrain",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"memory",
|
||||
"knowledge-base",
|
||||
"mcp",
|
||||
"search",
|
||||
"agent",
|
||||
"brain",
|
||||
"pgvector"
|
||||
],
|
||||
"skills": "./plugin/skills/",
|
||||
"mcpServers": "./.codex-plugin/mcp.json",
|
||||
"interface": {
|
||||
"displayName": "GBrain",
|
||||
"shortDescription": "Give your agent a persistent brain: search, synthesis, memory",
|
||||
"longDescription": "GBrain wires a personal knowledge brain into every session: hybrid keyword+vector search, entity graph traversal, synthesis, and memory your agent writes itself — served on the starter MCP surface (the seven memory verbs plus the daily-driver brain ops). Bundles the curated brain-first skill set: setup (walks install + gbrain init), cold-start day-one brain filling, ingest, query, briefing, upgrade, and more. Requires the gbrain CLI (bun install -g github:garrytan/gbrain#latest-stable) and a brain (gbrain init); the bundled setup skill walks the rest. Unix (macOS/Linux) only.",
|
||||
"developerName": "Garry Tan",
|
||||
"category": "Productivity",
|
||||
"capabilities": [
|
||||
"Interactive",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://github.com/garrytan/gbrain",
|
||||
"defaultPrompt": [
|
||||
"Search my brain, recall context across sessions, and write new memory as we work"
|
||||
],
|
||||
"brandColor": "#1F6F5C"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
# GBrain E2E Test Configuration
|
||||
# Copy to .env.testing and fill in real values
|
||||
#
|
||||
# Tier 1 (required for E2E tests)
|
||||
# Option A: Local Docker Postgres (default)
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/gbrain_test
|
||||
# Option B: Real Supabase instance (tests the actual production path)
|
||||
# DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
|
||||
|
||||
# Tier 2 (required for skill tests, optional for mechanical tests)
|
||||
OPENAI_API_KEY=sk-...
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Something isn't working
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**What happened?**
|
||||
|
||||
|
||||
**What did you expect?**
|
||||
|
||||
|
||||
**Steps to reproduce**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Environment**
|
||||
- gbrain version: (`gbrain version`)
|
||||
- OS:
|
||||
- Bun version: (`bun --version`)
|
||||
- Database: Supabase / self-hosted Postgres
|
||||
|
||||
**`gbrain doctor --json` output**
|
||||
```json
|
||||
(paste output here)
|
||||
```
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an improvement
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
**What problem does this solve?**
|
||||
|
||||
|
||||
**What does the solution look like?**
|
||||
|
||||
|
||||
**Alternatives considered**
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<!--
|
||||
Tier 5.5 Externally-Authored Query Submission template
|
||||
See eval/CONTRIBUTING.md for the full workflow.
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
Submitting **N** Tier 5.5 queries for BrainBench.
|
||||
|
||||
- Author handle: `@your-handle`
|
||||
- File location: `eval/external-authors/your-handle/queries.json`
|
||||
- Queries authored fresh (not copy-pasted from a model output)
|
||||
- Slugs verified against `eval/data/world-v1/` (via `bun run eval:world:view`)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `bun run eval:query:validate eval/external-authors/your-handle/queries.json` passes
|
||||
- [ ] At least 20 queries
|
||||
- [ ] Each query has either `gold.relevant` (with real slugs) or `gold.expected_abstention: true`
|
||||
- [ ] Temporal queries have `as_of_date` set (`corpus-end` | `per-source` | ISO-8601)
|
||||
- [ ] Phrasing is varied (not all the same template)
|
||||
- [ ] `author` field matches my handle
|
||||
|
||||
## Phrasing variety (optional self-audit)
|
||||
|
||||
Tick the styles represented in your batch:
|
||||
|
||||
- [ ] Full sentence questions
|
||||
- [ ] Fragment-style ("crypto founder Goldman Sachs background")
|
||||
- [ ] Comparison ("X vs Y")
|
||||
- [ ] Follow-up ("And who else...")
|
||||
- [ ] Imperative ("Pull up Alice Davis")
|
||||
- [ ] Trait-based ("the demanding engineering leader")
|
||||
- [ ] Abstention bait (answer is "not in corpus")
|
||||
|
||||
## Notes to reviewer
|
||||
|
||||
Anything worth flagging — ambiguous cases, corpus gaps you found, specific
|
||||
phrasings you were uncertain about.
|
||||
@@ -1,100 +0,0 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Nightly at 6am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tier1:
|
||||
name: Tier 1 (Mechanical)
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
- name: Run Tier 1 E2E tests
|
||||
run: bun test test/e2e/mechanical.test.ts test/e2e/mcp.test.ts
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
|
||||
|
||||
tier2:
|
||||
name: Tier 2 (LLM Skills)
|
||||
runs-on: ubuntu-latest
|
||||
# Runs on every push/PR now (promoted from schedule-only in v0.19.0).
|
||||
# Tier 1 must pass first; Tier 2 uses OPENAI_API_KEY + ANTHROPIC_API_KEY
|
||||
# from repo/org secrets. Nightly + manual triggers still supported via
|
||||
# the workflow-level `on:` list.
|
||||
needs: tier1
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
- name: Install OpenClaw
|
||||
run: npm install -g openclaw@2026.4.9
|
||||
- name: Configure OpenClaw MCP
|
||||
run: |
|
||||
mkdir -p ~/.openclaw
|
||||
cat > ~/.openclaw/config.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"gbrain": {
|
||||
"command": "bun",
|
||||
"args": ["run", "src/cli.ts", "serve"],
|
||||
"env": {
|
||||
"DATABASE_URL": "${{ env.DATABASE_URL }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
- name: Run Tier 2 skill tests
|
||||
run: bun test test/e2e/skills.test.ts test/e2e/zeroentropy-live.test.ts
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# v0.33.3.0: ZE live API tests skip gracefully when this is unset,
|
||||
# so forks without the secret stay green. The test exercises the
|
||||
# zeroEntropyCompatFetch response-rewriter + URL rewrite + flexible
|
||||
# dim handling + gateway.rerank against the real provider.
|
||||
ZEROENTROPY_API_KEY: ${{ secrets.ZEROENTROPY_API_KEY }}
|
||||
@@ -1,89 +0,0 @@
|
||||
name: Heavy Tests
|
||||
|
||||
# Heavy ops-shape tests under tests/heavy/. Cost minutes per run; NOT part
|
||||
# of default PR CI. Two triggers:
|
||||
# - Nightly schedule (catches regressions within 24h of merge to master).
|
||||
# - On-demand opt-in via PR label `heavy-tests` (slow loop kept off by default).
|
||||
# - Manual workflow_dispatch for triage.
|
||||
#
|
||||
# See CLAUDE.md "tests/heavy/*.sh" entry and tests/heavy/README.md.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 8 * * *' # 08:17 UTC daily — staggered to avoid noisy slots
|
||||
pull_request:
|
||||
# `synchronize` + `reopened` fire on subsequent pushes / reopens — without
|
||||
# them, a PR labeled `heavy-tests` would NEVER re-run heavy on later
|
||||
# commits. The job-level `if:` below filters to PRs that still carry the
|
||||
# label so we don't fan out on unrelated label changes.
|
||||
types: [labeled, synchronize, reopened]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# When a PR gets the heavy-tests label, cancel any in-flight heavy-tests run on
|
||||
# the same ref so we only ever measure the latest commit.
|
||||
concurrency:
|
||||
group: heavy-tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
heavy:
|
||||
name: Heavy tests
|
||||
# On pull_request: only run when the PR currently carries the `heavy-tests`
|
||||
# label. Works for all three trigger types (labeled, synchronize, reopened)
|
||||
# because `contains(labels.*.name, ...)` reads the live label set, not the
|
||||
# event payload's `label.name` (which is only populated for `labeled`).
|
||||
if: |
|
||||
github.event_name != 'pull_request' ||
|
||||
contains(github.event.pull_request.labels.*.name, 'heavy-tests')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
|
||||
- name: Run heavy tests
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
|
||||
run: bun run test:heavy
|
||||
|
||||
# The heavy runner writes per-script logs to ~/.gbrain/audit/ on every
|
||||
# run. Upload those + the rss workload JSON on failure for triage
|
||||
# without re-running locally.
|
||||
#
|
||||
# actions/upload-artifact runs as a node action — `~` is NOT expanded by
|
||||
# the shell here. Stage logs into the workspace first, then upload from
|
||||
# the stable workspace-relative path.
|
||||
- name: Stage heavy-test logs into workspace
|
||||
if: always()
|
||||
run: |
|
||||
mkdir -p heavy-artifacts
|
||||
cp -r "$HOME/.gbrain/audit"/heavy-* heavy-artifacts/ 2>/dev/null || true
|
||||
cp tests/heavy/rss-baseline.json heavy-artifacts/ 2>/dev/null || true
|
||||
- name: Upload heavy-test artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: heavy-tests-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: heavy-artifacts/
|
||||
retention-days: 14
|
||||
if-no-files-found: ignore
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
target: bun-darwin-arm64
|
||||
artifact: gbrain-darwin-arm64
|
||||
- os: ubuntu-latest
|
||||
target: bun-linux-x64
|
||||
artifact: gbrain-linux-x64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- run: bun install
|
||||
- run: bun test
|
||||
- run: bun build --compile --target=${{ matrix.target }} --outfile bin/${{ matrix.artifact }} src/cli.ts
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: bin/${{ matrix.artifact }}
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
|
||||
with:
|
||||
files: |
|
||||
artifacts/gbrain-darwin-arm64/gbrain-darwin-arm64
|
||||
artifacts/gbrain-linux-x64/gbrain-linux-x64
|
||||
generate_release_notes: true
|
||||
@@ -1,263 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# cache-check: runs first, computes the content hash of every tracked
|
||||
# file EXCEPT the deny-list (CHANGELOG.md, README.md, docs/**/*.md, etc.
|
||||
# — see scripts/ci-cache-hash.sh for the full list). Looks up
|
||||
# `ci-pass-<hash>` in actions/cache; if hit, the test matrix + verify
|
||||
# + serial jobs all skip and test-status reports green immediately.
|
||||
# If miss, the full suite runs and cache-write seals it on success.
|
||||
#
|
||||
# Hit rate covers re-pushes (same SHA twice), branch rebases that
|
||||
# don't touch tracked code, and any branch update that touches only
|
||||
# the deny-listed doc files.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
cache-check:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
hit: ${{ steps.lookup.outputs.cache-hit }}
|
||||
hash: ${{ steps.compute.outputs.hash }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Compute content hash
|
||||
id: compute
|
||||
run: |
|
||||
# --verbose writes the "X/Y files in hash" diagnostic to stderr;
|
||||
# stdout carries the 16-char hash. Capture both.
|
||||
HASH=$(bash scripts/ci-cache-hash.sh --verbose 2>/tmp/cache-diag)
|
||||
cat /tmp/cache-diag
|
||||
echo "Computed cache hash: $HASH"
|
||||
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
|
||||
- name: Lookup actions/cache for ci-pass-<hash>
|
||||
id: lookup
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
key: ci-pass-${{ steps.compute.outputs.hash }}
|
||||
path: .ci-cache-marker
|
||||
# `lookup-only: true` means we only probe whether the cache
|
||||
# entry exists — we don't download it (the marker contents
|
||||
# don't matter, only the key match does). `cache-hit` returns
|
||||
# true only on EXACT key match (per actions/cache docs); a
|
||||
# restore-keys prefix fallback would set cache-hit=false, so
|
||||
# it's deliberately omitted here. Cross-branch scoping works
|
||||
# naturally: PR branches can read default-branch (master)
|
||||
# cache entries via exact key match when the content hash
|
||||
# matches, which happens whenever the tree is doc-only
|
||||
# different from a green master run.
|
||||
lookup-only: true
|
||||
- name: Cache status
|
||||
run: |
|
||||
if [ "${{ steps.lookup.outputs.cache-hit }}" = "true" ]; then
|
||||
echo "✓ cache HIT for hash ${{ steps.compute.outputs.hash }} — test jobs will skip"
|
||||
else
|
||||
echo "✗ cache MISS for hash ${{ steps.compute.outputs.hash }} — full suite will run"
|
||||
fi
|
||||
|
||||
gitleaks:
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
verify:
|
||||
# Pre-test gates: privacy/jsonb/source-id/etc + typecheck + admin-build.
|
||||
# Lives in its own runner so the matrix shards aren't carrying ~2-3min
|
||||
# of verify work in addition to their test files (the old shape stuffed
|
||||
# this into `test (1)` via `if: matrix.shard == 1`, which made shard 1
|
||||
# the slowest matrix worker). scripts/run-verify-parallel.sh fans out
|
||||
# the 20 checks via & + wait (~5s vs ~15-25s sequential).
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
- run: bun install
|
||||
- run: bun run verify
|
||||
|
||||
serial-tests:
|
||||
# *.serial.test.ts at --max-concurrency=1. Lives in its own runner so
|
||||
# the matrix shards aren't carrying the serial-pass tail (the old shape
|
||||
# stuffed this into `test (1)` after the matrix work, which compounded
|
||||
# shard 1's overload).
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
- run: bun install
|
||||
- run: bun run test:serial
|
||||
|
||||
slow-eval-longmemeval:
|
||||
# Dedicated runner for the LongMemEval end-to-end test file. The file
|
||||
# was originally 359s. TODO #1 (engine-sharing in runEvalLongMemEval
|
||||
# via RunOpts.engine) cut it to ~200s by amortizing PGLite cold-create
|
||||
# across all 13 runEvalLongMemEval calls in one beforeAll-shared brain.
|
||||
# Pulled out of the matrix (see scripts/test-shard.sh) so a single 200s
|
||||
# atom doesn't dominate a shard's wallclock. Companion file
|
||||
# test/eval-longmemeval.slow.test.ts (the pure-bucket half) stays in
|
||||
# the matrix because it's light (~42s).
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
- run: bun install
|
||||
- run: bun test test/eval-longmemeval-e2e.slow.test.ts --timeout=60000
|
||||
|
||||
slow-entity-resolve-perf:
|
||||
# Dedicated runner for the entity-resolve perf test (~159s, single perf
|
||||
# describe with one test that builds 5000+ pages and asserts the NEW
|
||||
# tryPrefixExpansion shape is 5x faster than the OLD shape — not
|
||||
# subdivisible without weakening the perf guarantee). Pulled out of the
|
||||
# matrix (see scripts/test-shard.sh) so a single 159s atom doesn't
|
||||
# dominate a shard's wallclock. Runs in parallel with the matrix.
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
- run: bun install
|
||||
- run: bun test test/entity-resolve-perf.slow.test.ts --timeout=300000
|
||||
|
||||
test:
|
||||
# Pure matrix shard — no verify, no serial. Each shard runs its slice
|
||||
# of the unit test set under one `bun test` invocation.
|
||||
#
|
||||
# 10 shards (was 6) drops per-shard total from 532s → 287s. With the two
|
||||
# dedicated jobs (slow-eval-longmemeval, slow-entity-resolve-perf) also
|
||||
# pulled out, the matrix is bounded by ~287s ≈ 4.8 min. Total CI ≈ max
|
||||
# of matrix + slow-eval (~3.3 min after engine-sharing in TODO #1) +
|
||||
# slow-entity-resolve-perf (~2.6 min) ≈ 4.8 min.
|
||||
#
|
||||
# Concurrency budget: 10 shards + verify + serial + slow-eval +
|
||||
# slow-entity-resolve-perf + gitleaks + cache-check + cache-write +
|
||||
# test-status = ~18 jobs × 2 concurrent PRs = 36. GitHub free-tier
|
||||
# caps at ~20 concurrent jobs, so multi-PR days will see some queue
|
||||
# pressure. Single-PR runs are unaffected.
|
||||
#
|
||||
# Partition policy is weight-aware LPT bin-packing via scripts/sharding.ts
|
||||
# (replaces FNV-1a path hash). Weights live in scripts/test-weights.json,
|
||||
# mined from real CI logs via scripts/mine-shard-weights.ts. Missing
|
||||
# weights fall back to corpus median — new test files work immediately.
|
||||
needs: cache-check
|
||||
if: needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
- run: bun install
|
||||
- name: Run test shard ${{ matrix.shard }}/10
|
||||
run: scripts/test-shard.sh ${{ matrix.shard }} 10
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# cache-write: ONLY runs when every gated job succeeded. Writes the
|
||||
# cache entry under `ci-pass-<hash>` so future runs at the same hash
|
||||
# hit cache. Codex's load-bearing correctness point: writing the
|
||||
# cache before the matrix completes would permanently bless bad states
|
||||
# (a future run at the same hash would skip tests because of a cache
|
||||
# entry written when tests hadn't actually passed).
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
cache-write:
|
||||
needs: [cache-check, gitleaks, verify, serial-tests, slow-eval-longmemeval, slow-entity-resolve-perf, test]
|
||||
if: success() && needs.cache-check.outputs.hit != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create cache marker
|
||||
run: |
|
||||
mkdir -p .ci-cache-marker
|
||||
echo "${{ needs.cache-check.outputs.hash }}" > .ci-cache-marker/hash
|
||||
echo "$GITHUB_SHA" > .ci-cache-marker/sha
|
||||
echo "$GITHUB_REF" > .ci-cache-marker/ref
|
||||
- uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
key: ci-pass-${{ needs.cache-check.outputs.hash }}
|
||||
path: .ci-cache-marker
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# test-status: the single user-visible "did CI pass?" check.
|
||||
# Runs always (if: always()), succeeds when EITHER cache-check.hit==true
|
||||
# OR all gated jobs (gitleaks, verify, serial-tests, test) succeeded.
|
||||
# Branch protection (when configured) gates on this single job name.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
test-status:
|
||||
needs: [cache-check, gitleaks, verify, serial-tests, slow-eval-longmemeval, slow-entity-resolve-perf, test]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Aggregate result
|
||||
run: |
|
||||
HIT="${{ needs.cache-check.outputs.hit }}"
|
||||
GITLEAKS="${{ needs.gitleaks.result }}"
|
||||
VERIFY="${{ needs.verify.result }}"
|
||||
SERIAL="${{ needs.serial-tests.result }}"
|
||||
SLOW_EVAL="${{ needs.slow-eval-longmemeval.result }}"
|
||||
SLOW_PERF="${{ needs.slow-entity-resolve-perf.result }}"
|
||||
TEST="${{ needs.test.result }}"
|
||||
echo "cache-check.hit=$HIT"
|
||||
echo "gitleaks=$GITLEAKS verify=$VERIFY serial-tests=$SERIAL slow-eval-longmemeval=$SLOW_EVAL slow-entity-resolve-perf=$SLOW_PERF test=$TEST"
|
||||
if [ "$HIT" = "true" ]; then
|
||||
echo "✓ cache HIT for hash ${{ needs.cache-check.outputs.hash }} — CI green"
|
||||
exit 0
|
||||
fi
|
||||
# Cache miss: every gated job must have succeeded.
|
||||
for r in "$GITLEAKS" "$VERIFY" "$SERIAL" "$SLOW_EVAL" "$SLOW_PERF" "$TEST"; do
|
||||
if [ "$r" != "success" ]; then
|
||||
echo "✗ gated job did not succeed (got $r) — CI fail"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "✓ all gated jobs succeeded — CI green"
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
node_modules/
|
||||
bin/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.*.example
|
||||
# Bun --compile temp artifacts. Each build emits a new hash-named .bun-build
|
||||
# file in cwd; glob catches all of them.
|
||||
*.bun-build
|
||||
.gstack/
|
||||
supabase/.temp/
|
||||
.claude/skills/
|
||||
# admin/dist/ is the React SPA bundle. CLAUDE.md says it's committed for
|
||||
# self-contained binaries (the bun --compile path embeds it via
|
||||
# `import path from 'admin/dist/index.html' with { type: 'file' }`).
|
||||
# Build via: cd admin && bun install && bun run build.
|
||||
admin/node_modules/
|
||||
.idea
|
||||
eval/reports/
|
||||
eval/data/world-v1/world.html
|
||||
|
||||
# BrainBench amara-life-v1 Opus cache (regenerate via eval:generate-amara-life)
|
||||
eval/data/amara-life-v1/_cache/
|
||||
|
||||
# claw-test E2E build cache (shim + scratch outputs)
|
||||
test/.cache/
|
||||
|
||||
.claude/
|
||||
export/
|
||||
|
||||
# Conductor workspace-local agent artifacts: plans, todos, run-unit-parallel
|
||||
# failure logs and per-shard test output. v0.26.4 (run-unit-parallel.sh)
|
||||
# writes .context/test-failures.log + .context/test-summary.txt +
|
||||
# .context/test-shards/. Workspace-local by design — never committed.
|
||||
.context/
|
||||
|
||||
# Tier 3 PGLite snapshot fixture (built on demand by build:pglite-snapshot)
|
||||
test/fixtures/pglite-snapshot.tar
|
||||
test/fixtures/pglite-snapshot.version
|
||||
|
||||
# Private brain reports — never check these in (per CLAUDE.md privacy rule)
|
||||
reports/network-intelligence/
|
||||
@@ -1,11 +0,0 @@
|
||||
title = "GBrain gitleaks config"
|
||||
|
||||
[allowlist]
|
||||
paths = [
|
||||
'''.env\.testing\.example''',
|
||||
'''.env\.example''',
|
||||
'''test/''',
|
||||
'''skills/''',
|
||||
'''.claude/skills/''',
|
||||
'''GBRAIN_SKILLPACK\.md''',
|
||||
]
|
||||
@@ -1,122 +0,0 @@
|
||||
# Agents working on GBrain
|
||||
|
||||
This is your install + operating protocol. Claude Code reads `./CLAUDE.md` automatically.
|
||||
Everyone else (Codex, Cursor, OpenClaw, Aider, Continue, or an LLM fetching via URL):
|
||||
start here.
|
||||
|
||||
## Install (5 min)
|
||||
|
||||
1. Install gbrain via Bun (the canonical path):
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
bun install -g github:garrytan/gbrain
|
||||
```
|
||||
If `bun install -g` aborts or `gbrain doctor` reports `schema_version: 0`,
|
||||
the CLI prints a recovery hint pointing at [#218](https://github.com/garrytan/gbrain/issues/218).
|
||||
Run `gbrain apply-migrations --yes` to recover, or fall back to the
|
||||
deterministic install: `git clone https://github.com/garrytan/gbrain.git ~/gbrain && cd ~/gbrain && bun install && bun link`.
|
||||
2. Init the brain: `gbrain init` (defaults to PGLite, zero-config). For 1000+ files or
|
||||
multi-machine sync, init suggests Postgres + pgvector via Supabase.
|
||||
3. **STOP — ask the user about search mode.** `gbrain init` auto-applied a
|
||||
default but printed a 9-cell cost matrix (mode × downstream model)
|
||||
preceded by `[AGENT]` markers. You MUST relay the matrix to the operator
|
||||
and confirm their choice before continuing. Cost spread between corners
|
||||
is 25x — silent acceptance is the wrong default. See
|
||||
[`./INSTALL_FOR_AGENTS.md`](./INSTALL_FOR_AGENTS.md) Step 3.5 for the
|
||||
exact ask-the-user protocol. Same banner fires on `gbrain post-upgrade`
|
||||
for existing users (search modes were added in v0.32.3).
|
||||
4. Read [`./INSTALL_FOR_AGENTS.md`](./INSTALL_FOR_AGENTS.md) for the full 9-step flow
|
||||
(API keys, identity, cron, verification).
|
||||
|
||||
## Read this order
|
||||
|
||||
1. `./AGENTS.md` (this file) — install + operating protocol.
|
||||
2. [`./CLAUDE.md`](./CLAUDE.md) — architecture reference, key files, trust boundaries,
|
||||
test layout.
|
||||
3. [`./docs/architecture/brains-and-sources.md`](./docs/architecture/brains-and-sources.md)
|
||||
— the two-axis mental model (brain = which DB, source = which repo in the DB). Every
|
||||
query routes on both axes. Read before writing anything that touches brain ops.
|
||||
4. [`./skills/conventions/brain-routing.md`](./skills/conventions/brain-routing.md) —
|
||||
agent-facing decision table: when to switch brain, when to switch source, how
|
||||
cross-brain federation works (latent-space only; the agent decides).
|
||||
5. [`./skills/RESOLVER.md`](./skills/RESOLVER.md) — skill dispatcher. Read before any task.
|
||||
|
||||
## Trust boundary (critical)
|
||||
|
||||
GBrain distinguishes **trusted local CLI callers** (`OperationContext.remote = false`,
|
||||
set by `src/cli.ts`) from **untrusted agent-facing callers** (`remote = true`, set by
|
||||
`src/mcp/server.ts`). Security-sensitive operations like `file_upload` tighten filesystem
|
||||
confinement when `remote = true` and default to strict behavior when unset. If you are
|
||||
writing or reviewing an operation, consult `src/core/operations.ts` for the contract.
|
||||
|
||||
## Common tasks
|
||||
|
||||
- **Configure:** [`docs/ENGINES.md`](./docs/ENGINES.md),
|
||||
[`docs/guides/live-sync.md`](./docs/guides/live-sync.md),
|
||||
[`docs/mcp/DEPLOY.md`](./docs/mcp/DEPLOY.md).
|
||||
- **Debug:** [`docs/GBRAIN_VERIFY.md`](./docs/GBRAIN_VERIFY.md),
|
||||
[`docs/guides/minions-fix.md`](./docs/guides/minions-fix.md), `gbrain doctor --fix`.
|
||||
- **Migrate / upgrade:** `gbrain upgrade` (binary self-update + schema migrations + post-upgrade prompts),
|
||||
[`docs/UPGRADING_DOWNSTREAM_AGENTS.md`](./docs/UPGRADING_DOWNSTREAM_AGENTS.md),
|
||||
[`skills/migrations/`](./skills/migrations/), `gbrain apply-migrations --yes` (manual schema-only).
|
||||
- **Eval retrieval changes:** capture is off by default. To benchmark a
|
||||
retrieval change against real captured queries, set
|
||||
`GBRAIN_CONTRIBUTOR_MODE=1`, then `gbrain eval export --since 7d > base.ndjson`
|
||||
and `gbrain eval replay --against base.ndjson`. For public benchmark
|
||||
coverage (LongMemEval, ground-truth scoring), `gbrain eval longmemeval
|
||||
<dataset.jsonl>` (v0.28.8) runs against an isolated in-memory PGLite
|
||||
per question — your `~/.gbrain` is never opened. Full guide:
|
||||
[`docs/eval-bench.md`](./docs/eval-bench.md).
|
||||
- **Drive the brain to a target health score (v0.36.4.0):** the one-command
|
||||
loop. `gbrain doctor --remediation-plan --json` previews what would be
|
||||
fixed; `gbrain doctor --remediate --yes --target-score 90 --max-usd 5`
|
||||
walks a dependency-ordered plan (sync before extract, embed after
|
||||
consolidate), re-checking score between every step, refusing to spend
|
||||
past the cost cap. Empty brains (no entity pages) or unconfigured embedding
|
||||
keys hit a `max_reachable_score` ceiling and bail with what's missing.
|
||||
Three phase handlers (synthesize / patterns / consolidate) are
|
||||
PROTECTED — only trusted local callers can submit them; MCP cannot.
|
||||
Reference: [`docs/architecture/topologies.md`](./docs/architecture/topologies.md)
|
||||
and the CHANGELOG entry for v0.36.4.0.
|
||||
- **Track a founder/company over time (v0.35.7):** when an entity has
|
||||
typed metric claims in its `## Facts` fence (`metric: mrr`, `value: 50000`,
|
||||
`unit: USD`, `period: monthly` columns), run
|
||||
`gbrain eval trajectory <entity-slug>` for the chronological history
|
||||
with regressions auto-flagged, or `gbrain founder scorecard <entity-slug>`
|
||||
for a four-signal JSON rollup (claim_accuracy / consistency /
|
||||
growth_trajectory / red_flags). MCP op `find_trajectory` exposes the
|
||||
same data — read scope, visibility-filtered for remote callers. **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,
|
||||
unit tests with `DATABASE_URL` unset, then all 29 E2E files sequentially against a
|
||||
fresh pgvector container) and tears down. Use `bun run ci:local:diff` for the
|
||||
diff-aware subset during fast iteration on a focused branch. Requires Docker
|
||||
(Docker Desktop / OrbStack / Colima) and `gitleaks` (`brew install gitleaks`).
|
||||
|
||||
Manual path: `bun test` plus the E2E lifecycle described in `./CLAUDE.md` (spin
|
||||
up the test Postgres container, run `bun run test:e2e`, tear it down).
|
||||
|
||||
Ship via the `/ship` skill, not by hand.
|
||||
|
||||
## Privacy
|
||||
|
||||
Never commit real names of people, companies, or funds into public artifacts. See the
|
||||
Privacy rule in `./CLAUDE.md`. GBrain pages reference real contacts; public docs must
|
||||
use generic placeholders (`alice-example`, `acme-example`, `fund-a`).
|
||||
|
||||
## Forks
|
||||
|
||||
If you are a fork, regenerate `llms.txt` + `llms-full.txt` with your own URL base before
|
||||
publishing: `LLMS_REPO_BASE=https://raw.githubusercontent.com/your-org/your-fork/main bun run build:llms`.
|
||||
-18939
File diff suppressed because it is too large
Load Diff
-292
@@ -1,292 +0,0 @@
|
||||
# Contributing to GBrain
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/garrytan/gbrain.git
|
||||
cd gbrain
|
||||
bun install
|
||||
bun test
|
||||
```
|
||||
|
||||
Requires Bun 1.0+.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
src/
|
||||
cli.ts CLI entry point
|
||||
commands/ CLI-only commands (init, upgrade, import, export, etc.)
|
||||
core/
|
||||
operations.ts Contract-first operation definitions (the foundation)
|
||||
engine.ts BrainEngine interface
|
||||
postgres-engine.ts Postgres implementation
|
||||
db.ts Connection management + schema loader
|
||||
import-file.ts Import pipeline (chunk + embed + tags)
|
||||
types.ts TypeScript types
|
||||
markdown.ts Frontmatter parsing
|
||||
config.ts Config file management
|
||||
storage.ts Pluggable storage interface
|
||||
storage/ Storage backends (S3, Supabase, local)
|
||||
supabase-admin.ts Supabase admin API
|
||||
file-resolver.ts MIME detection + content hashing
|
||||
migrate.ts Migration helpers
|
||||
yaml-lite.ts Lightweight YAML parser
|
||||
chunkers/ 3-tier chunking (recursive, semantic, llm)
|
||||
search/ Hybrid search (vector, keyword, hybrid, expansion, dedup)
|
||||
embedding.ts OpenAI embedding service
|
||||
mcp/
|
||||
server.ts MCP stdio server (generated from operations)
|
||||
schema.sql Postgres DDL
|
||||
skills/ Fat markdown skills for AI agents
|
||||
test/ Unit tests (bun test, no DB required)
|
||||
test/e2e/ E2E tests (requires DATABASE_URL, real Postgres+pgvector)
|
||||
fixtures/ Miniature realistic brain corpus (16 files)
|
||||
helpers.ts DB lifecycle, fixture import, timing
|
||||
mechanical.test.ts All operations against real DB
|
||||
mcp.test.ts MCP tool generation verification
|
||||
skills.test.ts Tier 2 skill tests (requires OpenClaw + API keys)
|
||||
docs/ Architecture docs
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
# Inner edit loop (~85s on a Mac dev box, 3700+ unit tests)
|
||||
bun run test # parallel 8-shard fan-out + serial post-pass
|
||||
bun test test/markdown.test.ts # specific unit test
|
||||
|
||||
# Pre-push gate (matches what CI runs on shard 1 + typecheck)
|
||||
bun run verify # privacy + jsonb + progress + test-isolation + wasm + admin-build + resolver + typecheck
|
||||
|
||||
# Pre-merge sanity (everything CI runs)
|
||||
bun run test:full # verify + parallel unit + slow + smart e2e
|
||||
|
||||
# Slow / serial / e2e in isolation
|
||||
bun run test:slow # *.slow.test.ts only (cold-path correctness)
|
||||
bun run test:serial # *.serial.test.ts only (--max-concurrency=1)
|
||||
bun run test:e2e # real-Postgres E2E (requires DATABASE_URL)
|
||||
|
||||
# E2E setup (Postgres with pgvector)
|
||||
docker compose -f docker-compose.test.yml up -d
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5434/gbrain_test bun run test:e2e
|
||||
|
||||
# Or use your own Postgres / Supabase
|
||||
DATABASE_URL=postgresql://... bun run test:e2e
|
||||
```
|
||||
|
||||
Use `bun run verify` before pushing. The guard chain catches: banned fork-name
|
||||
leaks (`scripts/check-privacy.sh`), `JSON.stringify(x)::jsonb` interpolation
|
||||
patterns (`scripts/check-jsonb-pattern.sh`), `\r` progress bleed to stdout
|
||||
(`scripts/check-progress-to-stdout.sh`), test-isolation rule violations
|
||||
(`scripts/check-test-isolation.sh` — see "Writing tests that survive the parallel
|
||||
loop" below), silent fallback to recursive chunking in the compiled binary
|
||||
(`scripts/check-wasm-embedded.sh`), stale admin-dashboard build artifacts
|
||||
(`scripts/check-admin-build.sh`), and resolver drift on bundled skills
|
||||
(`bun run check:resolver` — strict-mode `check-resolvable` that exit-1s on any
|
||||
warning, added in v0.41.14.0 to catch SKILL.md frontmatter ↔ RESOLVER.md drift
|
||||
before merge). `bun run check:all` runs the full historical sweep including the
|
||||
trailing-newline and exports-count checks.
|
||||
|
||||
### Writing tests that survive the parallel loop
|
||||
|
||||
`bun run test` shards 92+ unit-test files across 8 worker processes. Files in the
|
||||
same shard share a process, so process-global state leaks between them. Four
|
||||
lint rules (`scripts/check-test-isolation.sh`, R1-R4) enforce isolation:
|
||||
|
||||
| Rule | What it bans | Fix |
|
||||
|---|---|---|
|
||||
| **R1** | Direct `process.env.X = ...` mutation | Use `withEnv()` from `test/helpers/with-env.ts`, or rename to `*.serial.test.ts` |
|
||||
| **R2** | `mock.module(...)` anywhere in the file | Rename to `*.serial.test.ts` |
|
||||
| **R3** | `new PGLiteEngine(` outside ~50 lines after `beforeAll(` | Use the canonical PGLite block (see below) |
|
||||
| **R4** | `new PGLiteEngine(` without paired `afterAll(disconnect)` | Add the `afterAll(() => engine.disconnect())` |
|
||||
|
||||
Canonical PGLite block (R3 + R4 compliant — paste this verbatim):
|
||||
|
||||
```ts
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
|
||||
beforeAll(async () => {
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
});
|
||||
afterAll(async () => { await engine.disconnect(); });
|
||||
beforeEach(async () => { await resetPgliteState(engine); });
|
||||
```
|
||||
|
||||
Env-touching tests:
|
||||
|
||||
```ts
|
||||
import { withEnv } from './helpers/with-env.ts';
|
||||
|
||||
test('reads OPENAI_API_KEY', async () => {
|
||||
await withEnv({ OPENAI_API_KEY: 'sk-test' }, async () => {
|
||||
expect(loadConfig().openai_key).toBe('sk-test');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
`withEnv` saves and restores keys via try/finally including when the callback
|
||||
throws. Cross-test safe; **NOT** intra-file concurrent-safe (`process.env` is
|
||||
process-global). Files using `withEnv` stay outside the future
|
||||
`test.concurrent()` codemod's eligibility filter.
|
||||
|
||||
When to quarantine instead of fix: rename to `*.serial.test.ts` if the file
|
||||
uses `mock.module(...)`, is genuinely env-coupled (module-load env readers +
|
||||
ESM caching defeat dynamic-import-after-env tricks), or intentionally shares
|
||||
state across `it()` boundaries. Quarantine count cap: 10 (informational).
|
||||
|
||||
Files that violated these rules at the v0.26.7 baseline are listed in
|
||||
`scripts/check-test-isolation.allowlist`. **The allow-list MUST shrink over
|
||||
time** ... never add new entries. v0.26.8 (env sweep) and v0.26.9 (PGLite sweep
|
||||
+ codemod) remove entries as files get fixed.
|
||||
|
||||
### Local CI gate (recommended before pushing, v0.23.1+)
|
||||
|
||||
```bash
|
||||
bun run ci:local # full gate: gitleaks + unit + ALL 29 E2E files (sequential)
|
||||
bun run ci:local:diff # gate with diff-aware E2E selector
|
||||
bun run ci:select-e2e # print which E2E files the selector would run
|
||||
```
|
||||
|
||||
`ci:local` spins up `pgvector/pgvector:pg16` + `oven/bun:1` via
|
||||
`docker-compose.ci.yml`, runs everything PR CI runs plus the full E2E suite, then
|
||||
tears down. Named volumes keep the install warm across runs (~16-20 min sequential
|
||||
E2E after the first cold pull). Requires Docker (Docker Desktop, OrbStack, or
|
||||
Colima) and `gitleaks` on host (`brew install gitleaks`). Override the postgres
|
||||
host port with `GBRAIN_CI_PG_PORT=5435 bun run ci:local` if 5434 collides.
|
||||
|
||||
Fail-closed selector: an unmapped `src/` change runs all 29 E2E files. Hand-tune
|
||||
narrower mappings via `scripts/e2e-test-map.ts`.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
bun build --compile --outfile bin/gbrain src/cli.ts
|
||||
```
|
||||
|
||||
## Adding a new operation
|
||||
|
||||
GBrain uses a contract-first architecture. Add your operation to one file and it
|
||||
automatically appears in the CLI, MCP server, and tools-json:
|
||||
|
||||
1. Add your operation to `src/core/operations.ts` (define params, handler, cliHints)
|
||||
2. Add tests
|
||||
3. That's it. The CLI, MCP server, and tools-json are generated from operations.
|
||||
|
||||
For CLI-only commands (init, upgrade, import, export, files, embed, doctor, sync):
|
||||
1. Create `src/commands/mycommand.ts`
|
||||
2. Add the case to `src/cli.ts`
|
||||
|
||||
Parity tests (`test/parity.test.ts`) verify CLI/MCP/tools-json stay in sync.
|
||||
|
||||
## Adding a new engine
|
||||
|
||||
See `docs/ENGINES.md` for the full guide. In short:
|
||||
|
||||
1. Create `src/core/myengine-engine.ts` implementing `BrainEngine`
|
||||
2. Add to engine factory in `src/core/engine.ts`
|
||||
3. Run the test suite against your engine
|
||||
4. Document in `docs/`
|
||||
|
||||
The original SQLite engine plan was superseded by PGLite (embedded Postgres 17 via WASM), which uses the same SQL dialect as Postgres and eliminates the need for a separate FTS5/sqlite-vss translation layer. See [`docs/ENGINES.md`](docs/ENGINES.md) for the engine architecture and the rationale.
|
||||
|
||||
## CONTRIBUTOR_MODE — turn on the dev loop
|
||||
|
||||
gbrain captures retrieval traffic so you can replay real queries against
|
||||
your code changes before merging. **This is off by default** (production
|
||||
users get a quiet brain, no surprise data accumulation). Contributors turn
|
||||
it on with one shell rc line:
|
||||
|
||||
```bash
|
||||
# In ~/.zshrc or ~/.bashrc:
|
||||
export GBRAIN_CONTRIBUTOR_MODE=1
|
||||
```
|
||||
|
||||
That's it. Every `query` / `search` you (or agents pointed at your dev
|
||||
brain) run from that shell now writes a row to `eval_candidates`, and the
|
||||
[replay tool](#running-real-world-eval-benchmarks-touching-retrieval-code)
|
||||
has data to work against.
|
||||
|
||||
What CONTRIBUTOR_MODE actually does:
|
||||
|
||||
- Turns on `query`/`search` capture into the local `eval_candidates` table.
|
||||
Without it the gate is closed and capture is a no-op.
|
||||
- That's all. PII scrubbing, retention, and replay are independent.
|
||||
|
||||
Resolution order (most explicit wins):
|
||||
|
||||
1. `eval.capture: true` in `~/.gbrain/config.json` → on
|
||||
2. `eval.capture: false` in `~/.gbrain/config.json` → off
|
||||
3. `GBRAIN_CONTRIBUTOR_MODE=1` → on
|
||||
4. otherwise → off
|
||||
|
||||
Quick check that capture is actually running:
|
||||
|
||||
```bash
|
||||
gbrain query "anything" >/dev/null
|
||||
psql $DATABASE_URL -c 'SELECT count(*) FROM eval_candidates'
|
||||
# (or `gbrain doctor` — surfaces silent capture failures cross-process)
|
||||
```
|
||||
|
||||
To disable capture even with the env var set, write
|
||||
`{"eval": {"capture": false}}` to `~/.gbrain/config.json` — explicit config
|
||||
beats the env var both directions.
|
||||
|
||||
## Running real-world eval benchmarks (touching retrieval code)
|
||||
|
||||
If your PR touches retrieval — search ranking, RRF fusion, embeddings,
|
||||
intent classification, query expansion, source boost, or the `query` /
|
||||
`search` op handlers — run `gbrain eval replay` against a snapshot of
|
||||
real traffic before merging. Requires `CONTRIBUTOR_MODE` (above) so you
|
||||
have captured rows to replay against.
|
||||
|
||||
Quick loop:
|
||||
|
||||
```bash
|
||||
gbrain eval export --since 7d > baseline.ndjson # snapshot before your change
|
||||
# ... make your change ...
|
||||
gbrain eval replay --against baseline.ndjson # diff retrieval, get Jaccard@k
|
||||
```
|
||||
|
||||
Three numbers come back: mean Jaccard@k between captured and current slug
|
||||
sets, top-1 stability, and mean latency Δ. The replay tool flags the worst
|
||||
regressions so you can eyeball whether the change is hurting real queries.
|
||||
|
||||
Trigger paths (rerun if your diff touches any of these):
|
||||
|
||||
- `src/core/search/hybrid.ts`
|
||||
- `src/core/search/source-boost.ts`, `sql-ranking.ts`
|
||||
- `src/core/search/intent.ts`, `expansion.ts`, `dedup.ts`
|
||||
- `src/core/embedding.ts`
|
||||
- `src/core/operations.ts` (query / search handlers)
|
||||
- `src/core/postgres-engine.ts` / `pglite-engine.ts` (searchKeyword /
|
||||
searchVector SQL)
|
||||
|
||||
See [`docs/eval-bench.md`](./docs/eval-bench.md) for the full guide
|
||||
including CI integration, hand-crafted NDJSON corpora (so a fresh checkout
|
||||
without captured data can still replay), and cost considerations. The
|
||||
NDJSON wire format is documented in
|
||||
[`docs/eval-capture.md`](./docs/eval-capture.md).
|
||||
|
||||
For public benchmark coverage on top of replay, `gbrain eval longmemeval
|
||||
<dataset.jsonl>` (v0.28.1) runs LongMemEval against gbrain's hybrid
|
||||
retrieval. One in-memory PGLite per question, runtime-enumerated
|
||||
`TRUNCATE` between questions, ground-truth scoring via LongMemEval's
|
||||
published `evaluate_qa.py`. Use it alongside replay when changes affect
|
||||
retrieval quality on long-context conversational data — replay catches
|
||||
regressions on YOUR queries, LongMemEval catches them on a public set the
|
||||
benchmark community already cites. See the "Public benchmarks: LongMemEval"
|
||||
section in [`docs/eval-bench.md`](./docs/eval-bench.md).
|
||||
|
||||
## Welcome PRs
|
||||
|
||||
- SQLite engine implementation
|
||||
- Docker Compose for self-hosted Postgres
|
||||
- Additional migration sources
|
||||
- New enrichment API integrations
|
||||
- Performance optimizations
|
||||
@@ -1,148 +0,0 @@
|
||||
# DESIGN.md
|
||||
|
||||
The design system source of truth for gbrain. Born from the de facto tokens
|
||||
that landed in `admin/src/index.css` during the v0.26.0 admin SPA work and
|
||||
formalized during the v0.36.1.0 Hindsight calibration wave's design review.
|
||||
|
||||
This doc is the calibration target for `/plan-design-review` and `/design-review`.
|
||||
When a question is "does this UI fit the system?", the answer is here.
|
||||
|
||||
## Voice
|
||||
|
||||
GBrain talks like a smart friend who knows your past, not a clinical scoring
|
||||
system. Every user-facing string passes through this filter:
|
||||
|
||||
- Second person, contractions allowed.
|
||||
- Grounded in concrete data the user can verify ("2 of 3 missed" beats
|
||||
"Brier 0.31").
|
||||
- Never preachy. Never "we recommend." Never "according to your data."
|
||||
- Short. Under 25 words for narrative; under one line for status.
|
||||
- Numbers grounded in real outcomes, never abstract metrics without
|
||||
translation.
|
||||
|
||||
Five surfaces use this voice (v0.36.1.0+):
|
||||
`pattern_statement`, `nudge`, `forecast_blurb`, `dashboard_caption`,
|
||||
`morning_pulse`. All five pass through `gateVoice()` in
|
||||
`src/core/calibration/voice-gate.ts` with mode-specific rubrics. A Haiku
|
||||
judge rejects academic-sounding candidates; up to 2 regens; then fall
|
||||
back to a hand-written template from `src/core/calibration/templates.ts`.
|
||||
|
||||
## Color tokens
|
||||
|
||||
CSS variables in `admin/src/index.css`. SVG renderer inlines literals
|
||||
matching these tokens (`src/core/calibration/svg-renderer.ts`).
|
||||
|
||||
| Token | Value | Use |
|
||||
|--------------------|-----------|-------------------------------------------|
|
||||
| `--bg-primary` | `#0a0a0f` | Page background |
|
||||
| `--bg-secondary` | `#14141f` | Sidebar, cards |
|
||||
| `--bg-tertiary` | `#1e1e2e` | Subtle surfaces, borders |
|
||||
| `--text-primary` | `#e0e0e0` | Body text |
|
||||
| `--text-secondary` | `#888` | Headings, labels |
|
||||
| `--text-muted` | `#777` | Tertiary text — TD2 bumped from #555 for WCAG AA contrast (~5.5:1) |
|
||||
| `--accent` | `#3b82f6` | Active states, links, primary CTAs |
|
||||
| `--success` | `#22c55e` | Healthy / ok status |
|
||||
| `--warning` | `#f59e0b` | Doctor warnings |
|
||||
| `--error` | `#ef4444` | Failures, destructive confirmations |
|
||||
|
||||
Dark theme is the only theme. No light mode toggle planned — admin is an
|
||||
operator tool, not a marketing surface. Users live in the terminal with a
|
||||
dark theme already.
|
||||
|
||||
WCAG contrast:
|
||||
- Body text (#e0e0e0 on #0a0a0f) → ~14:1, AAA
|
||||
- Muted text (#777 on #0a0a0f) → ~5.5:1, AA (was 4.0 / fail before TD2)
|
||||
- Accent links (#3b82f6 on #0a0a0f) → ~5.7:1, AA
|
||||
|
||||
## Typography
|
||||
|
||||
| Variable | Value | Use |
|
||||
|--------------------|-----------------------------|---------------------------------|
|
||||
| `--font-sans` | `Inter, system-ui, sans-serif` | UI text, headings, body |
|
||||
| `--font-mono` | `JetBrains Mono, monospace` | Numbers, slugs, code, terminal-ish data |
|
||||
|
||||
Type scale (de facto, not formalized yet):
|
||||
- 18px: sidebar logo / page title
|
||||
- 14px: body
|
||||
- 13px: nav items
|
||||
- 12px: chart captions, secondary labels
|
||||
- 11px: tertiary labels in dense charts
|
||||
|
||||
Numbers in tables and metrics use JetBrains Mono so column alignment is
|
||||
mechanical. Avoid mixing Inter and JetBrains Mono in the same line.
|
||||
|
||||
## Spacing scale
|
||||
|
||||
4 / 8 / 16 / 24 / 32px. Linear-app-style density: 24-32px between major
|
||||
sections, 16px between row groups, 8px within a row. The Calibration tab
|
||||
(approved variant-B mockup) is the canonical example.
|
||||
|
||||
## Layout
|
||||
|
||||
- Sidebar 200px on the left. Active item gets a 3px left-border in `--accent`.
|
||||
- Main content area uses the remaining width.
|
||||
- Max content width: 720px for text-heavy pages (Calibration), 960px for
|
||||
data tables (Request Log).
|
||||
- No 3-column feature grids. No icons in colored circles. No decorative blobs.
|
||||
- Cards earn their existence — heading + content works without a card frame
|
||||
in most cases.
|
||||
|
||||
## Charts
|
||||
|
||||
Server-rendered SVG via `src/core/calibration/svg-renderer.ts`. Pure
|
||||
functions: data → SVG string. No DOM, no React component, no chart library.
|
||||
|
||||
XSS posture: server-side `escapeXml()` on every caller-controlled string.
|
||||
Numeric inputs `.toFixed()`-coerced. Admin SPA renders via
|
||||
`<TrustedSVG>` wrapper with `dangerouslySetInnerHTML`. Endpoint gated by
|
||||
`requireAdmin` middleware.
|
||||
|
||||
Why server-rendered SVG (per D23):
|
||||
- Chart logic stays close to the data math.
|
||||
- Zero new client-side chart-library dep.
|
||||
- SVG is accessible (text labels), scalable, copy-paste-friendly to PR
|
||||
descriptions and docs.
|
||||
- Sets the precedent for future admin charts (contradictions trend, takes
|
||||
scorecard, etc.).
|
||||
|
||||
Four chart renderers in v0.36.1.0:
|
||||
- `renderBrierTrend({ series })` — sparkline + baseline reference at 0.25
|
||||
- `renderDomainBars({ bars })` — horizontal accuracy bars
|
||||
- `renderAbandonedThreadsCard(threads)` — text rows + "revisit now" links
|
||||
- `renderPatternStatementsCard(statements)` — clickable drill-down anchors
|
||||
|
||||
## Interaction patterns
|
||||
|
||||
- Keyboard navigation is REQUIRED for all CLI interaction surfaces. The
|
||||
propose-queue review uses J/K/space/u/q shortcuts (gmail-style).
|
||||
- Loading states: "Loading...". Don't show spinners on sub-200ms operations.
|
||||
- Empty states ARE features: warmth + primary action + context. Cold-brain
|
||||
Calibration page tells the user EXACTLY how to build a profile, not
|
||||
"no data available."
|
||||
- Error states: name what failed + name the next step. Never "an error
|
||||
occurred — please try again."
|
||||
|
||||
## What's NOT here yet (v0.37+ roadmap)
|
||||
|
||||
- Type scale formalization (current values are de facto, not enforced)
|
||||
- Animation tokens (admin SPA has zero animations on purpose; v0.37 may
|
||||
add subtle progress / loading transitions)
|
||||
- Print stylesheet
|
||||
- Light mode (NOT planned — see "Dark theme is the only theme" above)
|
||||
- Component library extraction (the React components live inline in admin/src/pages/;
|
||||
no `<Button>` / `<Card>` abstraction layer yet)
|
||||
|
||||
## How to use this document
|
||||
|
||||
When adding a new UI surface to gbrain:
|
||||
|
||||
1. Pick existing tokens before introducing new ones. New tokens go through
|
||||
`/plan-design-review`.
|
||||
2. Match the voice rules. Run candidates through `gateVoice()` before
|
||||
shipping any user-facing string in the calibration surfaces.
|
||||
3. Match the spacing scale and density. Linear-calm-clarity over
|
||||
dashboard-card-mosaic.
|
||||
4. Match the typography: Inter for UI, JetBrains Mono for numbers.
|
||||
|
||||
When updating this document: it's a living target, not a frozen spec.
|
||||
Major changes go through `/plan-design-review` to keep the system coherent.
|
||||
@@ -1,331 +0,0 @@
|
||||
# GBrain Installation Guide for AI Agents
|
||||
|
||||
Read this entire file, then follow the steps. Ask the user for API keys when needed.
|
||||
Target: ~30 minutes to a fully working brain.
|
||||
|
||||
## Step 0: If you are not Claude Code
|
||||
|
||||
Read `AGENTS.md` at the repo root first. It's the non-Claude-agent operating
|
||||
protocol (install, read order, trust boundary, common tasks). Claude Code reads
|
||||
`CLAUDE.md` automatically and can skip ahead.
|
||||
|
||||
If you fetched this file by URL without cloning yet, the companion files live at:
|
||||
- `https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md` — start here
|
||||
- `https://raw.githubusercontent.com/garrytan/gbrain/master/llms.txt` — full doc map
|
||||
- `https://raw.githubusercontent.com/garrytan/gbrain/master/llms-full.txt` — same map, inlined
|
||||
|
||||
## Step 1: Install GBrain
|
||||
|
||||
Default path (Bun is required — gbrain is a Bun + TypeScript runtime):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
bun install -g github:garrytan/gbrain
|
||||
```
|
||||
|
||||
Verify: `gbrain --version` should print a version number. If `gbrain` is not found,
|
||||
restart the shell or add the PATH export to the shell profile.
|
||||
|
||||
> **If `bun install -g` aborts or `gbrain doctor` reports `schema_version: 0`** (Bun
|
||||
> occasionally blocks the top-level postinstall hook on global installs, so schema
|
||||
> migrations don't run automatically), the CLI prints a recovery hint pointing at
|
||||
> [#218](https://github.com/garrytan/gbrain/issues/218). Run `gbrain apply-migrations --yes`
|
||||
> to recover. If that doesn't work, fall back to the deterministic install path:
|
||||
>
|
||||
> ```bash
|
||||
> git clone https://github.com/garrytan/gbrain.git ~/gbrain && cd ~/gbrain
|
||||
> bun install && bun link
|
||||
> ```
|
||||
|
||||
## Step 2: API Keys
|
||||
|
||||
Ask the user for these. gbrain defaults to the ZeroEntropy embedding + reranker stack
|
||||
(as of v0.36.2.0); OpenAI/Voyage are still supported as fallbacks via `gbrain config
|
||||
set embedding_model <provider:model>`.
|
||||
|
||||
```bash
|
||||
export ZEROENTROPY_API_KEY=ze-... # default embedding + reranker (v0.36.2.0+)
|
||||
export OPENAI_API_KEY=sk-... # fallback for vector search; also used for chat models
|
||||
export ANTHROPIC_API_KEY=sk-ant-... # optional, improves search quality via query expansion
|
||||
```
|
||||
|
||||
Save to shell profile or `.env`. Keys are picked up by `gbrain config set` automatically
|
||||
or can be stored in `~/.gbrain/config.json` (file plane). Without any embedding provider,
|
||||
keyword search still works. Without Anthropic, search works but skips query expansion.
|
||||
|
||||
## Step 3: Create the Brain
|
||||
|
||||
```bash
|
||||
gbrain init # PGLite, no server needed
|
||||
gbrain doctor --json # verify all checks pass
|
||||
```
|
||||
|
||||
The user's markdown files (notes, docs, brain repo) are SEPARATE from this tool repo.
|
||||
Ask the user where their files are, or create a new brain repo:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/brain && cd ~/brain && git init
|
||||
```
|
||||
|
||||
Read `~/gbrain/docs/GBRAIN_RECOMMENDED_SCHEMA.md` and set up the MECE directory
|
||||
structure (people/, companies/, concepts/, etc.) inside the user's brain repo,
|
||||
NOT inside ~/gbrain.
|
||||
|
||||
## Step 3.5: Confirm search mode with the user (DO NOT SKIP)
|
||||
|
||||
`gbrain init` auto-applied a default search mode (`tokenmax` unless your subagent
|
||||
tier is Haiku-class or no OpenAI key is configured). The init output included the
|
||||
cost matrix below preceded by `[AGENT]` markers. You must NOT silently accept the
|
||||
default. Stop and ask the operator.
|
||||
|
||||
**Present this matrix verbatim:**
|
||||
|
||||
```
|
||||
Per-query cost @ 10K queries/mo (typical single-user volume):
|
||||
|
||||
Haiku 4.5 Sonnet 4.6 Opus 4.7
|
||||
($1/M) ($3/M) ($5/M)
|
||||
conservative $40/mo $120/mo $200/mo
|
||||
balanced $100/mo $300/mo $500/mo
|
||||
tokenmax $200/mo $600/mo $1,000/mo
|
||||
|
||||
(scales linearly: ×10 for 100K/mo, ÷10 for 1K. 25x corner-to-corner spread.
|
||||
Natural diagonal pairings — cheap/cheap → frontier/frontier — span ~4x.)
|
||||
```
|
||||
|
||||
**Ask the operator (paraphrase if needed):**
|
||||
|
||||
> Your gbrain just installed with search mode `<auto-applied default>`. This is
|
||||
> a one-time setup decision that controls retrieval payload size. Which mode
|
||||
> do you want?
|
||||
>
|
||||
> 1) conservative — tight 4K budget, no LLM expansion, 10 chunks max.
|
||||
> Best for Haiku subagents, cost-sensitive setups, high-volume loops.
|
||||
>
|
||||
> 2) balanced — 12K budget, no expansion, 25 chunks. Sonnet-tier sweet spot.
|
||||
>
|
||||
> 3) tokenmax (recommended default — preserves v0.31.x retrieval shape) —
|
||||
> no budget, LLM expansion ON, 50 chunks. Best for Opus/frontier models.
|
||||
>
|
||||
> Cost depends on BOTH the mode AND the downstream model you run. See the
|
||||
> matrix above for the 9-cell breakdown.
|
||||
|
||||
If the operator picks a non-default mode, run:
|
||||
```bash
|
||||
gbrain config set search.mode <mode>
|
||||
```
|
||||
|
||||
If they pick tokenmax AND want to preserve the literal v0.31.x default
|
||||
(limit=20 instead of tokenmax's 50), also run:
|
||||
```bash
|
||||
gbrain config set search.searchLimit 20
|
||||
```
|
||||
|
||||
Verify the choice with `gbrain search modes` before continuing.
|
||||
|
||||
**Why this matters:** the cost spread between corners of the matrix is 25x.
|
||||
An agent that silently accepts the default and starts running queries against
|
||||
a user who didn't expect tokenmax-class context loads can rack up surprise
|
||||
spend. Confirm before continuing.
|
||||
|
||||
## Step 4: Import and Index
|
||||
|
||||
```bash
|
||||
gbrain import ~/brain/ --no-embed # import markdown files
|
||||
gbrain embed --stale # generate vector embeddings
|
||||
gbrain query "key themes across these documents?"
|
||||
```
|
||||
|
||||
## Step 4.5: Wire the Knowledge Graph
|
||||
|
||||
If the user already had a brain repo (Step 3 imported existing markdown), backfill
|
||||
the typed-link graph and structured timeline. This populates the `links` and
|
||||
`timeline_entries` tables that future writes will maintain automatically.
|
||||
|
||||
```bash
|
||||
gbrain extract links --source db --dry-run | head -20 # preview
|
||||
gbrain extract links --source db # commit
|
||||
gbrain extract timeline --source db # dated events
|
||||
gbrain stats # verify links > 0
|
||||
```
|
||||
|
||||
For brand-new empty brains, skip this step — auto-link populates the graph as the
|
||||
agent writes pages going forward. There is nothing to backfill yet.
|
||||
|
||||
After this step:
|
||||
- `gbrain graph-query <slug> --depth 2` works (relationship traversal)
|
||||
- Search ranks well-connected entities higher (backlink boost)
|
||||
- Every future `put_page` auto-creates typed links and reconciles stale ones
|
||||
|
||||
If a user has a very large brain (>10K pages), `extract --source db` is idempotent
|
||||
and supports `--since YYYY-MM-DD` for incremental runs.
|
||||
|
||||
## Step 5: Load Skills
|
||||
|
||||
If you're running an agent platform (OpenClaw, Hermes, or any repo with a workspace),
|
||||
scaffold the bundled skills into it:
|
||||
|
||||
```bash
|
||||
cd /path/to/agent/workspace
|
||||
gbrain skillpack scaffold --all # copy 43 curated skills + RESOLVER.md
|
||||
```
|
||||
|
||||
Scaffolded skills are first-class files in your repo. Edit freely; re-running scaffold
|
||||
refuses to overwrite anything that exists. Use `gbrain skillpack reference <name>` to
|
||||
diff against gbrain's bundle when you want upstream improvements. (The legacy
|
||||
`gbrain skillpack install` managed-block model was retired in v0.36.0.0 — run
|
||||
`gbrain skillpack migrate-fence` once if upgrading from an older release.)
|
||||
|
||||
Whether you scaffolded or not, read `skills/RESOLVER.md` (in your workspace, or the
|
||||
bundled copy at `~/gbrain/skills/RESOLVER.md` when running from the cloned repo). It's
|
||||
the skill dispatcher — tells you which skill to read for any task. Save this to your
|
||||
memory permanently.
|
||||
|
||||
The three most important skills to adopt immediately:
|
||||
|
||||
1. **Signal detector** (`skills/signal-detector/SKILL.md`) — fire this on EVERY
|
||||
inbound message. It captures ideas and entities in parallel. The brain compounds.
|
||||
|
||||
2. **Brain-ops** (`skills/brain-ops/SKILL.md`) — brain-first lookup on every response.
|
||||
Check the brain before any external API call.
|
||||
|
||||
3. **Conventions** (`skills/conventions/quality.md`) — citation format, back-linking
|
||||
iron law, source attribution. These are non-negotiable quality rules.
|
||||
|
||||
## Step 6: Identity (optional)
|
||||
|
||||
Run the soul-audit skill to customize the agent's identity:
|
||||
|
||||
```
|
||||
Read skills/soul-audit/SKILL.md and follow it.
|
||||
```
|
||||
|
||||
This generates SOUL.md (agent identity), USER.md (user profile), ACCESS_POLICY.md
|
||||
(who sees what), and HEARTBEAT.md (operational cadence) from the user's answers.
|
||||
|
||||
If skipped, minimal defaults are installed automatically.
|
||||
|
||||
## Step 7: Recurring Jobs
|
||||
|
||||
Set up using your platform's scheduler (OpenClaw cron, Railway cron, crontab), or skip the
|
||||
platform glue entirely with `gbrain autopilot --install` (built-in self-maintaining daemon):
|
||||
|
||||
- **Live sync** (every 15 min): `gbrain sync --repo ~/brain && gbrain embed --stale`
|
||||
— or `gbrain sync --watch` for a continuous loop.
|
||||
- **Auto-update** (daily): `gbrain check-update --json` (tell user, never auto-install).
|
||||
- **Dream cycle** (nightly): `gbrain dream` runs the 8-phase overnight maintenance cycle.
|
||||
Entity sweep, citation fixes, memory consolidation, plus (v0.23+) overnight conversation
|
||||
synthesis and cross-session pattern detection. One cron-friendly command. This is what
|
||||
makes the brain compound. Do not skip it. See `docs/guides/cron-schedule.md` for the
|
||||
full protocol.
|
||||
- **Weekly**: `gbrain doctor --json && gbrain embed --stale`
|
||||
|
||||
## Step 8: Integrations
|
||||
|
||||
Run `gbrain integrations list`. Each recipe in `~/gbrain/recipes/` is a self-contained
|
||||
installer. It tells you what credentials to ask for, how to validate, and what cron
|
||||
to register. Ask the user which integrations they want (email, calendar, voice, Twitter).
|
||||
|
||||
Verify: `gbrain integrations doctor` (after at least one is configured)
|
||||
|
||||
## Step 9: Verify
|
||||
|
||||
Read `docs/GBRAIN_VERIFY.md` and run all 7 verification checks. Check #4 (live sync
|
||||
actually works) is the most important.
|
||||
|
||||
## Upgrade
|
||||
|
||||
If you installed via `bun install -g`:
|
||||
|
||||
```bash
|
||||
gbrain upgrade # self-updates the binary, runs schema migrations,
|
||||
# and prints post-upgrade notes for the version range
|
||||
```
|
||||
|
||||
If you installed via `git clone + bun link`:
|
||||
|
||||
```bash
|
||||
cd ~/gbrain && git pull origin master && bun install
|
||||
gbrain apply-migrations --yes # apply schema migrations (idempotent)
|
||||
gbrain post-upgrade # show migration notes for the version range
|
||||
```
|
||||
|
||||
Then read `~/gbrain/skills/migrations/v<NEW_VERSION>.md` (and any intermediate
|
||||
versions you skipped) and run any backfill or verification steps it lists. Skipping
|
||||
this is how features ship in the binary but stay dormant in the user's brain.
|
||||
|
||||
**v0.32.3 search modes (one-time upgrade prompt):** if the user's brain was
|
||||
created before v0.32.3, `gbrain post-upgrade` prints a banner including the
|
||||
9-cell cost matrix (mode × downstream model) preceded by `[AGENT]` markers.
|
||||
**Do NOT silently move past the banner.** Present the matrix to the operator
|
||||
verbatim, ask which mode they want (recommended default: `tokenmax` to preserve
|
||||
v0.31.x retrieval shape), then run `gbrain config set search.mode <mode>`. See
|
||||
Step 3.5 above for the full ask-the-user protocol — the upgrade path uses the
|
||||
same matrix and same default.
|
||||
|
||||
For v0.12.0+ specifically: if your brain was created before v0.12.0, run
|
||||
`gbrain extract links --source db && gbrain extract timeline --source db` to
|
||||
backfill the new graph layer (see Step 4.5 above).
|
||||
|
||||
For v0.12.2+ specifically: if your brain is Postgres- or Supabase-backed and
|
||||
predates v0.12.2, the `v0_12_2` migration runs `gbrain repair-jsonb`
|
||||
automatically during `gbrain post-upgrade` to fix the double-encoded JSONB
|
||||
columns. PGLite brains no-op. If wiki-style imports were truncated by the old
|
||||
`splitBody` bug, run `gbrain sync --full` after upgrading to rebuild
|
||||
`compiled_truth` from source markdown.
|
||||
|
||||
## 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.
|
||||
@@ -1,373 +0,0 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
### Install it into your existing agent
|
||||
|
||||
Already running Codex, Claude Code, Cursor, or another coding agent? Paste the same instruction in:
|
||||
|
||||
```
|
||||
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)** — one command: `claude mcp add gbrain -- gbrain serve`. Zero server, zero tunnel.
|
||||
- **[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)** — Settings → Connectors → add the URL + bearer token. 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.
|
||||
|
||||
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.
|
||||
|
||||
**Self-wiring knowledge graph.** Every `put_page` extracts entity refs from markdown/wikilinks/typed-link syntax and writes edges with zero LLM calls. Typed edges (`attended`, `works_at`, `invested_in`, `founded`, `advises`, `mentions`, …). Multi-hop traversal via `gbrain graph-query`. The graph is what produces the +31.4 P@5 lift over vector-only RAG.
|
||||
|
||||
**Job queue (Minions).** BullMQ-shaped, Postgres-native job queue. Durable subagents (LLM tool loops that survive crashes via two-phase pending→done persistence), shell jobs with audit, child jobs with cascading timeouts, rate leases for outbound providers, attachments via S3/Supabase storage. Replaces "spawn subagent as fire-and-forget Promise" with something that recovers from anything.
|
||||
|
||||
**43 curated skills.** Routing lives in [`skills/RESOLVER.md`](skills/RESOLVER.md). Covers signal capture, ingest (idea / media / meeting), enrichment, querying, brain ops, citation fixing, daily task management, cron scheduling, reports, voice, soul audit, skill creation, eval framework, and migrations. Skills are markdown files (tool-agnostic), packaged as a single skillpack the installer drops into your agent workspace.
|
||||
|
||||
**Eval framework.** `gbrain eval longmemeval` runs the public [LongMemEval](https://huggingface.co/datasets/xiaowu0162/longmemeval) benchmark against your hybrid retrieval. `gbrain eval export` + `gbrain eval replay` capture real queries and replay them against code changes (set `GBRAIN_CONTRIBUTOR_MODE=1`). `gbrain eval cross-modal` cross-checks an output against the task using three different-provider frontier models. Full methodology in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md).
|
||||
|
||||
**Brain consistency.** `gbrain eval suspected-contradictions` samples retrieval pairs, layered date pre-filter, query-conditioned LLM judge, persistent cache. Surfaces conflicts between takes + facts the agent has written. Wired into the daily dream cycle.
|
||||
|
||||
**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.
|
||||
|
||||
## 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.
|
||||
-260
@@ -1,260 +0,0 @@
|
||||
# Security
|
||||
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
If you discover a security issue in GBrain, please report it privately by opening
|
||||
a [private security advisory](https://github.com/garrytan/gbrain/security/advisories/new)
|
||||
on GitHub.
|
||||
|
||||
Do not open a public issue for security vulnerabilities.
|
||||
|
||||
## Remote MCP Security
|
||||
|
||||
### ⚠️ Do NOT use open OAuth client registration for remote MCP
|
||||
|
||||
If you deploy GBrain's MCP server behind an HTTP wrapper with OAuth 2.1
|
||||
support, **never allow unauthenticated client registration**. An attacker
|
||||
who discovers your server URL can:
|
||||
|
||||
1. Register a new OAuth client via `POST /register`
|
||||
2. Use `client_credentials` grant to obtain a bearer token
|
||||
3. Access all brain data via the MCP tools
|
||||
|
||||
### Recommended: `gbrain serve --http`
|
||||
|
||||
As of v0.22.7, GBrain ships a built-in HTTP transport that uses the
|
||||
existing `access_tokens` table for authentication:
|
||||
|
||||
```bash
|
||||
# Create a token
|
||||
gbrain auth create "my-client"
|
||||
|
||||
# Start the HTTP server
|
||||
gbrain serve --http --port 8787
|
||||
|
||||
# Connect via ngrok, Tailscale, or any tunnel
|
||||
ngrok http 8787 --url your-brain.ngrok.app
|
||||
```
|
||||
|
||||
This is the recommended way to expose GBrain remotely. No OAuth, no
|
||||
registration endpoint, no self-service tokens. Tokens are managed
|
||||
exclusively via `gbrain auth create/list/revoke`.
|
||||
|
||||
### If you must use a custom HTTP wrapper
|
||||
|
||||
1. **Require a secret for client registration** — check a header or body
|
||||
parameter before creating new OAuth clients
|
||||
2. **Disable `client_credentials` grant** — only allow `authorization_code`
|
||||
with browser-based approval
|
||||
3. **Restrict scopes** — never issue tokens with unlimited scope
|
||||
4. **Log all token issuance** — alert on unexpected registrations
|
||||
5. **Rate-limit registration and token endpoints**
|
||||
|
||||
### Pre-registering claude.ai / ChatGPT clients without DCR (v0.41.3+)
|
||||
|
||||
The recommended hardening posture above is: ship `gbrain serve --http`
|
||||
**without** `--enable-dcr` and pre-register every client manually. As of
|
||||
v0.41.3, `gbrain auth register-client` accepts the OAuth fields
|
||||
browser-based clients need:
|
||||
|
||||
```bash
|
||||
# Pre-register claude.ai (confidential client; two redirect URIs)
|
||||
gbrain auth register-client claude-ai \
|
||||
--scopes "read write" \
|
||||
--redirect-uri https://claude.ai/api/mcp/auth_callback \
|
||||
--redirect-uri https://claude.com/api/mcp/auth_callback
|
||||
# --grant-types is auto-set to authorization_code,refresh_token when
|
||||
# --redirect-uri is passed; pass --grant-types explicitly to override.
|
||||
|
||||
# Pre-register ChatGPT (public PKCE client; no client_secret minted)
|
||||
gbrain auth register-client chatgpt \
|
||||
--scopes "read write" \
|
||||
--redirect-uri https://chatgpt.com/connector/oauth/<HASH> \
|
||||
--token-endpoint-auth-method none
|
||||
```
|
||||
|
||||
Auth methods (`--token-endpoint-auth-method`):
|
||||
|
||||
- `client_secret_post` (default) — confidential client, secret in body
|
||||
- `client_secret_basic` — confidential client, secret in `Authorization` header
|
||||
- `none` — public PKCE-only client (no secret minted; ChatGPT custom
|
||||
connector, Claude Code, Cursor)
|
||||
|
||||
The validator rejects unknown methods at the registration boundary, and
|
||||
the same gate applies to the admin endpoint `POST /admin/api/register-client`
|
||||
and the DCR `POST /register` path. Pre-v0.41.3 the CLI hard-coded
|
||||
`redirect_uris = []` and `token_endpoint_auth_method = NULL`, forcing
|
||||
operators to UPDATE `oauth_clients` rows by hand to make claude.ai work
|
||||
without `--enable-dcr`. That footgun is gone.
|
||||
|
||||
### Token Management
|
||||
|
||||
```bash
|
||||
gbrain auth create "claude-desktop" # Create a new token
|
||||
gbrain auth list # List all tokens
|
||||
gbrain auth revoke "claude-desktop" # Revoke a token
|
||||
gbrain auth test <url> --token <tok> # Smoke-test a remote server
|
||||
```
|
||||
|
||||
Tokens are stored as SHA-256 hashes in the `access_tokens` table. The
|
||||
plaintext token is shown once at creation and never stored.
|
||||
|
||||
## `gbrain serve --http` hardening (v0.22.7+)
|
||||
|
||||
The built-in HTTP transport ships with several layers of hardening on by
|
||||
default. All env vars below are optional; the defaults are intentionally
|
||||
conservative.
|
||||
|
||||
### Bind address (v0.34: loopback by default)
|
||||
|
||||
`gbrain serve --http` listens on `127.0.0.1` by default. Personal-laptop
|
||||
installs cannot accidentally publish the brain to the LAN. Self-hosted
|
||||
deployments that need remote access pass `--bind 0.0.0.0` (all
|
||||
interfaces) or `--bind <interface-ip>` (specific NIC). A stderr WARN
|
||||
fires when `--public-url` is set without `--bind` so the operator sees
|
||||
the binding before the first request — common cause of "ngrok forwards
|
||||
to me but the agent can't reach the upstream" misconfigurations.
|
||||
|
||||
### Postgres-only
|
||||
|
||||
`gbrain serve --http` requires a Postgres engine. PGLite is local-only by
|
||||
design and the `access_tokens` / `mcp_request_log` tables don't exist in
|
||||
the PGLite schema. Local agents continue to use stdio (`gbrain serve`).
|
||||
Running `--http` against a PGLite-backed install fails fast with a clear
|
||||
error message at startup.
|
||||
|
||||
### CORS
|
||||
|
||||
Default-deny: no `Access-Control-Allow-Origin` header is sent unless an
|
||||
allowlist is configured. To allow browser-based MCP clients:
|
||||
|
||||
```bash
|
||||
GBRAIN_HTTP_CORS_ORIGIN=https://claude.ai gbrain serve --http --port 8787
|
||||
# Multiple origins: comma-separated
|
||||
GBRAIN_HTTP_CORS_ORIGIN=https://claude.ai,https://your.app gbrain serve --http
|
||||
```
|
||||
|
||||
When the request `Origin` matches the allowlist, the server echoes it
|
||||
back in `Access-Control-Allow-Origin` (with `Vary: Origin`). Otherwise no
|
||||
CORS header is sent and the browser blocks the request.
|
||||
|
||||
**v0.41.3:** the same allowlist now gates every OAuth endpoint (`/mcp`,
|
||||
`/token`, `/authorize`, `/register`, `/revoke`). Pre-v0.41.3 these used
|
||||
default-wide-open `cors()` middleware, leaking
|
||||
`Access-Control-Allow-Origin: *` on every response — any web origin could
|
||||
complete a token exchange from a logged-in operator's browser. The CORS
|
||||
preflight handler in the legacy bearer transport was also asymmetric
|
||||
(actual-request path correctly default-deny, but OPTIONS preflight leaked
|
||||
`Access-Control-Allow-Methods` + `Access-Control-Allow-Headers` to every
|
||||
Origin); both are now consolidated through a single allowlist-gated path.
|
||||
A startup stderr WARN fires when `--bind 0.0.0.0` is set without
|
||||
`GBRAIN_HTTP_CORS_ORIGIN`, surfacing the default-deny posture before the
|
||||
first request.
|
||||
|
||||
### Rate limiting
|
||||
|
||||
Two buckets, both stored in a bounded LRU map (default 10K keys, evicts
|
||||
least-recently-used on overflow, prunes entries older than 2× the
|
||||
window):
|
||||
|
||||
| Bucket | When it fires | Default | Env var |
|
||||
|---|---|---|---|
|
||||
| Pre-auth IP | Before the DB lookup, on every `/mcp` request | 30 req / 60s | `GBRAIN_HTTP_RATE_LIMIT_IP` |
|
||||
| Post-auth token | After a valid token is resolved | 60 req / 60s | `GBRAIN_HTTP_RATE_LIMIT_TOKEN` |
|
||||
| LRU cap | Maximum distinct keys across both buckets | 10000 | `GBRAIN_HTTP_RATE_LIMIT_LRU` |
|
||||
|
||||
On exhaustion the server returns `429 Too Many Requests` with a
|
||||
`Retry-After` header.
|
||||
|
||||
**Caveat for tunneled deployments (ngrok, Tailscale Funnel, Cloudflare
|
||||
Tunnel):** all requests share one egress IP, so the pre-auth IP bucket
|
||||
becomes effectively shared by all clients on that tunnel. The
|
||||
post-auth token-id bucket is the load-bearing limiter for tunnel-fronted
|
||||
deployments.
|
||||
|
||||
### Reverse-proxy trust
|
||||
|
||||
**Loopback-only by default** (v0.41.3+ Express server agrees with the
|
||||
legacy transport; pre-v0.41.3 the Express server hardcoded `'loopback'`
|
||||
while docs claimed "disabled by default" — that disagreement is gone).
|
||||
The default trusts only same-host proxies (127.0.0.1, ::1, fc00::/7);
|
||||
external forwarded-for headers are ignored regardless. To widen or
|
||||
narrow trust:
|
||||
|
||||
```bash
|
||||
# Trust exactly one hop — Fly.io, Render, Vercel, single-layer nginx
|
||||
GBRAIN_HTTP_TRUST_PROXY=1 gbrain serve --http --port 8787
|
||||
|
||||
# Trust N hops — Cloudflare → nginx → gbrain
|
||||
GBRAIN_HTTP_TRUST_PROXY=2 gbrain serve --http --port 8787
|
||||
|
||||
# Disable entirely — direct-exposure deployment with no proxy
|
||||
GBRAIN_HTTP_TRUST_PROXY=0 gbrain serve --http --port 8787
|
||||
|
||||
# Named Express modes (uniquelocal, linklocal) or CIDR lists pass through
|
||||
GBRAIN_HTTP_TRUST_PROXY=uniquelocal gbrain serve --http --port 8787
|
||||
GBRAIN_HTTP_TRUST_PROXY="10.0.0.0/8,192.168.1.0/24" gbrain serve --http --port 8787
|
||||
```
|
||||
|
||||
Both transports (Express OAuth server in `src/commands/serve-http.ts` and
|
||||
the legacy bearer transport in `src/mcp/http-transport.ts`) read the same
|
||||
env var, so single source of truth.
|
||||
|
||||
**Critical safety contract:** only widen past `'loopback'` when **both**
|
||||
of these are true:
|
||||
|
||||
1. gbrain is reachable only via a trusted reverse proxy (not directly
|
||||
exposed to the internet on the configured port). As of v0.34
|
||||
`gbrain serve --http` binds `127.0.0.1` by default, so the
|
||||
reverse-proxy-only posture is the out-of-the-box shape; only
|
||||
override with `--bind 0.0.0.0` (or a specific interface IP) when
|
||||
gbrain itself needs to accept remote connections directly.
|
||||
2. The proxy strips any client-supplied `X-Forwarded-For` and `X-Real-IP`
|
||||
headers, then sets them itself. (nginx with `proxy_set_header
|
||||
X-Forwarded-For $remote_addr` does this; Cloudflare and most cloud
|
||||
load balancers handle it automatically.)
|
||||
|
||||
If gbrain is reachable directly AND `GBRAIN_HTTP_TRUST_PROXY=1` (or any
|
||||
non-loopback value) is set, clients can spoof their IP by sending
|
||||
arbitrary `X-Forwarded-For` headers, defeating the pre-auth IP rate
|
||||
limit. The `'loopback'` default protects against this by ignoring all
|
||||
forwarded-for headers and using the socket peer address.
|
||||
|
||||
### Body size cap
|
||||
|
||||
Default 1 MiB, stream-counted (chunked transfers without
|
||||
`Content-Length` are still capped). Override:
|
||||
|
||||
```bash
|
||||
GBRAIN_HTTP_MAX_BODY_BYTES=2097152 gbrain serve --http # 2 MiB
|
||||
```
|
||||
|
||||
Over-cap requests get `413 Payload Too Large` immediately, before any
|
||||
body is materialized in memory.
|
||||
|
||||
### Audit log
|
||||
|
||||
Every `/mcp` request writes one row to `mcp_request_log`:
|
||||
|
||||
```bash
|
||||
psql "$DATABASE_URL" -c \
|
||||
"SELECT created_at, token_name, operation, status, latency_ms
|
||||
FROM mcp_request_log
|
||||
ORDER BY created_at DESC LIMIT 100"
|
||||
```
|
||||
|
||||
`status` is one of: `success`, `error`, `auth_failed`, `rate_limited`,
|
||||
`body_too_large`, `parse_error`, `unknown_method`. Failed-auth rows have
|
||||
`token_name = NULL`. Inserts are fire-and-forget so audit failures
|
||||
never block requests.
|
||||
|
||||
**v0.26.9 redaction default.** The `params` column now stores
|
||||
`{redacted, kind, declared_keys, unknown_key_count, approx_bytes}` instead
|
||||
of raw JSON-RPC payloads. Declared keys (intersected against the operation's
|
||||
spec) preserve for debug visibility; unknown keys are counted but never
|
||||
named so attackers can't probe key existence; byte sizes bucket to 1KB so
|
||||
content sizes can't be binary-searched. The same shape is broadcast on the
|
||||
admin SSE feed at `/admin/events`. Operators on a personal laptop who want
|
||||
raw payloads back can pass `gbrain serve --http --log-full-params` (loud
|
||||
stderr warning at startup). Multi-tenant deployments should leave it
|
||||
on the redacted default.
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
# Design System — GBrain Admin Dashboard
|
||||
|
||||
## Product Context
|
||||
- **What this is:** Admin dashboard for GBrain MCP server — manage OAuth agents, API keys, monitor requests
|
||||
- **Who it's for:** GBrain operators managing multi-agent access to their brain
|
||||
- **Space/industry:** Developer infrastructure (peers: Supabase dashboard, Vercel, Railway)
|
||||
- **Project type:** Dense utilitarian admin panel — Steve Krug "Don't Make Me Think"
|
||||
|
||||
## Aesthetic Direction
|
||||
- **Direction:** Industrial/Utilitarian — function-first, data-dense, zero decoration
|
||||
- **Decoration level:** None — every pixel earns its place with information
|
||||
- **Mood:** Ops dashboard for someone who builds. Not a marketing site. Not a consumer app. A cockpit.
|
||||
- **Reference:** Supabase dashboard (dark + dense), Linear (restrained), Grafana (data-forward)
|
||||
|
||||
## Alignment
|
||||
- **Text alignment:** Left-align everything. No centered text in tables, cards, forms, or labels.
|
||||
- **Headings:** Left-aligned
|
||||
- **Table data:** Left-aligned (including numbers — contextual readability over columnar alignment)
|
||||
- **Form labels:** Left-aligned above inputs
|
||||
- **Buttons in forms:** Right-aligned (action flows left-to-right: Cancel → Submit)
|
||||
- **Modal titles:** Left-aligned
|
||||
- **Page titles:** Left-aligned
|
||||
- **Only exception:** Empty states and the login page lock icon can center for visual weight
|
||||
|
||||
## Typography
|
||||
- **Display/Headings:** Inter (Semibold 600) — clean, neutral, disappears into the content
|
||||
- **Body/UI:** Inter (Regular 400 / Medium 500)
|
||||
- **Data/Tables/Code:** JetBrains Mono (Regular 400 / Medium 500) — monospace for anything the user might copy, any ID, any token, any technical value
|
||||
- **Loading:** Google Fonts. `display=swap`.
|
||||
- **Scale:**
|
||||
- Page title: 24px / Inter Semibold
|
||||
- Section title: 14px / Inter Semibold, uppercase, letter-spacing 0.5px
|
||||
- Table header: 12px / Inter Medium, uppercase, letter-spacing 1px, muted color
|
||||
- Body: 14px / Inter Regular
|
||||
- Small/Caption: 13px
|
||||
- Micro: 12px (badges, timestamps)
|
||||
- Code/Data: 13px / JetBrains Mono
|
||||
|
||||
## Color
|
||||
- **Approach:** Monochrome base + semantic color only. No primary brand color. Color means something.
|
||||
- **Background:**
|
||||
- Base: #0a0a0f (near-black with blue undertone)
|
||||
- Surface/cards: #12121a
|
||||
- Hover: #1a1a2a
|
||||
- Input/code blocks: #0f0f1a
|
||||
- **Borders:** #1e1e2e (default), #3a3a5a (hover/active)
|
||||
- **Text:**
|
||||
- Primary: #e0e0e0
|
||||
- Secondary: #888888
|
||||
- Muted: #555555
|
||||
- Link: #88aaff
|
||||
- **Semantic (badges only):**
|
||||
- Success/active: #34a853
|
||||
- Error/danger: #ff6b6b
|
||||
- Warning: #f5a623
|
||||
- Read scope: #3b82f6
|
||||
- Write scope: #f59e0b
|
||||
- Admin scope: #ef4444
|
||||
- **No accent color.** The data IS the interface. Badges carry all the color.
|
||||
|
||||
## Spacing
|
||||
- **Base unit:** 4px
|
||||
- **Density:** Dense — this is an ops tool, not a landing page
|
||||
- **Scale:** 4px, 8px, 12px, 16px, 20px, 24px, 32px, 48px
|
||||
- **Table row padding:** 10px 16px
|
||||
- **Card padding:** 24px
|
||||
- **Modal padding:** 24px
|
||||
- **Section gaps:** 24px between sections, 12px between related elements
|
||||
|
||||
## Layout
|
||||
- **Sidebar:** Fixed left, 200px wide, dark (#0a0a0f)
|
||||
- **Main content:** Fluid, max-width none (fills available space)
|
||||
- **Grid:** Single column for tables (full width), 2-column for stats cards
|
||||
- **Border radius:**
|
||||
- Cards/panels: 16px
|
||||
- Buttons/inputs: 8px
|
||||
- Badges: 9999px (pill)
|
||||
- Tables: 0 (sharp edges — data is rectangular)
|
||||
|
||||
## Components
|
||||
|
||||
### Tables
|
||||
- Full-width, no outer border
|
||||
- Header row: uppercase, letter-spaced, muted color, no background
|
||||
- Data rows: subtle hover (#1a1a2a), pointer cursor when clickable
|
||||
- All text left-aligned
|
||||
- Monospace for IDs, tokens, latency values
|
||||
|
||||
### Badges
|
||||
- Pill shape (border-radius: 9999px)
|
||||
- Padding: 2px 8px
|
||||
- Font: 12px
|
||||
- Scoped to semantic meaning: `success`, `danger`, `read`, `write`, `admin`
|
||||
|
||||
### Buttons
|
||||
- Primary: white text on #3a3a5a, hover brightens
|
||||
- Secondary: muted text on transparent, border #1e1e2e
|
||||
- Danger: white text on #ff6b6b background
|
||||
- Size: 13px font, 6px 14px padding
|
||||
|
||||
### Modals
|
||||
- Overlay: rgba(0,0,0,0.7)
|
||||
- Card: #12121a, border #1e1e2e, border-radius 16px, max-width 480px
|
||||
- Title: 18px Semibold, left-aligned
|
||||
- Close: top-right ✕ button
|
||||
|
||||
### Drawers
|
||||
- Right-side panel, 400px wide
|
||||
- Slide in from right
|
||||
- Dark overlay behind
|
||||
- Close button top-right
|
||||
- Sections separated by section titles (uppercase, muted)
|
||||
|
||||
### Tabs
|
||||
- Inline horizontal, wrapping allowed
|
||||
- Active: white text, bottom border
|
||||
- Inactive: muted text, no border
|
||||
- No background color on tabs
|
||||
|
||||
### Code blocks
|
||||
- Background: rgba(0,0,0,0.3)
|
||||
- Border-radius: 8px
|
||||
- Padding: 10px 14px
|
||||
- Font: JetBrains Mono 12px
|
||||
- Copy button: right-aligned, subtle
|
||||
|
||||
### Empty states
|
||||
- Centered text (only exception to left-align rule)
|
||||
- Muted color
|
||||
- Suggest next action
|
||||
|
||||
## Motion
|
||||
- **Approach:** Minimal — transitions for hover states only
|
||||
- **Duration:** 150ms for hovers, 200ms for drawer slide
|
||||
- **No loading spinners** — show stale data until fresh arrives
|
||||
- **SSE live feed:** Real-time, no animation on new entries (just prepend)
|
||||
|
||||
## Anti-Patterns (do NOT do these)
|
||||
- ❌ Center-aligned table data
|
||||
- ❌ Center-aligned headings or labels (except empty states)
|
||||
- ❌ Gradient backgrounds
|
||||
- ❌ Shadows (the dark theme IS the depth model)
|
||||
- ❌ Rounded table corners
|
||||
- ❌ Icons as navigation (use text labels)
|
||||
- ❌ Loading skeletons (show real data or nothing)
|
||||
- ❌ Confirmation toasts (action → result is immediate and visible)
|
||||
- ❌ Color for decoration (every color means something)
|
||||
|
||||
## Decisions Log
|
||||
| Date | Decision | Rationale |
|
||||
|------|----------|-----------|
|
||||
| 2026-05-01 | Dark theme only | Ops dashboard. No light mode needed. |
|
||||
| 2026-05-01 | Steve Krug lens | Zero happy talk, mindless choices, scannable tables, billboard-speed comprehension. |
|
||||
| 2026-05-01 | JetBrains Mono for data | Anything copyable or technical should be monospace. |
|
||||
| 2026-05-03 | Left-align everything | Garry preference. Centered text is a design crutch. Left-align forces hierarchy through typography weight and spacing, not position. |
|
||||
| 2026-05-03 | Incorporate GStack design DNA | Same family: Inter + JetBrains Mono, dark base, semantic-only color. Diverges on accent (GStack: amber; GBrain: none — data is the color). |
|
||||
| 2026-05-03 | Per-client config export tabs | Claude Code, ChatGPT, Claude.ai, Cursor, Perplexity, JSON. Every agent has a copy-paste setup path. |
|
||||
| 2026-05-03 | Magic link auth | Login page tells you to ask your agent. No pasting hex strings into forms. |
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "gbrain-admin",
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001788", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
|
||||
|
||||
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
}
|
||||
}
|
||||
Vendored
-56
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-16
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GBrain Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<script type="module" crossorigin src="/admin/assets/index-DqP-zmqH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-GxkWX7v3.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GBrain Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "gbrain-admin",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"vite": "^6.3.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { LoginPage } from './pages/Login';
|
||||
import { DashboardPage } from './pages/Dashboard';
|
||||
import { AgentsPage } from './pages/Agents';
|
||||
import { RequestLogPage } from './pages/RequestLog';
|
||||
import { CalibrationPage } from './pages/Calibration';
|
||||
import { JobsWatchPage } from './pages/JobsWatch';
|
||||
import { api } from './api';
|
||||
|
||||
type Page = 'login' | 'dashboard' | 'agents' | 'log' | 'calibration' | 'jobs';
|
||||
|
||||
function getPage(): Page {
|
||||
const hash = window.location.hash.replace('#', '') || 'dashboard';
|
||||
if (['login', 'dashboard', 'agents', 'log', 'calibration', 'jobs'].includes(hash)) return hash as Page;
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [page, setPage] = useState<Page>(getPage);
|
||||
|
||||
useEffect(() => {
|
||||
const onHash = () => setPage(getPage());
|
||||
window.addEventListener('hashchange', onHash);
|
||||
return () => window.removeEventListener('hashchange', onHash);
|
||||
}, []);
|
||||
|
||||
const navigate = (p: Page) => {
|
||||
window.location.hash = p;
|
||||
setPage(p);
|
||||
};
|
||||
|
||||
if (page === 'login') {
|
||||
return <LoginPage onLogin={() => navigate('dashboard')} />;
|
||||
}
|
||||
|
||||
const handleSignOutEverywhere = async () => {
|
||||
if (!confirm('Sign out every active admin session, including other browsers and tabs? Each one will need to re-authenticate via a fresh magic link.')) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.signOutEverywhere();
|
||||
} catch {
|
||||
// Even if the call fails, push to login — cookie is likely already invalid.
|
||||
}
|
||||
navigate('login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<nav className="sidebar">
|
||||
<div className="sidebar-logo">GBrain</div>
|
||||
<div className="sidebar-nav">
|
||||
<a className={`nav-item ${page === 'dashboard' ? 'active' : ''}`}
|
||||
onClick={() => navigate('dashboard')}>Dashboard</a>
|
||||
<a className={`nav-item ${page === 'agents' ? 'active' : ''}`}
|
||||
onClick={() => navigate('agents')}>Agents</a>
|
||||
<a className={`nav-item ${page === 'log' ? 'active' : ''}`}
|
||||
onClick={() => navigate('log')}>Request Log</a>
|
||||
<a className={`nav-item ${page === 'calibration' ? 'active' : ''}`}
|
||||
onClick={() => navigate('calibration')}>Calibration</a>
|
||||
<a className={`nav-item ${page === 'jobs' ? 'active' : ''}`}
|
||||
onClick={() => navigate('jobs')}>Jobs Watch</a>
|
||||
</div>
|
||||
<div style={{ marginTop: 'auto', padding: '16px 12px', borderTop: '1px solid var(--border)' }}>
|
||||
<button
|
||||
onClick={handleSignOutEverywhere}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text-secondary)',
|
||||
padding: '6px 10px',
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
}}
|
||||
title="Revoke every active admin session — every browser, every tab"
|
||||
>
|
||||
Sign out everywhere
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="main">
|
||||
{page === 'dashboard' && <DashboardPage />}
|
||||
{page === 'agents' && <AgentsPage />}
|
||||
{page === 'log' && <RequestLogPage />}
|
||||
{page === 'calibration' && <CalibrationPage />}
|
||||
{page === 'jobs' && <JobsWatchPage />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
const BASE = '';
|
||||
|
||||
// v0.26.3 trust model (D11 + D12): the admin UI does NOT cache the
|
||||
// bootstrap token in browser JS state. On 401, redirect to login —
|
||||
// no auto-reauth via saved token, no localStorage/sessionStorage read.
|
||||
// The HttpOnly cookie set by /admin/login is the only session credential.
|
||||
async function apiFetch(path: string, options?: RequestInit) {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
...options,
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
||||
});
|
||||
if (res.status === 401) {
|
||||
// No token cache to retry from. Redirect to login.
|
||||
window.location.hash = '#login';
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// v0.36.1.0 (T15 / E6) — SVG fetch (text/plain payload, NOT JSON).
|
||||
async function apiFetchText(path: string) {
|
||||
const res = await fetch(`${BASE}${path}`, { credentials: 'same-origin' });
|
||||
if (res.status === 401) {
|
||||
window.location.hash = '#login';
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
export const api = {
|
||||
login: (token: string) => apiFetch('/admin/login', { method: 'POST', body: JSON.stringify({ token }) }),
|
||||
signOutEverywhere: () => apiFetch('/admin/api/sign-out-everywhere', { method: 'POST' }),
|
||||
stats: () => apiFetch('/admin/api/stats'),
|
||||
health: () => apiFetch('/admin/api/health-indicators'),
|
||||
agents: () => apiFetch('/admin/api/agents'),
|
||||
requests: (page = 1, qs = '') => apiFetch(`/admin/api/requests?page=${page}${qs}`),
|
||||
apiKeys: () => apiFetch('/admin/api/api-keys'),
|
||||
createApiKey: (name: string) => apiFetch('/admin/api/api-keys', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
revokeApiKey: (name: string) => apiFetch('/admin/api/api-keys/revoke', { method: 'POST', body: JSON.stringify({ name }) }),
|
||||
updateClientTtl: (clientId: string, tokenTtl: number | null) => apiFetch('/admin/api/update-client-ttl', { method: 'POST', body: JSON.stringify({ clientId, tokenTtl }) }),
|
||||
revokeClient: (clientId: string) => apiFetch('/admin/api/revoke-client', { method: 'POST', body: JSON.stringify({ clientId }) }),
|
||||
// v0.36.1.0 (T15 / E6) — calibration endpoints.
|
||||
calibrationProfile: (holder?: string) =>
|
||||
apiFetch(`/admin/api/calibration/profile${holder ? `?holder=${encodeURIComponent(holder)}` : ''}`),
|
||||
calibrationChart: (type: string, holder?: string) =>
|
||||
apiFetchText(`/admin/api/calibration/charts/${encodeURIComponent(type)}${holder ? `?holder=${encodeURIComponent(holder)}` : ''}`),
|
||||
// v0.41 D2 — live minion-jobs dashboard snapshot.
|
||||
jobsWatch: () => apiFetch('/admin/api/jobs/watch'),
|
||||
};
|
||||
@@ -1,359 +0,0 @@
|
||||
:root {
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-secondary: #14141f;
|
||||
--bg-tertiary: #1e1e2e;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #888;
|
||||
/* v0.36.1.0 TD2 — bumped from #555 (contrast 4.0 on #0a0a0f bg, below WCAG AA
|
||||
4.5 for body text) to #777 (contrast ~5.5, passes AA). Applies globally
|
||||
to Dashboard, Agents, RequestLog, and the new Calibration tab. */
|
||||
--text-muted: #777;
|
||||
--accent: #3b82f6;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.app { display: flex; min-height: 100vh; }
|
||||
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid #1e1e2e;
|
||||
padding: 16px 0;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
padding: 0 16px 24px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); }
|
||||
.nav-item.active {
|
||||
border-left-color: var(--accent);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.main { flex: 1; padding: 24px 32px; overflow-y: auto; }
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Metrics bar */
|
||||
.metrics { display: flex; gap: 16px; margin-bottom: 24px; }
|
||||
.metric {
|
||||
background: var(--bg-secondary);
|
||||
padding: 16px 20px;
|
||||
border-radius: 6px;
|
||||
min-width: 140px;
|
||||
}
|
||||
.metric-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.metric-label { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
|
||||
|
||||
/* Tables */
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
border-top: 1px solid #1a1a2a;
|
||||
}
|
||||
tr:hover td { background: var(--bg-tertiary); }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge-read { background: rgba(59,130,246,0.15); color: var(--accent); }
|
||||
.badge-write { background: rgba(245,158,11,0.15); color: var(--warning); }
|
||||
.badge-admin { background: rgba(239,68,68,0.15); color: var(--error); }
|
||||
.badge-success { background: rgba(34,197,94,0.15); color: var(--success); }
|
||||
.badge-error { background: rgba(239,68,68,0.15); color: var(--error); }
|
||||
|
||||
/* Status dots */
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.status-active { background: var(--success); }
|
||||
.status-warning { background: var(--warning); }
|
||||
.status-inactive { background: var(--text-muted); }
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-primary { background: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: #2563eb; }
|
||||
.btn-secondary { background: transparent; color: var(--text-secondary); border: 1px solid #333; }
|
||||
.btn-secondary:hover { border-color: var(--text-secondary); color: var(--text-primary); }
|
||||
.btn-danger { background: transparent; color: var(--error); border: 1px solid var(--error); }
|
||||
.btn-danger:hover { background: rgba(239,68,68,0.1); }
|
||||
|
||||
/* Forms */
|
||||
input, select {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid #333;
|
||||
color: var(--text-primary);
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-sans);
|
||||
width: 100%;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(59,130,246,0.2);
|
||||
}
|
||||
input::placeholder { color: var(--text-muted); }
|
||||
label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; }
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.modal {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
min-width: 420px;
|
||||
max-width: 520px;
|
||||
}
|
||||
.modal-title { font-size: 18px; font-weight: 600; margin-bottom: 20px; }
|
||||
|
||||
/* Drawer */
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 90;
|
||||
}
|
||||
.drawer {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 420px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--accent);
|
||||
padding: 24px;
|
||||
z-index: 91;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.drawer-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.5px;
|
||||
margin: 20px 0 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Health panel */
|
||||
.health-panel {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
.health-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
.code-block {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
.code-block .copy-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Activity feed */
|
||||
.feed { max-height: 400px; overflow-y: auto; }
|
||||
.feed-empty {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Sparkline */
|
||||
.sparkline { display: inline-block; vertical-align: middle; }
|
||||
|
||||
/* Filter bar */
|
||||
.filter-bar { display: flex; gap: 12px; margin-bottom: 16px; align-items: center; }
|
||||
.filter-bar select { width: auto; min-width: 140px; }
|
||||
|
||||
/* Pagination */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.pagination button {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid #333;
|
||||
color: var(--text-primary);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pagination button:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
/* Warning bar */
|
||||
.warning-bar {
|
||||
background: rgba(245,158,11,0.15);
|
||||
border: 1px solid var(--warning);
|
||||
color: var(--warning);
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
.checkbox-group { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; gap: 0; margin-bottom: 12px; }
|
||||
.tab {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
|
||||
/* Login page */
|
||||
.login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
.login-box { text-align: left; width: 340px; }
|
||||
.login-logo { font-size: 32px; font-weight: 600; margin-bottom: 32px; }
|
||||
.login-hint { color: var(--text-muted); font-size: 12px; margin-top: 12px; }
|
||||
.login-error { color: var(--error); font-size: 13px; margin-top: 8px; }
|
||||
|
||||
/* Monospace data */
|
||||
.mono { font-family: var(--font-mono); font-size: 12px; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { display: none; }
|
||||
.main { padding: 16px; }
|
||||
.metrics { flex-wrap: wrap; }
|
||||
.drawer { width: 100%; }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Admin SPA scope constants — HAND-MAINTAINED MIRROR of src/core/scope.ts.
|
||||
*
|
||||
* The admin tsconfig.json scopes `include: ['src']` to admin/src/, so we
|
||||
* cannot directly import from ../../src/core/scope.ts without breaking the
|
||||
* SPA's compile boundary. Instead, this file is a hand-maintained duplicate;
|
||||
* scripts/check-admin-scope-drift.sh fails the build if the two lists drift.
|
||||
*
|
||||
* If you change ALLOWED_SCOPES in src/core/scope.ts, update this file too,
|
||||
* or `bun run verify` will reject the change.
|
||||
*/
|
||||
|
||||
export type Scope = 'read' | 'write' | 'admin' | 'sources_admin' | 'users_admin' | 'agent';
|
||||
|
||||
// MIRROR OF src/core/scope.ts ALLOWED_SCOPES_LIST — keep alphabetically sorted.
|
||||
// v0.38: 'agent' added for the submit_agent remote-MCP op (sibling to admin,
|
||||
// NOT implied — existing admin clients must re-register to opt in).
|
||||
export const ALLOWED_SCOPES_LIST: ReadonlyArray<Scope> = [
|
||||
'admin',
|
||||
'agent',
|
||||
'read',
|
||||
'sources_admin',
|
||||
'users_admin',
|
||||
'write',
|
||||
];
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -1,633 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { api } from '../api';
|
||||
import { ALLOWED_SCOPES_LIST, type Scope } from '../lib/scope-constants';
|
||||
|
||||
function timeAgo(date: Date): string {
|
||||
const s = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (s < 60) return 'just now';
|
||||
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
||||
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
||||
return `${Math.floor(s / 86400)}d ago`;
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
auth_type: 'oauth' | 'api_key';
|
||||
client_id?: string; // compat
|
||||
client_name?: string; // compat
|
||||
grant_types: string[];
|
||||
scope: string;
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
total_requests: number;
|
||||
requests_today: number;
|
||||
token_ttl: number | null;
|
||||
status: 'active' | 'revoked';
|
||||
}
|
||||
|
||||
interface ApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
last_used_at: string | null;
|
||||
status: 'active' | 'revoked';
|
||||
}
|
||||
|
||||
export function AgentsPage() {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [hideRevoked, setHideRevoked] = useState(true);
|
||||
const [showRegister, setShowRegister] = useState(false);
|
||||
const [showCredentials, setShowCredentials] = useState<{ clientId: string; clientSecret: string; name: string } | null>(null);
|
||||
const [showApiKeyCreate, setShowApiKeyCreate] = useState(false);
|
||||
const [showApiKeyToken, setShowApiKeyToken] = useState<{ name: string; token: string } | null>(null);
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
|
||||
useEffect(() => { loadAgents(); }, []);
|
||||
|
||||
const loadAgents = () => { api.agents().then(setAgents).catch(() => {}); };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1 className="page-title" style={{ marginBottom: 0 }}>Agents</h1>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input type="checkbox" checked={hideRevoked} onChange={e => setHideRevoked(e.target.checked)} /> Hide revoked
|
||||
</label>
|
||||
<button className="btn btn-secondary" onClick={() => setShowApiKeyCreate(true)}>+ API Key</button>
|
||||
<button className="btn btn-primary" onClick={() => setShowRegister(true)}>+ OAuth Client</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
// Filter once and reuse, so the empty-state guard sees the same
|
||||
// rows the table renders. Pre-fix: agents.length === 0 used the
|
||||
// unfiltered array, so an all-revoked dataset with hideRevoked=on
|
||||
// showed a header-only table with no placeholder.
|
||||
const visibleAgents = agents.filter(a => !hideRevoked || a.status !== 'revoked');
|
||||
if (agents.length === 0) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 48, color: 'var(--text-muted)' }}>
|
||||
No agents registered. Register your first agent to get started.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (visibleAgents.length === 0) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 48, color: 'var(--text-muted)' }}>
|
||||
All agents are revoked. Uncheck "Hide revoked" to view them.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Scopes</th>
|
||||
<th>Status</th>
|
||||
<th>Requests</th>
|
||||
<th>Last Used</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleAgents.map(a => (
|
||||
<tr key={a.id} onClick={() => setSelectedAgent(a)}
|
||||
style={{ cursor: 'pointer' }}>
|
||||
<td style={{ fontWeight: 500 }}>{a.name || a.client_name}</td>
|
||||
<td>
|
||||
<span className={`badge ${a.auth_type === 'oauth' ? 'badge-read' : 'badge-write'}`} style={{ fontSize: 11 }}>
|
||||
{a.auth_type === 'oauth' ? 'OAuth' : 'API Key'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{(a.scope || '').split(' ').filter(Boolean).map(s => (
|
||||
<span key={s} className={`badge badge-${s}`} style={{ marginRight: 4 }}>{s}</span>
|
||||
))}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`badge ${a.status === 'active' ? 'badge-success' : 'badge-danger'}`}>{a.status}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style={{ fontWeight: 500 }}>{a.requests_today || 0}</span>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 12 }}> / {a.total_requests || 0}</span>
|
||||
</td>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>
|
||||
{a.last_used_at ? timeAgo(new Date(a.last_used_at)) : 'Never'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 13, marginTop: 12 }}>
|
||||
{agents.filter(a => a.status === 'active').length} active / {agents.length} total
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{showRegister && (
|
||||
<RegisterModal
|
||||
onClose={() => setShowRegister(false)}
|
||||
onRegistered={(creds) => { setShowRegister(false); setShowCredentials(creds); loadAgents(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCredentials && (
|
||||
<CredentialsModal
|
||||
credentials={showCredentials}
|
||||
onClose={() => setShowCredentials(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedAgent && (
|
||||
<AgentDrawer agent={selectedAgent} onClose={() => setSelectedAgent(null)} onRevoked={loadAgents} />
|
||||
)}
|
||||
|
||||
{showApiKeyCreate && (
|
||||
<ApiKeyCreateModal
|
||||
onClose={() => setShowApiKeyCreate(false)}
|
||||
onCreated={(result) => { setShowApiKeyCreate(false); setShowApiKeyToken(result); loadAgents(); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showApiKeyToken && (
|
||||
<ApiKeyTokenModal token={showApiKeyToken} onClose={() => setShowApiKeyToken(null)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiKeyCreateModal({ onClose, onCreated }: {
|
||||
onClose: () => void;
|
||||
onCreated: (result: { name: string; token: string }) => void;
|
||||
}) {
|
||||
const [name, setName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) { setError('Name required'); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.createApiKey(name.trim());
|
||||
onCreated({ name: data.name, token: data.token });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed');
|
||||
} finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<form className="modal" onClick={e => e.stopPropagation()} onSubmit={handleSubmit}>
|
||||
<div className="modal-title">Create API Key</div>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: 13, marginBottom: 16 }}>
|
||||
API keys use simple bearer token auth. They grant full read+write+admin access.
|
||||
For scoped access, use OAuth clients instead.
|
||||
</p>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label>Key Name</label>
|
||||
<input placeholder="e.g. claude-code-local" value={name} onChange={e => setName(e.target.value)} autoFocus />
|
||||
</div>
|
||||
{error && <div style={{ color: 'var(--error)', fontSize: 13, marginBottom: 12 }}>{error}</div>}
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>Cancel</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? 'Creating...' : 'Create Key'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ApiKeyTokenModal({ token, onClose }: {
|
||||
token: { name: string; token: string };
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const copy = (text: string) => navigator.clipboard.writeText(text);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal" style={{ maxWidth: 560 }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 36, color: 'var(--success)', marginBottom: 8 }}>✓</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>API Key Created</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ fontSize: 12 }}>Name</label>
|
||||
<div className="code-block"><span>{token.name}</span></div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ fontSize: 12 }}>Bearer Token</label>
|
||||
<div className="code-block">
|
||||
<span>{token.token}</span>
|
||||
<button className="copy-btn" onClick={() => copy(token.token)}>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ fontSize: 12 }}>Usage</label>
|
||||
<div className="code-block">
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0, fontSize: 12 }}>{`Authorization: Bearer ${token.token}`}</pre>
|
||||
<button className="copy-btn" onClick={() => copy(`Authorization: Bearer ${token.token}`)}>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="warning-bar">Save this token now. It will not be shown again.</div>
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end', marginTop: 20 }}>
|
||||
<button className="btn btn-primary" onClick={onClose}>Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RegisterModal({ onClose, onRegistered }: {
|
||||
onClose: () => void;
|
||||
onRegistered: (creds: { clientId: string; clientSecret: string; name: string }) => void;
|
||||
}) {
|
||||
const [name, setName] = useState('');
|
||||
// v0.28: scope set sourced from admin/src/lib/scope-constants.ts (mirror
|
||||
// of src/core/scope.ts). CI drift check at scripts/check-admin-scope-drift.sh
|
||||
// fails the build if these diverge.
|
||||
const [scopes, setScopes] = useState<Record<Scope, boolean>>(() =>
|
||||
Object.fromEntries(ALLOWED_SCOPES_LIST.map(s => [s, s === 'read'])) as Record<Scope, boolean>,
|
||||
);
|
||||
const [ttl, setTtl] = useState('86400'); // 24h default
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const ttlOptions = [
|
||||
{ label: '1 hour', value: '3600' },
|
||||
{ label: '24 hours', value: '86400' },
|
||||
{ label: '7 days', value: '604800' },
|
||||
{ label: '30 days', value: '2592000' },
|
||||
{ label: '1 year', value: '31536000' },
|
||||
{ label: 'No expiry', value: '0' },
|
||||
];
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) { setError('Name required'); return; }
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
// Use the CLI registration endpoint (POST to admin API)
|
||||
const selectedScopes = Object.entries(scopes).filter(([, v]) => v).map(([k]) => k).join(' ');
|
||||
const res = await fetch('/admin/api/register-client', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name.trim(), scopes: selectedScopes, tokenTtl: ttl === '0' ? 315360000 : Number(ttl) }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Registration failed');
|
||||
const data = await res.json();
|
||||
onRegistered({ clientId: data.clientId, clientSecret: data.clientSecret, name: name.trim() });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Registration failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<form className="modal" onClick={e => e.stopPropagation()} onSubmit={handleSubmit}>
|
||||
<div className="modal-title">Register Agent</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label>Agent Name</label>
|
||||
<input placeholder="e.g. perplexity-production" value={name} onChange={e => setName(e.target.value)} autoFocus />
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label>Scopes</label>
|
||||
<div className="checkbox-group">
|
||||
{ALLOWED_SCOPES_LIST.map(s => (
|
||||
<label key={s} className="checkbox-label">
|
||||
<input type="checkbox" checked={scopes[s]} onChange={e => setScopes(p => ({ ...p, [s]: e.target.checked }))} />
|
||||
{s}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<label>Token Lifetime</label>
|
||||
<select value={ttl} onChange={e => setTtl(e.target.value)}
|
||||
style={{ width: '100%', background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 10px', fontSize: 14 }}>
|
||||
{ttlOptions.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
{error && <div style={{ color: 'var(--error)', fontSize: 13, marginBottom: 12 }}>{error}</div>}
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={onClose}>Cancel</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? 'Registering...' : 'Register'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CredentialsModal({ credentials, onClose }: {
|
||||
credentials: { clientId: string; clientSecret: string; name: string };
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const copy = (text: string) => navigator.clipboard.writeText(text);
|
||||
const downloadJson = () => {
|
||||
const blob = new Blob([JSON.stringify(credentials, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = `${credentials.name}-credentials.json`; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal" style={{ maxWidth: 560 }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 36, color: 'var(--success)', marginBottom: 8 }}>✓</div>
|
||||
<div style={{ fontSize: 20, fontWeight: 600 }}>Agent Registered</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ fontSize: 12 }}>Client ID</label>
|
||||
<div className="code-block">
|
||||
<span>{credentials.clientId}</span>
|
||||
<button className="copy-btn" onClick={() => copy(credentials.clientId)}>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ fontSize: 12 }}>Client Secret</label>
|
||||
<div className="code-block">
|
||||
<span>{credentials.clientSecret}</span>
|
||||
<button className="copy-btn" onClick={() => copy(credentials.clientSecret)}>Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="warning-bar">
|
||||
Save this secret now. It will not be shown again.
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end', marginTop: 20 }}>
|
||||
<button className="btn btn-secondary" onClick={downloadJson}>Download as JSON</button>
|
||||
<button className="btn btn-primary" onClick={onClose}>Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentDrawer({ agent, onClose, onRevoked }: { agent: Agent; onClose: () => void; onRevoked: () => void }) {
|
||||
const [tab, setTab] = useState<'claude-code' | 'chatgpt' | 'claude-cowork' | 'perplexity' | 'cursor' | 'json'>('claude-code');
|
||||
const copy = (text: string) => navigator.clipboard.writeText(text);
|
||||
const serverUrl = window.location.origin;
|
||||
|
||||
const cid = agent.id || agent.client_id || '';
|
||||
const isOAuth = agent.auth_type === 'oauth';
|
||||
const agentName = agent.name || agent.client_name || 'unknown';
|
||||
|
||||
// For API keys, we can't show the actual token (it was shown once at creation).
|
||||
// For OAuth, we show the client_id and tell them to use their secret.
|
||||
|
||||
const configSnippets: Record<string, string> = {
|
||||
'claude-code': isOAuth
|
||||
? [
|
||||
`The user wants to connect GBrain MCP to your context. Here's how.`,
|
||||
``,
|
||||
`# Step 1 — read the client secret from the operator without putting it in shell history.`,
|
||||
`# read -s suppresses the echo; export keeps it scoped to this shell session.`,
|
||||
`read -rs -p "Client Secret: " GBRAIN_CS && echo`,
|
||||
``,
|
||||
`# Step 2 — mint a token and register GBrain in one go.`,
|
||||
`claude mcp add --transport http gbrain ${serverUrl}/mcp \\`,
|
||||
` --header "Authorization: Bearer $(curl -s -X POST ${serverUrl}/token \\`,
|
||||
` -d 'grant_type=client_credentials' \\`,
|
||||
` -d 'client_id=${cid}' \\`,
|
||||
` --data-urlencode "client_secret=$GBRAIN_CS" \\`,
|
||||
` -d 'scope=${agent.scope || 'read write'}' | jq -r .access_token)"`,
|
||||
``,
|
||||
`# Step 3 — clear the secret from this shell.`,
|
||||
`unset GBRAIN_CS`,
|
||||
``,
|
||||
`The token will last ${agent.token_ttl ? (agent.token_ttl >= 86400 ? Math.floor(agent.token_ttl / 86400) + ' days' : Math.floor(agent.token_ttl / 3600) + ' hours') : '1 hour (default)'}.`,
|
||||
``,
|
||||
`─── Fallback: 2-step curl + paste ───`,
|
||||
``,
|
||||
`If your shell doesn't support read -s, mint the token first, then paste:`,
|
||||
``,
|
||||
`curl -s -X POST ${serverUrl}/token \\`,
|
||||
` -d 'grant_type=client_credentials' \\`,
|
||||
` -d 'client_id=${cid}' \\`,
|
||||
` -d 'client_secret=PASTE_YOUR_CLIENT_SECRET_HERE' \\`,
|
||||
` -d 'scope=${agent.scope || 'read write'}' | jq -r .access_token`,
|
||||
``,
|
||||
`claude mcp add --transport http gbrain ${serverUrl}/mcp \\`,
|
||||
` --header "Authorization: Bearer PASTE_TOKEN_FROM_ABOVE"`,
|
||||
``,
|
||||
`# Then run: history -d $((HISTCMD-1)) # zsh: print -s -- '' && fc -p`,
|
||||
].join('\n')
|
||||
: [
|
||||
`The user wants to connect GBrain MCP to your context. Here's how.`,
|
||||
``,
|
||||
`Run this command in terminal to register GBrain:`,
|
||||
``,
|
||||
`claude mcp add --transport http gbrain ${serverUrl}/mcp \\`,
|
||||
` --header "Authorization: Bearer PASTE_YOUR_API_KEY_HERE"`,
|
||||
``,
|
||||
`Replace PASTE_YOUR_API_KEY_HERE with the API key shown when "${agentName}" was created.`,
|
||||
`API keys never expire.`,
|
||||
].join('\n'),
|
||||
|
||||
'chatgpt': [
|
||||
`The user wants to connect GBrain MCP to ChatGPT.`,
|
||||
``,
|
||||
`1. Open ChatGPT > Settings > Tools & Integrations > Add MCP Server`,
|
||||
`2. Enter this URL — ChatGPT will auto-discover the OAuth configuration:`,
|
||||
``,
|
||||
` ${serverUrl}/.well-known/oauth-authorization-server`,
|
||||
``,
|
||||
`3. When prompted for credentials:`,
|
||||
` Client ID: ${cid}`,
|
||||
` Client Secret: (the secret from agent registration)`,
|
||||
` Grant Type: client_credentials`,
|
||||
` Scope: ${agent.scope || 'read write'}`,
|
||||
].join('\n'),
|
||||
|
||||
'claude-cowork': [
|
||||
`The user wants to connect GBrain MCP to Claude.ai.`,
|
||||
``,
|
||||
`1. Open claude.ai > Settings > Connected Apps > Add MCP Server`,
|
||||
`2. Server URL: ${serverUrl}/mcp`,
|
||||
`3. When prompted for auth:`,
|
||||
` Token endpoint: ${serverUrl}/token`,
|
||||
` Client ID: ${cid}`,
|
||||
` Client Secret: (the secret from agent registration)`,
|
||||
` Scope: ${agent.scope || 'read write'}`,
|
||||
``,
|
||||
`Discovery URL: ${serverUrl}/.well-known/oauth-authorization-server`,
|
||||
].join('\n'),
|
||||
|
||||
cursor: isOAuth
|
||||
? [
|
||||
`The user wants to connect GBrain MCP to Cursor.`,
|
||||
``,
|
||||
`Cursor supports OAuth for remote MCP. Add to .cursor/mcp.json:`,
|
||||
``,
|
||||
`{`,
|
||||
` "mcpServers": {`,
|
||||
` "gbrain": {`,
|
||||
` "url": "${serverUrl}/mcp",`,
|
||||
` "transport": "sse"`,
|
||||
` }`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
`Cursor will auto-discover OAuth via:`,
|
||||
`${serverUrl}/.well-known/oauth-authorization-server`,
|
||||
``,
|
||||
`When prompted: Client ID ${cid}, use the secret from registration.`,
|
||||
].join('\n')
|
||||
: [
|
||||
`The user wants to connect GBrain MCP to Cursor.`,
|
||||
``,
|
||||
`Add to .cursor/mcp.json:`,
|
||||
``,
|
||||
`{`,
|
||||
` "mcpServers": {`,
|
||||
` "gbrain": {`,
|
||||
` "url": "${serverUrl}/mcp",`,
|
||||
` "transport": "sse",`,
|
||||
` "headers": {`,
|
||||
` "Authorization": "Bearer PASTE_YOUR_API_KEY_HERE"`,
|
||||
` }`,
|
||||
` }`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
`Replace PASTE_YOUR_API_KEY_HERE with the API key shown when "${agentName}" was created.`,
|
||||
].join('\n'),
|
||||
|
||||
perplexity: [
|
||||
`The user wants to connect GBrain MCP to Perplexity.`,
|
||||
``,
|
||||
`1. Go to Settings > Connectors > Add MCP`,
|
||||
`2. Server URL: ${serverUrl}/mcp`,
|
||||
`3. Client ID: ${cid}`,
|
||||
`4. Client Secret: (the secret from agent registration)`,
|
||||
].join('\n'),
|
||||
|
||||
json: JSON.stringify({
|
||||
server_url: serverUrl + '/mcp',
|
||||
token_url: serverUrl + '/token',
|
||||
discovery_url: serverUrl + '/.well-known/oauth-authorization-server',
|
||||
client_id: cid,
|
||||
client_name: agentName,
|
||||
auth_type: agent.auth_type,
|
||||
scope: agent.scope,
|
||||
}, null, 2),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="drawer-overlay" onClick={onClose} />
|
||||
<div className="drawer">
|
||||
<button className="drawer-close" onClick={onClose}>✕</button>
|
||||
<div style={{ fontSize: 18, fontWeight: 600, marginBottom: 4 }}>{agent.name || agent.client_name}</div>
|
||||
<span className={`badge ${agent.status === 'active' ? 'badge-success' : 'badge-danger'}`}>{agent.status}</span>
|
||||
|
||||
<div className="section-title">Details</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '100px 1fr', gap: '6px 12px', fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Client ID</span>
|
||||
<span className="mono">{(agent.id || agent.id || agent.client_id || '').substring(0, 24)}...</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Scopes</span>
|
||||
<span>{(agent.scope || '').split(' ').filter(Boolean).map(s => (
|
||||
<span key={s} className={`badge badge-${s}`} style={{ marginRight: 4 }}>{s}</span>
|
||||
))}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Registered</span>
|
||||
<span>{new Date(agent.created_at).toLocaleDateString()}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Token TTL</span>
|
||||
<span>{agent.token_ttl ? (agent.token_ttl >= 31536000 ? 'No expiry' : agent.token_ttl >= 86400 ? `${Math.floor(agent.token_ttl / 86400)}d` : agent.token_ttl >= 3600 ? `${Math.floor(agent.token_ttl / 3600)}h` : `${agent.token_ttl}s`) : '1h (default)'}</span>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
Config Export visible for both auth_type=oauth AND auth_type=api_key.
|
||||
Claude Code + Cursor + JSON tabs render real snippets regardless
|
||||
(commit 15's snippets are auth-type-aware for those two clients;
|
||||
JSON is just structured metadata). ChatGPT, Claude.ai, and
|
||||
Perplexity tabs render an "OAuth client required" message on
|
||||
api_key agents — those MCP clients only speak OAuth 2.0
|
||||
client_credentials, not raw bearer tokens.
|
||||
|
||||
Pre-fix (Wintermute commit 16): the entire Config Export
|
||||
section was hidden for api_key agents, dropping the working
|
||||
Claude Code + Cursor snippets along with the broken ones.
|
||||
(D5=C in the eng review.)
|
||||
*/}
|
||||
<div className="section-title">Config Export</div>
|
||||
<div className="tabs" style={{ flexWrap: 'wrap' }}>
|
||||
<div className={`tab ${tab === 'claude-code' ? 'active' : ''}`} onClick={() => setTab('claude-code')}>Claude Code</div>
|
||||
<div className={`tab ${tab === 'chatgpt' ? 'active' : ''}`} onClick={() => setTab('chatgpt')}>ChatGPT</div>
|
||||
<div className={`tab ${tab === 'claude-cowork' ? 'active' : ''}`} onClick={() => setTab('claude-cowork')}>Claude.ai</div>
|
||||
<div className={`tab ${tab === 'cursor' ? 'active' : ''}`} onClick={() => setTab('cursor')}>Cursor</div>
|
||||
<div className={`tab ${tab === 'perplexity' ? 'active' : ''}`} onClick={() => setTab('perplexity')}>Perplexity</div>
|
||||
<div className={`tab ${tab === 'json' ? 'active' : ''}`} onClick={() => setTab('json')}>JSON</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const oauthOnlyTabs = new Set(['chatgpt', 'claude-cowork', 'perplexity']);
|
||||
if (!isOAuth && oauthOnlyTabs.has(tab)) {
|
||||
const clientName = { chatgpt: 'ChatGPT', 'claude-cowork': 'Claude.ai', perplexity: 'Perplexity' }[tab] || tab;
|
||||
return (
|
||||
<div style={{
|
||||
background: 'rgba(255, 200, 100, 0.08)',
|
||||
border: '1px solid rgba(255, 200, 100, 0.2)',
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
marginTop: 12,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: 'var(--text-secondary)',
|
||||
}}>
|
||||
<div style={{ fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>
|
||||
{clientName} requires an OAuth client
|
||||
</div>
|
||||
{clientName} only supports OAuth 2.0 (client_credentials). API keys use raw bearer tokens, which {clientName} does not accept. Register a separate OAuth client and use that to connect this AI.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="code-block">
|
||||
<pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>{configSnippets[tab]}</pre>
|
||||
<button className="copy-btn" onClick={() => copy(configSnippets[tab])}>Copy</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div style={{ marginTop: 32 }}>
|
||||
{agent.status === 'active' && (
|
||||
<button className="btn btn-danger" onClick={async () => {
|
||||
if (!confirm(`Revoke ${agent.name || agent.client_name}? All active tokens will be invalidated.`)) return;
|
||||
try {
|
||||
if (agent.auth_type === 'oauth') {
|
||||
await api.revokeClient(agent.id || agent.client_id || '');
|
||||
} else {
|
||||
await api.revokeApiKey(agent.name || '');
|
||||
}
|
||||
onRevoked();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
alert('Revoke failed: ' + (e instanceof Error ? e.message : 'unknown error'));
|
||||
}
|
||||
}}>Revoke Agent</button>
|
||||
)}
|
||||
{agent.status === 'revoked' && (
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 13 }}>This agent has been revoked.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/**
|
||||
* v0.36.1.0 (T15 / E6) — Calibration tab.
|
||||
*
|
||||
* Fetches the active calibration profile + 4 server-rendered SVG charts.
|
||||
* Layout: Linear calm clarity (per D23 mockup variant-B) — single column,
|
||||
* generous whitespace, ONE big sparkline as hero, then patterns, then
|
||||
* domain bars, then abandoned threads.
|
||||
*
|
||||
* Per D23 — SVG markup comes from the server (image/svg+xml endpoint).
|
||||
* Admin SPA renders inside a TrustedSVG wrapper that uses
|
||||
* dangerouslySetInnerHTML. XSS posture: server-side escapeXml() on all
|
||||
* caller-controlled strings + requireAdmin middleware on the endpoint.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
interface CalibrationProfileSummary {
|
||||
holder: string;
|
||||
source_id: string;
|
||||
generated_at: string;
|
||||
published: boolean;
|
||||
total_resolved: number;
|
||||
brier: number | null;
|
||||
accuracy: number | null;
|
||||
partial_rate: number | null;
|
||||
grade_completion: number;
|
||||
pattern_statements: string[];
|
||||
active_bias_tags: string[];
|
||||
voice_gate_passed: boolean;
|
||||
voice_gate_attempts: number;
|
||||
}
|
||||
|
||||
interface ChartSvgProps {
|
||||
type: string;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
function TrustedSVG({ markup }: { markup: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{ width: '100%', overflow: 'auto' }}
|
||||
// Server-rendered SVG (image/svg+xml) gated by requireAdmin middleware.
|
||||
// All caller-controlled strings pass through escapeXml() server-side.
|
||||
dangerouslySetInnerHTML={{ __html: markup }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartSvg({ type, ariaLabel }: ChartSvgProps) {
|
||||
const [markup, setMarkup] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
api
|
||||
.calibrationChart(type)
|
||||
.then(svg => {
|
||||
if (!cancelled) setMarkup(svg);
|
||||
})
|
||||
.catch(err => {
|
||||
if (!cancelled) setError(err.message ?? 'fetch failed');
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [type]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 16, color: 'var(--error)' }} role="alert">
|
||||
{ariaLabel}: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!markup) {
|
||||
return <div style={{ padding: 16, color: 'var(--text-muted)' }}>{ariaLabel} loading...</div>;
|
||||
}
|
||||
return <TrustedSVG markup={markup} />;
|
||||
}
|
||||
|
||||
export function CalibrationPage() {
|
||||
const [profile, setProfile] = useState<CalibrationProfileSummary | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.calibrationProfile()
|
||||
.then(p => {
|
||||
setProfile(p);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
setError(err.message ?? 'fetch failed');
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: 24, color: 'var(--text-secondary)' }}>Loading calibration profile…</div>;
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 24, color: 'var(--error)' }} role="alert">
|
||||
Could not load calibration profile: {error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!profile) {
|
||||
return (
|
||||
<div style={{ padding: 24, maxWidth: 700 }}>
|
||||
<h1 style={{ marginBottom: 16 }}>Calibration</h1>
|
||||
<p style={{ color: 'var(--text-secondary)' }}>
|
||||
No calibration profile yet. Builds after 5+ resolved takes.
|
||||
</p>
|
||||
<pre
|
||||
style={{
|
||||
background: 'var(--bg-secondary)',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
color: 'var(--text-primary)',
|
||||
marginTop: 12,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}}
|
||||
>
|
||||
gbrain dream --phase calibration_profile
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const generated = new Date(profile.generated_at);
|
||||
const generatedAgo = Math.floor((Date.now() - generated.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
return (
|
||||
<div style={{ padding: 32, maxWidth: 720 }}>
|
||||
<h1 style={{ marginBottom: 8 }}>Calibration</h1>
|
||||
<div style={{ color: 'var(--text-muted)', fontSize: 13, marginBottom: 24 }}>
|
||||
Holder: {profile.holder}
|
||||
{' · '}
|
||||
Updated {generatedAgo === 0 ? 'today' : `${generatedAgo}d ago`}
|
||||
{profile.published && ' · published'}
|
||||
{profile.grade_completion < 0.9 && ` · ~${Math.round(profile.grade_completion * 100)}% graded`}
|
||||
{!profile.voice_gate_passed && ' · voice gate fell back to template'}
|
||||
</div>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<ChartSvg type="brier-trend" ariaLabel="Brier trend" />
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<h2 style={{ fontSize: 14, color: 'var(--text-secondary)', marginBottom: 12, fontWeight: 400 }}>
|
||||
Pattern statements
|
||||
</h2>
|
||||
<ChartSvg type="pattern-statements" ariaLabel="Pattern statements" />
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<ChartSvg type="domain-bars" ariaLabel="Per-domain accuracy" />
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: 32 }}>
|
||||
<ChartSvg type="abandoned-threads" ariaLabel="Abandoned threads" />
|
||||
</section>
|
||||
|
||||
{profile.active_bias_tags.length > 0 && (
|
||||
<section style={{ marginBottom: 32, color: 'var(--text-muted)', fontSize: 13 }}>
|
||||
Active bias tags: {profile.active_bias_tags.join(', ')}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
interface FeedEvent {
|
||||
agent: string;
|
||||
operation: string;
|
||||
scopes: string;
|
||||
latency_ms: number;
|
||||
status: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const [stats, setStats] = useState({ connected_agents: 0, requests_today: 0, active_tokens: 0 });
|
||||
const [health, setHealth] = useState({ expiring_soon: 0, error_rate: '0%' });
|
||||
const [events, setEvents] = useState<FeedEvent[]>([]);
|
||||
const [sseStatus, setSseStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.stats().then(setStats).catch(() => {});
|
||||
api.health().then(setHealth).catch(() => {});
|
||||
|
||||
const es = new EventSource('/admin/events');
|
||||
eventSourceRef.current = es;
|
||||
es.onopen = () => setSseStatus('connected');
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const event = JSON.parse(e.data) as FeedEvent;
|
||||
setEvents(prev => [event, ...prev].slice(0, 50));
|
||||
} catch {}
|
||||
};
|
||||
es.onerror = () => {
|
||||
setSseStatus('disconnected');
|
||||
setTimeout(() => {
|
||||
setSseStatus('connecting');
|
||||
es.close();
|
||||
// Reconnect handled by browser EventSource auto-retry
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const interval = setInterval(() => {
|
||||
api.stats().then(setStats).catch(() => {});
|
||||
api.health().then(setHealth).catch(() => {});
|
||||
}, 30000);
|
||||
|
||||
return () => { es.close(); clearInterval(interval); };
|
||||
}, []);
|
||||
|
||||
const timeAgo = (ts: string) => {
|
||||
const diff = Date.now() - new Date(ts).getTime();
|
||||
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
|
||||
return `${Math.floor(diff / 3600000)}h ago`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="page-title">Dashboard</h1>
|
||||
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="metrics">
|
||||
<div className="metric">
|
||||
<div className="metric-value">{stats.connected_agents}</div>
|
||||
<div className="metric-label">Connected Agents</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-value">{stats.requests_today}</div>
|
||||
<div className="metric-label">Requests Today</div>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<div className="metric-value">{stats.active_tokens}</div>
|
||||
<div className="metric-label">Active Tokens</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="section-title">
|
||||
Live Activity
|
||||
<span style={{ marginLeft: 8, fontSize: 10, color: sseStatus === 'connected' ? 'var(--success)' : sseStatus === 'connecting' ? 'var(--warning)' : 'var(--error)' }}>
|
||||
{sseStatus === 'connected' ? '● connected' : sseStatus === 'connecting' ? '● connecting...' : '● disconnected'}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="feed">
|
||||
{events.length === 0 ? (
|
||||
<div className="feed-empty">
|
||||
{sseStatus === 'connected' ? 'No requests yet. Agents will appear when they connect.' : 'Connecting...'}
|
||||
</div>
|
||||
) : (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Agent</th>
|
||||
<th>Operation</th>
|
||||
<th>Scopes</th>
|
||||
<th>Latency</th>
|
||||
<th>Status</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map((e, i) => (
|
||||
<tr key={i}>
|
||||
<td className="mono">{e.agent}</td>
|
||||
<td className="mono">{e.operation}</td>
|
||||
<td>{e.scopes.split(',').map(s => (
|
||||
<span key={s} className={`badge badge-${s.trim()}`} style={{ marginRight: 4 }}>{s.trim()}</span>
|
||||
))}</td>
|
||||
<td className="mono">{e.latency_ms} ms</td>
|
||||
<td><span className={`badge badge-${e.status}`}>{e.status}</span></td>
|
||||
<td style={{ color: 'var(--text-secondary)' }}>{timeAgo(e.timestamp)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ width: 220 }}>
|
||||
<h2 className="section-title">Token Health</h2>
|
||||
<div className="health-panel">
|
||||
<div className="health-row">
|
||||
<span style={{ color: 'var(--warning)' }}>Expiring Soon</span>
|
||||
<span className="mono">{health.expiring_soon}</span>
|
||||
</div>
|
||||
<div className="health-row">
|
||||
<span style={{ color: 'var(--error)' }}>Error Rate</span>
|
||||
<span className="mono">{health.error_rate}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
/**
|
||||
* v0.41 D2 — live jobs dashboard. Browser counterpart to the TTY
|
||||
* `gbrain jobs watch` command. Polls `/admin/api/jobs/watch` every
|
||||
* 1s (matches TTY refresh cadence; SSE upgrade is a v0.42 follow-up
|
||||
* once the same wiring lands in serve-http for the TTY command).
|
||||
*
|
||||
* Layout intentionally matches the TTY 1:1 so an operator looking at
|
||||
* both surfaces sees the same panels in the same order.
|
||||
*/
|
||||
|
||||
interface WatchSnapshot {
|
||||
ts_ms: number;
|
||||
by_type: Array<{ name: string; total: number; completed: number; failed: number; dead: number }>;
|
||||
queue_health: { waiting: number; active: number; stalled: number };
|
||||
lease_pressure_1h: number;
|
||||
top_errors: Array<{ cluster: string; count: number }>;
|
||||
budget_owners: Array<{ owner_id: number; remaining_cents: number; total_spent_cents: number }>;
|
||||
}
|
||||
|
||||
function leasePressureColor(n: number): string {
|
||||
if (n === 0) return 'var(--accent-success, #2ea043)';
|
||||
if (n >= 100) return 'var(--accent-danger, #f85149)';
|
||||
return 'var(--accent-warn, #d29922)';
|
||||
}
|
||||
|
||||
function dollars(cents: number): string {
|
||||
return `$${(cents / 100).toFixed(2)}`;
|
||||
}
|
||||
|
||||
export function JobsWatchPage() {
|
||||
const [snap, setSnap] = useState<WatchSnapshot | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const tick = async () => {
|
||||
try {
|
||||
const data = await api.jobsWatch();
|
||||
if (alive) {
|
||||
setSnap(data);
|
||||
setErr(null);
|
||||
}
|
||||
} catch (e) {
|
||||
if (alive) setErr(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
if (alive) timer = setTimeout(tick, 1000);
|
||||
};
|
||||
|
||||
tick();
|
||||
return () => {
|
||||
alive = false;
|
||||
if (timer) clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<div style={{ padding: 24, color: 'var(--accent-danger, #f85149)' }}>
|
||||
<h2>Jobs Watch — error</h2>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{err}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!snap) {
|
||||
return <div style={{ padding: 24, color: 'var(--text-muted, #777)' }}>Loading jobs watch…</div>;
|
||||
}
|
||||
|
||||
const ts = new Date(snap.ts_ms).toLocaleTimeString();
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, fontFamily: 'var(--font-mono, "JetBrains Mono", monospace)' }}>
|
||||
<h1 style={{ fontSize: 18, marginBottom: 4 }}>
|
||||
Jobs Watch
|
||||
<span style={{ marginLeft: 12, color: 'var(--text-muted, #777)', fontSize: 12, fontWeight: 'normal' }}>
|
||||
updated {ts}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h2 style={{ fontSize: 14, marginBottom: 8 }}>Queue</h2>
|
||||
<div>
|
||||
waiting=<b>{snap.queue_health.waiting}</b>{' '}
|
||||
active=<b>{snap.queue_health.active}</b>{' '}
|
||||
stalled=<b style={{ color: snap.queue_health.stalled > 0 ? 'var(--accent-warn, #d29922)' : undefined }}>
|
||||
{snap.queue_health.stalled}
|
||||
</b>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{snap.by_type.length > 0 && (
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h2 style={{ fontSize: 14, marginBottom: 8 }}>By type (24h)</h2>
|
||||
<table style={{ borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ color: 'var(--text-muted, #777)', fontSize: 12 }}>
|
||||
<th style={{ textAlign: 'left', padding: '4px 12px 4px 0' }}>name</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 12px' }}>total</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 12px' }}>done</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 12px' }}>fail</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 12px' }}>dead</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snap.by_type.slice(0, 6).map(t => (
|
||||
<tr key={t.name}>
|
||||
<td style={{ padding: '4px 12px 4px 0' }}>{t.name}</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px' }}>{t.total}</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px' }}>{t.completed}</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px' }}>{t.failed}</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px' }}>{t.dead}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h2 style={{ fontSize: 14, marginBottom: 8 }}>Lease pressure (1h)</h2>
|
||||
<div style={{ color: leasePressureColor(snap.lease_pressure_1h) }}>
|
||||
{snap.lease_pressure_1h} bounce{snap.lease_pressure_1h === 1 ? '' : 's'}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{snap.top_errors.length > 0 && (
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h2 style={{ fontSize: 14, marginBottom: 8 }}>Top errors (24h)</h2>
|
||||
<table style={{ borderCollapse: 'collapse' }}>
|
||||
<tbody>
|
||||
{snap.top_errors.slice(0, 5).map(e => (
|
||||
<tr key={e.cluster}>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px 4px 0', color: 'var(--text-muted, #777)' }}>
|
||||
{e.count}×
|
||||
</td>
|
||||
<td style={{ padding: '4px 12px 4px 0' }}>{e.cluster}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{snap.budget_owners.length > 0 && (
|
||||
<section style={{ marginTop: 24 }}>
|
||||
<h2 style={{ fontSize: 14, marginBottom: 8 }}>Budget owners</h2>
|
||||
<table style={{ borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ color: 'var(--text-muted, #777)', fontSize: 12 }}>
|
||||
<th style={{ textAlign: 'left', padding: '4px 12px 4px 0' }}>owner</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 12px' }}>spent</th>
|
||||
<th style={{ textAlign: 'right', padding: '4px 12px' }}>remaining</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snap.budget_owners.slice(0, 5).map(b => (
|
||||
<tr key={b.owner_id}>
|
||||
<td style={{ padding: '4px 12px 4px 0' }}>{b.owner_id}</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px' }}>{dollars(b.total_spent_cents)}</td>
|
||||
<td style={{ textAlign: 'right', padding: '4px 12px' }}>{dollars(b.remaining_cents)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
// v0.26.3 trust model (D11 + D12):
|
||||
// - The bootstrap token is NEVER stored in browser JS state. No
|
||||
// localStorage, no sessionStorage, no React state beyond the form
|
||||
// submit cycle. After successful POST /admin/login the operator's
|
||||
// token only lives in the HttpOnly cookie that the server set.
|
||||
// - Magic-link URLs use single-use server-issued nonces, not the
|
||||
// bootstrap token itself (see /admin/api/issue-magic-link). The
|
||||
// bootstrap token never appears in a URL.
|
||||
// - Closing the tab ends the session client-side. Reopening the
|
||||
// dashboard 401s and shows this page again. Operator asks the agent
|
||||
// for a fresh magic link or pastes the bootstrap token from the
|
||||
// server's terminal scrollback.
|
||||
export function LoginPage({ onLogin }: { onLogin: () => void }) {
|
||||
const [token, setToken] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.login(token);
|
||||
// Don't persist the token. The HttpOnly cookie is the only
|
||||
// session credential after this point.
|
||||
setToken('');
|
||||
onLogin();
|
||||
} catch (err) {
|
||||
setError('Invalid token.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-box">
|
||||
<div className="login-logo">GBrain</div>
|
||||
|
||||
<div style={{
|
||||
background: 'rgba(136, 170, 255, 0.08)',
|
||||
border: '1px solid rgba(136, 170, 255, 0.2)',
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
marginBottom: 20,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.5,
|
||||
color: 'var(--text-secondary)',
|
||||
}}>
|
||||
<div style={{ fontWeight: 600, color: 'var(--text-primary)', marginBottom: 6 }}>
|
||||
🔒 This is a protected dashboard
|
||||
</div>
|
||||
Ask your AI agent for the admin login link:
|
||||
<div style={{
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
borderRadius: 6,
|
||||
padding: '8px 12px',
|
||||
marginTop: 8,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12,
|
||||
color: '#88aaff',
|
||||
wordBreak: 'break-all',
|
||||
}}>
|
||||
"Give me the GBrain admin login link"
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
Each link is single-use. Your agent generates a fresh one each time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details style={{ marginBottom: 16 }}>
|
||||
<summary style={{ cursor: 'pointer', fontSize: 13, color: 'var(--text-muted)' }}>
|
||||
Or paste bootstrap token manually
|
||||
</summary>
|
||||
<form onSubmit={handleSubmit} style={{ marginTop: 12 }}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Admin Token"
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-primary" style={{ width: '100%' }} disabled={loading}>
|
||||
{loading ? 'Authenticating...' : 'Submit'}
|
||||
</button>
|
||||
{error && <div className="login-error">{error}</div>}
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { api } from '../api';
|
||||
|
||||
interface LogEntry {
|
||||
id: number;
|
||||
token_name: string;
|
||||
agent_name: string;
|
||||
operation: string;
|
||||
latency_ms: number;
|
||||
status: string;
|
||||
params: Record<string, unknown> | null;
|
||||
error_message: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export function RequestLogPage() {
|
||||
const [data, setData] = useState<{ rows: LogEntry[]; total: number; page: number; pages: number }>({
|
||||
rows: [], total: 0, page: 1, pages: 1,
|
||||
});
|
||||
const [page, setPage] = useState(1);
|
||||
const [agentFilter, setAgentFilter] = useState('all');
|
||||
const [expandedRow, setExpandedRow] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => { loadPage(page); }, [page, agentFilter]);
|
||||
|
||||
const loadPage = (p: number) => {
|
||||
const qs = agentFilter !== 'all' ? `&agent=${encodeURIComponent(agentFilter)}` : '';
|
||||
api.requests(p, qs).then(setData).catch(() => {});
|
||||
};
|
||||
|
||||
const timeAgo = (ts: string) => {
|
||||
const diff = Date.now() - new Date(ts).getTime();
|
||||
if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
||||
return new Date(ts).toLocaleDateString();
|
||||
};
|
||||
|
||||
|
||||
|
||||
const formatParams = (params: Record<string, unknown> | null) => {
|
||||
if (!params) return null;
|
||||
const { query, slug, partial, limit, ...rest } = params as any;
|
||||
const parts: string[] = [];
|
||||
if (query) parts.push(`"${query}"`);
|
||||
if (slug) parts.push(slug);
|
||||
if (partial) parts.push(`~${partial}`);
|
||||
if (limit) parts.push(`limit=${limit}`);
|
||||
if (Object.keys(rest).length > 0) parts.push(`+${Object.keys(rest).length} params`);
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
// Collect unique agents for filter (use name for display, token_name for value)
|
||||
const agentMap = new Map<string, string>();
|
||||
data.rows.forEach(r => { if (r.token_name) agentMap.set(r.token_name, r.agent_name || r.token_name); });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1 className="page-title" style={{ marginBottom: 0 }}>Request Log</h1>
|
||||
<select value={agentFilter} onChange={e => { setAgentFilter(e.target.value); setPage(1); }}
|
||||
style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border)', borderRadius: 6, padding: '4px 8px', fontSize: 13 }}>
|
||||
<option value="all">All agents</option>
|
||||
{[...agentMap.entries()].map(([id, name]) => <option key={id} value={id}>{name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{data.rows.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: 48, color: 'var(--text-muted)' }}>
|
||||
No requests yet.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Agent</th>
|
||||
<th>Operation</th>
|
||||
<th>Params</th>
|
||||
<th>Latency</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.rows.map(r => (
|
||||
<React.Fragment key={r.id}>
|
||||
<tr onClick={() => setExpandedRow(expandedRow === r.id ? null : r.id)}
|
||||
style={{ cursor: 'pointer' }}>
|
||||
<td style={{ color: 'var(--text-secondary)', whiteSpace: 'nowrap' }}>{timeAgo(r.created_at)}</td>
|
||||
<td>
|
||||
<a style={{ color: 'var(--text-link, #88aaff)', cursor: 'pointer', textDecoration: 'none', fontWeight: 500 }}
|
||||
onClick={(e) => { e.stopPropagation(); setAgentFilter(r.token_name); setPage(1); }}>
|
||||
{r.agent_name || r.token_name}
|
||||
</a>
|
||||
</td>
|
||||
<td className="mono">{r.operation}</td>
|
||||
<td style={{ color: 'var(--text-secondary)', fontSize: 12, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{formatParams(r.params)}
|
||||
</td>
|
||||
<td className="mono">{r.latency_ms}ms</td>
|
||||
<td><span className={`badge badge-${r.status}`}>{r.status}</span></td>
|
||||
</tr>
|
||||
{expandedRow === r.id && (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ background: 'var(--bg-secondary, #0f0f1a)', padding: 16 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '100px 1fr', gap: '6px 12px', fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Time</span>
|
||||
<span>{new Date(r.created_at).toLocaleString()}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Agent</span>
|
||||
<span className="mono">{r.token_name}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Operation</span>
|
||||
<span className="mono">{r.operation}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Latency</span>
|
||||
<span>{r.latency_ms}ms</span>
|
||||
{r.params && (
|
||||
<>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Params</span>
|
||||
<pre className="mono" style={{ margin: 0, whiteSpace: 'pre-wrap', fontSize: 12 }}>
|
||||
{JSON.stringify(r.params, null, 2)}
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
{r.error_message && (
|
||||
<>
|
||||
<span style={{ color: 'var(--error, #ff6b6b)' }}>Error</span>
|
||||
<span style={{ color: 'var(--error, #ff6b6b)' }}>{r.error_message}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="pagination">
|
||||
<span>Page {data.page} of {data.pages} ({data.total} total)</span>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button disabled={data.page <= 1} onClick={() => setPage(p => p - 1)}>Previous</button>
|
||||
<button disabled={data.page >= data.pages} onClick={() => setPage(p => p + 1)}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: '/admin/',
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@@ -1,624 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "gbrain",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.71",
|
||||
"@ai-sdk/google": "^3.0.64",
|
||||
"@ai-sdk/openai": "^3.0.53",
|
||||
"@ai-sdk/openai-compatible": "^2.0.41",
|
||||
"@anthropic-ai/sdk": "^0.30.0",
|
||||
"@aws-sdk/client-s3": "^3.1028.0",
|
||||
"@dqbd/tiktoken": "^1.0.22",
|
||||
"@electric-sql/pglite": "0.4.3",
|
||||
"@jsquash/avif": "^2.1.1",
|
||||
"@jsquash/png": "^3.1.1",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"ai": "^6.0.168",
|
||||
"chokidar": "^4.0.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"eventsource-parser": "^3.0.8",
|
||||
"exifr": "^7.1.3",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"heic-decode": "^2.1.0",
|
||||
"js-yaml": "^3.14.2",
|
||||
"marked": "^18.0.0",
|
||||
"openai": "^4.0.0",
|
||||
"pgvector": "^0.2.0",
|
||||
"postgres": "^3.4.0",
|
||||
"tree-sitter-wasms": "0.1.13",
|
||||
"web-tree-sitter": "0.22.6",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/js-yaml": "^3.12.10",
|
||||
"bun-types": "^1.3.13",
|
||||
"fast-check": "^4.8.0",
|
||||
"typescript": "^5.6.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@electric-sql/pglite",
|
||||
],
|
||||
"packages": {
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.74", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Xew9rfz9WWhDSyF8rNhjT/XWOWelNfJrMlmG0Ahw210hStisRpQZ1s+7VeI9JTJOZ5y5tXqBi5kfPwYnCfyRTA=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.109", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-r6dOqThjODp1vOhGRJg2OCmyB/ZOQtGx1esZ2SDvwDX5XoX8dBqYaYjLg8MPXTzMGJSgOkJyCxWgUcZtAl16pw=="],
|
||||
|
||||
"@ai-sdk/google": ["@ai-sdk/google@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Qeq+SidYtzMrcf0fdw3L0QLmtXK+ErwdBzbxS4+0Q/2UP85Ges8RJJcbAj7SO8e2JbeJoM35BLqkeNy1o3wJvQ=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2+5xGMROmrBboJuoOwqLL3b/o3i56+NRdxXDNVAiTyYjLiBj6KzembeuyuBT217be1X+zkEfAqD1H0irJlGIyw=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5YBvurNL7Oj7mT3srws4Rh4cQidoorfEGObAOb5jV40eld8IC7EkXWARZjnWYqgYzabUs6Sn6muiXfQVkgOyOQ=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.30.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-nuKvp7wOIz6BFei8WrTdhmSsx5mwnArYyJgh4+vYu3V4J0Ltb8Xm3odPm51n1aSI0XxNCrDl7O88cxCtUdAkaw=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
|
||||
|
||||
"@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="],
|
||||
|
||||
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||
|
||||
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||
|
||||
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1028.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/credential-provider-node": "^3.972.30", "@aws-sdk/middleware-bucket-endpoint": "^3.972.9", "@aws-sdk/middleware-expect-continue": "^3.972.9", "@aws-sdk/middleware-flexible-checksums": "^3.974.7", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-location-constraint": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-sdk-s3": "^3.972.28", "@aws-sdk/middleware-ssec": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/signature-v4-multi-region": "^3.996.16", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/eventstream-serde-browser": "^4.2.13", "@smithy/eventstream-serde-config-resolver": "^4.3.13", "@smithy/eventstream-serde-node": "^4.2.13", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-blob-browser": "^4.2.14", "@smithy/hash-node": "^4.2.13", "@smithy/hash-stream-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/md5-js": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.15", "tslib": "^2.6.2" } }, "sha512-KL8PREFJxyWXUjMQR6Krq/OjZ5qbcV1QFjtA7Q7oMW5XaFO9YoSBtBxQeeXO4um6vYSmRVYVDTvEKZDcNbyeXw=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.973.27", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/xml-builder": "^3.972.17", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A=="],
|
||||
|
||||
"@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.6", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.27", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-login": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.30", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.25", "@aws-sdk/credential-provider-http": "^3.972.27", "@aws-sdk/credential-provider-ini": "^3.972.29", "@aws-sdk/credential-provider-process": "^3.972.25", "@aws-sdk/credential-provider-sso": "^3.972.29", "@aws-sdk/credential-provider-web-identity": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.25", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/token-providers": "3.1026.0", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew=="],
|
||||
|
||||
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.13", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg=="],
|
||||
|
||||
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA=="],
|
||||
|
||||
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/crc64-nvme": "^3.972.6", "@aws-sdk/types": "^3.973.7", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.13", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w=="],
|
||||
|
||||
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ=="],
|
||||
|
||||
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w=="],
|
||||
|
||||
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog=="],
|
||||
|
||||
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ=="],
|
||||
|
||||
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.28", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg=="],
|
||||
|
||||
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ=="],
|
||||
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.29", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-retry": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.19", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.27", "@aws-sdk/middleware-host-header": "^3.972.9", "@aws-sdk/middleware-logger": "^3.972.9", "@aws-sdk/middleware-recursion-detection": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/region-config-resolver": "^3.972.11", "@aws-sdk/types": "^3.973.7", "@aws-sdk/util-endpoints": "^3.996.6", "@aws-sdk/util-user-agent-browser": "^3.972.9", "@aws-sdk/util-user-agent-node": "^3.973.15", "@smithy/config-resolver": "^4.4.14", "@smithy/core": "^3.23.14", "@smithy/fetch-http-handler": "^5.3.16", "@smithy/hash-node": "^4.2.13", "@smithy/invalid-dependency": "^4.2.13", "@smithy/middleware-content-length": "^4.2.13", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-retry": "^4.5.0", "@smithy/middleware-serde": "^4.2.17", "@smithy/middleware-stack": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/node-http-handler": "^4.5.2", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.45", "@smithy/util-defaults-mode-node": "^4.2.49", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q=="],
|
||||
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/config-resolver": "^4.4.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.16", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.28", "@aws-sdk/types": "^3.973.7", "@smithy/protocol-http": "^5.3.13", "@smithy/signature-v4": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1026.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.27", "@aws-sdk/nested-clients": "^3.996.19", "@aws-sdk/types": "^3.973.7", "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.7", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg=="],
|
||||
|
||||
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
|
||||
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-endpoints": "^3.3.4", "tslib": "^2.6.2" } }, "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg=="],
|
||||
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.7", "@smithy/types": "^4.14.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.15", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.29", "@aws-sdk/types": "^3.973.7", "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.17", "", { "dependencies": { "@smithy/types": "^4.14.0", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg=="],
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
|
||||
|
||||
"@dqbd/tiktoken": ["@dqbd/tiktoken@1.0.22", "", {}, "sha512-RYhO8xeHkMNX5Ixqf4M1Ve3siCYJY/dI0yLnlX4M4oIEDOvjMIQ+E+3OUpAaZcWTaMtQJzGcDAghYfllpx3i/w=="],
|
||||
|
||||
"@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
|
||||
|
||||
"@jsquash/avif": ["@jsquash/avif@2.1.1", "", { "dependencies": { "wasm-feature-detect": "^1.2.11" } }, "sha512-LMRxd0fMgfCLtobDh0/sFYJMMiRJTNYSEEWvRDKXlAeZ08t3gI5V+1thIT0XjXJ+SVG7Zug9B0XPyx0Ti5VRNA=="],
|
||||
|
||||
"@jsquash/png": ["@jsquash/png@3.1.1", "", {}, "sha512-C10pc+0H6j0h8fENOfnGOvkXCmvpSQTDGlfGd0sHphZhPSGTyLjIrHba0FaZZdsKqA/wlmhYicUHb92vfZphaw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="],
|
||||
|
||||
"@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="],
|
||||
|
||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.14", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.4", "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.23.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ=="],
|
||||
|
||||
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.13", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg=="],
|
||||
|
||||
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA=="],
|
||||
|
||||
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.13", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.13", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA=="],
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.16", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ=="],
|
||||
|
||||
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.14", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA=="],
|
||||
|
||||
"@smithy/hash-node": ["@smithy/hash-node@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA=="],
|
||||
|
||||
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg=="],
|
||||
|
||||
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="],
|
||||
|
||||
"@smithy/md5-js": ["@smithy/md5-js@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ=="],
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.29", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-serde": "^4.2.17", "@smithy/node-config-provider": "^4.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw=="],
|
||||
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.1", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/node-config-provider": "^4.3.13", "@smithy/protocol-http": "^5.3.13", "@smithy/service-error-classification": "^4.2.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "@smithy/util-middleware": "^4.2.13", "@smithy/util-retry": "^4.3.1", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA=="],
|
||||
|
||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.17", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ=="],
|
||||
|
||||
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw=="],
|
||||
|
||||
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.13", "", { "dependencies": { "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.2", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA=="],
|
||||
|
||||
"@smithy/property-provider": ["@smithy/property-provider@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ=="],
|
||||
|
||||
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg=="],
|
||||
|
||||
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="],
|
||||
|
||||
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA=="],
|
||||
|
||||
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0" } }, "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw=="],
|
||||
|
||||
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.8", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.13", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg=="],
|
||||
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.12.9", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-stack": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.14.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ=="],
|
||||
|
||||
"@smithy/url-parser": ["@smithy/url-parser@4.2.13", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw=="],
|
||||
|
||||
"@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="],
|
||||
|
||||
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="],
|
||||
|
||||
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="],
|
||||
|
||||
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
|
||||
|
||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="],
|
||||
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.45", "", { "dependencies": { "@smithy/property-provider": "^4.2.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw=="],
|
||||
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.49", "", { "dependencies": { "@smithy/config-resolver": "^4.4.14", "@smithy/credential-provider-imds": "^4.2.13", "@smithy/node-config-provider": "^4.3.13", "@smithy/property-provider": "^4.2.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ=="],
|
||||
|
||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog=="],
|
||||
|
||||
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="],
|
||||
|
||||
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.13", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow=="],
|
||||
|
||||
"@smithy/util-retry": ["@smithy/util-retry@4.3.1", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ=="],
|
||||
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.5.22", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew=="],
|
||||
|
||||
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
|
||||
|
||||
"@smithy/util-waiter": ["@smithy/util-waiter@4.2.15", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg=="],
|
||||
|
||||
"@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
|
||||
"@types/cookie-parser": ["@types/cookie-parser@1.4.10", "", { "peerDependencies": { "@types/express": "*" } }, "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg=="],
|
||||
|
||||
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
|
||||
|
||||
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@3.12.10", "", {}, "sha512-/Mtaq/wf+HxXpvhzFYzrzCqNRcA958sW++7JOFC8nPrZcvfi/TrzOaaGbvt27ltJB2NQbHVAg5a1wUCsyMH7NA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
||||
|
||||
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||
|
||||
"ai": ["ai@6.0.174", "", { "dependencies": { "@ai-sdk/gateway": "3.0.109", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bTrfLUWHWtkjzWyCY4bmyuk4Qvmj4S4NSNsXyNSVVqkmftQNtxRj7dzUoMeQDBBwlJO6fC7m2Q/lNOPqQQfAGA=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-parser": ["cookie-parser@1.4.7", "", { "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.6" } }, "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="],
|
||||
|
||||
"exifr": ["exifr@7.1.3", "", {}, "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="],
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||
|
||||
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"heic-decode": ["heic-decode@2.1.0", "", { "dependencies": { "libheif-js": "^1.19.8" } }, "sha512-0fB3O3WMk38+PScbHLVp66jcNhsZ/ErtQ6u2lMYu/YxXgbBtl+oKOhGQHa4RpvE68k8IzbWkABzHnyAIjR758A=="],
|
||||
|
||||
"hono": ["hono@4.12.10", "", {}, "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"libheif-js": ["libheif-js@1.19.8", "", {}, "sha512-vQJWusIxO7wavpON1dusciL8Go9jsIQ+EUrckauFYAiSTjcmLAsuJh3SszLpvkwPci3JcL41ek2n+LUZGFpPIQ=="],
|
||||
|
||||
"marked": ["marked@18.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-2e7Qiv/HJSXj8rDEpgTvGKsP8yYtI9xXHKDnrftrmnrJPaFNM7VRb2YCzWaX4BP1iCJ/XPduzDJZMFoqTCcIMA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
|
||||
|
||||
"pgvector": ["pgvector@0.2.1", "", {}, "sha512-nKaQY9wtuiidwLMdVIce1O3kL0d+FxrigCVzsShnoqzOSaWWWOvuctb/sYwlai5cTwwzRSNa+a/NtN2kVZGNJw=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
|
||||
|
||||
"strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"tree-sitter-wasms": ["tree-sitter-wasms@0.1.13", "", { "dependencies": { "tree-sitter-wasms": "^0.1.11" } }, "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"wasm-feature-detect": ["wasm-feature-detect@1.8.0", "", {}, "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ=="],
|
||||
|
||||
"web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||
|
||||
"web-tree-sitter": ["web-tree-sitter@0.22.6", "", {}, "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@8.3.2", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
|
||||
|
||||
"@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"eventsource/eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
[test]
|
||||
# PGLite WASM cold start + initSchema() runs ~5–20s on loaded machines.
|
||||
# Default 5s is too short for those tests' beforeAll hooks. 60s is the
|
||||
# empirical ceiling we observed for the slowest cold-init paths.
|
||||
#
|
||||
# v0.26.4: scripts/run-unit-parallel.sh and scripts/run-unit-shard.sh
|
||||
# also pass `--timeout=60000` explicitly so the ceiling is consistent
|
||||
# whether tests are invoked through the wrapper or directly via bun test.
|
||||
timeout = 60_000
|
||||
|
||||
# v0.37 fix wave: pin gateway defaults to legacy OpenAI/1536 BEFORE any
|
||||
# test runs, so the 20+ test files with hardcoded 1536-d Float32Array
|
||||
# fixtures still match the schema. v0.37's production default is ZE/1280;
|
||||
# tests that want the new default call configureGateway() explicitly in
|
||||
# their own beforeAll.
|
||||
preload = ["./test/helpers/legacy-embedding-preload.ts"]
|
||||
@@ -1,117 +0,0 @@
|
||||
# docker-compose.ci.yml
|
||||
#
|
||||
# Local CI gate with 4-way E2E sharding. Spins up 4 pgvector services + a bun
|
||||
# runner that bind-mounts the repo. Used by `bun run ci:local` and
|
||||
# `bun run ci:local:diff` (see scripts/ci-local.sh).
|
||||
#
|
||||
# All services are pulled as `image:` (no build) so `docker compose pull`
|
||||
# refreshes everything. The bun version floats with `oven/bun:1` to track CI's
|
||||
# `bun-version: latest`. Named volumes isolate the Linux container's deps from
|
||||
# the host's darwin-arm64 deps and keep bun + postgres data warm across runs.
|
||||
#
|
||||
# Why 4 postgres services: bun's E2E suite shares one DB across 36 files and
|
||||
# uses TRUNCATE CASCADE in setupDB(). Running files in parallel against ONE DB
|
||||
# races (file A's TRUNCATE clobbers file B's fixture import). 4 separate DBs
|
||||
# remove the race; we shard the file list 1/4..4/4 and run shards in parallel.
|
||||
# Within a shard, files still run sequentially. Total wall-time on a 16-core
|
||||
# host: ~6 min sequential -> ~1.5-2 min sharded.
|
||||
#
|
||||
# Postgres host ports default to 5434-5437 (avoid 5432 manual `gbrain-test-pg`
|
||||
# and 5433 sibling-project conflicts). Override BASE port with GBRAIN_CI_PG_PORT;
|
||||
# shards take BASE..BASE+3.
|
||||
|
||||
services:
|
||||
postgres-1:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- "${GBRAIN_CI_PG_PORT:-5434}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d gbrain_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- gbrain-ci-pg-data-1:/var/lib/postgresql/data
|
||||
|
||||
postgres-2:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- "${GBRAIN_CI_PG_PORT_2:-5435}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d gbrain_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- gbrain-ci-pg-data-2:/var/lib/postgresql/data
|
||||
|
||||
postgres-3:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- "${GBRAIN_CI_PG_PORT_3:-5436}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d gbrain_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- gbrain-ci-pg-data-3:/var/lib/postgresql/data
|
||||
|
||||
postgres-4:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- "${GBRAIN_CI_PG_PORT_4:-5437}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d gbrain_test"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- gbrain-ci-pg-data-4:/var/lib/postgresql/data
|
||||
|
||||
runner:
|
||||
image: oven/bun:1
|
||||
working_dir: /app
|
||||
depends_on:
|
||||
postgres-1:
|
||||
condition: service_healthy
|
||||
postgres-2:
|
||||
condition: service_healthy
|
||||
postgres-3:
|
||||
condition: service_healthy
|
||||
postgres-4:
|
||||
condition: service_healthy
|
||||
# No global DATABASE_URL — scripts/ci-local.sh sets per-shard URL via -e.
|
||||
# Unit phase explicitly unsets DATABASE_URL so test/e2e/* gracefully skip.
|
||||
volumes:
|
||||
- .:/app
|
||||
# Linux container's node_modules MUST be isolated from host darwin-arm64.
|
||||
# Without this, container `bun install` stomps host node_modules and
|
||||
# subsequent `bun test` on host fails with binary-incompat errors.
|
||||
- gbrain-ci-node-modules:/app/node_modules
|
||||
# Warm install cache across runs.
|
||||
- gbrain-ci-bun-cache:/root/.bun/install/cache
|
||||
|
||||
volumes:
|
||||
gbrain-ci-pg-data-1:
|
||||
gbrain-ci-pg-data-2:
|
||||
gbrain-ci-pg-data-3:
|
||||
gbrain-ci-pg-data-4:
|
||||
gbrain-ci-node-modules:
|
||||
gbrain-ci-bun-cache:
|
||||
@@ -1,14 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: gbrain_test
|
||||
ports:
|
||||
- "5434:5432"
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
# Pluggable Engine Architecture
|
||||
|
||||
## The idea
|
||||
|
||||
Every GBrain operation goes through `BrainEngine`. The engine is the contract between "what the brain can do" and "how it's stored." Swap the engine, keep everything else.
|
||||
|
||||
v0 shipped `PostgresEngine` backed by Supabase. v0.7 adds `PGLiteEngine` -- embedded Postgres 17.5 via WASM (@electric-sql/pglite), zero-config default. The interface is designed so a `DuckDBEngine`, `TursoEngine`, or any custom backend could slot in without touching the CLI, MCP server, skills, or any consumer code.
|
||||
|
||||
## Why this matters
|
||||
|
||||
Different users have different constraints:
|
||||
|
||||
| User | Needs | Best engine |
|
||||
|------|-------|-------------|
|
||||
| Getting started | Zero-config, no accounts, no server | PGLiteEngine (default since v0.7) |
|
||||
| Power user (you) | World-class search, 7K+ pages, zero-ops | PostgresEngine + Supabase |
|
||||
| Open source hacker | Single file, no server, git-friendly | PGLiteEngine |
|
||||
| Team/enterprise | Multi-user, RLS, audit trail | PostgresEngine + self-hosted |
|
||||
| Researcher | Analytics, bulk exports, embeddings | DuckDBEngine (someday) |
|
||||
| Edge/mobile | Offline-first, sync later | PGLiteEngine + sync (someday) |
|
||||
|
||||
The engine interface means we don't have to choose. PGLite is the zero-friction default. Supabase is the production scale path. `gbrain migrate --to supabase/pglite` moves between them.
|
||||
|
||||
## The interface
|
||||
|
||||
```typescript
|
||||
// src/core/engine.ts
|
||||
|
||||
export interface BrainEngine {
|
||||
// Lifecycle
|
||||
connect(config: EngineConfig): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
initSchema(): Promise<void>;
|
||||
transaction<T>(fn: (engine: BrainEngine) => Promise<T>): Promise<T>;
|
||||
|
||||
// Pages CRUD
|
||||
getPage(slug: string): Promise<Page | null>;
|
||||
putPage(slug: string, page: PageInput): Promise<Page>;
|
||||
deletePage(slug: string): Promise<void>;
|
||||
listPages(filters: PageFilters): Promise<Page[]>;
|
||||
|
||||
// Search
|
||||
searchKeyword(query: string, opts?: SearchOpts): Promise<SearchResult[]>;
|
||||
searchVector(embedding: Float32Array, opts?: SearchOpts): Promise<SearchResult[]>;
|
||||
|
||||
// Chunks
|
||||
upsertChunks(slug: string, chunks: ChunkInput[]): Promise<void>;
|
||||
getChunks(slug: string): Promise<Chunk[]>;
|
||||
|
||||
// Links
|
||||
addLink(from: string, to: string, context?: string, linkType?: string): Promise<void>;
|
||||
removeLink(from: string, to: string): Promise<void>;
|
||||
getLinks(slug: string): Promise<Link[]>;
|
||||
getBacklinks(slug: string): Promise<Link[]>;
|
||||
traverseGraph(slug: string, depth?: number): Promise<GraphNode[]>;
|
||||
|
||||
// Tags
|
||||
addTag(slug: string, tag: string): Promise<void>;
|
||||
removeTag(slug: string, tag: string): Promise<void>;
|
||||
getTags(slug: string): Promise<string[]>;
|
||||
|
||||
// Timeline
|
||||
addTimelineEntry(slug: string, entry: TimelineInput): Promise<void>;
|
||||
getTimeline(slug: string, opts?: TimelineOpts): Promise<TimelineEntry[]>;
|
||||
|
||||
// Raw data
|
||||
putRawData(slug: string, source: string, data: object): Promise<void>;
|
||||
getRawData(slug: string, source?: string): Promise<RawData[]>;
|
||||
|
||||
// Versions
|
||||
createVersion(slug: string): Promise<PageVersion>;
|
||||
getVersions(slug: string): Promise<PageVersion[]>;
|
||||
revertToVersion(slug: string, versionId: number): Promise<void>;
|
||||
|
||||
// Stats + health
|
||||
getStats(): Promise<BrainStats>;
|
||||
getHealth(): Promise<BrainHealth>;
|
||||
|
||||
// Ingest log
|
||||
logIngest(entry: IngestLogInput): Promise<void>;
|
||||
getIngestLog(opts?: IngestLogOpts): Promise<IngestLogEntry[]>;
|
||||
|
||||
// Config
|
||||
getConfig(key: string): Promise<string | null>;
|
||||
setConfig(key: string, value: string): Promise<void>;
|
||||
|
||||
// Migration + advanced (added v0.7)
|
||||
runMigration(sql: string): Promise<void>;
|
||||
getChunksWithEmbeddings(slug: string): Promise<ChunkWithEmbedding[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### Key design choices
|
||||
|
||||
**Slug-based API, not ID-based.** Every method takes slugs, not numeric IDs. The engine resolves slugs to IDs internally. This keeps the interface portable... slugs are strings, IDs are database-specific.
|
||||
|
||||
**Embedding is NOT in the engine.** The engine stores embeddings and searches by vector, but it doesn't generate embeddings. `src/core/embedding.ts` handles that. This is intentional: embedding is an external API call (OpenAI), not a storage concern. All engines share the same embedding service.
|
||||
|
||||
**Chunking is NOT in the engine.** Same logic. `src/core/chunkers/` handles chunking. The engine stores and retrieves chunks. All engines share the same chunkers.
|
||||
|
||||
**Search returns `SearchResult[]`, not raw rows.** The engine is responsible for its own search implementation (tsvector vs FTS5, pgvector vs sqlite-vss) but must return a uniform result type. RRF fusion and dedup happen above the engine, in `src/core/search/hybrid.ts`.
|
||||
|
||||
**`traverseGraph` exists but is engine-specific.** Postgres uses recursive CTEs. SQLite would use a loop with depth tracking. The interface is the same: give me a slug and max depth, return the graph.
|
||||
|
||||
## How search works across engines
|
||||
|
||||
```
|
||||
+-------------------+
|
||||
| hybrid.ts |
|
||||
| (RRF fusion + |
|
||||
| dedup, shared) |
|
||||
+--------+----------+
|
||||
|
|
||||
+------------+------------+
|
||||
| |
|
||||
+--------v--------+ +--------v--------+
|
||||
| engine.search | | engine.search |
|
||||
| Keyword() | | Vector() |
|
||||
+-----------------+ +-----------------+
|
||||
| |
|
||||
+-----------+-----------+ +---------+---------+
|
||||
| | | |
|
||||
+-------v-------+ +-------v---+ +-------v---+ +----v--------+
|
||||
| Postgres: | | PGLite: | | Postgres: | | PGLite: |
|
||||
| tsvector + | | tsvector +| | pgvector | | pgvector |
|
||||
| ts_rank + | | ts_rank | | HNSW | | HNSW |
|
||||
| websearch_to_ | | (same SQL)| | cosine | | cosine |
|
||||
| tsquery | | | | | | (same SQL) |
|
||||
+---------------+ +-----------+ +-----------+ +-------------+
|
||||
```
|
||||
|
||||
RRF fusion, multi-query expansion, and 4-layer dedup are engine-agnostic. They operate on `SearchResult[]` arrays. Only the raw keyword and vector searches are engine-specific.
|
||||
|
||||
## PostgresEngine (v0, ships)
|
||||
|
||||
**Dependencies:** `postgres` (porsager/postgres), `pgvector`
|
||||
|
||||
**Postgres-specific features used:**
|
||||
- `tsvector` + `GIN` index for full-text search with `ts_rank` weighting
|
||||
- `pgvector` HNSW index for cosine similarity vector search
|
||||
- `pg_trgm` + `GIN` for fuzzy slug resolution
|
||||
- Recursive CTEs for graph traversal
|
||||
- Trigger-based search_vector (spans pages + timeline_entries)
|
||||
- JSONB for frontmatter with GIN index
|
||||
- Connection pooling via Supabase Supavisor (port 6543)
|
||||
|
||||
**Hosting:** Supabase Pro ($25/mo). Zero-ops. Managed Postgres with pgvector built in.
|
||||
|
||||
**Why not self-hosted for v0:** The brain should be infrastructure agents use, not something you maintain. Self-hosted Postgres with Docker is a welcome community PR, but v0 optimizes for zero ops.
|
||||
|
||||
## PGLiteEngine (v0.7, ships)
|
||||
|
||||
**Dependencies:** `@electric-sql/pglite` (v0.4.4+)
|
||||
|
||||
**What it is:** Embedded Postgres 17.5 compiled to WASM via ElectricSQL's PGLite. Runs in-process, no server, no Docker, no accounts. Same SQL as PostgresEngine -- not a separate dialect. All 37 BrainEngine methods implemented.
|
||||
|
||||
**PGLite-specific details:**
|
||||
- Uses `pglite-schema.ts` for DDL (pgvector extension, pg_trgm, triggers, indexes)
|
||||
- Parameterized queries throughout (shared utilities in `src/core/utils.ts`)
|
||||
- `hybridSearch` keyword-only fallback when `OPENAI_API_KEY` is not set
|
||||
- Data stored at `~/.gbrain/brain.db` (configurable)
|
||||
- pgvector HNSW index for cosine similarity vector search (same as Postgres)
|
||||
- tsvector + ts_rank for full-text search (same as Postgres)
|
||||
- pg_trgm for fuzzy slug resolution (same as Postgres)
|
||||
|
||||
**When to use PGLite vs Postgres:**
|
||||
|
||||
| Factor | PGLite | PostgresEngine + Supabase |
|
||||
|--------|--------|--------------------------|
|
||||
| Setup | `gbrain init` (zero-config) | Account + connection string |
|
||||
| Scale | Good for < 1,000 files | Production-proven at 10K+ |
|
||||
| Multi-device | Single machine only | Any device via remote MCP |
|
||||
| Cost | Free | Supabase Pro ($25/mo) |
|
||||
| Concurrency | Single process | Connection pooling |
|
||||
| Backups | Manual (file copy) | Managed by Supabase |
|
||||
|
||||
**Migration:** `gbrain migrate --to supabase` exports everything (pages, chunks, embeddings, links, tags, timeline) and imports into Supabase. `gbrain migrate --to pglite` goes the other direction. Bidirectional, lossless.
|
||||
|
||||
## Adding a new engine
|
||||
|
||||
1. Create `src/core/<name>-engine.ts` implementing `BrainEngine`
|
||||
2. Add to engine factory in `src/core/engine-factory.ts`:
|
||||
```typescript
|
||||
export function createEngine(type: string): BrainEngine {
|
||||
switch (type) {
|
||||
case 'pglite': return new PGLiteEngine();
|
||||
case 'postgres': return new PostgresEngine();
|
||||
case 'myengine': return new MyEngine();
|
||||
default: throw new Error(`Unknown engine: ${type}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
The factory uses dynamic imports so engines are only loaded when selected.
|
||||
3. Store engine type in `~/.gbrain/config.json`: `{ "engine": "myengine", ... }`
|
||||
4. Add tests. The test suite should be engine-agnostic where possible... same test cases, different engine constructor.
|
||||
5. Document in this file + add a design doc in `docs/`
|
||||
|
||||
### What you DON'T need to touch
|
||||
|
||||
- `src/cli.ts` (dispatches to engine, doesn't know which one)
|
||||
- `src/mcp/server.ts` (same)
|
||||
- `src/core/chunkers/*` (shared across engines)
|
||||
- `src/core/embedding.ts` (shared across engines)
|
||||
- `src/core/search/hybrid.ts`, `expansion.ts`, `dedup.ts` (shared, operate on SearchResult[])
|
||||
- `skills/*` (fat markdown, engine-agnostic)
|
||||
|
||||
### What you DO need to implement
|
||||
|
||||
Every method in `BrainEngine`. The full interface. No optional methods, no feature flags. If your engine can't do vector search (e.g., a pure-text engine), implement `searchVector` to return `[]` and document the limitation.
|
||||
|
||||
## Capability matrix
|
||||
|
||||
| Capability | PostgresEngine | PGLiteEngine | Notes |
|
||||
|-----------|---------------|-------------|-------|
|
||||
| CRUD | Full | Full | Same SQL |
|
||||
| Keyword search | tsvector + ts_rank | tsvector + ts_rank | Identical (real Postgres) |
|
||||
| Vector search | pgvector HNSW | pgvector HNSW | Identical (real Postgres) |
|
||||
| Fuzzy slug | pg_trgm | pg_trgm | Identical (real Postgres) |
|
||||
| Graph traversal | Recursive CTE | Recursive CTE | Same SQL |
|
||||
| Transactions | Full ACID | Full ACID | Both support this |
|
||||
| JSONB queries | GIN index | GIN index | Identical |
|
||||
| Concurrent access | Connection pooling | Single process | PGLite limitation |
|
||||
| Hosting | Supabase, self-hosted, Docker | Local file | |
|
||||
| Migration methods | runMigration, getChunksWithEmbeddings | Same | Added v0.7 |
|
||||
|
||||
## Future engine ideas
|
||||
|
||||
**TursoEngine.** libSQL (SQLite fork) with embedded replicas and HTTP edge access. Would give SQLite's simplicity with cloud sync. Interesting for mobile/edge use cases.
|
||||
|
||||
**DuckDBEngine.** Analytical workloads. Bulk exports, embedding analysis, brain-wide statistics. Not for OLTP. Could be a secondary engine for analytics alongside Postgres for operations.
|
||||
|
||||
**Custom/Remote.** The interface is clean enough that someone could build an engine backed by any storage: Firestore, DynamoDB, a REST API, even a flat file system. The interface doesn't assume SQL.
|
||||
|
||||
Note: The original SQLite engine plan (`docs/SQLITE_ENGINE.md`) was superseded by PGLite. PGLite uses the same SQL as Postgres, eliminating the need for a separate SQLite dialect with FTS5/sqlite-vss translation.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,143 +0,0 @@
|
||||
<!-- skillpack-version: 0.7.0 -->
|
||||
<!-- source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/GBRAIN_SKILLPACK.md -->
|
||||
# GBrain Skillpack: Reference Architecture for AI Agents
|
||||
|
||||
This is a reference architecture for how a production AI agent uses gbrain as its
|
||||
knowledge backbone. Based on patterns from a real deployment with 14,700+ brain
|
||||
files, 40+ skills, and 20+ cron jobs running continuously.
|
||||
|
||||
**The memex vision, realized.** Vannevar Bush imagined a device where an individual
|
||||
stores everything, mechanized so it may be consulted with exceeding speed. GBrain is
|
||||
that device, except the memex builds itself. The agent detects entities, enriches
|
||||
pages, creates cross-references, and maintains compiled truth automatically.
|
||||
|
||||
Each section below is a standalone guide. Click through to the full content.
|
||||
|
||||
---
|
||||
|
||||
## Core Patterns
|
||||
|
||||
The foundational read-write loop and data model.
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [The Brain-Agent Loop](guides/brain-agent-loop.md) | The read-write cycle that makes the brain compound over time |
|
||||
| [Entity Detection](guides/entity-detection.md) | Run it on every message. Capture original thinking + entity mentions |
|
||||
| [The Originals Folder](guides/originals-folder.md) | Capturing WHAT YOU THINK, not just what you found |
|
||||
| [Brain-First Lookup](guides/brain-first-lookup.md) | Check the brain before calling any external API |
|
||||
| [Compiled Truth + Timeline](guides/compiled-truth.md) | Above the line: current synthesis. Below: append-only evidence |
|
||||
| [Source Attribution](guides/source-attribution.md) | Every fact needs a citation. Format and hierarchy |
|
||||
|
||||
## Data Pipelines
|
||||
|
||||
Getting data in and keeping it current.
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [Enrichment Pipeline](guides/enrichment-pipeline.md) | 7-step protocol, tier system (Tier 1/2/3 by importance) |
|
||||
| [Meeting Ingestion](guides/meeting-ingestion.md) | Always pull complete transcript, propagate to all entity pages |
|
||||
| [Content & Media Ingestion](guides/content-media.md) | YouTube, social media bundles, PDFs/documents |
|
||||
| [Diligence Ingestion](guides/diligence-ingestion.md) | Data room materials: pitch decks, financial models, cap tables |
|
||||
| [Deterministic Collectors](guides/deterministic-collectors.md) | Code for data, LLMs for judgment. The collector pattern |
|
||||
| [Idea Capture & Originals](guides/idea-capture.md) | Depth test, originality distribution, deep cross-linking |
|
||||
| [Getting Data In](integrations/README.md) | Integration recipes: voice, email, X, calendar |
|
||||
|
||||
## Operations
|
||||
|
||||
Running a production brain.
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [Reference Cron Schedule](guides/cron-schedule.md) | 20+ recurring jobs, quiet hours, dream cycle |
|
||||
| [Cron via Minions](../skills/conventions/cron-via-minions.md) | Why scheduled work runs as Minion jobs, not `agentTurn`. Auto-applied by v0.11.0 migration for built-in handlers; host-specific handlers use the plugin contract below. |
|
||||
| [Plugin Handlers](guides/plugin-handlers.md) | Registering host-specific Minion handlers via code (no data-file exec surface). |
|
||||
| [Minions fix](guides/minions-fix.md) | Repairing a half-migrated v0.11.0 install. |
|
||||
| [Shell jobs (v0.14.0+)](guides/minions-shell-jobs.md) | Move deterministic crons (API fetch, token refresh, scrape+write) off the LLM gateway. Zero tokens per fire, ~60% gateway headroom. Follow `skills/migrations/v0.14.0.md` for the adoption playbook. |
|
||||
| [Quiet Hours & Timezone](guides/quiet-hours.md) | Hold notifications during sleep, timezone-aware delivery |
|
||||
| [Executive Assistant Pattern](guides/executive-assistant.md) | Email triage, meeting prep, scheduling |
|
||||
| [Operational Disciplines](guides/operational-disciplines.md) | Signal detection, brain-first, sync-after-write, heartbeat, dream cycle |
|
||||
| [Skill Development Cycle](guides/skill-development.md) | 5-step cycle: concept, prototype, evaluate, codify, cron |
|
||||
|
||||
**Subagent routing (v0.11.0+):** agents that dispatch background work should route through
|
||||
`skills/conventions/subagent-routing.md` — it reads `~/.gbrain/preferences.json#minion_mode`
|
||||
and branches between native subagents and Minion jobs. The v0.11.0 migration auto-injects
|
||||
a marker into AGENTS.md pointing at this convention.
|
||||
|
||||
**Cron routing (v0.11.0+):** scheduled work goes through Minions, not OpenClaw's `agentTurn`.
|
||||
See `skills/conventions/cron-via-minions.md` for the rewrite pattern. The v0.11.0 migration
|
||||
auto-rewrites entries whose handler is a gbrain builtin; host-specific handlers (e.g.
|
||||
`ea-inbox-sweep`) need a code-level registration per `docs/guides/plugin-handlers.md`.
|
||||
|
||||
## Architecture
|
||||
|
||||
How to structure your system.
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [Two-Repo Architecture](guides/repo-architecture.md) | Agent repo vs brain repo, boundary rules, decision tree |
|
||||
| [Sub-Agent Model Routing](guides/sub-agent-routing.md) | Which model for which task, signal detector pattern, cost optimization |
|
||||
| [The Three Search Modes](guides/search-modes.md) | Keyword, hybrid, direct. When to use each |
|
||||
| [Brain vs Agent Memory](guides/brain-vs-memory.md) | 3 layers: GBrain (world knowledge), agent memory, session |
|
||||
|
||||
## Integrations
|
||||
|
||||
Wiring up your life.
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [Credential Gateway](integrations/credential-gateway.md) | ClawVisor / Hermes for Gmail, Calendar, Contacts |
|
||||
| [Meeting & Call Webhooks](integrations/meeting-webhooks.md) | Circleback transcripts + Quo/OpenPhone SMS/calls |
|
||||
| [Voice-to-Brain](../recipes/twilio-voice-brain.md) | Phone calls + WebRTC browser calls create brain pages. 25 production patterns: identity separation, bid system, conversation timing, proactive advisor, prompt compression, caller routing, dynamic VAD, real-time logging, belt-and-suspenders post-call |
|
||||
| [Email-to-Brain](../recipes/email-to-brain.md) | Gmail messages flow into entity pages via deterministic collector |
|
||||
| [X-to-Brain](../recipes/x-to-brain.md) | Twitter monitoring with deletion detection + engagement velocity |
|
||||
| [Calendar-to-Brain](../recipes/calendar-to-brain.md) | Google Calendar events become searchable daily brain pages |
|
||||
| [Meeting Sync](../recipes/meeting-sync.md) | Circleback transcripts auto-import with attendee propagation |
|
||||
|
||||
## Administration
|
||||
|
||||
Keeping it running and up to date.
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [Upgrades & Auto-Update](guides/upgrades-auto-update.md) | check-update, agent notifications, migration files |
|
||||
| [Live Sync](guides/live-sync.md) | Keep the index current: cron, --watch, webhook approaches |
|
||||
|
||||
## Getting Started
|
||||
|
||||
After setup, the brain is empty. The cold-start skill sequences the highest-leverage
|
||||
data sources to populate it:
|
||||
|
||||
| Guide | What It Covers |
|
||||
|-------|---------------|
|
||||
| [Cold Start](../skills/cold-start/SKILL.md) | Day-one bootstrapping: contacts, calendar, email, conversations, social, archives. Uses ClawVisor for safe credential handling — agents never hold raw API keys. |
|
||||
| [Ask User](../skills/ask-user/SKILL.md) | Choice-gate pattern for human input at decision points. Used by cold-start and other skills. |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: GBrain CLI Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `gbrain search "term"` | Keyword search across all brain pages |
|
||||
| `gbrain query "question"` | Hybrid search (vector + keyword + RRF) |
|
||||
| `gbrain get <slug>` | Read a specific brain page by slug |
|
||||
| `gbrain sync` | Sync local markdown repo to gbrain index |
|
||||
| `gbrain import <path>` | Import files into the brain |
|
||||
| `gbrain embed --stale` | Re-embed pages with stale or missing embeddings |
|
||||
| `gbrain integrations` | Manage integration recipes (senses + reflexes) |
|
||||
| `gbrain stats` | Show brain statistics (page count, last sync, etc.) |
|
||||
| `gbrain doctor` | Diagnose brain health issues |
|
||||
| `gbrain check-update` | Check for new versions and integration recipes |
|
||||
|
||||
Run `gbrain --help` for the full command reference.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Philosophy
|
||||
|
||||
- [Infrastructure Layer](architecture/infra-layer.md) — Import pipeline, chunking, embedding, search
|
||||
- [Thin Harness, Fat Skills](ethos/THIN_HARNESS_FAT_SKILLS.md) — Architecture philosophy
|
||||
- [Markdown Skills as Recipes](ethos/MARKDOWN_SKILLS_AS_RECIPES.md) — Why markdown is code and your agent is a package manager
|
||||
- [Homebrew for Personal AI](designs/HOMEBREW_FOR_PERSONAL_AI.md) — The 10-star vision
|
||||
- [Recommended Schema](GBRAIN_RECOMMENDED_SCHEMA.md) — Directory structure for your brain repo
|
||||
- [Verification Runbook](GBRAIN_VERIFY.md) — End-to-end installation verification
|
||||
@@ -1,551 +0,0 @@
|
||||
# GBrain v0: Postgres-Native Personal Knowledge Brain
|
||||
|
||||
> **Historical design doc.** This is the original v0 spec from before PGLite landed. Several
|
||||
> forward-looking sections — most notably the SQLite engine plan — were superseded by
|
||||
> PGLite (embedded Postgres via WASM), which uses the same SQL dialect as Postgres and
|
||||
> eliminates the need for a separate FTS5/sqlite-vss translation layer. Kept here for
|
||||
> historical context; see [`ENGINES.md`](ENGINES.md) for the current engine architecture and
|
||||
> the [`CHANGELOG.md`](../CHANGELOG.md) for the actual implementation history.
|
||||
|
||||
## What this is
|
||||
|
||||
GBrain is a compiled intelligence system. Not a note-taking app. Not "chat with your notes."
|
||||
|
||||
Every page is an intelligence assessment. Above the line: compiled truth (your current best understanding, rewritten when evidence changes). Below the line: timeline (append-only evidence trail). AI agents maintain the brain. MCP clients query it. The intelligence lives in fat markdown skills, not application code.
|
||||
|
||||
The core insight: personal knowledge at scale is an intelligence problem, not a storage problem.
|
||||
|
||||
## Why it exists
|
||||
|
||||
A 7,471-file / 2.3GB markdown wiki is choking git. Git doesn't scale past ~5K files for wiki-style use. The compiled truth + timeline model (Karpathy-style knowledge pages) is right, but it needs a real database underneath.
|
||||
|
||||
There's already a production-grade RAG system (Ruby on Rails, Postgres + pgvector) with 3-tier chunking, hybrid search with RRF, multi-query expansion, and 4-layer dedup. GBrain ports these proven patterns to a standalone Bun + TypeScript tool.
|
||||
|
||||
## The knowledge model
|
||||
|
||||
```
|
||||
+--------------------------------------------------+
|
||||
| Page: concepts/do-things-that-dont-scale |
|
||||
| |
|
||||
| --- frontmatter (YAML) --- |
|
||||
| type: concept |
|
||||
| tags: [startups, growth, pg-essay] |
|
||||
| |
|
||||
| === COMPILED TRUTH === |
|
||||
| Current best understanding. |
|
||||
| Rewritten on new evidence. |
|
||||
| This is the "what we know now" section. |
|
||||
| |
|
||||
| --- |
|
||||
| |
|
||||
| === TIMELINE === |
|
||||
| Append-only evidence trail. |
|
||||
| - 2013-07-01: Published on paulgraham.com |
|
||||
| - 2024-11-15: Referenced in batch kickoff talk |
|
||||
| Never edited, only appended. |
|
||||
+--------------------------------------------------+
|
||||
| |
|
||||
v v
|
||||
[Semantic chunks] [Recursive chunks]
|
||||
(best quality for (predictable format
|
||||
compiled truth) for timeline)
|
||||
| |
|
||||
v v
|
||||
[Embeddings: text-embedding-3-large, 1536 dims]
|
||||
|
|
||||
v
|
||||
[HNSW index + tsvector + pg_trgm]
|
||||
|
|
||||
v
|
||||
[Hybrid search: vector + keyword + RRF fusion]
|
||||
```
|
||||
|
||||
## Architecture decisions
|
||||
|
||||
### v0 stack
|
||||
|
||||
| Layer | Choice | Why |
|
||||
|-------|--------|-----|
|
||||
| Database | Postgres + pgvector | Proven RAG patterns, production-tested. World-class hybrid search. |
|
||||
| Hosting | Supabase Pro ($25/mo) | Zero-ops. Managed Postgres, pgvector, connection pooling. 8GB storage. |
|
||||
| Runtime | Bun + TypeScript | Consistent with GStack ecosystem. Fast. Compiles to single binary. |
|
||||
| Embeddings | OpenAI text-embedding-3-large | 1536 dims (reduced from 3072 via dimensions API). ~$0.13/1M tokens. |
|
||||
| LLM (chunking/expansion) | Claude Haiku | Cheapest model for topic boundary detection and query expansion. |
|
||||
| Background jobs | Trigger.dev | Serverless. Embed backfill, stale detection, orphan audit, tag consistency. |
|
||||
| Distribution | npm package + compiled binary + MCP server | Library for OpenClaw, CLI for humans, MCP for agents. |
|
||||
|
||||
### What we chose and why
|
||||
|
||||
**Postgres over SQLite.** We have 3+ years of proven RAG patterns running on Postgres. tsvector for full-text search, pgvector HNSW for semantic search, pg_trgm for fuzzy slug matching. Porting these to SQLite would mean reimplementing search from scratch. SQLite is a future pluggable engine for lightweight open source users (see `docs/ENGINES.md`).
|
||||
|
||||
**Supabase over self-hosted.** Zero maintenance. The brain should be infrastructure that AI agents use, not something you administer. Free tier has pgvector but only 500MB (not enough for 7K+ pages with embeddings, which need ~750MB). Pro tier at $25/mo gives 8GB. No Docker, no self-hosted Postgres in v1.
|
||||
|
||||
**Full port over minimal viable.** The patterns are proven. The port is mechanical. Shipping the full 3-tier chunking + hybrid search + 4-layer dedup means world-class RAG from day one. "We'll add that later" means rebuilding everything later.
|
||||
|
||||
**Library-first distribution.** gbrain is an npm package. OpenClaw installs it as a dependency (`bun add gbrain`), imports the engine directly. Zero-overhead function calls, shared connection pool, TypeScript types. The CLI and MCP server are thin wrappers over the same engine.
|
||||
|
||||
**Trigger-based tsvector (not generated column).** To include timeline_entries content in full-text search, the tsvector needs to span multiple tables. Generated columns can't do cross-table references. A trigger on pages + timeline_entries updates the search_vector.
|
||||
|
||||
**Auto-embed during import.** No separate embed step. `gbrain import` chunks and embeds in one pass. Progress bar shows status. `--no-embed` flag for users who want to defer. `embedded_at` column enables `gbrain embed --stale` for backfill.
|
||||
|
||||
## Distribution model
|
||||
|
||||
```
|
||||
+-------------------+ +-------------------+ +-------------------+
|
||||
| npm package | | Compiled binary | | MCP server |
|
||||
| (library) | | (CLI) | | (stdio) |
|
||||
+-------------------+ +-------------------+ +-------------------+
|
||||
| | | | | |
|
||||
| bun add gbrain | | GitHub Releases | | gbrain serve |
|
||||
| import { Postgres | | npx gbrain | | in mcp.json |
|
||||
| Engine } | | | | |
|
||||
| | | | | |
|
||||
| WHO: OpenClaw, | | WHO: Humans | | WHO: Claude Code, |
|
||||
| AlphaClaw | | | | Cursor, etc. |
|
||||
+-------------------+ +-------------------+ +-------------------+
|
||||
| | |
|
||||
+-------------------------+-------------------------+
|
||||
|
|
||||
+--------v--------+
|
||||
| BrainEngine |
|
||||
| (pluggable |
|
||||
| interface) |
|
||||
+-----------------+
|
||||
|
|
||||
+-------------+-------------+
|
||||
| |
|
||||
+------v------+ +-------v-------+
|
||||
| Postgres | | SQLite |
|
||||
| Engine | | Engine |
|
||||
| (v0, ships) | | (future, see |
|
||||
+-------------+ | ENGINES.md) |
|
||||
+---------------+
|
||||
```
|
||||
|
||||
package.json exports:
|
||||
- Library: `src/core/index.ts` (BrainEngine interface, PostgresEngine, types)
|
||||
- CLI binary: `src/cli.ts`
|
||||
|
||||
## First-time experience
|
||||
|
||||
### Path 1: OpenClaw user (primary)
|
||||
|
||||
OpenClaw is the AI orchestrator that uses gbrain as its knowledge backend. This is the most common install path.
|
||||
|
||||
```bash
|
||||
# 1. Install gbrain as a ClawHub skill
|
||||
clawhub install gbrain
|
||||
|
||||
# 2. The skill runs guided setup on first use:
|
||||
# - Detects if Supabase CLI is available
|
||||
# - If yes: auto-provisions a new Supabase project
|
||||
# - If no: prompts for connection URL
|
||||
# - Runs schema migration
|
||||
# - Scans for markdown repos and imports user's content
|
||||
# - Shows live entity/edge extraction animation
|
||||
# - Brain is ready
|
||||
|
||||
# 3. From OpenClaw, brain tools are now available:
|
||||
# "Search the brain for [topic from your data]"
|
||||
# "Ingest my meeting notes from today"
|
||||
# "How many pages are in the brain?"
|
||||
```
|
||||
|
||||
Behind the scenes, `clawhub install gbrain`:
|
||||
1. Installs the `gbrain` npm package
|
||||
2. Ships SKILL.md files (ingest, query, maintain, enrich, briefing, migrate)
|
||||
3. Registers brain tools with the orchestrator
|
||||
4. Runs `gbrain init --supabase` on first use (guided wizard)
|
||||
|
||||
### Path 2: CLI user (standalone)
|
||||
|
||||
```bash
|
||||
# 1. Install
|
||||
npm install -g gbrain
|
||||
# or: download binary from GitHub Releases
|
||||
|
||||
# 2. Initialize with Supabase
|
||||
gbrain init --supabase
|
||||
# Guided wizard:
|
||||
# Try 1: Supabase CLI auto-provision (npx supabase)
|
||||
# Try 2: If CLI not installed or not logged in, fallback:
|
||||
# "Enter your Supabase connection URL:"
|
||||
# Then: runs schema migration, verifies pgvector extension
|
||||
# Then: verifies database is ready for import
|
||||
# Output: "Brain ready. Run: gbrain import <your-repo>"
|
||||
|
||||
# 3. Import your data
|
||||
gbrain import /path/to/markdown/wiki/
|
||||
# Progress bar: 7,471 files, auto-chunk, auto-embed
|
||||
# ~30s for text import, ~10-15 min for embedding
|
||||
|
||||
# 4. Query
|
||||
gbrain query "what does PG say about doing things that don't scale?"
|
||||
```
|
||||
|
||||
### Path 3: MCP user (Claude Code, Cursor)
|
||||
|
||||
```json
|
||||
// ~/.config/claude/mcp.json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gbrain": {
|
||||
"command": "gbrain",
|
||||
"args": ["serve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then in Claude Code: "Search my brain for people who know about robotics"
|
||||
|
||||
### The init wizard in detail
|
||||
|
||||
`gbrain init --supabase` runs through these steps:
|
||||
|
||||
```
|
||||
Step 1: Database Setup
|
||||
├── Check for Supabase CLI (npx supabase --version)
|
||||
│ ├── Found + logged in → auto-create project
|
||||
│ │ ├── Create project via supabase CLI
|
||||
│ │ ├── Wait for project to be ready
|
||||
│ │ └── Extract connection string
|
||||
│ ├── Found + not logged in →
|
||||
│ │ └── Error: "Supabase CLI found but not logged in."
|
||||
│ │ Cause: "You need to authenticate first."
|
||||
│ │ Fix: "Run: npx supabase login"
|
||||
│ │ Docs: "https://supabase.com/docs/guides/cli"
|
||||
│ └── Not found → fallback to manual
|
||||
│ └── Prompt: "Enter your Supabase connection URL:"
|
||||
│
|
||||
Step 2: Schema Migration
|
||||
├── Connect to database
|
||||
├── CREATE EXTENSION IF NOT EXISTS vector
|
||||
├── CREATE EXTENSION IF NOT EXISTS pg_trgm
|
||||
├── Run src/schema.sql (all tables, indexes, triggers)
|
||||
└── Verify: test insert + vector query
|
||||
|
||||
Step 3: Config
|
||||
├── Write ~/.gbrain/config.json (0600 permissions)
|
||||
│ { "database_url": "...", "service_role_key": "..." }
|
||||
└── Verify connection
|
||||
|
||||
Step 4: Kindling Import
|
||||
├── Import 10 bundled PG essays as demo data
|
||||
├── Chunk + embed each essay
|
||||
├── Show live entity/edge extraction animation:
|
||||
│ "Extracting entities... Paul Graham (person), Y Combinator (company)..."
|
||||
│ "Creating links... Paul Graham → Y Combinator (founded)..."
|
||||
└── Output: "Brain ready. 10 pages imported."
|
||||
|
||||
Step 5: First Query
|
||||
└── "Try: gbrain query 'what does PG say about doing things that don't scale?'"
|
||||
```
|
||||
|
||||
Every error follows the style guide: problem + cause + fix + docs link.
|
||||
|
||||
## CLI commands
|
||||
|
||||
```
|
||||
gbrain init [--supabase|--url <conn>] # create brain
|
||||
gbrain get <slug> # read a page
|
||||
gbrain put <slug> [< file.md] # write/update a page
|
||||
gbrain search <query> # keyword search (tsvector)
|
||||
gbrain query <question> # hybrid search (RRF + expansion)
|
||||
gbrain ingest <file> [--type ...] # ingest a source document
|
||||
gbrain link <from> <to> [--type <type>] # create typed link
|
||||
gbrain unlink <from> <to> # remove link
|
||||
gbrain graph <slug> [--depth 5] # traverse link graph (recursive CTE)
|
||||
gbrain backlinks <slug> # incoming links
|
||||
gbrain tags <slug> # list tags
|
||||
gbrain tag <slug> <tag> # add tag
|
||||
gbrain untag <slug> <tag> # remove tag
|
||||
gbrain timeline [<slug>] # view timeline
|
||||
gbrain timeline-add <slug> <date> <text> # add timeline entry
|
||||
gbrain list [--type] [--tag] [--limit] # list with filters
|
||||
gbrain stats # brain statistics
|
||||
gbrain health # brain health dashboard
|
||||
gbrain import <dir> [--no-embed] # import from markdown directory
|
||||
gbrain export [--dir ./export/] # export to markdown (round-trip)
|
||||
gbrain embed [<slug>|--all|--stale] # generate/refresh embeddings
|
||||
gbrain serve # MCP server (stdio)
|
||||
gbrain call <tool> '<json>' # raw tool invocation
|
||||
gbrain upgrade # self-update (npm, binary, ClawHub)
|
||||
gbrain version # version info
|
||||
gbrain config [get|set] <key> [value] # brain config
|
||||
```
|
||||
|
||||
CLI and MCP expose identical operations. Drift tests assert identical results for all operations across both interfaces.
|
||||
|
||||
## Database schema
|
||||
|
||||
9 tables in Postgres + pgvector:
|
||||
|
||||
```
|
||||
+------------------+ +-------------------+ +------------------+
|
||||
| pages |---->| content_chunks | | links |
|
||||
|------------------| |-------------------| |------------------|
|
||||
| id (PK) | | id (PK) | | id (PK) |
|
||||
| slug (UNIQUE) | | page_id (FK) | | from_page_id(FK) |
|
||||
| type | | chunk_index | | to_page_id (FK) |
|
||||
| title | | chunk_text | | link_type |
|
||||
| compiled_truth | | chunk_source | | context |
|
||||
| timeline | | embedding (1536) | +------------------+
|
||||
| frontmatter(JSONB)| | model |
|
||||
| search_vector | | token_count | +------------------+
|
||||
| created_at | | embedded_at | | tags |
|
||||
| updated_at | +-------------------+ |------------------|
|
||||
+------------------+ | id (PK) |
|
||||
| | page_id (FK) |
|
||||
+-----> +--------------------+ | tag |
|
||||
| | timeline_entries | +------------------+
|
||||
| |--------------------|
|
||||
| | id (PK) | +------------------+
|
||||
| | page_id (FK) | | page_versions |
|
||||
| | date | |------------------|
|
||||
| | source | | id (PK) |
|
||||
| | summary | | page_id (FK) |
|
||||
| | detail (markdown) | | compiled_truth |
|
||||
| +--------------------+ | frontmatter |
|
||||
| | snapshot_at |
|
||||
+-----> +--------------------+ +------------------+
|
||||
| | raw_data |
|
||||
| |--------------------| +------------------+
|
||||
| | id (PK) | | config |
|
||||
| | page_id (FK) | |------------------|
|
||||
| | source | | key (PK) |
|
||||
| | data (JSONB) | | value |
|
||||
| +--------------------+ +------------------+
|
||||
|
|
||||
+-----> +--------------------+
|
||||
| ingest_log |
|
||||
|--------------------|
|
||||
| id (PK) |
|
||||
| source_type |
|
||||
| source_ref |
|
||||
| pages_updated |
|
||||
| summary |
|
||||
+--------------------+
|
||||
```
|
||||
|
||||
Indexes:
|
||||
- `pages.slug`: UNIQUE constraint (implicit B-tree)
|
||||
- `pages.type`: B-tree
|
||||
- `pages.search_vector`: GIN (full-text search)
|
||||
- `pages.frontmatter`: GIN (JSONB queries)
|
||||
- `pages.title`: GIN with pg_trgm (fuzzy slug resolution)
|
||||
- `content_chunks.embedding`: HNSW with cosine ops (vector search)
|
||||
- `content_chunks.page_id`: B-tree
|
||||
- `links.from_page_id`, `links.to_page_id`: B-tree
|
||||
- `tags.tag`, `tags.page_id`: B-tree
|
||||
- `timeline_entries.page_id`, `timeline_entries.date`: B-tree
|
||||
|
||||
## Search architecture
|
||||
|
||||
```
|
||||
Query: "when should you ignore conventional wisdom?"
|
||||
|
|
||||
v
|
||||
+---------------------+
|
||||
| Multi-query expansion|
|
||||
| (Claude Haiku) |
|
||||
| "contrarian thinking"
|
||||
| "going against the crowd"
|
||||
+---------------------+
|
||||
| | |
|
||||
v v v
|
||||
[embed all 3 queries]
|
||||
| | |
|
||||
+---+---+
|
||||
|
|
||||
+----+----+
|
||||
| |
|
||||
v v
|
||||
+--------+ +--------+
|
||||
| Vector | | Keyword|
|
||||
| Search | | Search |
|
||||
| (HNSW | | (tsv + |
|
||||
| cosine)| | ts_rank)|
|
||||
+--------+ +--------+
|
||||
| |
|
||||
+----+----+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| RRF Fusion |
|
||||
| score = sum( |
|
||||
| 1/(60 + rank)) |
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| 4-Layer Dedup |
|
||||
| 1. By source |
|
||||
| 2. Cosine > 0.85 |
|
||||
| 3. Type cap 60% |
|
||||
| 4. Per-page max |
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
+------------------+
|
||||
| Stale alerts |
|
||||
| (compiled_truth |
|
||||
| older than |
|
||||
| latest timeline)|
|
||||
+------------------+
|
||||
|
|
||||
v
|
||||
[Results]
|
||||
```
|
||||
|
||||
## Chunking strategies
|
||||
|
||||
| Strategy | Input | Algorithm | When to use |
|
||||
|----------|-------|-----------|-------------|
|
||||
| Recursive | Any text | 5-level delimiter hierarchy (paragraphs > lines > sentences > clauses > whitespace). 300-word chunks, 50-word overlap. | Timeline (predictable format), bulk import |
|
||||
| Semantic | Quality text | Embed each sentence, Savitzky-Golay filter for topic boundaries, cosine similarity minima. Falls back to recursive. | Compiled truth (intelligence assessments) |
|
||||
| LLM-guided | High-value text | Pre-split to 128-word candidates, Claude Haiku finds topic shifts in sliding windows. 3 retries per window. | Explicitly requested via `--chunker llm` |
|
||||
|
||||
Dispatch: compiled_truth gets semantic chunker. Timeline gets recursive chunker. Override with `--chunker` flag or `chunk_strategy` in frontmatter.
|
||||
|
||||
## Skills (fat markdown, no code)
|
||||
|
||||
Each skill is a markdown file that AI agents (Claude Code, OpenClaw) read and follow. The skill contains the workflow, heuristics, and quality rules. No skill logic is in the binary.
|
||||
|
||||
| Skill | What it does |
|
||||
|-------|-------------|
|
||||
| `skills/ingest/SKILL.md` | Ingest meetings, docs, articles. Update compiled truth, append timeline, create links. |
|
||||
| `skills/query/SKILL.md` | 3-layer search (FTS + vector + structured). Synthesize answer with citations. |
|
||||
| `skills/maintain/SKILL.md` | Find contradictions, stale info, orphans, dead links, tag inconsistency. |
|
||||
| `skills/enrich/SKILL.md` | Enrich from external APIs (Crustdata, Happenstance, Exa). Store raw data, distill to compiled truth. |
|
||||
| `skills/briefing/SKILL.md` | Daily briefing: meetings with context, active deals, open threads. |
|
||||
| `skills/migrate/SKILL.md` | Universal migration from Obsidian, Notion, Logseq, plain markdown, CSV, JSON, Roam. |
|
||||
|
||||
## CEO scope expansions (accepted for v0)
|
||||
|
||||
1. **CLI/MCP parity with drift tests.** Both interfaces are thin wrappers over the engine. Tests assert identical output.
|
||||
2. **Smart slug resolution.** Fuzzy matching via pg_trgm for reads. Writes require exact slugs. `gbrain get "dont scale"` resolves to `concepts/do-things-that-dont-scale`.
|
||||
3. **Brain health dashboard.** `gbrain health` shows page count, embed coverage, stale pages, orphans, dead links.
|
||||
4. **Normalized timeline.** `timeline_entries` table only (no TEXT column). `detail` field supports markdown.
|
||||
5. **Page version control.** `page_versions` table stores full snapshots (compiled_truth + frontmatter + links + tags). `gbrain history`, `gbrain diff`, `gbrain revert` commands. Revert re-chunks and re-embeds.
|
||||
6. **Typed links + graph traversal.** `link_type` column (knows, invested_in, works_at, etc.). `gbrain graph` uses recursive CTE with max depth (default 5, configurable via `--depth`).
|
||||
7. **Trigger.dev data cleanup jobs.** Daily embed backfill, weekly stale detection + orphan audit + tag consistency.
|
||||
8. **Stale alert annotations.** Search results flag pages where compiled_truth is older than latest timeline entry.
|
||||
9. **Timeline merge on ingest.** Same event created across all mentioned entities.
|
||||
|
||||
## Security model (v0)
|
||||
|
||||
Single-user, local-only:
|
||||
- Supabase service role key in `~/.gbrain/config.json` (0600 permissions)
|
||||
- MCP stdio transport is inherently local (client spawns `gbrain serve` as subprocess)
|
||||
- No multi-user, no RLS, no OAuth in v0
|
||||
- Multi-user path (future): Supabase RLS + per-user API keys
|
||||
|
||||
## Upgrade mechanism
|
||||
|
||||
`gbrain upgrade` detects the installation method and updates accordingly:
|
||||
|
||||
| Path | How |
|
||||
|------|-----|
|
||||
| npm | `bun update gbrain` (or npm equivalent) |
|
||||
| Compiled binary | Download new binary to temp dir, atomic rename swap, exec new process |
|
||||
| ClawHub | `clawhub update gbrain` |
|
||||
|
||||
Version check: compare local version against latest GitHub release tag.
|
||||
|
||||
## Storage and cost estimates
|
||||
|
||||
### Storage (~750MB for 7,471 pages)
|
||||
|
||||
| Component | Size |
|
||||
|-----------|------|
|
||||
| Page text (compiled_truth + timeline) | ~150MB |
|
||||
| JSONB frontmatter | ~20MB |
|
||||
| tsvector + GIN indexes | ~50MB |
|
||||
| Content chunks (~22K, text) | ~80MB |
|
||||
| Embeddings (22K x 1536 floats x 4 bytes) | ~134MB |
|
||||
| HNSW index overhead (~2x embeddings) | ~270MB |
|
||||
| Links, tags, timeline, raw_data, versions | ~50MB |
|
||||
| **Total** | **~750MB** |
|
||||
|
||||
Supabase free tier (500MB) won't fit. Supabase Pro ($25/mo, 8GB) is the starting point.
|
||||
|
||||
### Embedding cost (~$4-5 for initial import)
|
||||
|
||||
| Step | Cost |
|
||||
|------|------|
|
||||
| Semantic chunker sentence embeddings (~374K sentences) | ~$1 |
|
||||
| Chunk embeddings (~22K chunks) | ~$0.30 |
|
||||
| Query expansion (per query, ~3 embeds) | negligible |
|
||||
| **Total initial import** | **~$4-5** |
|
||||
|
||||
Budget alternative: `gbrain import --chunker recursive` skips sentence-level embeddings, then `gbrain embed --rechunk --chunker semantic` upgrades later.
|
||||
|
||||
## Serverless operations stack
|
||||
|
||||
```
|
||||
+------------------+ +------------------+ +------------------+
|
||||
| Supabase | | Vercel | | Trigger.dev |
|
||||
| (Postgres + | | (web/API, | | (background |
|
||||
| pgvector) | | optional) | | jobs) |
|
||||
+------------------+ +------------------+ +------------------+
|
||||
| Database | | Future web UI | | Embed backfill |
|
||||
| Connection pool | | API endpoints | | Stale detection |
|
||||
| pgvector HNSW | | Edge functions | | Orphan audit |
|
||||
| tsvector FTS | | | | Tag consistency |
|
||||
| pg_trgm fuzzy | | | | Daily briefing |
|
||||
+------------------+ +------------------+ +------------------+
|
||||
```
|
||||
|
||||
The CLI connects directly to Supabase Postgres. Trigger.dev and Vercel are for async/scheduled work. The CLI works without them.
|
||||
|
||||
## Verification checklist
|
||||
|
||||
1. `gbrain import /data/brain/` migrates all 7,471 files losslessly
|
||||
2. `gbrain export` round-trips to semantically identical markdown
|
||||
3. `gbrain query "what does PG say about doing things that don't scale?"` returns relevant hybrid search results
|
||||
4. `gbrain serve` starts MCP server connectable by Claude Code
|
||||
5. All 3 chunkers produce correct output with test fixtures
|
||||
6. `gbrain init --supabase` works end-to-end
|
||||
7. `bun test` passes all tests
|
||||
8. `clawhub install gbrain` installs the skill and runs guided setup
|
||||
9. `bun add gbrain` + `import { PostgresEngine } from 'gbrain'` works in external project
|
||||
10. Drift tests pass: CLI and MCP produce identical results
|
||||
11. `gbrain health` outputs accurate brain health metrics
|
||||
12. Migration skill successfully imports an Obsidian vault
|
||||
|
||||
## Future plans
|
||||
|
||||
See `docs/ENGINES.md` for the pluggable engine architecture and future backend plans.
|
||||
|
||||
### v1 candidates (deferred from v0)
|
||||
|
||||
- **`gbrain ask` natural language CLI alias.** Trivial to add. P1 TODO.
|
||||
- **Intelligence compiler.** Treat every fact as a first-class claim with source span, entity links, validity window, confidence, and contradiction status. "What changed, why, and what evidence would flip it again?" From Codex review. Builds on compiled truth model.
|
||||
- **Active skills via Trigger.dev.** Application-specific briefings, meeting prep. Belongs in OpenClaw, not generic brain infra.
|
||||
- **Multi-user access.** Supabase RLS + per-user API keys. v0 is single-user.
|
||||
- **SQLite engine.** Superseded by PGLite (embedded Postgres 17 via WASM) before v1. See [`ENGINES.md`](ENGINES.md) for the current engine architecture.
|
||||
- **Docker Compose for self-hosted Postgres.** Community PRs welcome.
|
||||
- **Web UI.** Optional Vercel-hosted dashboard for browsing brain pages.
|
||||
|
||||
### Interface abstraction principle
|
||||
|
||||
All operations go through `BrainEngine`. The engine interface is the contract. Postgres-specific features (tsvector, pgvector HNSW, pg_trgm, recursive CTEs) are implementation details inside `PostgresEngine`. The interface exposes capabilities, not SQL.
|
||||
|
||||
This means:
|
||||
- A SQLite engine can implement `searchKeyword` using FTS5 instead of tsvector
|
||||
- A SQLite engine can implement `searchVector` using sqlite-vss instead of pgvector
|
||||
- A future DuckDB engine could implement analytics-heavy workloads
|
||||
- The CLI, MCP server, and library consumers never know which engine runs underneath
|
||||
|
||||
See [`ENGINES.md`](ENGINES.md) for the full interface spec. (The original SQLite engine plan was superseded by PGLite; the contract-first `BrainEngine` interface made that swap clean.)
|
||||
|
||||
## Review history
|
||||
|
||||
| Review | Runs | Status | Key findings |
|
||||
|--------|------|--------|-------------|
|
||||
| /office-hours | 1 | APPROVED | Builder mode. Full port approach chosen. |
|
||||
| /plan-ceo-review | 1 | CLEAR | 11 proposals, 10 accepted, 1 deferred. SCOPE EXPANSION mode. |
|
||||
| /codex review | 1 | issues_found | 24 points challenged, 3 accepted (fuzzy slug, revert spec, tsvector). |
|
||||
| /plan-eng-review | 2 | CLEAR | 3 issues (upgrade paths, import guardrails, init wizard), 0 critical gaps. |
|
||||
| /plan-devex-review | 1 | CLEAR | DX score 5/10 to 7/10. TTHW 25min to 90s. Champion tier. |
|
||||
@@ -1,293 +0,0 @@
|
||||
# GBrain Installation Verification Runbook
|
||||
|
||||
Run these checks after install to confirm every part of GBrain is working.
|
||||
Each check includes the command, expected output, and what to do if it fails.
|
||||
|
||||
The most important check is #4 (live sync). "Sync ran" is not the same as
|
||||
"sync worked." A sync that silently skips pages because of a pooler bug is
|
||||
worse than no sync at all, because you think it's working.
|
||||
|
||||
---
|
||||
|
||||
## 1. Schema Verification
|
||||
|
||||
**Command:**
|
||||
|
||||
```bash
|
||||
gbrain doctor --json
|
||||
```
|
||||
|
||||
**Expected:** All checks return `"ok"`:
|
||||
- `connection`: connected, N pages
|
||||
- `pgvector`: extension installed
|
||||
- `rls`: enabled on all tables
|
||||
- `schema_version`: current
|
||||
- `embeddings`: coverage percentage
|
||||
|
||||
**If it fails:** The doctor output includes specific fix instructions for each
|
||||
check. See `skills/setup/SKILL.md` Error Recovery table.
|
||||
|
||||
---
|
||||
|
||||
## 2. Skillpack Loaded
|
||||
|
||||
**Check:** Ask the agent: "What is the brain-agent loop?"
|
||||
|
||||
**Expected:** The agent references GBRAIN_SKILLPACK.md Section 2 and describes
|
||||
the read-write cycle: detect entities, read brain, respond with context, write
|
||||
brain, sync.
|
||||
|
||||
**If it fails:** The agent hasn't loaded the skillpack. Run step 6 from the
|
||||
install paste (read `docs/GBRAIN_SKILLPACK.md`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Auto-Update Configured
|
||||
|
||||
**Command:**
|
||||
|
||||
```bash
|
||||
gbrain check-update --json
|
||||
```
|
||||
|
||||
**Expected:** Returns JSON with `current_version`, `latest_version`,
|
||||
`update_available` (boolean). The cron `gbrain-update-check` is registered.
|
||||
|
||||
**If it fails:** Run step 7 from the install paste. See GBRAIN_SKILLPACK.md
|
||||
Section 17.
|
||||
|
||||
---
|
||||
|
||||
## 4. Live Sync Actually Works
|
||||
|
||||
This is the most important check. Three parts.
|
||||
|
||||
### 4a. Coverage Check
|
||||
|
||||
Compare page count in the DB against syncable file count in the repo:
|
||||
|
||||
```bash
|
||||
gbrain stats
|
||||
```
|
||||
|
||||
Then count syncable files:
|
||||
|
||||
```bash
|
||||
find /data/brain -name '*.md' \
|
||||
-not -path '*/.*' \
|
||||
-not -path '*/.raw/*' \
|
||||
-not -path '*/ops/*' \
|
||||
-not -name 'README.md' \
|
||||
-not -name 'index.md' \
|
||||
-not -name 'schema.md' \
|
||||
-not -name 'log.md' \
|
||||
| wc -l
|
||||
```
|
||||
|
||||
**Expected:** Page count in `gbrain stats` should be close to the file count.
|
||||
Some difference is normal (files added since last sync), but if page count is
|
||||
less than half the file count, sync is silently skipping pages.
|
||||
|
||||
**If page count is way too low:** The #1 cause is the connection pooler bug.
|
||||
Check your `DATABASE_URL`:
|
||||
- If it contains `pooler.supabase.com:6543`, verify it's using **Session mode**,
|
||||
not Transaction mode.
|
||||
- Transaction mode breaks `engine.transaction()` and causes `.begin() is not a
|
||||
function` errors.
|
||||
- Fix: switch to Session mode pooler string, then run `gbrain sync --full`
|
||||
to reimport everything.
|
||||
|
||||
### 4b. Embed Check
|
||||
|
||||
```bash
|
||||
gbrain stats
|
||||
```
|
||||
|
||||
**Expected:** Embedded chunk count should be close to total chunk count.
|
||||
|
||||
**If embedded is much lower than total:**
|
||||
|
||||
```bash
|
||||
gbrain embed --stale
|
||||
```
|
||||
|
||||
If `OPENAI_API_KEY` is not set, embeddings can't be generated. Keyword search
|
||||
still works without embeddings, but hybrid/semantic search won't.
|
||||
|
||||
### 4c. End-to-End Test
|
||||
|
||||
This is the real test. Edit a brain page, push, wait, search.
|
||||
|
||||
1. Edit a page in the brain repo (e.g., correct a fact on a person's page):
|
||||
|
||||
```bash
|
||||
# Example: fix a line in Gustaf's page
|
||||
cd /data/brain
|
||||
# Make a small edit to any .md file
|
||||
git add -A && git commit -m "test: verify live sync" && git push
|
||||
```
|
||||
|
||||
2. Wait for the next sync cycle (cron interval or `--watch` poll).
|
||||
|
||||
3. Search for the corrected text:
|
||||
|
||||
```bash
|
||||
gbrain search "<text from the correction>"
|
||||
```
|
||||
|
||||
**Expected:** The search returns the **corrected** text, not the old version.
|
||||
|
||||
**If it returns old text:** Sync failed silently. Check:
|
||||
- Is the sync cron registered and running?
|
||||
- Is `gbrain sync --watch` still alive (if using watch mode)?
|
||||
- Run `gbrain config get sync.last_run` to see when sync last ran.
|
||||
- Run `gbrain sync --repo /data/brain` manually and check for errors.
|
||||
- If you see `.begin() is not a function`, fix the pooler (see 4a above).
|
||||
|
||||
---
|
||||
|
||||
## 5. Embedding Coverage
|
||||
|
||||
**Command:**
|
||||
|
||||
```bash
|
||||
gbrain stats
|
||||
```
|
||||
|
||||
**Expected:** Embedded chunk count matches (or is close to) total chunk count.
|
||||
|
||||
**If zero or very low:** `OPENAI_API_KEY` may be missing or invalid. Check:
|
||||
|
||||
```bash
|
||||
echo $OPENAI_API_KEY | head -c 10
|
||||
```
|
||||
|
||||
If blank, set the key. Then:
|
||||
|
||||
```bash
|
||||
gbrain embed --stale
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Brain-First Lookup Protocol
|
||||
|
||||
**Check:** Ask the agent about a person or concept that exists in the brain.
|
||||
|
||||
**Expected:** The agent uses `gbrain search` or `gbrain query` FIRST, not grep
|
||||
or external APIs. The response includes brain-sourced context with source
|
||||
attribution.
|
||||
|
||||
**If it fails:** The brain-first lookup protocol isn't injected into the agent's
|
||||
system context. See `skills/setup/SKILL.md` Phase D.
|
||||
|
||||
---
|
||||
|
||||
## 7. Knowledge Graph Wired
|
||||
|
||||
The v0.12.0 graph layer needs to be populated for existing brains. New writes are
|
||||
auto-linked, but historical pages need a one-time backfill.
|
||||
|
||||
**Command:**
|
||||
|
||||
```bash
|
||||
gbrain stats | grep -E 'links|timeline'
|
||||
```
|
||||
|
||||
**Expected:** Both `links` and `timeline_entries` are non-zero (assuming the brain
|
||||
has content with entity references and dated markdown).
|
||||
|
||||
**If it's zero on a brain with imported content:** Run the backfill.
|
||||
|
||||
```bash
|
||||
gbrain extract links --source db --dry-run | head -5 # preview
|
||||
gbrain extract links --source db # commit
|
||||
gbrain extract timeline --source db
|
||||
gbrain stats # confirm > 0
|
||||
```
|
||||
|
||||
**Bonus check** — graph traversal works:
|
||||
|
||||
```bash
|
||||
# Pick any well-connected slug from your brain
|
||||
gbrain graph-query people/<some-person-slug> --depth 2
|
||||
```
|
||||
|
||||
**Expected:** Indented tree of typed edges (`--attended-->`, `--works_at-->`, etc.).
|
||||
If the slug has no inbound or outbound links, try a different one or run extract
|
||||
again.
|
||||
|
||||
**If extract finds nothing:** Your pages may not use entity-reference syntax. The
|
||||
extractor matches `[Name](people/slug)`, `[Name](../people/slug.md)`, and bare
|
||||
`people/slug` references. If your brain uses a different format, the auto-link
|
||||
heuristics won't find them — file an issue with a sample page.
|
||||
|
||||
---
|
||||
|
||||
## 8. JSONB Frontmatter Integrity (v0.12.2)
|
||||
|
||||
Postgres-backed brains created before v0.12.2 had double-encoded JSONB columns
|
||||
(`frontmatter->>'key'` returned NULL, GIN indexes were inert). `gbrain upgrade`
|
||||
runs `gbrain repair-jsonb` automatically via the `v0_12_2` orchestrator.
|
||||
Verify the repair succeeded.
|
||||
|
||||
**Command:**
|
||||
|
||||
```bash
|
||||
gbrain repair-jsonb --dry-run --json
|
||||
```
|
||||
|
||||
**Expected:** `totalRepaired: 0` across all 5 columns (`pages.frontmatter`,
|
||||
`raw_data.data`, `ingest_log.pages_updated`, `files.metadata`,
|
||||
`page_versions.frontmatter`). A zero count means every row is properly-typed
|
||||
JSON objects, not string-encoded JSON.
|
||||
|
||||
**If the count is > 0:** The repair didn't run or was interrupted. Re-run
|
||||
without `--dry-run`:
|
||||
|
||||
```bash
|
||||
gbrain repair-jsonb
|
||||
```
|
||||
|
||||
Idempotent. PGLite brains always report 0 (unaffected by the original bug).
|
||||
|
||||
**Bonus check** — frontmatter-keyed queries actually resolve:
|
||||
|
||||
```bash
|
||||
gbrain call list_pages '{"frontmatterKey": "type", "frontmatterValue": "person"}'
|
||||
```
|
||||
|
||||
If this returns rows on a brain with person pages, the JSONB path is healthy.
|
||||
|
||||
---
|
||||
|
||||
## Quick Verification (all checks in one pass)
|
||||
|
||||
```bash
|
||||
# 1. Schema
|
||||
gbrain doctor --json
|
||||
|
||||
# 2. Sync recency
|
||||
gbrain config get sync.last_run
|
||||
|
||||
# 3. Page count + embed coverage
|
||||
gbrain stats
|
||||
|
||||
# 4. Search works
|
||||
gbrain search "test query from your brain content"
|
||||
|
||||
# 5. Catch any unembedded chunks
|
||||
gbrain embed --stale
|
||||
|
||||
# 6. Auto-update
|
||||
gbrain check-update --json
|
||||
|
||||
# 7. Knowledge graph populated (links + timeline > 0)
|
||||
gbrain stats | grep -E 'links|timeline'
|
||||
|
||||
# 8. JSONB integrity (v0.12.2 — Postgres only, PGLite always 0)
|
||||
gbrain repair-jsonb --dry-run --json
|
||||
```
|
||||
|
||||
If all eight return successfully, the installation is healthy. For the full
|
||||
end-to-end sync test (4c), push a real change and verify it appears in search.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Install
|
||||
|
||||
Three install paths. Pick one. Mix later if needed.
|
||||
|
||||
## 1. Run with an agent platform (recommended)
|
||||
|
||||
Already running [OpenClaw](https://github.com/garrytan/openclaw) or [Hermes](https://github.com/garrytan/hermes)?
|
||||
|
||||
```bash
|
||||
bun install -g github:garrytan/gbrain
|
||||
gbrain init --pglite # 2 seconds; no server
|
||||
gbrain skillpack scaffold --all # 43 skills scaffolded into your agent workspace
|
||||
gbrain doctor # green checks all the way down
|
||||
```
|
||||
|
||||
Your agent now reads `skills/RESOLVER.md` once per request, routes intent to the right skill, executes. New entity mentions create new pages. Daily cron runs enrichment overnight.
|
||||
|
||||
Scaffolded skills are first-class files in your agent repo — edit freely. To pull upstream gbrain improvements later, `gbrain skillpack reference <name>` diffs your local copy vs the bundle. The legacy `skillpack install` managed-block model was retired in v0.36.0.0; if you're upgrading from an older release, run `gbrain skillpack migrate-fence` once to strip the legacy fence and keep your existing skill rows.
|
||||
|
||||
To upgrade later: `gbrain upgrade` runs schema migrations + post-upgrade prompts (chunker bumps, the v0.36.2.0 ZeroEntropy switch). Always TTY-only; non-TTY upgrades skip prompts with informational stderr lines.
|
||||
|
||||
## 2. CLI standalone
|
||||
|
||||
No agent platform, just shell + MCP-aware editor.
|
||||
|
||||
```bash
|
||||
bun install -g github:garrytan/gbrain
|
||||
gbrain init --pglite
|
||||
```
|
||||
|
||||
> **If `bun install -g` hits a postinstall error** (Bun blocks postinstall hooks in some environments), the CLI prints a recovery hint pointing at [#218](https://github.com/garrytan/gbrain/issues/218). Run `gbrain doctor` to diagnose, then `gbrain apply-migrations --yes` manually. The deterministic fallback is `git clone https://github.com/garrytan/gbrain.git ~/gbrain && cd ~/gbrain && bun install && bun link`.
|
||||
|
||||
The init flow detects your repo size and suggests Supabase for brains > 1000 markdown files. To switch later:
|
||||
|
||||
```bash
|
||||
gbrain migrate --to supabase # PGLite → Postgres
|
||||
gbrain migrate --to pglite # Postgres → PGLite (rare)
|
||||
```
|
||||
|
||||
For shared / large / multi-machine deployments (a team or company brain with multiple users hitting one server over HTTP MCP with OAuth scoping per user), follow the dedicated walkthrough: **[Tutorial: set up GBrain as your company brain](tutorials/company-brain.md)**.
|
||||
|
||||
API keys live in `~/.gbrain/config.json` (file plane) or env vars (`OPENAI_API_KEY`, `ZEROENTROPY_API_KEY`, `VOYAGE_API_KEY`, `ANTHROPIC_API_KEY`). Set via CLI:
|
||||
|
||||
```bash
|
||||
gbrain config set zeroentropy_api_key sk-...
|
||||
gbrain config set anthropic_api_key sk-ant-...
|
||||
```
|
||||
|
||||
Common follow-ups:
|
||||
|
||||
```bash
|
||||
gbrain import ~/my-knowledge # bulk-import a markdown folder
|
||||
gbrain sync --watch # live-sync a git repo (autopilot mode)
|
||||
gbrain autopilot --install # background daemon for nightly enrichment
|
||||
```
|
||||
|
||||
## 3. MCP server (any MCP client)
|
||||
|
||||
```bash
|
||||
gbrain serve # stdio MCP (Claude Desktop / Code / Cursor)
|
||||
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard
|
||||
```
|
||||
|
||||
Per-client setup guides live in [`docs/mcp/`](mcp/):
|
||||
|
||||
- [`docs/mcp/CLAUDE_CODE.md`](mcp/CLAUDE_CODE.md)
|
||||
- [`docs/mcp/CLAUDE_DESKTOP.md`](mcp/CLAUDE_DESKTOP.md)
|
||||
- [`docs/mcp/CHATGPT.md`](mcp/CHATGPT.md)
|
||||
- [`docs/mcp/PERPLEXITY.md`](mcp/PERPLEXITY.md)
|
||||
- [`docs/mcp/DEPLOY.md`](mcp/DEPLOY.md) — production deploy patterns
|
||||
|
||||
The HTTP server ships with an admin SPA at `/admin`, an SSE activity feed at `/admin/events`, DCR-style client registration, scope-gated `read`/`write`/`admin` access, and rate limiting.
|
||||
|
||||
## Thin-client mode
|
||||
|
||||
Connect to someone else's brain without running a local engine:
|
||||
|
||||
```bash
|
||||
gbrain init --mcp-only # configures remote MCP, skips local DB
|
||||
```
|
||||
|
||||
Useful for: team mounts, brain-as-a-service deployments, dev machines without disk space. Most local commands refuse with a paste-ready hint. See [`docs/architecture/topologies.md`](architecture/topologies.md).
|
||||
|
||||
## Verifying the install
|
||||
|
||||
```bash
|
||||
gbrain doctor --json # full health check
|
||||
gbrain models # which AI models are configured for what
|
||||
gbrain models doctor # 1-token probe per configured model
|
||||
```
|
||||
|
||||
If anything's yellow, `gbrain doctor` names the fix command in the message. Most issues are missing API keys or stale schema (`gbrain upgrade --force-schema`).
|
||||
@@ -1,612 +0,0 @@
|
||||
# Upgrading Downstream Agents
|
||||
|
||||
GBrain ships skills in `skills/`. Downstream agents (custom OpenClaw deployments,
|
||||
agent forks of any kind) often **copy** these skill files into their own workspace and
|
||||
diverge over time — adding agent-specific phases, removing irrelevant ones, tightening
|
||||
language. Once that happens, gbrain can't push updates to those forks. The agent has
|
||||
to apply the diffs by hand.
|
||||
|
||||
This doc lists the exact diffs each downstream agent needs to apply when upgrading.
|
||||
Cross-reference against your fork's local skill files.
|
||||
|
||||
## Why this exists
|
||||
|
||||
`gbrain upgrade` ships the new binary. `gbrain post-upgrade [--execute --yes]` runs
|
||||
the schema migrations and backfills the data. But the **skill files themselves**
|
||||
that tell the agent how to behave — those are user-owned. If your `~/git/<your-agent>/workspace/skills/brain-ops/SKILL.md`
|
||||
says `# Based on gbrain v0.10.0` at the top, it doesn't know about v0.12.0 features.
|
||||
|
||||
The agent will keep manually calling `gbrain link` after every `put_page` (now redundant —
|
||||
auto-link does it), miss out on `gbrain graph-query` for relationship questions, and
|
||||
not know to backfill the structured timeline.
|
||||
|
||||
## How to apply
|
||||
|
||||
1. Identify your forked skill files. Typically at `~/git/<your-agent>/workspace/skills/` or wherever your agent's skill directory lives.
|
||||
2. For each skill listed below, find the matching phase/section in your fork.
|
||||
3. Apply the diff (paste the new block in the indicated location).
|
||||
4. Update the version banner at the top of your fork (`# Based on gbrain v0.12.0`).
|
||||
5. Verify: ask the agent to write a test page and confirm the response includes
|
||||
`auto_links: { created, removed, errors }`.
|
||||
|
||||
Total time: ~10 minutes for all four skills.
|
||||
|
||||
---
|
||||
|
||||
## 1. brain-ops/SKILL.md
|
||||
|
||||
**Where:** Insert a new `### Phase 2.5` section immediately after `### Phase 2: On Every Inbound Signal`.
|
||||
|
||||
**Why:** Phase 2.5 declares that auto-link runs automatically. Without this, the
|
||||
agent's mental model says it must call `gbrain link` after every `put_page`, which
|
||||
is now redundant and can cause double-add warnings.
|
||||
|
||||
```markdown
|
||||
### Phase 2.5: Structured Graph Updates (automatic)
|
||||
|
||||
Every `put_page` call automatically extracts entity references and writes them
|
||||
to the graph (`links` table) with inferred relationship types. Stale links
|
||||
(refs no longer in the page text) are removed in the same call. This is
|
||||
"auto-link" reconciliation.
|
||||
|
||||
- No manual `add_link` calls needed for ordinary page writes.
|
||||
- Inferred link types: `attended` (meeting -> person), `works_at`, `invested_in`,
|
||||
`founded`, `advises`, `source` (frontmatter), `mentions` (default).
|
||||
- The `put_page` MCP response includes `auto_links: { created, removed, errors }`
|
||||
so the agent can verify outcomes.
|
||||
- To disable: `gbrain config set auto_link false`. Default is on.
|
||||
- Timeline entries with specific dates still need explicit `gbrain timeline-add`
|
||||
(or batch via `gbrain extract timeline --source db`).
|
||||
```
|
||||
|
||||
**Also update the Iron Law section.** If your fork still says "Back-links maintained
|
||||
on every brain write (Iron Law)" without qualification, append:
|
||||
|
||||
```markdown
|
||||
**v0.12.0 update:** Auto-link satisfies the Iron Law for entity-reference links
|
||||
on every `put_page`. The agent's Iron Law obligation is now: include the
|
||||
entity reference in the page content (e.g., `[Alice](people/alice)`); auto-link
|
||||
handles the structured row. Manual `add_link` calls are reserved for
|
||||
relationships you can't express in markdown content.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. meeting-ingestion/SKILL.md
|
||||
|
||||
**Where:** Append to the end of `### Phase 3: Attendee enrichment`.
|
||||
|
||||
**Why:** Eliminates redundant `gbrain link` calls per attendee (auto-link handles them
|
||||
when the meeting page references attendees as `[Name](people/slug)`).
|
||||
|
||||
```markdown
|
||||
**Note (v0.12.0):** Once the meeting page is written via `gbrain put`, the
|
||||
auto-link post-hook automatically creates `attended` links from the meeting
|
||||
to each attendee whose page is referenced as `[Name](people/slug)`. You don't
|
||||
need to call `gbrain link` for attendees. You DO still need `gbrain timeline-add`
|
||||
for dated events (auto-link only handles links, not timeline entries).
|
||||
```
|
||||
|
||||
**Where:** In `### Phase 4: Entity propagation`, the line "Back-link from entity page
|
||||
to meeting page" can be replaced with:
|
||||
|
||||
```markdown
|
||||
4. Entity references in the meeting page body auto-create the link via auto-link.
|
||||
For incoming references on the entity page (entity page → meeting page), edit
|
||||
the entity page to mention the meeting and `put_page` it — auto-link handles
|
||||
the rest.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. signal-detector/SKILL.md
|
||||
|
||||
**Where:** Append to the end of `### Phase 2: Entity Detection`.
|
||||
|
||||
**Why:** Same logic as brain-ops — eliminates manual `gbrain link` after writing
|
||||
originals/ideas pages that reference people or companies.
|
||||
|
||||
```markdown
|
||||
**Auto-link (v0.12.0):** When you write/update an originals or ideas page that
|
||||
references a person or company, the auto-link post-hook on `put_page`
|
||||
automatically creates the link from the new page to that entity. You don't
|
||||
need to call `gbrain link` manually. Timeline entries still need explicit calls.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. enrich/SKILL.md
|
||||
|
||||
**Where:** Replace `### Step 7: Cross-reference` with the v0.12.0 version.
|
||||
|
||||
**Why:** Step 7 used to be primarily about creating links between related entity
|
||||
pages. With auto-link, that's automatic. Step 7 is now about content updates,
|
||||
not link creation.
|
||||
|
||||
Old (delete):
|
||||
```markdown
|
||||
### Step 7: Cross-reference
|
||||
|
||||
- Update company pages from person enrichment (and vice versa)
|
||||
- Update related project/deal pages if relevant context surfaced
|
||||
- Check index files if the brain uses them
|
||||
- Add back-links manually via `gbrain link` for any new entity references
|
||||
```
|
||||
|
||||
New (paste):
|
||||
```markdown
|
||||
### Step 7: Cross-reference
|
||||
|
||||
- Update company pages from person enrichment (and vice versa)
|
||||
- Update related project/deal pages if relevant context surfaced
|
||||
- Check index files if the brain uses them
|
||||
|
||||
**Note (v0.12.0):** Links between brain pages are auto-created on every
|
||||
`put_page` call (auto-link post-hook). Step 7 focuses on content
|
||||
cross-references (updating related pages' compiled truth with new signal
|
||||
from this enrichment), not on creating links. Verify via the `auto_links`
|
||||
field in the put_page response (`{ created, removed, errors }`).
|
||||
Timeline entries still need explicit `gbrain timeline-add` calls.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## After all four diffs are applied
|
||||
|
||||
1. **Bump the version banner** at the top of each forked file:
|
||||
```
|
||||
# Based on gbrain v0.12.0 skills/<skill-name>, extended with <your-agent>-specific config
|
||||
```
|
||||
|
||||
2. **Run the v0.12.0 backfill** (this populates the graph for your existing brain):
|
||||
```bash
|
||||
gbrain post-upgrade
|
||||
```
|
||||
The v0.12.0 release wires post-upgrade to call `apply-migrations --yes`
|
||||
automatically, which runs the v0_12_0 orchestrator (schema → config check →
|
||||
`extract links --source db` → `extract timeline --source db` → verify).
|
||||
Idempotent; cheap when nothing is pending.
|
||||
|
||||
3. **Verify auto-link works:** ask the agent to write a test page that references
|
||||
`[Some Person](people/some-person)`. Confirm the put_page response includes
|
||||
`auto_links: { created: 1, removed: 0, errors: 0 }`.
|
||||
|
||||
4. **Verify graph traversal works:**
|
||||
```bash
|
||||
gbrain graph-query people/some-well-connected-person --depth 2
|
||||
```
|
||||
Should return an indented tree of typed edges.
|
||||
|
||||
---
|
||||
|
||||
## v0.12.2 hotfix (data-correctness, no skill edits)
|
||||
|
||||
v0.12.2 is a Postgres data-correctness hotfix. No forked skill files need to
|
||||
change — the skill contracts are unchanged. But you DO need to run the migration,
|
||||
and you should know about one behavior change in markdown parsing.
|
||||
|
||||
### 1. Run the migration (Postgres-backed brains)
|
||||
|
||||
```bash
|
||||
gbrain upgrade
|
||||
```
|
||||
|
||||
The `v0_12_2` orchestrator runs `gbrain repair-jsonb` automatically. It rewrites
|
||||
rows where `jsonb_typeof = 'string'` across `pages.frontmatter`, `raw_data.data`,
|
||||
`ingest_log.pages_updated`, `files.metadata`, and `page_versions.frontmatter`.
|
||||
Idempotent, safe to re-run. PGLite brains no-op cleanly.
|
||||
|
||||
Verify after upgrade:
|
||||
|
||||
```bash
|
||||
gbrain repair-jsonb --dry-run --json # expect totalRepaired: 0
|
||||
```
|
||||
|
||||
### 2. Recover any truncated wiki articles
|
||||
|
||||
If your brain imported wiki-style markdown before v0.12.2, some pages were
|
||||
silently truncated (any standalone `---` in body content was treated as a
|
||||
timeline separator). Re-import from source:
|
||||
|
||||
```bash
|
||||
gbrain sync --full
|
||||
```
|
||||
|
||||
The new `splitBody` rebuilds `compiled_truth` correctly.
|
||||
|
||||
### 3. Know the splitBody contract going forward
|
||||
|
||||
`splitBody` now requires an explicit timeline sentinel. Recognized markers
|
||||
(priority order):
|
||||
|
||||
1. `<!-- timeline -->` (preferred — what `serializeMarkdown` emits)
|
||||
2. `--- timeline ---` (decorated separator)
|
||||
3. `---` directly before `## Timeline` or `## History` heading (backward-compat)
|
||||
|
||||
A bare `---` in body text is now a markdown horizontal rule, not a timeline
|
||||
separator. If your agent writes pages with a bare `---` delimiter, migrate to
|
||||
`<!-- timeline -->` — the `serializeMarkdown` helper already does this.
|
||||
|
||||
### 4. Wiki subtypes now auto-typed
|
||||
|
||||
`inferType` now auto-detects five additional directory patterns as their own
|
||||
page types (previously they all defaulted to `concept`):
|
||||
|
||||
| Path pattern | New type |
|
||||
|------------------------|----------------|
|
||||
| `/wiki/analysis/` | `analysis` |
|
||||
| `/wiki/guides/` | `guide` |
|
||||
| `/wiki/hardware/` | `hardware` |
|
||||
| `/wiki/architecture/` | `architecture` |
|
||||
| `/writing/` | `writing` |
|
||||
|
||||
If your skills or queries filter by `type=concept` and expect wiki content in
|
||||
that bucket, update them to include the new types.
|
||||
|
||||
---
|
||||
|
||||
## v0.13.0 — Frontmatter Relationship Indexing
|
||||
|
||||
**Verdict: no action required for most skills.** v0.13 projects YAML frontmatter fields into the graph as typed edges. The ingestion API is unchanged — keep calling `put_page` with frontmatter the way you do today; the graph auto-populates behind the scenes.
|
||||
|
||||
Three skills get an optional new phase if you want to consume the new `auto_links.unresolved` response field. Without this, unresolvable frontmatter names silently skip (same as v0.12 behavior).
|
||||
|
||||
### 1. meeting-ingestion/SKILL.md (optional)
|
||||
|
||||
**Where:** Add a new section after "Phase 3: Write Meeting Page".
|
||||
|
||||
```markdown
|
||||
### Phase 3.5: Check for unresolved attendees (v0.13+)
|
||||
|
||||
After `put_page`, inspect `response.auto_links.unresolved` — an array of frontmatter
|
||||
references that did not resolve to existing pages. For meetings, this usually means
|
||||
attendees you haven't created a person page for yet.
|
||||
|
||||
If `unresolved.length > 0`:
|
||||
- Option 1 (create pages now): trigger an enrichment pass to build the missing people pages.
|
||||
- Option 2 (defer): log the unresolved names to the enrichment queue for later.
|
||||
- Option 3 (accept the gap): the attendee edge will not be created until a page exists.
|
||||
Re-running `gbrain extract links --source db --include-frontmatter` after creating
|
||||
the page fills in the missing edges.
|
||||
```
|
||||
|
||||
### 2. enrich/SKILL.md (optional)
|
||||
|
||||
**Where:** Add to the enrichment trigger list.
|
||||
|
||||
```markdown
|
||||
### Drain unresolved frontmatter names (v0.13+)
|
||||
|
||||
If any `put_page` response includes `auto_links.unresolved` entries, the enrichment
|
||||
tier should pick up those (field, name) pairs and try to create the missing entity
|
||||
pages. Example flow:
|
||||
|
||||
1. signal-detector captures a meeting with `attendees: [Alice Known, Unknown Person]`
|
||||
2. put_page returns `auto_links.unresolved = [{field: 'attendees', name: 'Unknown Person'}]`
|
||||
3. enrichment tier consumes `Unknown Person` → web search → creates `people/unknown-person.md`
|
||||
4. The next put_page (or a backfill run) wires up the `attended` edge automatically
|
||||
```
|
||||
|
||||
### 3. idea-ingest/SKILL.md (optional)
|
||||
|
||||
**Where:** Same pattern as meeting-ingestion — check `auto_links.unresolved` after `put_page`, route names to enrichment.
|
||||
|
||||
### Unchanged skills (no diffs needed)
|
||||
|
||||
- **brain-ops/SKILL.md** — auto-link mechanics are internal; the write path stays the same.
|
||||
- **signal-detector/SKILL.md** — signal capture path unchanged.
|
||||
- **query/SKILL.md** — `traverse_graph` now returns richer results automatically.
|
||||
- **daily-task-manager/SKILL.md**, **briefing/SKILL.md**, **citation-fixer/SKILL.md**, **media-ingest/SKILL.md** — unchanged.
|
||||
|
||||
### New edge types you can filter in graph queries
|
||||
|
||||
v0.13 edges carry new `link_type` values. If your fork has graph-query skills that filter by type, these are now available:
|
||||
|
||||
- `works_at` (person → company) — from `company:`, `companies:`, or `key_people:`
|
||||
- `founded` (person → company) — from `founded:`
|
||||
- `invested_in` (investor → deal/company) — from `investors:` or `lead:`
|
||||
- `led_round` (lead → deal) — from `lead:`
|
||||
- `yc_partner` (partner → company) — from `partner:`
|
||||
- `attended` (person → meeting) — from `attendees:`
|
||||
- `discussed_in` (source → page) — from `sources:`
|
||||
- `source` (page → source) — from `source:`
|
||||
- `related_to` (page → target) — from `related:` or `see_also:`
|
||||
|
||||
### Migration timing
|
||||
|
||||
`gbrain upgrade` takes 2-5 min on a 46K-page brain (one-time). Runs out-of-process via `gbrain post-upgrade`. If your agent holds a DB connection during the upgrade, reconnect after; otherwise keep serving.
|
||||
|
||||
### Type normalization NOT in v0.13
|
||||
|
||||
Legacy rows with `link_type='attendee'` or `link_type='mention'` coexist with new `'attended'` / `'mentions'` rows. Your queries filtering on old type names keep working. A separate opt-in `gbrain normalize-types` command in v0.14 handles the rename.
|
||||
## v0.14.0 shell jobs (optional adoption, no skill edits)
|
||||
|
||||
Adds a `shell` job type to Minions so deterministic cron scripts (API fetch, token
|
||||
refresh, scrape + write) move off the LLM gateway. Zero tokens per fire. ~60%
|
||||
gateway CPU headroom at typical scale. Feature is **off by default**, existing
|
||||
installs keep running exactly as they did before. Nothing breaks.
|
||||
|
||||
To adopt, follow `skills/migrations/v0.14.0.md`. The short version:
|
||||
|
||||
1. Set `GBRAIN_ALLOW_SHELL_JOBS=1` on the worker process, then `gbrain jobs work`
|
||||
(Postgres). On PGLite, every crontab invocation uses `--follow` for inline
|
||||
execution; no persistent worker.
|
||||
2. Classify each of your host's cron entries: LLM-requiring (keep on gateway) vs
|
||||
deterministic (candidate for shell). Typical splits:
|
||||
- **Deterministic → shell:** `ycli-token-refresh`, `x-oauth2-refresh`,
|
||||
`x-garrytan-unified`, `calendar-sync-to-brain`, `github-pulse`,
|
||||
`frameio-scan`, `flight-tracker`, `x-raw-json-backfill`.
|
||||
- **LLM-requiring → stay:** `social-radar`, `content-ideas`, `adversary-vacuum`,
|
||||
`ea-inbox-sweep`, `morning-briefing`, `brain-maintenance`.
|
||||
3. For each deterministic cron, rewrite as:
|
||||
```cron
|
||||
3 13,16,19,22,1,4,7,10 * * * \
|
||||
gbrain jobs submit shell \
|
||||
--params '{"cmd":"node scripts/your-script.mjs","cwd":"/data/.openclaw/workspace"}' \
|
||||
--max-attempts 3 --timeout-ms 300000
|
||||
```
|
||||
4. Watch `gbrain jobs get <id>` for exit_code / stdout_tail / stderr_tail on each
|
||||
fire. Compare against pre-migration behavior before approving the next batch.
|
||||
|
||||
**No skill edits required.** The handler runs worker-side; skill files don't
|
||||
change. If your host exposed custom handlers via the plugin contract (v0.11.0),
|
||||
they still work the same way.
|
||||
|
||||
Iron rule: **never auto-rewrite the operator's crontab.** Every rewrite is
|
||||
per-cron, human-approved, with a diff. If you want automation later, the
|
||||
upcoming `gbrain crontab-to-minions <file>` helper is P1 in TODOS.
|
||||
|
||||
---
|
||||
|
||||
## v0.16.0: durable agent runtime
|
||||
|
||||
v0.15 ships `gbrain agent run` / `gbrain agent logs`, a new `subagent` handler
|
||||
type in Minions, and a plugin contract for host-repo subagent defs. None of the
|
||||
existing skills need surgery. The question for downstream agents is *how* to
|
||||
adopt the new runtime, not how to patch around a breaking change.
|
||||
|
||||
### 1. Run a worker with an Anthropic key
|
||||
|
||||
The subagent handlers (`subagent` and `subagent_aggregator`) are always
|
||||
registered on the worker. No separate opt-in flag — `ANTHROPIC_API_KEY` is
|
||||
the natural cost gate (no key, the SDK call fails on the first turn), and
|
||||
who-can-submit is already protected (`PROTECTED_JOB_NAMES` + trusted-submit:
|
||||
MCP callers get `permission_denied`; only `gbrain agent run` can insert
|
||||
these rows).
|
||||
|
||||
```bash
|
||||
ANTHROPIC_API_KEY=sk-ant-... gbrain jobs work
|
||||
```
|
||||
|
||||
Worker startup prints:
|
||||
|
||||
```
|
||||
[minion worker] subagent handlers enabled
|
||||
```
|
||||
|
||||
### 2. Ship your subagents as a plugin (OpenClaw + similar)
|
||||
|
||||
Move your custom subagent definitions out of your gbrain fork and into your own
|
||||
repo as a plugin. Concretely:
|
||||
|
||||
```
|
||||
~/<your-agent>/gbrain-plugin/
|
||||
├── gbrain.plugin.json
|
||||
└── subagents/
|
||||
├── meeting-ingestion.md
|
||||
├── signal-detector.md
|
||||
└── daily-task-prep.md
|
||||
```
|
||||
|
||||
`gbrain.plugin.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "your-openclaw",
|
||||
"version": "2026.4.20",
|
||||
"plugin_version": "gbrain-plugin-v1"
|
||||
}
|
||||
```
|
||||
|
||||
Each `subagents/*.md` is a plain-text agent definition — YAML frontmatter +
|
||||
body-as-system-prompt. Recognized frontmatter fields: `name`, `model`,
|
||||
`max_turns`, `allowed_tools` (must subset the derived brain-tool registry).
|
||||
|
||||
Turn it on:
|
||||
|
||||
```bash
|
||||
export GBRAIN_PLUGIN_PATH="$HOME/<your-agent>/gbrain-plugin"
|
||||
```
|
||||
|
||||
Worker startup prints `[plugin-loader] loaded '<name>' v<ver> (N subagents)`
|
||||
per plugin; any rejection (bad manifest, unknown tool in `allowed_tools`,
|
||||
version mismatch) shows up as a loud warning at startup, not a silent dispatch-
|
||||
time failure. See `docs/guides/plugin-authors.md` for the full contract.
|
||||
|
||||
### 3. Replace ephemeral subagent runs with durable ones
|
||||
|
||||
If your agent currently spawns ephemeral subagents (OpenClaw `Agent()`, ad-hoc
|
||||
Anthropic API calls, etc.) for work that should survive crashes, sleeps, or
|
||||
worker restarts, migrate those to `gbrain agent run`. The durability is free:
|
||||
|
||||
```bash
|
||||
gbrain agent run "analyze my last 50 journal pages for recurring themes" \
|
||||
--subagent-def analyzer --fanout-manifest manifests/journal-pages.json
|
||||
```
|
||||
|
||||
Every turn persists to `subagent_messages`, every tool call is a two-phase
|
||||
ledger, and `gbrain agent logs <job>` shows where it died + what the last
|
||||
successful call returned. No more "re-run from scratch because the session
|
||||
context evaporated."
|
||||
|
||||
### 4. `put_page` from subagents writes under an agent namespace
|
||||
|
||||
If you adopted the v0.15 subagent runtime, note that `put_page` calls
|
||||
originating from a subagent's tool dispatch MUST target
|
||||
`wiki/agents/<subagent_id>/...`. The schema shown to the model enforces this
|
||||
on first try; a server-side fail-closed check rejects anything else. This
|
||||
does NOT affect your skill files, CLI put_page calls, or MCP put_page —
|
||||
only tool-dispatched writes from inside an LLM loop.
|
||||
|
||||
Aggregation output (the final "here's what all N children found" brain page)
|
||||
goes via a separate trusted CLI path, not through a subagent tool call, so
|
||||
it can write anywhere you want.
|
||||
|
||||
Iron rule: **never grant an agent write access beyond its namespace**. The
|
||||
server-side check exists because dispatcher bugs happen; treat it as defense
|
||||
in depth, not the primary boundary.
|
||||
|
||||
---
|
||||
|
||||
## v0.22.4 — frontmatter-guard adoption
|
||||
|
||||
### 1. Stop hand-rolling frontmatter validators
|
||||
|
||||
If your fork has scripts that call `js-yaml` directly to validate brain page
|
||||
frontmatter, replace them with `gbrain frontmatter validate` calls. The CLI
|
||||
covers the seven canonical error classes and ships a `--json` envelope that's
|
||||
stable across releases.
|
||||
|
||||
```diff
|
||||
- # Custom validator script
|
||||
- node scripts/validate-frontmatter.mjs <path>
|
||||
+ gbrain frontmatter validate <path> --json
|
||||
```
|
||||
|
||||
For consumers that need the validator inside another script, import from
|
||||
gbrain's `markdown` export instead of duplicating logic:
|
||||
|
||||
```ts
|
||||
import { parseMarkdown } from 'gbrain/markdown';
|
||||
|
||||
const parsed = parseMarkdown(content, filePath, { validate: true, expectedSlug });
|
||||
for (const err of parsed.errors ?? []) {
|
||||
// err.code: MISSING_OPEN | MISSING_CLOSE | YAML_PARSE | SLUG_MISMATCH |
|
||||
// NULL_BYTES | NESTED_QUOTES | EMPTY_FRONTMATTER
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Drop any references to `lib/brain-writer.mjs`
|
||||
|
||||
If your fork's skills or scripts referenced an aspirational
|
||||
`lib/brain-writer.mjs` (it never shipped — the spec was in PR #392 and never
|
||||
landed), replace those references with the gbrain CLI. The `frontmatter-guard`
|
||||
skill lives at `skills/frontmatter-guard/SKILL.md` and points at
|
||||
`gbrain frontmatter validate` / `audit` / `install-hook`.
|
||||
|
||||
### 3. Wire the doctor subcheck into your health pipeline
|
||||
|
||||
`gbrain doctor` now reports `frontmatter_integrity` automatically. If your
|
||||
fork has a custom health pipeline (e.g. a daily Slack post about brain
|
||||
health), pull from `gbrain doctor --json` and surface the
|
||||
`frontmatter_integrity` row counts.
|
||||
|
||||
### 4. (Optional) Install the pre-commit hook on brain repos
|
||||
|
||||
For sources backed by git, the v0.22.4 install-hook helper drops a
|
||||
pre-commit script that blocks commits with malformed frontmatter:
|
||||
|
||||
```bash
|
||||
gbrain frontmatter install-hook
|
||||
```
|
||||
|
||||
Skip this if your brain isn't a git repo or if your downstream agent already
|
||||
enforces validation at write time. See `docs/integrations/pre-commit.md` for
|
||||
the full recipe.
|
||||
|
||||
### 5. Migration ergonomics — read pending-host-work.jsonl
|
||||
|
||||
After `gbrain apply-migrations --yes` runs the v0.22.4 audit, your agent
|
||||
should read `~/.gbrain/migrations/pending-host-work.jsonl` (filter to
|
||||
`migration === "0.22.4"`) and walk each entry's `command` field. Each entry
|
||||
points to a per-source `gbrain frontmatter validate <source_path> --fix`
|
||||
command — surface counts to the user, get explicit consent, then run.
|
||||
|
||||
The migration is **audit-only**. It never mutates brain content during
|
||||
`apply-migrations`. Your agent runs the fix command with user consent.
|
||||
|
||||
---
|
||||
|
||||
## Future versions
|
||||
|
||||
When gbrain ships a new version, this doc will be updated with the diffs for that
|
||||
version. Each new version appends a section; old sections stay so you can catch up
|
||||
multiple versions at once.
|
||||
|
||||
To check what your fork is missing:
|
||||
```bash
|
||||
diff <(grep -A3 "Based on gbrain" ~/<your-fork>/skills/brain-ops/SKILL.md) \
|
||||
<(grep "v[0-9]" ~/gbrain/skills/migrations/ | tail -3)
|
||||
```
|
||||
|
||||
|
||||
## v0.36.5.0 — Free-form secret inheritance for shell jobs calling `gbrain` CLI
|
||||
|
||||
**The change.** Shell-job params get a new `inherit:` field. Pass any
|
||||
snake_case config-key name on it; the worker resolves the value from its
|
||||
`loadConfig()` at child-spawn time and injects it into the child env. Names
|
||||
land in the row; values never persist from `inherit:`. Validation runs
|
||||
**pre-enqueue** in both submit paths (CLI + `submit_job` op), so a malformed
|
||||
payload never lands in `minion_jobs.data`.
|
||||
|
||||
**Why.** Pre-v0.36.5.0, agents that wanted to call `gbrain` from shell jobs
|
||||
had to either write `database_url` to `~/.gbrain/config.json` plaintext or
|
||||
pass `env: { GBRAIN_DATABASE_URL: "..." }` per-job. Both left plaintext
|
||||
secrets somewhere — disk or DB row. `inherit:` keeps names in the row and
|
||||
resolves values at spawn time.
|
||||
|
||||
**What your agent can do.** `inherit:` is free-form. Pass any config-key:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"cmd": "gbrain sync --skip-failed && gbrain embed --stale",
|
||||
"cwd": "/data/gbrain",
|
||||
"inherit": ["database_url", "anthropic_api_key", "voyage_api_key"]
|
||||
}
|
||||
```
|
||||
|
||||
The env-key name in the child is derived by uppercasing the config-key:
|
||||
`database_url` → `GBRAIN_DATABASE_URL`, `anthropic_api_key` →
|
||||
`ANTHROPIC_API_KEY`, `voyage_api_key` → `VOYAGE_API_KEY`, etc. The validator
|
||||
does NOT police which config keys you inherit — the agent is in the same
|
||||
uid as the worker, so it's the agent's call.
|
||||
|
||||
**You can still use `env:`.** v0.36.5.0 does not forbid `env:{ ANYTHING }`.
|
||||
If you have a reason to put a value in the row plaintext (a non-secret
|
||||
correlation token, or a secret you know is OK to persist), pass it via
|
||||
`env:`. Prefer `inherit:` when you want the value out of the row.
|
||||
|
||||
**Worker setup** (one-time, per host):
|
||||
|
||||
- `gbrain config set database_url postgresql://...` (or any other key you
|
||||
want available for inherit)
|
||||
- OR put the key in `~/.gbrain/config.json` directly
|
||||
- OR set `GBRAIN_DATABASE_URL` / `DATABASE_URL` / per-provider env on the
|
||||
worker process
|
||||
|
||||
If the worker can't resolve a requested name, the validator fail-fasts at
|
||||
submit time with `gbrain config set <X>` hint. No more silent "No database
|
||||
URL" failures in child stderr minutes after submission.
|
||||
|
||||
**Also new.** A `gbrain doctor` check `home_dir_in_worktree` warns if
|
||||
`~/.gbrain/` lives inside a git worktree. A retroactive `~/.gbrain/.gitignore`
|
||||
(single line `*`) is now laid down by every `saveConfig()` call AND by
|
||||
`gbrain post-upgrade`, so existing users get coverage without re-running
|
||||
`gbrain init`. Honest scope: the `.gitignore` covers casual `git add` but does
|
||||
NOT cover already-tracked files, screenshots, backups, or `git add -f`.
|
||||
|
||||
**Strategy framing.** For agent-to-gbrain calls, the new canonical guide is
|
||||
`docs/guides/agent-to-gbrain.md`. Two distinct surfaces: HTTP MCP via OAuth
|
||||
for ops with MCP equivalents (`search`, `query`, `put_page`, etc.), and shell
|
||||
job + `inherit:` for `localOnly` admin ops (`sync`, `embed`, `dream`,
|
||||
`doctor`, etc.). Not a fallback hierarchy — pick by op.
|
||||
|
||||
**Errors to handle** (your agent submits shell jobs; surface these clearly):
|
||||
|
||||
| Error | What it means | Agent action |
|
||||
|---|---|---|
|
||||
| `shell: inherit must be an array of config-key names` | `inherit` wasn't an array. | Pass `"inherit": ["database_url", ...]`. |
|
||||
| `shell: inherit entries must be non-empty strings` | Element was empty, non-string, or null. | Use snake_case config-key names. |
|
||||
| `shell: inherit name "<X>" must match [a-z][a-z0-9_]*` | Name failed snake_case regex (uppercase, leading underscore, etc.). | Use the config-key verbatim — `database_url`, not `DATABASE_URL`. |
|
||||
| `shell: inherit requested "<X>" but worker has no <X> configured` | Worker can't resolve the name from its `loadConfig()`. | Run `gbrain config set <X> <value>` on the worker host. |
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
# llama-server reranker (local) — Qwen3-Reranker, self-hosted ZE, any ZE-wire-shape provider
|
||||
|
||||
[`llama-server`](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)
|
||||
is the HTTP wrapper that ships with llama.cpp. With `--reranking`, it
|
||||
exposes an OpenAI-style `POST /v1/rerank` endpoint that returns
|
||||
`{results: [{index, relevance_score}]}` — exactly the wire shape gbrain
|
||||
already drives for ZeroEntropy's hosted reranker. The
|
||||
`llama-server-reranker` recipe (added in v0.40.6.1) routes
|
||||
`gateway.rerank()` at your local llama.cpp instance instead of ZE.
|
||||
|
||||
Two flavors of "local" this recipe covers:
|
||||
|
||||
- **Qwen3-Reranker** (0.6B / 4B / 8B) — open-weight cross-encoder; pull
|
||||
the GGUF from HuggingFace and serve.
|
||||
- **Self-hosted ZeroEntropy** (`zerank-2`, `zerank-1-small`) — the
|
||||
weights are on HuggingFace too. GGUF-convert them and serve them the
|
||||
same way. **Quality is not guaranteed to match ZE-hosted:** GGUF
|
||||
conversion + quantization + pooling/rank metadata + tokenizer special
|
||||
tokens all affect scores. If you self-host ZE for production
|
||||
retrieval, pin your own brain-relevant eval (
|
||||
[docs/eval-bench.md](../eval-bench.md)) as a regression guard.
|
||||
|
||||
This recipe is the path override + recipe shape. Any provider whose
|
||||
request/response wire matches ZE/llama.cpp can use it by just pointing
|
||||
at a different base URL. Providers whose wire shape differs (Voyage uses
|
||||
`top_k` not `top_n`, returns `data[]` not `results[]`) need a separate
|
||||
recipe with adapter hooks — that lands in a follow-up plan.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Build llama.cpp (or download a release)
|
||||
|
||||
```bash
|
||||
# Clone and build (CPU only; add `-DGGML_CUDA=ON` for GPU)
|
||||
git clone https://github.com/ggml-org/llama.cpp.git
|
||||
cd llama.cpp
|
||||
cmake -B build
|
||||
cmake --build build --config Release -j
|
||||
```
|
||||
|
||||
Pin a specific commit when you ship — `llama-server`'s path aliases
|
||||
(`/rerank`, `/v1/rerank`, `/reranking`, `/v1/reranking`) have shifted
|
||||
across releases. The recipe sends to `/v1/rerank`.
|
||||
|
||||
### 2. Pull a reranker GGUF
|
||||
|
||||
For Qwen3-Reranker-4B (quantized Q4_K_M is the sweet spot for CPU):
|
||||
|
||||
```bash
|
||||
# Pick a quant level — Q4_K_M is the usual CPU sweet spot.
|
||||
huggingface-cli download \
|
||||
Qwen/Qwen3-Reranker-4B-GGUF qwen3-reranker-4b-q4_k_m.gguf \
|
||||
--local-dir ./models
|
||||
```
|
||||
|
||||
For self-hosted ZeroEntropy weights, find a community GGUF conversion
|
||||
or convert from the HuggingFace weights yourself (out of scope of this
|
||||
doc — see llama.cpp's `convert_hf_to_gguf.py`).
|
||||
|
||||
### 3. Launch llama-server with --reranking AND --alias
|
||||
|
||||
```bash
|
||||
./build/bin/llama-server \
|
||||
--model ./models/qwen3-reranker-4b-q4_k_m.gguf \
|
||||
--alias qwen3-reranker-4b \
|
||||
--reranking \
|
||||
--port 8081
|
||||
```
|
||||
|
||||
The `--alias` matters: without it, llama-server's `/v1/models` (and the
|
||||
`model` field rerank requests echo) defaults to the full gguf file
|
||||
path, which makes the gbrain config string ugly and brittle. With
|
||||
`--alias qwen3-reranker-4b`, your config string is short and stable.
|
||||
|
||||
`--reranking` and `--embeddings` are mutually exclusive at server
|
||||
launch. If you also run a local embedder via the
|
||||
[`llama-server`](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md)
|
||||
recipe, run two separate llama-server processes on two different ports
|
||||
(typically 8080 for embeddings, 8081 for reranking — gbrain's defaults
|
||||
match that convention).
|
||||
|
||||
### 4. Wire gbrain at your server
|
||||
|
||||
```bash
|
||||
# Point gbrain at the llama.cpp host (skip if running locally on default port)
|
||||
gbrain config set provider_base_urls.llama-server-reranker http://your-host:8081/v1
|
||||
|
||||
# Tell search to use this reranker
|
||||
gbrain config set search.reranker.model llama-server-reranker:qwen3-reranker-4b
|
||||
gbrain config set search.reranker.enabled true
|
||||
```
|
||||
|
||||
The `qwen3-reranker-4b` after the colon is your `--alias` value from
|
||||
step 3. Any string works as long as it matches your server's alias.
|
||||
|
||||
Env vars work too as an alternative to the config set above:
|
||||
|
||||
```bash
|
||||
export LLAMA_SERVER_RERANKER_BASE_URL=http://your-host:8081/v1
|
||||
# Optional: if you front llama-server with nginx + bearer auth
|
||||
export LLAMA_SERVER_RERANKER_API_KEY=your-bearer-token
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
|
||||
```bash
|
||||
gbrain models doctor
|
||||
# Expect: ✔ reranker_config llama-server-reranker:qwen3-reranker-4b ok
|
||||
# ✔ reranker_config llama-server-reranker:qwen3-reranker-4b ok (reachability)
|
||||
|
||||
gbrain search "some query" --json | jq '.[].rerank_score'
|
||||
# Expect: rerank_score on every row
|
||||
```
|
||||
|
||||
If `gbrain models doctor` reports the reachability probe as `network`
|
||||
status, two common causes:
|
||||
|
||||
1. The server is reachable but in embedding mode, not reranking mode.
|
||||
`--reranking` and `--embeddings` are mutually exclusive at launch
|
||||
— relaunch the right one.
|
||||
2. The recipe path doesn't match what your llama.cpp version serves.
|
||||
This recipe sends `/v1/rerank`; older llama.cpp installs may only
|
||||
serve `/rerank`. Pin to a recent llama.cpp commit.
|
||||
|
||||
## Cold-start headroom
|
||||
|
||||
CPU-only first-call warmup on a 4B reranker can take 8-15 seconds. The
|
||||
recipe declares `default_timeout_ms: 30000` so the first call after a
|
||||
server restart doesn't fail-open silently. That value flows through
|
||||
search-mode resolution unless you override it:
|
||||
|
||||
```bash
|
||||
# Tighten or loosen per-search timeout (overrides recipe default):
|
||||
gbrain config set search.reranker.timeout_ms 60000
|
||||
```
|
||||
|
||||
Per-call overrides in `SearchOpts.reranker_timeout_ms` still win for
|
||||
any single call.
|
||||
|
||||
## Budget caps + local rerank
|
||||
|
||||
The recipe declares `cost_per_1m_tokens_usd: 0` and registers under
|
||||
`FREE_LOCAL_RERANK_PROVIDERS` in the budget tracker, so
|
||||
`--max-cost`-bounded callers (autopilot loops, batch jobs) do NOT
|
||||
hard-fail when configured for local rerank. Local rerank costs
|
||||
electricity, not API tokens.
|
||||
|
||||
```bash
|
||||
GBRAIN_MAX_USD=0.01 gbrain search "..." --reranker llama-server-reranker:qwen3-reranker-4b
|
||||
# Works: rerank fires, recorded at $0, cumulative cap untouched.
|
||||
```
|
||||
|
||||
## Fail-open contract preserved
|
||||
|
||||
`applyReranker` in `src/core/search/rerank.ts` still has the
|
||||
fail-open posture: any error class (network, timeout, malformed
|
||||
response) logs to `~/.gbrain/audit/rerank-failures-*.jsonl` and
|
||||
returns the original RRF order unchanged. Search reliability beats
|
||||
reranker quality. If your llama.cpp host goes down, your searches keep
|
||||
working — they just stop ranking against the cross-encoder until you
|
||||
restart the server.
|
||||
@@ -1,173 +0,0 @@
|
||||
# ZeroEntropy — zembed-1 + zerank-2
|
||||
|
||||
[ZeroEntropy](https://zeroentropy.dev) ships two specialized small models
|
||||
for retrieval pipelines:
|
||||
|
||||
- **`zembed-1`** — multilingual embedding distilled from zerank-2.
|
||||
Flexible Matryoshka dims (2560/1280/640/320/160/80/40), 32K context,
|
||||
asymmetric `input_type: query|document` encoding. $0.025/1M tokens
|
||||
(sale) / $0.05 regular.
|
||||
- **`zerank-2`** — SOTA multilingual cross-encoder reranker.
|
||||
$0.025/1M tokens (~50% cheaper than Cohere/Voyage rerankers).
|
||||
Plus `zerank-1` and `zerank-1-small` for legacy / open-source needs.
|
||||
|
||||
Both land in gbrain v0.35.0.0 behind the openai-compatible recipe path,
|
||||
alongside OpenAI and Voyage.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Get an API key at
|
||||
[dashboard.zeroentropy.dev](https://dashboard.zeroentropy.dev).
|
||||
2. Export it:
|
||||
```bash
|
||||
export ZEROENTROPY_API_KEY=<your-key>
|
||||
```
|
||||
|
||||
## Embedding switch — zembed-1
|
||||
|
||||
**Important:** `gbrain config set embedding_model …` is NOT a live
|
||||
gateway switch. `embedding_model` and `embedding_dimensions` size the
|
||||
schema and must be stable across engine connects, so they only resolve
|
||||
from the **file plane** (`~/.gbrain/config.json`) and the **env plane**
|
||||
(`GBRAIN_EMBEDDING_MODEL` / `GBRAIN_EMBEDDING_DIMENSIONS`). The DB plane
|
||||
is intentionally ignored for these two keys (same posture as today's
|
||||
Voyage setup).
|
||||
|
||||
### Option A — file plane (recommended for stable installs)
|
||||
|
||||
Edit `~/.gbrain/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"embedding_model": "zeroentropyai:zembed-1",
|
||||
"embedding_dimensions": 2560
|
||||
}
|
||||
```
|
||||
|
||||
Valid dims: `2560` (default), `1280`, `640`, `320`, `160`, `80`, `40`.
|
||||
Matryoshka-style — smaller trades quality for storage monotonically.
|
||||
Pick the largest that fits your column width.
|
||||
|
||||
### Option B — env plane (CI / Docker)
|
||||
|
||||
```bash
|
||||
export GBRAIN_EMBEDDING_MODEL=zeroentropyai:zembed-1
|
||||
export GBRAIN_EMBEDDING_DIMENSIONS=2560
|
||||
```
|
||||
|
||||
### Re-embed
|
||||
|
||||
Switching embedding models invalidates the vector index. Re-embed:
|
||||
|
||||
```bash
|
||||
gbrain embed --stale --limit 50 # smoke a small batch
|
||||
gbrain embed --stale # full re-embed
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
gbrain models doctor --json | jq '.probes[] | select(.touchpoint=="embedding_config")'
|
||||
```
|
||||
|
||||
Expected: `status: "ok"`. Invalid dims (e.g. `1024`, `1536`, `3072`)
|
||||
surface as `status: "config"` with a paste-ready
|
||||
`gbrain config set embedding_dimensions <one of 2560|1280|640|320|160|80|40>` fix hint.
|
||||
|
||||
## Reranker switch — zerank-2
|
||||
|
||||
The reranker is the bigger story: gbrain had no cross-encoder reranker
|
||||
stage before v0.35.0.0. It slots between RRF dedup and token-budget
|
||||
enforcement in hybrid search.
|
||||
|
||||
### Default-on with `tokenmax` mode
|
||||
|
||||
`tokenmax` mode now defaults `search.reranker.enabled = true` with
|
||||
`zerank-2`. If you already use `tokenmax` AND have `ZEROENTROPY_API_KEY`
|
||||
set, reranker fires automatically. Without the key, every rerank call
|
||||
fails-open (audit-logged) and search returns RRF order — same UX as
|
||||
before, just with an observable failure surfaced via `gbrain doctor`.
|
||||
|
||||
### Opt-in on `conservative` or `balanced` mode
|
||||
|
||||
```bash
|
||||
gbrain config set search.reranker.enabled true
|
||||
```
|
||||
|
||||
The override sits above the mode-bundle default; opt-out is one flip.
|
||||
|
||||
### Cost anchor
|
||||
|
||||
At 30 candidates × ~400 tokens/chunk × $0.025/1M = **~$0.0003/query**.
|
||||
Rounding error against the `tokenmax + Opus` pairing's ~$700/mo at
|
||||
single-user volume per the CLAUDE.md cost matrix.
|
||||
|
||||
### Verify
|
||||
|
||||
```bash
|
||||
gbrain models doctor --json | jq '.probes[] | select(.touchpoint=="reranker_config")'
|
||||
```
|
||||
|
||||
Two probes run for reranker:
|
||||
- `reranker_config` (zero-network) — validates the model resolves
|
||||
through the recipe registry and is in the touchpoint's allowlist.
|
||||
- A reachability probe sends a minimal `{query: "probe", documents:
|
||||
["probe"]}` rerank to verify auth + URL.
|
||||
|
||||
## Knobs reference
|
||||
|
||||
| Config key | Default | Notes |
|
||||
|---|---|---|
|
||||
| `search.reranker.enabled` | `true` for tokenmax, `false` for others | One-flip opt-in/out |
|
||||
| `search.reranker.model` | `zeroentropyai:zerank-2` | Try `zerank-1` (older SOTA) or `zerank-1-small` (Apache-2.0 open) |
|
||||
| `search.reranker.top_n_in` | `30` | Candidates sent to reranker (caps API spend) |
|
||||
| `search.reranker.top_n_out` | `null` (no truncate) | Truncate reranked output to this many; `null` preserves full length |
|
||||
| `search.reranker.timeout_ms` | `5000` | HTTP timeout; long stalls degrade UX worse than RRF fallback |
|
||||
|
||||
## Failure observability
|
||||
|
||||
Reranker is fail-open by construction: every error class (auth, rate-limit,
|
||||
network, timeout, payload-too-large, unknown) returns the original RRF
|
||||
order unchanged. Failures log to
|
||||
`~/.gbrain/audit/rerank-failures-YYYY-Www.jsonl` (ISO-week rotation).
|
||||
|
||||
`gbrain doctor` reads the audit and surfaces:
|
||||
- **auth failures** — any single one warns (config-time problem doctor's
|
||||
own probe should have caught)
|
||||
- **payload-too-large** — any single one warns (workload-mismatch signal)
|
||||
- **transient (network/timeout/rate_limit)** — warns at >=5 in 7 days
|
||||
|
||||
Query text is SHA-256 hashed in the audit; never logged raw.
|
||||
|
||||
## Asymmetric input_type
|
||||
|
||||
ZE zembed-1 (and Voyage v3+) use asymmetric query/document encoding for
|
||||
better retrieval. The gateway's `embedQuery(text)` companion threads
|
||||
`input_type: 'query'`; standard `embed(texts)` defaults to
|
||||
`'document'`. Hybrid search's two query-side embed sites use
|
||||
`embedQuery()` automatically; all ingest paths use `embed()`.
|
||||
|
||||
Symmetric providers (OpenAI text-embedding-3, fixed-dim Voyage models)
|
||||
ignore the field — no behavior change.
|
||||
|
||||
## Cache key versioning
|
||||
|
||||
v0.35.0.0 bumped `KNOBS_HASH_VERSION` 1 → 2 to fold reranker config into
|
||||
the `query_cache.knobs_hash` column. During a rolling deploy:
|
||||
|
||||
- Expect a temporary cache hit-rate dip (~1 hour at default
|
||||
`cache.ttl_seconds = 3600s`)
|
||||
- Hot queries may briefly double their cache row count (one row per
|
||||
version)
|
||||
|
||||
Both clear naturally; no operator action required.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| `embedding_config` probe says invalid dim | Defaulting to 1536 (OpenAI default) | Set `embedding_dimensions` to one of 2560/1280/640/320/160/80/40 |
|
||||
| `reranker_config` probe says model not in allowlist | Typo in `search.reranker.model` | Use one of `zerank-2` / `zerank-1` / `zerank-1-small` |
|
||||
| `reranker_health` doctor warns about auth | `ZEROENTROPY_API_KEY` not set or invalid | Re-export the env var; `gbrain models doctor` to verify |
|
||||
| `reranker_health` doctor warns about transient failures | Upstream flake or rate limit | Reranker fails open to RRF; check ZE status page if persistent |
|
||||
| Cache hit rate dipped after upgrade | Expected during rolling deploy | Clears within `cache.ttl_seconds` (default 3600s) |
|
||||
@@ -1,130 +0,0 @@
|
||||
# Why the hybrid + graph stack works
|
||||
|
||||
Vector search alone underdelivers on real personal-knowledge queries. This doc explains why gbrain layers four strategies together and how they compound.
|
||||
|
||||
## The four strategies in concert
|
||||
|
||||
1. **Vector (HNSW on pgvector)** — semantic similarity. Catches "who works on retrieval quality at YC?" → pages mentioning "Garry Tan + retrieval" even when the user never typed "YC".
|
||||
2. **BM25 keyword** — lexical match. Catches names, exact phrases, code identifiers, anything where the user remembers the literal token. Survives the cases where vector search drifts into thematic neighbors.
|
||||
3. **Reciprocal-rank fusion (RRF)** — merges vector + keyword rankings without weighting one over the other globally. Each strategy gets to vote.
|
||||
4. **Knowledge graph traversal** — follows typed edges. Catches "what did Bob invest in this quarter?" by walking `bob ── invested_in ──> company ── dated ──> Q1`. Vector search can't see causal chains; the graph can.
|
||||
|
||||
## Why each one alone fails
|
||||
|
||||
**Vector only.** Returns chunks semantically close to the query. Misses any factual relationship not directly encoded in the embedding. "Companies in Garry's portfolio" returns essays about portfolios, not company pages.
|
||||
|
||||
**Keyword only (ripgrep-style).** Brittle to phrasing. "Who works on retrieval?" misses pages that say "search ranking" instead of "retrieval." Garbage on synonyms, near-misses, or paraphrases.
|
||||
|
||||
**Graph only.** Excellent at "neighbors of Alice" but blind to anything not yet linked. Sparse on fresh pages until backlinks accumulate.
|
||||
|
||||
**Hybrid (vector + keyword + RRF), no graph.** Decent at "what is X?" type queries. Fails on "what is Y's relationship to X?" — those are graph queries and no amount of embedding tuning recovers them.
|
||||
|
||||
## The benchmark
|
||||
|
||||
BrainBench (corpus + harness in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo) measures retrieval P@5, R@5, MRR, nDCG@5 on a 240-page Opus-generated rich-prose corpus.
|
||||
|
||||
| Strategy | P@5 | R@5 | Notes |
|
||||
|---|---|---|---|
|
||||
| ripgrep BM25 only | ~18 | ~75 | Lexical-only baseline |
|
||||
| vector-only RAG | ~18 | ~80 | Standard RAG implementation |
|
||||
| gbrain graph-disabled (hybrid + RRF, no graph traversal) | ~18 | ~85 | Hybrid alone |
|
||||
| **gbrain default (full stack)** | **49.1** | **97.9** | Graph + extract-quality lift |
|
||||
|
||||
**+31 P@5 points** from the graph + extract quality work. The graph isn't a marginal feature; it's the load-bearing wall.
|
||||
|
||||
## Auto-link: why zero-LLM-call edge extraction works
|
||||
|
||||
Every `put_page` runs `extractEntityRefs` on the markdown body. It matches:
|
||||
|
||||
- Standard markdown links: `[Garry Tan](wiki/people/garry-tan)`
|
||||
- Obsidian wikilinks: `[[wiki/people/garry-tan|Garry Tan]]`
|
||||
- Typed-link blockquotes: `> **Convention:** see [path](path).`
|
||||
|
||||
Three regexes, zero LLM tokens, single SQL `addLinksBatch` call with `INSERT ... SELECT FROM unnest(...) JOIN pages ON CONFLICT DO NOTHING RETURNING 1`. The graph grows on every write at near-zero cost. On a 17K-page brain, full graph extract completes in seconds.
|
||||
|
||||
Heuristic link-type inference (`attended`, `works_at`, `invested_in`, `founded`, `advises`) fires from surrounding sentence context — also LLM-free. Power users who want richer types add them via the typed-link blockquote convention.
|
||||
|
||||
## ZeroEntropy as reranker: 60% top-1 reshuffle
|
||||
|
||||
v0.36.0.0 ships ZeroEntropy's `zerank-2` as the default reranker (on for the `balanced` mode bundle). On a real-corpus benchmark across 20 queries, zerank-2 reshuffles **60% of top-1 results** after the hybrid + RRF + graph stack. That's the headline number.
|
||||
|
||||
The mechanical reason: hybrid ranking is locally optimal per strategy but globally suboptimal. A cross-encoder reranker reads the query + each candidate document jointly, with full attention. It catches the cases where the vector + keyword + graph signals all agreed on a document that's semantically related but topically wrong.
|
||||
|
||||
The cost: +150ms p50 latency, ~$0.025/M tokens. Disabled with `gbrain config set search.reranker.enabled false`. For agent loops that do downstream LLM work after retrieval, the latency is invisible.
|
||||
|
||||
## Source-aware ranking
|
||||
|
||||
Hybrid search applies a source-factor CASE expression at the SQL layer (lives in `src/core/search/sql-ranking.ts`). Curated content like `originals/`, `concepts/`, `writing/` outranks bulk content like `your-openclaw/chat/`, `daily/`, `media/x/`. Hard-exclude prefixes (`test/`, `archive/`, `attachments/`, `.raw/`) filter at retrieval, not post-rank.
|
||||
|
||||
The boost map is configurable via `GBRAIN_SOURCE_BOOST` env var or per-call `SearchOpts.exclude_slug_prefixes`. Temporal queries (`detail: 'high'`) bypass the boost so chat pages re-surface for time-sensitive lookups.
|
||||
|
||||
## Intent-aware query rewriting
|
||||
|
||||
`src/core/search/intent.ts` classifies queries into `entity`, `temporal`, `event`, or `general`. Each routes through different ranking knobs:
|
||||
|
||||
- **Entity** queries ("who works at X?") apply a higher graph-traversal weight.
|
||||
- **Temporal** queries ("what happened last week?") bypass source-boost so chat/daily pages surface.
|
||||
- **Event** queries ("Acme AI Series A") engage the timeline index.
|
||||
- **General** queries hit the standard hybrid stack.
|
||||
|
||||
The classifier is deterministic (no LLM call). Wrong classification degrades gracefully — the hybrid stack still works without it.
|
||||
|
||||
## Multi-query expansion
|
||||
|
||||
For `detail: 'high'` searches, `src/core/search/expansion.ts` runs a Haiku-class LLM call to produce 2-3 query variants. Each variant runs through the full hybrid stack; results merge via RRF. Catches synonym misses without recall loss.
|
||||
|
||||
Expansion is opt-in per mode bundle (`tokenmax` on by default; `balanced` + `conservative` off). Default off in the cheap tiers because the LLM call adds ~$0.001/query and ~200ms — real money at scale.
|
||||
|
||||
## Putting it together
|
||||
|
||||
The full pipeline for a `query` op:
|
||||
|
||||
```
|
||||
intent classify
|
||||
│
|
||||
▼
|
||||
expansion (if enabled)
|
||||
│
|
||||
▼
|
||||
hybrid search:
|
||||
├── vector (HNSW on chunk embeddings)
|
||||
├── keyword (BM25 via tsvector)
|
||||
├── source-aware re-rank (CASE in SQL)
|
||||
└── RRF fusion → top 30
|
||||
│
|
||||
▼
|
||||
graph augment (typed-edge traversal from any seed)
|
||||
│
|
||||
▼
|
||||
reranker (zerank-2 cross-encoder, top 30 → reordered)
|
||||
│
|
||||
▼
|
||||
token-budget enforcement (per mode bundle)
|
||||
│
|
||||
▼
|
||||
deduplication (same slug, different chunks → keep best)
|
||||
│
|
||||
▼
|
||||
results
|
||||
```
|
||||
|
||||
Each stage is testable in isolation. Each stage is replaceable. The whole pipeline is < 1ms of orchestration cost; the latency budget goes to the upstream HTTP calls (embedding, rerank) and the index scans.
|
||||
|
||||
## How to verify on your own brain
|
||||
|
||||
```bash
|
||||
# Run the public LongMemEval benchmark
|
||||
gbrain eval longmemeval datasets/longmemeval_s.jsonl
|
||||
|
||||
# Capture your own queries and replay against retrieval changes
|
||||
export GBRAIN_CONTRIBUTOR_MODE=1
|
||||
# ... use gbrain normally ...
|
||||
gbrain eval export > before.ndjson
|
||||
# ... change something ...
|
||||
gbrain eval replay --against before.ndjson
|
||||
|
||||
# A/B retrieval strategies on a labeled fixture
|
||||
gbrain eval --qrels labels.tsv --config balanced.json
|
||||
```
|
||||
|
||||
Methodology + metric glossary in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](../eval/SEARCH_MODE_METHODOLOGY.md).
|
||||
@@ -1,242 +0,0 @@
|
||||
# Brains and Sources — the mental model
|
||||
|
||||
GBrain has two orthogonal axes for organizing knowledge. Users and agents both
|
||||
need to understand both of them, or queries misroute silently.
|
||||
|
||||
**TL;DR:**
|
||||
- A **brain** is a database. You can have many.
|
||||
- A **source** is a named repo of content *inside* a brain. One brain can hold many.
|
||||
- `--brain <id>` picks WHICH DATABASE.
|
||||
- `--source <id>` picks WHICH REPO WITHIN that database.
|
||||
- They're independent. You can target any combination.
|
||||
|
||||
---
|
||||
|
||||
## The two axes
|
||||
|
||||
### Brains (the DB axis)
|
||||
|
||||
A **brain** is one database — PGLite file, self-hosted Postgres, or Supabase.
|
||||
Each brain has:
|
||||
- Its own `pages` table, `chunks` table, `embeddings`, etc.
|
||||
- Its own OAuth surface if served over HTTP MCP (v0.19+, PR 2).
|
||||
- Its own separate lifecycle, backup, access control.
|
||||
|
||||
Brains are enumerated by:
|
||||
- **host** — your default brain, configured in `~/.gbrain/config.json`.
|
||||
- **mounts** — additional brains registered in `~/.gbrain/mounts.json` via
|
||||
`gbrain mounts add <id>` (v0.19+).
|
||||
|
||||
Routing: `--brain <id>`, `GBRAIN_BRAIN_ID`, `.gbrain-mount` dotfile, or
|
||||
longest-path match against registered mount paths. Falls back to `host`.
|
||||
|
||||
### Sources (the repo axis, v0.18.0+)
|
||||
|
||||
A **source** is a named content repo *inside* one brain. Every `pages` row
|
||||
carries a `source_id`. Slugs are unique per source, not globally.
|
||||
|
||||
Example: in one brain, the slug `topics/ai` can exist under `source=wiki`
|
||||
AND under `source=gstack` — they're different pages.
|
||||
|
||||
Routing: `--source <id>`, `GBRAIN_SOURCE`, `.gbrain-source` dotfile, or
|
||||
registered `local_path` match in the `sources` table.
|
||||
|
||||
### When does each axis move?
|
||||
|
||||
| You want to | Adjust |
|
||||
|---|---|
|
||||
| Work in a different repo within the same brain (wiki → gstack notes) | `--source` |
|
||||
| Query a team-published brain that isn't yours | `--brain` |
|
||||
| Isolate a topic so it never leaks into personal search | `--source` with `federated=false` |
|
||||
| Share a brain with teammates | `--brain` (mount the team brain) |
|
||||
| Add a new repo to your personal brain | `--source` via `gbrain sources add` |
|
||||
| Add a team brain | `--brain` via `gbrain mounts add` |
|
||||
|
||||
**Rule of thumb:** if the data owner changes, it's a brain boundary. If the
|
||||
data owner stays the same but the topic/repo changes, it's a source boundary.
|
||||
|
||||
---
|
||||
|
||||
## Topology: a single-person developer
|
||||
|
||||
Simplest case. One brain, one source.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ host brain (~/.gbrain) │
|
||||
│ ├── source: default (federated=true) │
|
||||
│ │ └── all pages │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`gbrain query "retry budgets"` finds everything. No `--brain`, no `--source`
|
||||
needed.
|
||||
|
||||
---
|
||||
|
||||
## Topology: a personal brain with multiple repos
|
||||
|
||||
You maintain several codebases or writing streams. Each is its own source
|
||||
inside one brain. Cross-source search is on by default so a query about
|
||||
"caching" returns hits from every repo.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ host brain (~/.gbrain) │
|
||||
│ ├── source: wiki (federated=true) │
|
||||
│ │ └── personal notes, people, companies │
|
||||
│ ├── source: gstack (federated=true) │
|
||||
│ │ └── gstack plans, learnings │
|
||||
│ ├── source: openclaw (federated=true) │
|
||||
│ │ └── openclaw docs, memos │
|
||||
│ └── source: essays (federated=false) │
|
||||
│ └── draft essays, isolated on purpose │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Inside `~/openclaw/` the `.gbrain-source` dotfile pins every command to
|
||||
`source=openclaw`. Inside `~/gstack/` the dotfile pins to `source=gstack`.
|
||||
Everything still targets one DB.
|
||||
|
||||
Use this topology when:
|
||||
- You own all the content.
|
||||
- You want cross-repo search to just work.
|
||||
- You don't need to share any of it with someone who isn't you.
|
||||
|
||||
---
|
||||
|
||||
## Topology: personal brain + one team brain
|
||||
|
||||
You're on a team that publishes a shared brain. Your personal brain stays
|
||||
as-is; you mount the team brain alongside it.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ host brain (~/.gbrain) — YOUR personal DB │
|
||||
│ ├── source: wiki │
|
||||
│ ├── source: gstack │
|
||||
│ └── ... │
|
||||
└──────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ mount: media-team │
|
||||
│ path: ~/team-brains/media │
|
||||
│ engine: postgres (team's Supabase) │
|
||||
│ └── sources: wiki, raw, enriched │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
`gbrain query "X"` (no flags) → runs against host (your personal brain).
|
||||
`gbrain query "X" --brain media-team` → runs against the team's DB.
|
||||
Inside `~/team-brains/media/` a `.gbrain-mount` dotfile pins brain to
|
||||
`media-team` automatically.
|
||||
|
||||
Use this topology when:
|
||||
- You're on a team and someone publishes a brain the team subscribes to.
|
||||
- You need data isolation between work and personal.
|
||||
- Different teams/orgs own different brains.
|
||||
|
||||
---
|
||||
|
||||
## Topology: a CEO-class user with multiple team memberships
|
||||
|
||||
You're senior enough to sit across multiple teams. You maintain your personal
|
||||
brain (with N sources inside) AND mount several work team brains. Each team
|
||||
brain is itself a multi-source brain in the v0.18.0 sense — organized
|
||||
internally however the team owner chose.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ host brain — YOUR personal DB │
|
||||
│ ├── source: wiki │
|
||||
│ ├── source: essays │
|
||||
│ ├── source: gstack │
|
||||
│ └── source: openclaw │
|
||||
└──────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ mount: media-team (your media team's brain) │
|
||||
│ └── sources: wiki, pipeline, enriched │
|
||||
└──────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ mount: policy-team (your policy team's) │
|
||||
│ └── sources: wiki, research, letters │
|
||||
└──────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ mount: portfolio (another team's) │
|
||||
│ └── sources: companies, deals, diligence │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Inside each team's checkout, a `.gbrain-mount` dotfile pins the brain. Inside
|
||||
a specific subdirectory, a `.gbrain-source` dotfile pins the source. So `cd
|
||||
~/team-brains/policy/research && gbrain query "X"` targets
|
||||
`brain=policy-team, source=research` with zero flags.
|
||||
|
||||
Use this topology when:
|
||||
- You cross-cut multiple teams.
|
||||
- Each team owns its own brain with its own access policy.
|
||||
- You need latent-space federation (agent decides when to query across
|
||||
brains), not SQL federation.
|
||||
|
||||
Cross-brain queries are **not deterministic** in v0.19. The agent sees the
|
||||
brain list and re-queries as needed. That's the feature — it keeps debugging
|
||||
sane and access control clean.
|
||||
|
||||
---
|
||||
|
||||
## Resolution precedence (one page to remember)
|
||||
|
||||
```
|
||||
WHICH BRAIN (DB)? WHICH SOURCE (repo in DB)?
|
||||
1. --brain <id> 1. --source <id>
|
||||
2. GBRAIN_BRAIN_ID env 2. GBRAIN_SOURCE env
|
||||
3. .gbrain-mount dotfile 3. .gbrain-source dotfile
|
||||
4. longest-prefix mount path match 4. longest-prefix source path match
|
||||
5. (reserved: brains.default v2) 5. sources.default config
|
||||
6. fallback: 'host' 6. fallback: 'default'
|
||||
```
|
||||
|
||||
Both axes follow the same layered pattern on purpose. If you know one, you
|
||||
know the other.
|
||||
|
||||
---
|
||||
|
||||
## For agents reading this
|
||||
|
||||
- Default assumption when the user asks a question: start in the current
|
||||
brain (resolved via the precedence above). Don't jump brains without a
|
||||
reason.
|
||||
- If the user asks a question that crosses topic areas a team might own
|
||||
(e.g. "what did Team X decide last week?"), the right move is to *query
|
||||
the team's brain explicitly* rather than searching host with "team x".
|
||||
- Cross-brain federation is YOUR JOB, not the DB's. You have the brain list
|
||||
(`gbrain mounts list`). You decide when to fan out. You synthesize
|
||||
findings. You cite `brain:source:slug`.
|
||||
- When writing a page, respect the brain boundary. A fact about a team's
|
||||
work belongs in the team's brain, not in the user's personal brain. Ask
|
||||
before writing cross-brain.
|
||||
- See `skills/conventions/brain-routing.md` for the full decision table.
|
||||
|
||||
## For users reading this
|
||||
|
||||
- **Default path:** set up your personal brain (`gbrain init`), add a source
|
||||
per repo you care about (`gbrain sources add gstack --path ~/gstack`).
|
||||
You'll almost never need `--brain`.
|
||||
- **When a team publishes a brain:** `gbrain mounts add <team-id> --path
|
||||
<clone> --db-url <url>` and the `.gbrain-mount` dotfile in that checkout
|
||||
routes queries there automatically.
|
||||
- **When you are the CEO-class user with multiple team memberships:** mount
|
||||
each team brain. Trust the resolver — inside a team's directory the
|
||||
dotfile picks the brain, inside a subdirectory the dotfile picks the
|
||||
source. The flags are for when you want to query across the boundary
|
||||
deliberately.
|
||||
|
||||
## Further reading
|
||||
|
||||
- v0.18.0 CHANGELOG — introduced `sources` primitive.
|
||||
- v0.19.0 CHANGELOG (TBD after PR 0+1+2 ship) — introduces `mounts`.
|
||||
- `docs/mounts/publishing-a-team-brain.md` (PR 2) — how to be the brain
|
||||
publisher, not just the subscriber.
|
||||
@@ -1,215 +0,0 @@
|
||||
# Calibration Quality Gate — Falsifiability Filter + Category Classification
|
||||
|
||||
> **Historical context.** This is the source spec absorbed from PR #1191 into
|
||||
> two waves of implementation:
|
||||
>
|
||||
> - **v0.37.2.0 hotfix** (this release): widens the `takes_resolution_consistency`
|
||||
> CHECK constraint to accept `quality='unresolvable'` as a 4th valid state.
|
||||
> Unblocks the production grading script. Adds `unresolvable_count` +
|
||||
> `unresolvable_rate` to `TakesScorecard` as sibling fields (preserves
|
||||
> v0.36.1.0 historical comparison semantics). Migration renumbered v74→v79→v80
|
||||
> during successive master merges — v0.37.0.0's autonomous-remediation wave
|
||||
> claimed v68-v78, then v0.37.1.0 (brainstorm/lsd) claimed v79.
|
||||
> - **Follow-up minor** (forthcoming): falsifiability + category extraction at
|
||||
> `propose_takes`, SQL-side grade gate, per-category calibration scorecards,
|
||||
> pg_trgm-based proposal dedup. Wave-blocking on cat15 F1 re-validation
|
||||
> against the v0.36.1.0 fixtures.
|
||||
>
|
||||
> Preserved here per the hotfix plan's PR #1191 close protocol so the
|
||||
> production context (96K-page brain, 6.8% falsifiability rate, category
|
||||
> breakdown) doesn't get lost in the CHANGELOG → release-notes condensation.
|
||||
|
||||
## Problem
|
||||
|
||||
v0.36.1.0 ships `propose_takes`, `grade_takes`, and `calibration_profile` as a
|
||||
connected pipeline: extract claims → grade them against outcomes → build a
|
||||
calibration profile showing systematic biases.
|
||||
|
||||
In production on a 96K-page brain with 36K takes across 6,239 holders, the
|
||||
grade_takes phase produces noisy results:
|
||||
|
||||
- **6.8% falsifiability rate**: Of 500 candidate takes (weight ≥ 0.7), only 34
|
||||
passed an LLM falsifiability filter. The other 93% were philosophical beliefs,
|
||||
present-state observations, advice, logistics, or vague vibes.
|
||||
- **50% unresolvable**: Even after filtering, 17/34 predictions couldn't be
|
||||
graded because evidence was insufficient or the claim was too ambiguous.
|
||||
- **Duplicates**: Same claim from the same page extracted multiple times with
|
||||
slightly different wording.
|
||||
|
||||
The root cause: `propose_takes` extracts everything that looks like a belief or
|
||||
assertion. That's correct for the *takes* table (epistemological layer), but
|
||||
`grade_takes` needs a much narrower subset: **falsifiable predictions about
|
||||
future outcomes** where we can check what actually happened.
|
||||
|
||||
### Example classifications from production testing
|
||||
|
||||
**Genuine predictions (grade-worthy):**
|
||||
- "X will reach $1M ARR very soon" → company_outcome
|
||||
- "X is going to leave Y" → people_move
|
||||
- "AI will make authentic authorship more important" → technology
|
||||
- "X was convinced Y would win the Z market" → market_call
|
||||
|
||||
**Not predictions (should skip grading):**
|
||||
- "Desire is mimetic" → philosophical belief
|
||||
- "X should charge 10x more" → advice
|
||||
- "Return from Toronto on Monday" → logistics
|
||||
- "Something is going to happen there" → vague/unfalsifiable
|
||||
- "X is growing very quickly" → present-state observation
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Falsifiability score at extraction time
|
||||
|
||||
Add a `falsifiability` column to the `takes` table (real, 0.0–1.0, nullable,
|
||||
default null). `propose_takes` sets this during extraction using the same LLM
|
||||
call that already produces the take — one additional field in the JSON schema.
|
||||
|
||||
```sql
|
||||
ALTER TABLE takes ADD COLUMN IF NOT EXISTS falsifiability real;
|
||||
ALTER TABLE takes ADD COLUMN IF NOT EXISTS falsifiability_category text;
|
||||
```
|
||||
|
||||
The LLM prompt addition (appended to the existing propose_takes extraction prompt):
|
||||
|
||||
```
|
||||
For each claim, also assess:
|
||||
- falsifiability (0.0-1.0): Can this claim be checked against future reality?
|
||||
1.0 = specific, measurable, time-bounded prediction about an outcome
|
||||
0.5 = directional claim that's partially checkable
|
||||
0.0 = philosophical belief, advice, observation, or unfalsifiable assertion
|
||||
- falsifiability_category: one of
|
||||
company_outcome | fundraising | technology | people_move | market_call | other_prediction | not_prediction
|
||||
```
|
||||
|
||||
Cost: ~0 incremental tokens (the claim is already being extracted; this adds
|
||||
two fields to the JSON output schema).
|
||||
|
||||
### 2. Grade gate in `grade_takes`
|
||||
|
||||
Before attempting grading, filter:
|
||||
|
||||
```typescript
|
||||
const gradeable = candidates.filter(t =>
|
||||
t.falsifiability !== null && t.falsifiability >= 0.7
|
||||
&& t.falsifiability_category !== 'not_prediction'
|
||||
);
|
||||
```
|
||||
|
||||
This reduces grading volume by ~93% in production, which means:
|
||||
- LLM cost for grading drops proportionally
|
||||
- Evidence retrieval load drops (each grade attempt triggers hybrid search)
|
||||
- Calibration profiles are built on real predictions, not noise
|
||||
|
||||
### 3. Deduplication at extraction
|
||||
|
||||
`propose_takes` should check for near-duplicate claims before inserting:
|
||||
|
||||
```typescript
|
||||
// Before inserting a new take, check if a similar claim exists
|
||||
// for the same holder from the same page
|
||||
const existing = await engine.sql`
|
||||
SELECT id, claim FROM takes
|
||||
WHERE holder = ${holder}
|
||||
AND page_id = ${pageId}
|
||||
AND similarity(claim, ${newClaim}) > 0.8
|
||||
LIMIT 1
|
||||
`;
|
||||
if (existing.length > 0) {
|
||||
// Skip — near-duplicate
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
Requires `pg_trgm` extension (already available on most Postgres installations).
|
||||
Falls back gracefully: if `similarity()` isn't available, skip the dedup check.
|
||||
|
||||
### 4. Category-aware calibration profiles
|
||||
|
||||
The `calibration_profile` phase can now group resolved takes by
|
||||
`falsifiability_category` to produce per-domain scorecards:
|
||||
|
||||
```
|
||||
"Your company_outcome calls are 73% accurate.
|
||||
Your people_move calls are 90% accurate.
|
||||
Your technology calls are 60% accurate — you tend to be ~18 months early."
|
||||
```
|
||||
|
||||
This is the tweetable output: a calibration profile that says "here's how you're
|
||||
systematically right and wrong by category."
|
||||
|
||||
## Schema Changes
|
||||
|
||||
```sql
|
||||
-- Migration: add falsifiability columns to takes
|
||||
ALTER TABLE takes ADD COLUMN IF NOT EXISTS falsifiability real;
|
||||
ALTER TABLE takes ADD COLUMN IF NOT EXISTS falsifiability_category text;
|
||||
|
||||
-- Index for grade_takes filter
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_takes_falsifiability
|
||||
ON takes (falsifiability)
|
||||
WHERE falsifiability IS NOT NULL AND falsifiability >= 0.7;
|
||||
|
||||
-- Optional: pg_trgm for dedup (CREATE EXTENSION IF NOT EXISTS pg_trgm;)
|
||||
```
|
||||
|
||||
## Evidence Retrieval (v0.36.1.0 → v0.37 enhancement)
|
||||
|
||||
The current `grade_takes` evidence retriever returns a stub placeholder. In
|
||||
production testing, we wired real evidence retrieval via `gbrain query` (hybrid
|
||||
search). The pattern that works:
|
||||
|
||||
1. Extract the core claim from the take (first 150 chars)
|
||||
2. Run `engine.query(claim)` to get relevant pages
|
||||
3. Filter to pages updated AFTER the take's `since_date` (evidence must be newer)
|
||||
4. Pass top-5 chunks as the evidence block to the judge
|
||||
|
||||
This should replace the stub in the `evidenceRetriever` injection point.
|
||||
|
||||
## Production Results
|
||||
|
||||
After implementing the falsifiability filter (as a pre-processing step outside
|
||||
the cycle):
|
||||
|
||||
| Metric | Before (v2, no filter) | After (v3, with filter) |
|
||||
|--------|----------------------|----------------------|
|
||||
| Candidates evaluated | 50 | 34 (from 500 screened) |
|
||||
| Falsifiable predictions | ~19 (38%) | 34 (100%) |
|
||||
| Correct | 10 (52.6% of resolvable) | 10 (58.8% of resolvable) |
|
||||
| Incorrect | 5 (26.3%) | 2 (11.8%) |
|
||||
| Partial | 4 (21.1%) | 5 (29.4%) |
|
||||
| Unresolvable | 31 (62%) | 17 (50%) |
|
||||
| Category breakdown | N/A | people_move:13, company_outcome:11, technology:4, market_call:2 |
|
||||
|
||||
Key improvement: **the false positive rate dropped from 62% noise to 0% noise**
|
||||
in the gradeable set. The remaining 50% unresolvable rate is genuine — those
|
||||
predictions are about outcomes that haven't happened yet or where the brain
|
||||
lacks evidence. That's correct behavior, not noise.
|
||||
|
||||
## Files to Change
|
||||
|
||||
1. **`src/core/cycle/propose-takes.ts`** — Add falsifiability + category to
|
||||
extraction prompt and output schema
|
||||
2. **`src/core/cycle/grade-takes.ts`** — Add falsifiability gate before grading;
|
||||
wire real evidence retrieval
|
||||
3. **`src/core/cycle/calibration-profile.ts`** — Group scorecards by category
|
||||
4. **`src/core/engine.ts`** — Add `similarity()` helper for dedup (graceful
|
||||
fallback)
|
||||
5. **New migration** — Add columns + index
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit test: falsifiability classifier on 20 known-good and 20 known-noise takes
|
||||
- Unit test: dedup correctly merges near-identical claims
|
||||
- Unit test: grade gate filters below threshold
|
||||
- Integration test: full cycle with falsifiability → grade → profile pipeline
|
||||
- Regression test: existing takes without falsifiability score are not broken
|
||||
(null falsifiability = ungated, backward compatible)
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- `falsifiability` defaults to null. Existing takes are unaffected.
|
||||
- `grade_takes` with null falsifiability: configurable behavior. Default:
|
||||
grade all (backward compat). Operator can set
|
||||
`cycle.grade_takes.require_falsifiability: true` to gate.
|
||||
- Category column is purely additive.
|
||||
- Dedup is opt-in: `cycle.propose_takes.dedup.enabled: true`.
|
||||
@@ -1,200 +0,0 @@
|
||||
# Frontmatter scan: DB-backed incremental state (Phase 2 design sketch)
|
||||
|
||||
**Status:** Designed, not built. Captured here as the starting point for the
|
||||
follow-up PR after v0.38.2.0.
|
||||
|
||||
## Why this exists
|
||||
|
||||
v0.38.2.0 fixed the load-bearing bug class that caused `gbrain doctor` to
|
||||
hang on large brains: the disk walker descended into `node_modules/`, `.git/`,
|
||||
and other vendor trees on every tick. After that fix doctor completes in
|
||||
seconds on most brains, and bounded wall-clock (default 30s, with honest
|
||||
partial-state surfacing) on any brain.
|
||||
|
||||
But the steady-state cost of `frontmatter_integrity` is still O(N) in real
|
||||
syncable pages: every doctor tick re-walks the filesystem and re-parses
|
||||
every `.md` file. For users with 200K+ pages the steady-state cost is in
|
||||
the seconds even after Fix 1. For sub-second steady-state doctor (the
|
||||
right shape for cron-monitored health checks), the scan needs to become
|
||||
incremental.
|
||||
|
||||
This document captures the Phase 2 design before the follow-up PR starts,
|
||||
so the implementer doesn't have to re-derive it.
|
||||
|
||||
## Goal
|
||||
|
||||
Doctor's `frontmatter_integrity` check completes in O(1) SQL queries
|
||||
regardless of brain size, with the same per-source breakdown and partial-
|
||||
state semantics as v0.38.2.0's bounded-walk approach. Incremental refresh
|
||||
runs as a sync-side write + an autopilot cycle phase, so the steady-state
|
||||
work is amortized across the workflow that already touches each file.
|
||||
|
||||
## Schema
|
||||
|
||||
New table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE frontmatter_scan_state (
|
||||
source_id TEXT NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
|
||||
path TEXT NOT NULL, -- relative to source.local_path
|
||||
mtime_ms BIGINT NOT NULL,
|
||||
content_hash TEXT NOT NULL, -- sha256 of file content at scan time
|
||||
codes JSONB NOT NULL DEFAULT '[]'::jsonb, -- ParseValidationCode[]
|
||||
last_scanned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (source_id, path)
|
||||
);
|
||||
|
||||
CREATE INDEX frontmatter_scan_state_has_issues_idx
|
||||
ON frontmatter_scan_state (source_id)
|
||||
WHERE codes != '[]'::jsonb;
|
||||
```
|
||||
|
||||
Why these columns:
|
||||
- `mtime_ms` + `content_hash`: incremental check picks one. mtime is faster
|
||||
(no read); content_hash is the truth (defeats touch-without-change cases).
|
||||
The incremental walker uses mtime as a fast gate and content_hash as the
|
||||
fallback when mtime suggests change.
|
||||
- `codes` JSONB: per-row error code list, NULL/`[]` means clean. Doctor
|
||||
aggregates with `jsonb_array_length(codes) > 0`.
|
||||
- Partial index on `WHERE codes != '[]'::jsonb`: doctor's aggregate query
|
||||
only walks rows with issues, which is a small fraction of pages.
|
||||
|
||||
This follows the canonical `applyForwardReferenceBootstrap` pattern in
|
||||
`src/core/pglite-engine.ts` (and `postgres-engine.ts`) — the new column /
|
||||
table additions go into the bootstrap probe set per CLAUDE.md so old brains
|
||||
walking forward through the schema chain don't wedge on the table not
|
||||
existing.
|
||||
|
||||
## Migration shape
|
||||
|
||||
```ts
|
||||
// src/core/migrate.ts — append after the v80 entry
|
||||
const migrations = [
|
||||
// ...existing v1-v80...
|
||||
{
|
||||
version: 81,
|
||||
name: 'frontmatter_scan_state',
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS frontmatter_scan_state (...);
|
||||
CREATE INDEX IF NOT EXISTS frontmatter_scan_state_has_issues_idx ...;
|
||||
`,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Plus the forward-reference probe entries in both engine bootstraps. Plus
|
||||
the `REQUIRED_BOOTSTRAP_COVERAGE` extension in
|
||||
`test/schema-bootstrap-coverage.test.ts`.
|
||||
|
||||
## Writers
|
||||
|
||||
Two paths write rows:
|
||||
|
||||
1. **Sync-side write** (canonical). `src/core/sync.ts:performSync` already
|
||||
parses every file it touches. After the existing `parseMarkdown` call,
|
||||
`UPSERT` into `frontmatter_scan_state` with the file's path / mtime /
|
||||
content_hash / codes. Cost: one row per file synced. Zero extra parse
|
||||
work — the parse already happened.
|
||||
|
||||
2. **Incremental scan** (`gbrain frontmatter scan --incremental`). Walks
|
||||
the disk via `walkBrainTree`, for each file checks `mtime > last_scanned_at`
|
||||
OR `content_hash != stored`, only re-parses changed files. Most ticks:
|
||||
zero work after the first full backfill. Also exposed as an autopilot
|
||||
cycle phase (`frontmatter_scan`) so it runs alongside the other periodic
|
||||
maintenance phases.
|
||||
|
||||
The incremental walker handles two cases sync misses:
|
||||
- Files edited outside sync (user opens an editor, saves, never `git
|
||||
commit`s).
|
||||
- Sources whose `local_path` isn't a git repo (sync only sees git-touched
|
||||
files).
|
||||
|
||||
## Doctor reader
|
||||
|
||||
```ts
|
||||
// src/commands/doctor.ts:frontmatter_integrity (Phase 2 shape)
|
||||
const rows = await engine.executeRaw<{ source_id: string; issues: number }>(
|
||||
`SELECT source_id, count(*) FILTER (WHERE jsonb_array_length(codes) > 0)::int AS issues
|
||||
FROM frontmatter_scan_state
|
||||
GROUP BY source_id`,
|
||||
);
|
||||
```
|
||||
|
||||
One SQL query, constant time regardless of brain size. The partial-state
|
||||
surfacing from v0.38.2.0 stays — when `frontmatter_scan_state` is stale
|
||||
(no rows for a registered source, or `last_scanned_at` >24h old for any
|
||||
source), doctor warns about freshness rather than reporting potentially-
|
||||
stale data as authoritative.
|
||||
|
||||
## Sequencing concerns
|
||||
|
||||
1. **First-ever scan.** A fresh upgrade has no rows in
|
||||
`frontmatter_scan_state`. Two options:
|
||||
- Lazy: doctor reports "no scan state yet; run `gbrain frontmatter scan
|
||||
--incremental` once" (operator-driven).
|
||||
- Eager: the migration that creates the table also enqueues an autopilot
|
||||
cycle job to do the first full scan.
|
||||
|
||||
Recommendation: lazy, with a clear hint. The autopilot path is heavier
|
||||
surface (must add the new `frontmatter_scan` phase to the existing
|
||||
cycle.ts machinery + the doctor-routed background job system).
|
||||
|
||||
2. **Source archival / deletion.** `frontmatter_scan_state` has `ON DELETE
|
||||
CASCADE` on `sources(id)`, so soft-delete + 72h TTL + purge already
|
||||
clean it up. No additional logic needed.
|
||||
|
||||
3. **Path renames inside a source.** Sync would `DELETE` the old row by
|
||||
path (via a periodic reconcile step) and `INSERT` the new row. Without
|
||||
that step, the table accumulates stale path rows. Either:
|
||||
- A reconcile step in the incremental scanner: any path-row not seen
|
||||
during the walk gets deleted.
|
||||
- Or: doctor reports "N stale rows in frontmatter_scan_state" as a
|
||||
freshness signal, with `gbrain frontmatter scan --reconcile` as the
|
||||
remediation.
|
||||
|
||||
## Cost estimate
|
||||
|
||||
- One UPSERT per file synced. Negligible vs the parse + DB write that sync
|
||||
already does.
|
||||
- Incremental refresh runtime: dominated by mtime stats. ~ms per 1000 files
|
||||
on SSD.
|
||||
- Doctor read: one indexed SQL query. Sub-100ms on any brain size.
|
||||
|
||||
## What this design deliberately does NOT do
|
||||
|
||||
- **Replace v0.38.2.0's bounded-walk safety net.** Phase 2 makes the
|
||||
steady-state cheap, but the disk walker (with its deadline check) stays
|
||||
as the source-of-truth fallback for sources whose scan state is missing
|
||||
or stale. Belt-and-suspenders.
|
||||
- **Introduce a separate frontmatter validation rule set.** Reuses
|
||||
`parseMarkdown(..., {validate: true})` and the existing
|
||||
`ParseValidationCode` enum. Single source of truth.
|
||||
- **Add a new background daemon.** Wires into the existing
|
||||
`autopilot-cycle` Minion handler as a new phase, alongside sync /
|
||||
extract / embed / etc.
|
||||
|
||||
## Open questions for the implementer
|
||||
|
||||
1. **Path normalization.** `pages.source_path` and the disk walker's
|
||||
relative path computation are similar but not identical (slashes,
|
||||
leading `./`, etc.). The incremental scanner needs to match what sync
|
||||
stores so UPSERTs key correctly. Audit before writing.
|
||||
2. **Soft-delete interaction.** A page that gets soft-deleted in the DB
|
||||
(v0.26.5) still has a file on disk. Should the incremental scan
|
||||
continue to track its frontmatter state? Probably yes (so a future
|
||||
`restore_page` doesn't surprise with stale frontmatter), but worth
|
||||
confirming with the soft-delete owner.
|
||||
3. **Two-phase rollout.** Land the table + writes first, let it backfill
|
||||
for a release cycle, then switch the doctor reader. Avoids the
|
||||
"Phase 2 ships but the table is empty" case where doctor regresses to
|
||||
reporting "no scan state."
|
||||
|
||||
## TODO file entry
|
||||
|
||||
```
|
||||
- [ ] Implement Phase 2: DB-backed frontmatter scan state.
|
||||
Design lives at docs/architecture/frontmatter-scan-incremental.md.
|
||||
Schema migration v81 + sync-side UPSERT + incremental scan command
|
||||
+ autopilot cycle phase + doctor reader. Two-phase rollout: ship
|
||||
table + writes first; flip the reader one release later.
|
||||
```
|
||||
@@ -1,105 +0,0 @@
|
||||
# GBrain Infrastructure Layer
|
||||
|
||||
The shared foundation that all skills, recipes, and integrations build on.
|
||||
|
||||
## Data Pipeline
|
||||
|
||||
```
|
||||
INPUT (markdown files, git repo)
|
||||
↓
|
||||
FILE RESOLUTION (local → .redirect → .supabase → error)
|
||||
↓
|
||||
MARKDOWN PARSER (gray-matter frontmatter + body)
|
||||
→ compiled_truth + timeline separation
|
||||
↓
|
||||
CONTENT HASH (SHA-256 idempotency check — skip if unchanged)
|
||||
↓
|
||||
CHUNKING (3 strategies, configurable)
|
||||
├── Recursive: 300-word chunks, 50-word overlap, 5-level delimiter hierarchy
|
||||
├── Semantic: embed sentences, cosine similarity, Savitzky-Golay smoothing
|
||||
└── LLM-guided: Claude Haiku identifies topic shifts in 128-word candidates
|
||||
↓
|
||||
EMBEDDING (OpenAI text-embedding-3-large, 1536 dimensions)
|
||||
→ batch 100, exponential backoff, non-fatal if fails
|
||||
↓
|
||||
DATABASE TRANSACTION (atomic: page + chunks + tags + version)
|
||||
↓
|
||||
SEARCH (hybrid, available immediately)
|
||||
```
|
||||
|
||||
## Search Architecture
|
||||
|
||||
GBrain uses Reciprocal Rank Fusion (RRF) to merge vector and keyword search:
|
||||
|
||||
```
|
||||
User Query
|
||||
↓
|
||||
EXPANSION (optional: Claude Haiku generates 2 alternative phrasings)
|
||||
↓
|
||||
├── VECTOR SEARCH (pgvector HNSW, cosine distance)
|
||||
│ → 2x limit results per query variant
|
||||
│
|
||||
└── KEYWORD SEARCH (PostgreSQL tsvector, ts_rank)
|
||||
→ 2x limit results
|
||||
↓
|
||||
RRF MERGE (score = Σ(1/(60 + rank)), balances both fairly)
|
||||
↓
|
||||
4-LAYER DEDUP
|
||||
├── Best 3 chunks per page (source dedup)
|
||||
├── Jaccard similarity > 0.85 (text dedup)
|
||||
├── No type exceeds 60% (diversity)
|
||||
└── Max 2 chunks per page (page cap)
|
||||
↓
|
||||
TOP N RESULTS (default 20)
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/core/engine.ts` | Pluggable engine interface (BrainEngine) |
|
||||
| `src/core/postgres-engine.ts` | Postgres + pgvector implementation |
|
||||
| `src/core/import-file.ts` | importFromFile + importFromContent pipeline |
|
||||
| `src/core/sync.ts` | Git-based incremental change detection |
|
||||
| `src/core/markdown.ts` | YAML frontmatter + compiled_truth/timeline parsing |
|
||||
| `src/core/embedding.ts` | OpenAI embedding with batch, retry, backoff |
|
||||
| `src/core/chunkers/recursive.ts` | Base chunker (300w, 5-level delimiters) |
|
||||
| `src/core/chunkers/semantic.ts` | Embedding-based topic boundary detection |
|
||||
| `src/core/chunkers/llm.ts` | Claude Haiku guided chunking |
|
||||
| `src/core/search/hybrid.ts` | RRF merge of vector + keyword |
|
||||
| `src/core/search/dedup.ts` | 4-layer result deduplication |
|
||||
| `src/core/search/expansion.ts` | Multi-query expansion via Claude Haiku |
|
||||
| `src/core/storage.ts` | Pluggable storage (S3, Supabase, local) |
|
||||
| `src/core/operations.ts` | Contract-first operation definitions (31 ops) |
|
||||
| `src/schema.sql` | Full DDL (10 tables, RLS, tsvector, HNSW) |
|
||||
|
||||
## Schema Overview
|
||||
|
||||
10 tables in Postgres:
|
||||
|
||||
- **pages** — slug (unique), type, title, compiled_truth, timeline, frontmatter (JSONB)
|
||||
- **content_chunks** — pgvector 1536-dim embedding, chunk_source (compiled_truth|timeline)
|
||||
- **links** — typed edges (knows, works_at, invested_in, founded, etc.)
|
||||
- **tags** — many-to-many page tagging
|
||||
- **timeline_entries** — structured events (date, source, summary, detail)
|
||||
- **page_versions** — snapshot history for diff/revert
|
||||
- **raw_data** — sidecar JSON from external APIs (preserves provenance)
|
||||
- **files** — binary attachments in storage backend
|
||||
- **ingest_log** — audit trail of import operations
|
||||
- **config** — brain-level settings (version, embedding model, chunk strategy)
|
||||
|
||||
Full-text search uses weighted tsvector: title (A), compiled_truth (B), timeline (C).
|
||||
Vector search uses HNSW index with cosine distance on content_chunks.embedding.
|
||||
|
||||
## The Thin Harness Principle
|
||||
|
||||
GBrain is the deterministic layer. Skills and recipes are the latent space layer.
|
||||
|
||||
See [Thin Harness, Fat Skills](../ethos/THIN_HARNESS_FAT_SKILLS.md) for the full
|
||||
architecture philosophy.
|
||||
|
||||
- **GBrain CLI** = thin harness (same input → same output)
|
||||
- **Skills** (ingest, query, maintain, enrich, briefing, migrate, setup) = fat skills
|
||||
- **Recipes** (voice-to-brain, email-to-brain) = fat skills that install infrastructure
|
||||
|
||||
The agent reads the skill/recipe and uses GBrain's deterministic tools to do the work.
|
||||
@@ -1,143 +0,0 @@
|
||||
# Lens packs (v0.41.2.0)
|
||||
|
||||
Four bundled schema packs that turn the gbrain dream cycle into a multi-lens
|
||||
brain. Activate one with `gbrain config set schema_pack <name>` and the cycle
|
||||
picks up the pack's declared phases on the next `gbrain dream` run.
|
||||
|
||||
## The four packs
|
||||
|
||||
```
|
||||
gbrain-base (shipped v0.38)
|
||||
▲
|
||||
│ extends
|
||||
┌──────────────┼──────────────────────┐
|
||||
│ │ │
|
||||
gbrain-creator gbrain-investor gbrain-engineer
|
||||
(atom + concept (deal/thesis/ (learning bridge
|
||||
lifecycle) bet_resolution) for gstack)
|
||||
│ │ │
|
||||
└──────────────┼───────────────────────┘
|
||||
│ extends + borrow chain
|
||||
▼
|
||||
gbrain-everything (meta-pack)
|
||||
one brain, three lenses active
|
||||
```
|
||||
|
||||
### gbrain-creator
|
||||
Atom + concept content-creator lifecycle. Drives two cycle phases:
|
||||
|
||||
- `extract_atoms` — per source, Haiku extracts 1-3 atoms from each
|
||||
transcript with the closed 11-value `atom_type` enum (insight,
|
||||
anecdote, quote, framework, statistic, story_angle, strategy_angle,
|
||||
strategy, endorsement, critique, collection). Writes
|
||||
`atoms/{YYYY-MM-DD}/{slug}` pages. Budget cap $0.30/source/run.
|
||||
- `synthesize_concepts` — globally aggregates atoms by frontmatter
|
||||
`concepts:` ref. Tier by count: T1 ≥10, T2 ≥5, T3 ≥2. T1/T2 get
|
||||
Sonnet narratives; T3 falls back to a deterministic stub. Writes
|
||||
`concepts/{slug}` pages. Budget cap $1.50/run.
|
||||
|
||||
One calibration domain: `concept_themes` / cluster_summary / [concept]
|
||||
— tier histogram + page count, not Brier (concepts don't have binary
|
||||
outcomes to score against).
|
||||
|
||||
### gbrain-investor
|
||||
YC / investor lens. Declares 2 net-new page types on top of
|
||||
gbrain-base's deal/person/company/yc seed:
|
||||
|
||||
- `thesis` (NEW) — investment thesis with thesis_text + key_bets[] +
|
||||
market_view + vintage. Files at `investing/theses/{slug}`. Extractable
|
||||
(the LLM mines claims into facts).
|
||||
- `bet_resolution_log` (NEW) — outcome record for a thesis's bet. FK
|
||||
to a take row via take_id; carries resolved_outcome + resolved_at +
|
||||
learned_pattern. Files at `investing/bets/{YYYY-MM}/{slug}`.
|
||||
|
||||
No new cycle phases — consumes the existing
|
||||
extract_facts/propose_takes/grade_takes/calibration_profile loop. Three
|
||||
calibration domains: `deal_success` (scalar_brier over deal-attached
|
||||
takes), `founder_evaluation` (scalar_brier over person-attached takes),
|
||||
`market_call` (weighted_brier over thesis-attached takes; weighted by
|
||||
conviction so high-stakes misses cost more).
|
||||
|
||||
### gbrain-engineer
|
||||
Bridge-only pack. Declares `learning` page type + reuses base `code`.
|
||||
No new cycle phases — the daemon-side `gstack-learnings` IngestionSource
|
||||
(T8) watches `~/.gstack/projects/{repo}/learnings.jsonl` and emits
|
||||
each JSONL line as a `learning` page when this pack is active. Three
|
||||
calibration domains: `architecture_calls` (scalar_brier),
|
||||
`effort_estimates` (weighted_brier), `risk_assessment` (scalar_brier).
|
||||
|
||||
Speculative ADR/postmortem/refactor_thesis/tech_debt types deferred
|
||||
to v0.42+ — they'll ship when a real user authors the first one (D8).
|
||||
|
||||
### gbrain-everything
|
||||
Meta-pack stacking creator + investor + engineer via the v0.38
|
||||
`extends` + `borrow_from` chain. Single-active-pack constraint
|
||||
preserved — this IS the active pack; the registry walks extends +
|
||||
borrow to materialize the merged view.
|
||||
|
||||
Activate via `gbrain config set schema_pack gbrain-everything` and
|
||||
calibration_profile produces all 7 domain scorecards in one JSONB.
|
||||
|
||||
## Calibration profile widening (T10)
|
||||
|
||||
Before v0.41.2.0, `calibration_profiles.domain_scorecards` was a
|
||||
`JSON.stringify({})` placeholder. v0.41.2.0 widens it: each declared
|
||||
domain produces a `{n, brier, accuracy, aggregator, page_types,
|
||||
extras}` entry. Four aggregator algorithms (closed enum):
|
||||
|
||||
- **scalar_brier** — `AVG(POWER(weight - outcome::int, 2))`. Default for
|
||||
probabilistic predictions.
|
||||
- **weighted_brier** — Brier weighted by `ABS(weight - 0.5) * 2`
|
||||
(conviction proxy). High-conviction misses cost more.
|
||||
- **count_based** — simple `SUM(hit) / COUNT(*)` accuracy without
|
||||
Brier. Use when probability isn't natural.
|
||||
- **cluster_summary** — descriptive rollup (page count + tier
|
||||
histogram). For domains like `concept_themes` where there's no
|
||||
binary outcome.
|
||||
|
||||
Pack manifests declare domains with `{name, aggregator, page_types}`.
|
||||
Domain names are OPEN (third-party packs can declare new domain labels
|
||||
without a gbrain release). Aggregator algorithms are CLOSED (safe SQL
|
||||
stays in code, validated at pack-load).
|
||||
|
||||
## take_domain_assignments table (T1)
|
||||
|
||||
New JOIN table (migration v94):
|
||||
`take_domain_assignments(take_id BIGINT FK, domain TEXT, pack TEXT,
|
||||
source TEXT, confidence REAL, assigned_at TIMESTAMPTZ, PK(take_id,
|
||||
domain))`. Multi-domain assignment honest — a take about "Sequoia's
|
||||
investment in Anthropic" can land in BOTH `deal_success` AND
|
||||
`market_call` rather than being force-bucketed.
|
||||
|
||||
## What this enables for the user
|
||||
|
||||
- **Atoms + concepts ship in the binary.** Your OpenClaw's parallel
|
||||
atom-pipeline-coordinator + atom-backfill-coordinator + concept-
|
||||
synthesis crons can retire (T12 follow-up). One `gbrain dream` cron
|
||||
covers everything.
|
||||
- **gstack learnings reach gbrain.** Engineer-pack-active brains
|
||||
surface every gstack-logged learning as a queryable page within
|
||||
seconds of being written.
|
||||
- **Multi-lens calibration.** Activate gbrain-everything and see how
|
||||
often you're wrong on deals AND market calls AND architecture
|
||||
AND effort estimates in one `gbrain calibration --json` call.
|
||||
- **Lossless OpenClaw migration.** The `markdown-greenfield`
|
||||
importer (T7, mode='migration') re-ingests existing OpenClaw
|
||||
pages with permanent slug-keyed idempotency + per-row JSONL audit
|
||||
+ the `imported_from` marker so extract_atoms + synthesize_concepts
|
||||
don't re-extract already-atomized material.
|
||||
|
||||
## v0.41.2.1 follow-ups (filed in plan)
|
||||
|
||||
- Per-page-type `frontmatter_validators` on PageTypeSchema so the
|
||||
atom_type enum (currently hardcoded in extract_atoms.ts) reads from
|
||||
the active pack manifest at runtime per D11.
|
||||
- 3-check quality gate (truism / punchline / entity-page reject) as
|
||||
a multi-pass extract_atoms refinement.
|
||||
- Embedding-similarity dedup in synthesize_concepts (currently
|
||||
exact-string concept ref match only).
|
||||
- Voice gate integration for T1 Canon narratives.
|
||||
- op_checkpoint resumability for cross-cycle continuation in both
|
||||
phases.
|
||||
- Parity-baseline eval gates against your OpenClaw's existing 13K atoms
|
||||
+ 11K concepts on a 500-page sample subset.
|
||||
@@ -1,246 +0,0 @@
|
||||
# Pack-Upgrade Mechanism (v0.41.22)
|
||||
|
||||
> How `gbrain-base@1.x → gbrain-base-v2@1.0.0` (and any future pack
|
||||
> succession) wires through the onboard cathedral.
|
||||
|
||||
## The contract
|
||||
|
||||
A schema pack manifest can declare a `migration_from` field:
|
||||
|
||||
```yaml
|
||||
api_version: gbrain-schema-pack-v1
|
||||
name: gbrain-base-v2
|
||||
version: 1.0.0
|
||||
migration_from:
|
||||
pack: gbrain-base
|
||||
version: "1.x"
|
||||
```
|
||||
|
||||
When this declaration is present + a `mapping_rules:` block is
|
||||
populated, the pack registers itself as the successor to
|
||||
`(parent_pack, version_range)`. Any brain whose active pack matches
|
||||
that tuple lights up the `pack_upgrade_available` onboard check.
|
||||
|
||||
## End-to-end flow
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ PACK AUTHORING │
|
||||
│ │
|
||||
│ Author declares: migration_from: {pack: P, version: R} │
|
||||
│ + mapping_rules: [retype/page_to_link/page_to_alias] │
|
||||
│ Pack ships bundled OR via ~/.gbrain/schema-packs/<name>/ │
|
||||
└──────────────────────────┬─────────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ ONBOARD CHECK DISCOVERY │
|
||||
│ │
|
||||
│ checkPackUpgradeAvailable(engine) at src/core/onboard/ │
|
||||
│ checks.ts: │
|
||||
│ 1. Read engine.getConfig('schema_pack') for dbConfig tier │
|
||||
│ 2. loadActivePack({cfg: null, remote: false, dbConfig}) │
|
||||
│ 3. findPackSuccessors(active.name, active.version) │
|
||||
│ → walks BUNDLED_PACK_NAMES + ~/.gbrain/schema-packs/ │
|
||||
│ → matches via _versionRangeMatches(version, range) │
|
||||
│ → returns ResolvedPack[] sorted by successor version │
|
||||
│ 4. If successors.length > 0, emit OnboardCheckResult │
|
||||
│ with RemediationStep targeting `unify-types` handler │
|
||||
│ + protected: true (D17 → manual_only via render │
|
||||
│ allowlist) │
|
||||
└──────────────────────────┬─────────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ USER DECIDES │
|
||||
│ │
|
||||
│ gbrain onboard --check shows finding │
|
||||
│ gbrain onboard --check --explain shows per-cluster narrative │
|
||||
│ User reviews; if OK, runs: │
|
||||
│ gbrain jobs submit unify-types --allow-protected \ │
|
||||
│ --params '{"target_pack":"gbrain-base-v2"}' │
|
||||
│ (Autopilot never auto-fires this; manual_only) │
|
||||
└──────────────────────────┬─────────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ HANDLER EXECUTION (src/core/schema-pack/unify-types-handler.ts) │
|
||||
│ │
|
||||
│ 1. Preflight: load target pack; assert mapping_rules present │
|
||||
│ 2. Stats snapshot (pre-state for celebration) │
|
||||
│ 3. Acquire gbrain-unify db-lock (60min TTL) │
|
||||
│ 4. Apply phases (4): │
|
||||
│ a. Explicit retype rules (chunked UPDATE 1000/batch) │
|
||||
│ - frontmatter.legacy_type ALWAYS preserved (D8) │
|
||||
│ - frontmatter.subtype stamped when subtype set │
|
||||
│ b. Catch-all retype: synthesize per-unknown-type rule │
|
||||
│ excluding declared types + explicit targets + page_to_ │
|
||||
│ link/alias sources (D12 + critical bug fix) │
|
||||
│ c. Page-to-link: parse body+frontmatter, insert link row, │
|
||||
│ soft-delete source page (per-page atomicity per F7) │
|
||||
│ d. Page-to-alias: insert slug_aliases row, soft-delete │
|
||||
│ source page (NO rewriteLinks per D15) │
|
||||
│ 5. Final sync: path-prefix typing for residual UNTYPED rows │
|
||||
│ 6. ACTIVE-PACK FLIP (D13): │
|
||||
│ - engine.setConfig('schema_pack', target_pack) │
|
||||
│ - saveConfig({...existing, schema_pack: target_pack}) │
|
||||
│ 7. Verify: re-run stats; warn if ≤ declared + 5 violated │
|
||||
│ 8. Celebration summary to stderr + audit JSONL │
|
||||
│ 9. Release db-lock │
|
||||
└──────────────────────────┬─────────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ POST-UPGRADE STATE │
|
||||
│ │
|
||||
│ • pages.type updated with canonical types │
|
||||
│ • frontmatter.legacy_type preserved for rollback │
|
||||
│ • slug_aliases populated for old-slug → canonical lookup │
|
||||
│ • links table has new partner_of / relates_to rows │
|
||||
│ • Source pages soft-deleted (72h TTL for restore) │
|
||||
│ • Active pack flipped to target_pack │
|
||||
│ • Next gbrain onboard --check shows ok │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Version-range semantics
|
||||
|
||||
`migration_from.version` accepts three shapes:
|
||||
|
||||
| Form | Matches |
|
||||
|------|---------|
|
||||
| `1.0.0` (exact literal) | `1.0.0` only |
|
||||
| `1.x` (major wildcard) | `1.0.0`, `1.5.2`, `1.99.99` |
|
||||
| `1.0.x` (minor wildcard) | `1.0.0`, `1.0.5`, `1.0.99` |
|
||||
|
||||
`*` is accepted as an alias for `x`.
|
||||
|
||||
Implementation: `_versionRangeMatches(version, range)` in
|
||||
`src/core/schema-pack/load-active.ts`. Pinned by
|
||||
`test/schema-pack-find-pack-successors.test.ts`.
|
||||
|
||||
## findPackSuccessors discovery
|
||||
|
||||
Walks `BUNDLED_PACK_NAMES` (currently `gbrain-base`,
|
||||
`gbrain-recommended`, `gbrain-creator`, `gbrain-investor`,
|
||||
`gbrain-engineer`, `gbrain-everything`, `gbrain-base-v2`). For each
|
||||
candidate ≠ the active pack name, loads the manifest via
|
||||
`loadActivePack({ perCall: candidate })`, checks
|
||||
`migration_from.pack === activeName && _versionRangeMatches(activeVer,
|
||||
migration_from.version)`. Returns matching packs sorted by version
|
||||
descending.
|
||||
|
||||
v0.41.22 covers bundled packs only. v0.43+ TODO: enumerate user-installed
|
||||
packs at `~/.gbrain/schema-packs/*/pack.yaml` (defer to v0.43 since the
|
||||
filesystem-scan cost needs the cache invalidation strategy from
|
||||
`registry.ts`).
|
||||
|
||||
## The manual_only apply policy
|
||||
|
||||
The shipped onboard contract has 3 apply_policy values:
|
||||
|
||||
| Policy | Meaning |
|
||||
|--------|---------|
|
||||
| `auto_apply` | Autopilot runs unattended |
|
||||
| `prompt_required` | Autopilot in `--auto-with-prompt` mode prompts user |
|
||||
| `manual_only` | Autopilot NEVER auto-fires; user must explicitly submit |
|
||||
|
||||
`pack_upgrade_available` emits a `RemediationStep` with `protected:
|
||||
true` + `job: 'unify-types'`. `toOnboardRecommendation` in
|
||||
`src/core/onboard/render.ts` maps this to `manual_only` via the
|
||||
`MANUAL_ONLY_PROTECTED_JOBS` allowlist (which also contains
|
||||
`extract-takes-from-pages` per v0.41.18 A12+A24).
|
||||
|
||||
Rationale: pack upgrades change the brain's taxonomy. Taxonomy is a
|
||||
user judgment call — not autopilot's call. Even with `--auto-with-
|
||||
prompt`, prompting the user to confirm a pack upgrade mid-tick is the
|
||||
wrong UX (the user came to fix orphans, not to be interrupted with
|
||||
"hey want to migrate your taxonomy?"). Explicit submission is the
|
||||
right boundary.
|
||||
|
||||
## Authoring a successor pack
|
||||
|
||||
Minimal example for an academic-research brain that adds a
|
||||
`researcher` canonical:
|
||||
|
||||
```yaml
|
||||
api_version: gbrain-schema-pack-v1
|
||||
name: gbrain-academic-v1
|
||||
version: 1.0.0
|
||||
description: Academic research brain — adds researcher canonical
|
||||
gbrain_min_version: 0.42.0
|
||||
extends: null
|
||||
|
||||
migration_from:
|
||||
pack: gbrain-base-v2
|
||||
version: "1.x"
|
||||
|
||||
page_types:
|
||||
# Inherit gbrain-base-v2's 15 types here (or use extends to merge
|
||||
# automatically once v0.43+ extends-chain composition lands)
|
||||
- { name: person, primitive: entity, path_prefixes: [people/], expert_routing: true }
|
||||
- { name: company, primitive: entity, path_prefixes: [companies/], expert_routing: true }
|
||||
# ... all 13 other v2 canonicals ...
|
||||
- { name: note, primitive: concept, path_prefixes: [notes/], extractable: true }
|
||||
# Academic addition:
|
||||
- name: researcher
|
||||
primitive: entity
|
||||
path_prefixes: [researchers/]
|
||||
aliases: [academic, professor, scholar]
|
||||
extractable: false
|
||||
expert_routing: true
|
||||
|
||||
mapping_rules:
|
||||
# All v2 mapping rules (copy from v2 yaml)
|
||||
# ... ~40 rules ...
|
||||
# Custom: relocate v2-tagged academics to researcher
|
||||
- { kind: retype, from_type: person, to_type: researcher, path_filter: 'researchers/%' }
|
||||
# Catch-all
|
||||
- kind: retype
|
||||
from_type: "*unknown*"
|
||||
to_type: note
|
||||
subtype_field: legacy_type
|
||||
subtype: "*original_type*"
|
||||
```
|
||||
|
||||
Drop at `~/.gbrain/schema-packs/gbrain-academic-v1/pack.yaml`.
|
||||
Discoverable via `gbrain schema list`. Activatable via
|
||||
`gbrain schema use gbrain-academic-v1`. Once active, the
|
||||
`pack_upgrade_available` check fires for any brain on
|
||||
`gbrain-base-v2@1.x` and surfaces a `unify-types` RemediationStep
|
||||
targeting your pack.
|
||||
|
||||
## Lock + concurrency
|
||||
|
||||
`gbrain-unify` is a dedicated `gbrain_cycle_locks` row name (60min
|
||||
TTL). The handler acquires it before any apply phase + releases in
|
||||
`finally`. Two simultaneous `gbrain jobs submit unify-types`
|
||||
invocations: second one fails fast at lock acquisition with a clear
|
||||
error. Same pattern as `gbrain-sync` (v0.22.13 PR #490).
|
||||
|
||||
## Audit trail
|
||||
|
||||
Every unify run writes to `~/.gbrain/audit/schema-unify-YYYY-Www.jsonl`
|
||||
(ISO-week rotation, mirrors existing audit channels). Records: pack
|
||||
identities (before + after), per-phase counts (would_apply + applied),
|
||||
warnings, completion timestamp. Privacy: page slugs are NOT logged in
|
||||
bulk (only the per-rule sample_slugs[≤10]); for forensic debugging
|
||||
add `GBRAIN_AUDIT_FULL=1` (v0.43+ TODO; not yet wired).
|
||||
|
||||
## What's NOT yet supported
|
||||
|
||||
- Subprocess sandbox for the publish-gate (v0.43+ TODO)
|
||||
- Per-source pack-upgrade (the handler accepts `sourceId` but
|
||||
`findPackSuccessors` doesn't yet pass it through)
|
||||
- Cross-brain federated mounts that disagree on canonical packs
|
||||
- Automatic rollback (today: manual SQL or `gbrain pages restore`)
|
||||
- LLM-assisted mapping_rules codegen from production data (`gbrain
|
||||
schema detect-mappings`; deferred to v0.43+)
|
||||
|
||||
## Reference
|
||||
|
||||
- Pack file: `src/core/schema-pack/base/gbrain-base-v2.yaml`
|
||||
- Manifest extension: `src/core/schema-pack/manifest-v1.ts`
|
||||
- Successor walker: `src/core/schema-pack/load-active.ts:findPackSuccessors`
|
||||
- Onboard check: `src/core/onboard/checks.ts:checkPackUpgradeAvailable`
|
||||
- Render allowlist: `src/core/onboard/render.ts:MANUAL_ONLY_PROTECTED_JOBS`
|
||||
- Handler: `src/core/schema-pack/unify-types-handler.ts`
|
||||
- Migration: `src/core/migrate.ts:105` (slug_aliases table)
|
||||
- Type taxonomy doc: `docs/architecture/type-taxonomy.md`
|
||||
- Skill: `skills/schema-unify/SKILL.md`
|
||||
@@ -1,230 +0,0 @@
|
||||
# Schema Packs
|
||||
|
||||
A schema pack tells gbrain what shape your brain takes — which directories
|
||||
exist, what types live in them, how the agent should infer types from
|
||||
paths, and which link verbs connect what to what. The schema pack is the
|
||||
**dynamic, always-consulted artifact** every skill reads when filing,
|
||||
querying, or routing experts. It is the single source of truth for
|
||||
"what's in your brain."
|
||||
|
||||
The v0.39.0.0 wave shipped a full schema-pack cathedral. This doc is the
|
||||
user-facing reference; for implementation details see
|
||||
`docs/designs/V038_SCHEMA_PACKS.md` (CEO plan) and the engine layer in
|
||||
`src/core/schema-pack/`.
|
||||
|
||||
## What ships in the box
|
||||
|
||||
Two bundled packs:
|
||||
|
||||
- **`gbrain-base`** (default) — reproduces pre-v0.38 hardcoded behavior
|
||||
byte-for-byte. Existing brains see zero behavior change after upgrade.
|
||||
Covers: person, company, deal, meeting, project, place, concept, writing,
|
||||
analysis, guide, hardware, architecture, etc. (the original
|
||||
`ALL_PAGE_TYPES` list).
|
||||
|
||||
- **`gbrain-recommended`** — extends `gbrain-base` with the 13 additional
|
||||
directories described in `docs/GBRAIN_RECOMMENDED_SCHEMA.md`: deal,
|
||||
meeting, concept, project, source, daily, personal, civic, original,
|
||||
place, trip, conversation, writing. If you like the documented
|
||||
operational-brain pattern, activate this with:
|
||||
|
||||
```bash
|
||||
gbrain schema use gbrain-recommended
|
||||
```
|
||||
|
||||
Plus user-installed packs at `~/.gbrain/schema-packs/<name>/pack.yaml`
|
||||
that you author with `gbrain schema init` or `gbrain schema fork`.
|
||||
|
||||
## CLI surface
|
||||
|
||||
Five inspection verbs (shipped in v0.38):
|
||||
|
||||
```bash
|
||||
gbrain schema active # show resolved pack + which tier set it
|
||||
gbrain schema list # list bundled + installed packs
|
||||
gbrain schema show # pretty-print the active pack
|
||||
gbrain schema validate # validate a manifest's shape
|
||||
gbrain schema use <pack> # activate a pack (writes ~/.gbrain/config.json)
|
||||
```
|
||||
|
||||
Eight authoring + discovery verbs (shipped in v0.39):
|
||||
|
||||
```bash
|
||||
gbrain schema detect # propose types matching brain shape
|
||||
gbrain schema suggest # LLM-refined proposals on top of detect
|
||||
gbrain schema review-candidates # promote / rename / ignore candidates
|
||||
gbrain schema review-orphans # surface pages with no matching type
|
||||
gbrain schema init <name> # scaffold a stub pack (experimental)
|
||||
gbrain schema fork <a> <b> # copy + rename a pack (experimental)
|
||||
gbrain schema edit <name> # surface the pack path (experimental)
|
||||
gbrain schema diff <a> <b> # set-diff two packs (experimental)
|
||||
gbrain schema graph # ASCII type listing (experimental)
|
||||
gbrain schema lint # flag duplicates + missing prefixes
|
||||
gbrain schema explain <type> # plain-English type description (experimental)
|
||||
gbrain schema downgrade --to <p> # restore previous pack (recovery)
|
||||
gbrain schema usage --since 30d # per-verb invocation counts (D14 telemetry)
|
||||
```
|
||||
|
||||
The verbs marked `experimental` are demand-gated per D14: their usage is
|
||||
tracked via T15's schema-events audit, and v0.40+ retro decides whether
|
||||
to deprecate any that stay <5% usage.
|
||||
|
||||
## Resolution chain (7 tiers)
|
||||
|
||||
When the engine decides "which pack is active for this query?", it walks
|
||||
this chain top-down. First match wins.
|
||||
|
||||
| Tier | Source | Notes |
|
||||
|------|--------|-------|
|
||||
| 1 | Per-call `schema_pack` opt | CLI only (`ctx.remote === false`); MCP rejected. |
|
||||
| 2 | `GBRAIN_SCHEMA_PACK` env | Process-scope override. |
|
||||
| 3 | Per-source DB config key `schema_pack:source:<id>` | New in v0.38. |
|
||||
| 4 | Brain-wide DB config key `schema_pack` | |
|
||||
| 5 | `gbrain.yml schema:` section | Repo-checked. |
|
||||
| 6 | `~/.gbrain/config.json` `schema_pack` field | What `gbrain schema use` writes. |
|
||||
| 7 | Default: `gbrain-base` | Always present. |
|
||||
|
||||
## How the agent uses the active pack
|
||||
|
||||
Every read + write path consults the active pack at runtime:
|
||||
|
||||
- **`parseMarkdown`** infers page `type` from path prefixes declared in
|
||||
the active pack (`page_types[].path_prefixes`). Without an active pack
|
||||
threaded, falls back to the legacy hardcoded `inferType()` so the
|
||||
byte-for-byte parity gate stays green.
|
||||
- **`whoknows` / `find_experts`** scopes candidates to `expert_routing:
|
||||
true` types in the active pack.
|
||||
- **`extract_facts`** runs only on `extractable: true` types.
|
||||
- **`enrichment-service`** routes person/company enrichment based on the
|
||||
pack's primitive declarations.
|
||||
- **Search hybrid cache** (`knobsHash`) folds in pack name + version
|
||||
(v0.39 T21). A cache row written under pack A is unreachable when pack
|
||||
B is active. Cross-pack contamination is structurally impossible.
|
||||
|
||||
## The magical moment (T2-T4 + T10)
|
||||
|
||||
Persona A (Notion refugee) installs gbrain, imports her exports, and the
|
||||
brain looks unfamiliar — the default `gbrain-base` pack expects
|
||||
`people/`, `companies/`, etc., but her files live under `Projects/`,
|
||||
`Reading/`, `Daily Notes/`. The friction signal fires in two places:
|
||||
|
||||
1. **Import warn (T7):** the end of `gbrain import` prints
|
||||
`[schema] X of Y pages (Z%) have no type matching the active schema
|
||||
pack. Run gbrain schema detect to propose a pack matching your
|
||||
content shape.`
|
||||
2. **`gbrain doctor` schema_pack_consistency check** keeps surfacing
|
||||
the warning persistently after the import session ends.
|
||||
|
||||
She runs the magical moment:
|
||||
|
||||
```bash
|
||||
gbrain schema detect # heuristic clustering on her actual shape
|
||||
gbrain schema suggest # LLM-refined proposals
|
||||
gbrain schema review-candidates # human gate on promotion
|
||||
gbrain schema review-candidates --apply Projects/ # accept
|
||||
```
|
||||
|
||||
The agent (via the new EIIRP skill) automates phases 1-3 of this for any
|
||||
significant work session. The brain's schema becomes a living artifact
|
||||
the agent maintains, not a hardcoded ceremony the user authors.
|
||||
|
||||
## Authoring your own pack
|
||||
|
||||
```bash
|
||||
gbrain schema init my-pack # scaffolds ~/.gbrain/schema-packs/my-pack/pack.yaml
|
||||
$EDITOR ~/.gbrain/schema-packs/my-pack/pack.yaml
|
||||
gbrain schema validate my-pack # check shape
|
||||
gbrain schema use my-pack # activate
|
||||
gbrain schema active # confirm
|
||||
```
|
||||
|
||||
A minimal pack:
|
||||
|
||||
```yaml
|
||||
api_version: gbrain-schema-pack-v1
|
||||
name: my-pack
|
||||
version: 0.0.1
|
||||
gbrain_min_version: 0.39.0
|
||||
extends: gbrain-base # inherits everything from base; add overrides below
|
||||
description: |
|
||||
My personal pack.
|
||||
|
||||
page_types:
|
||||
- name: project-x
|
||||
primitive: entity
|
||||
path_prefixes:
|
||||
- Projects/
|
||||
aliases: []
|
||||
extractable: false
|
||||
expert_routing: false
|
||||
|
||||
# Add more types here. Each maps a path prefix to a primitive +
|
||||
# opt-in flags. See src/core/schema-pack/base/gbrain-recommended.yaml
|
||||
# for a worked example.
|
||||
|
||||
link_types: []
|
||||
takes_kinds: [fact, take, bet, hunch]
|
||||
borrow_from: []
|
||||
frontmatter_links: []
|
||||
enrichable_types: []
|
||||
filing_rules: []
|
||||
```
|
||||
|
||||
## Recovery + revert
|
||||
|
||||
The single-PR cathedral is hard to revert atomically. Per codex finding
|
||||
#4 from plan-eng-review, T20 ships `gbrain schema downgrade` to restore
|
||||
the active-pack config field:
|
||||
|
||||
```bash
|
||||
gbrain schema downgrade --to gbrain-base
|
||||
# OR auto-detect previous from ~/.gbrain/schema-pack-history.jsonl:
|
||||
gbrain schema downgrade
|
||||
```
|
||||
|
||||
**Code revert alone is NOT sufficient.** The full revert procedure:
|
||||
|
||||
1. `git revert <merge-commit>` — restores the code.
|
||||
2. `gbrain schema downgrade --to gbrain-base` — restores config.
|
||||
3. (Optional) `gbrain pages purge-deleted --older-than 0h` — drops
|
||||
v0.39-typed pages that no longer have a matching type in the active
|
||||
pack.
|
||||
|
||||
The cache + eval rows that pack-aware code wrote are isolated by the
|
||||
`knobsHash` pack-folding (T21) — they become unreachable under the
|
||||
restored pack so no eviction is needed.
|
||||
|
||||
## Distribution
|
||||
|
||||
`.gbrain-schema` tarballs ride the same v0.37 skillpack pipeline as
|
||||
`.gbrain-skillpack` tarballs (T14 artifact abstraction). The
|
||||
discriminator is `api_version` in the manifest:
|
||||
|
||||
- `gbrain-schema-pack-v1` → schemapack
|
||||
- `gbrain-skillpack-v1` → skillpack
|
||||
|
||||
Both install via the same scaffold + copy path; install targets are
|
||||
`~/.gbrain/schema-packs/<name>/` and `~/.gbrain/skillpacks/<name>/`
|
||||
respectively.
|
||||
|
||||
Publication to the public registries (`garrytan/gbrain-schema-registry`,
|
||||
`garrytan/gbrain-skillpack-registry`) follows the same publish-as-PR
|
||||
workflow as v0.37 skillpack publishing.
|
||||
|
||||
## What's deferred to v0.40+
|
||||
|
||||
- **Per-source pack federation across mounts.** A query crossing multiple
|
||||
sources currently rejects with `permission_denied` when those sources
|
||||
have divergent active packs (T19 + codex finding #2). The v0.40+ work
|
||||
computes a true per-source closure via the existing
|
||||
`buildSourceClosureCte` engine surface.
|
||||
- **`extends` chain semver compatibility checks** between pack versions.
|
||||
- **`skillpack ↔ schemapack` cross-reference declarations** — a skillpack
|
||||
can declare "I work best with these primitives present in your pack."
|
||||
- **Live schema migration helpers** — when you add a type, auto-suggest
|
||||
backfill of existing pages.
|
||||
- **Authoring vs derivation thesis reframe (D14).** v0.39.0.0 ships the
|
||||
full 11-verb cathedral with 6 verbs marked experimental-tier. v0.40+
|
||||
retro reads T23 usage telemetry to decide which to deprecate.
|
||||
|
||||
See `TODOS.md` v0.40+ section for the full deferred list.
|
||||
@@ -1,198 +0,0 @@
|
||||
# System of record
|
||||
|
||||
**The GitHub repo (markdown + frontmatter) is the system of record.
|
||||
The Postgres/PGLite database is a derived cache. We do not back up
|
||||
the database — we rebuild it from the repo.**
|
||||
|
||||
This document is the canonical reference for that contract. Every code
|
||||
path that writes user-knowledge state should match the pattern
|
||||
described here. The CI gate at `scripts/check-system-of-record.sh`
|
||||
enforces it programmatically.
|
||||
|
||||
## Why this matters
|
||||
|
||||
The DB is a derived index over the markdown content. It exists to make
|
||||
search fast, to dedup embedding-similar claims, to materialize the
|
||||
cross-page graph. None of that data is irreplaceable — as long as the
|
||||
markdown is intact, `gbrain sync && gbrain extract all` rebuilds the
|
||||
entire DB from scratch.
|
||||
|
||||
This means:
|
||||
|
||||
- **Disaster recovery is one command.** If your DB volume corrupts, if
|
||||
Postgres eats itself, if PGLite's WASM lock wedges — you don't need
|
||||
a backup. You wipe the DB, re-import from your brain repo, and the
|
||||
derived state regenerates. v0.32.3 ships `gbrain rebuild
|
||||
--confirm-destructive` as the documented one-liner.
|
||||
- **Multi-machine sync is git.** Your brain is a repo. Push from one
|
||||
machine, pull from another, and the second machine's DB rebuilds on
|
||||
its next sync. No "back up the database" step.
|
||||
- **Privacy is in your hands.** Sensitive entity pages can be
|
||||
gitignored (via `gbrain.yml` `db_only` paths or per-page) and they
|
||||
stay on disk but not in git. The fence respects whatever git
|
||||
tracking choice you make at the page level.
|
||||
- **Cross-agent collaboration is possible.** Multiple agents can write
|
||||
to the same brain because the fence is the merge point, not the DB.
|
||||
Git handles concurrent edits the way git handles concurrent edits.
|
||||
|
||||
## The three categories
|
||||
|
||||
Every table in the gbrain schema belongs to exactly one of three
|
||||
categories. The category determines how it gets rebuilt during
|
||||
disaster recovery.
|
||||
|
||||
### FS-canonical (markdown is the source of truth)
|
||||
|
||||
These are user-authored knowledge. The DB row is a derived index over
|
||||
the markdown — wipe the table and `gbrain extract` rebuilds it
|
||||
identically. The CI gate keeps direct DB writes from drifting away
|
||||
from the markdown contract.
|
||||
|
||||
| Category | How it's stored in markdown | Derived DB table | Reconciler |
|
||||
|---|---|---|---|
|
||||
| **Takes** (incl. hunches, bets) | `## Takes` fenced table between `<!--- gbrain:takes:begin -->` / `:end -->` markers | `takes` | `extract takes` |
|
||||
| **Facts** | `## Facts` fenced table between `<!--- gbrain:facts:begin -->` / `:end -->` markers | `facts` | `extract_facts` cycle phase |
|
||||
| **Links** | Inline `[text](slug)` / `[[slug]]` in markdown body + frontmatter `direction: incoming` | `links` | `extract links` |
|
||||
| **Timeline** | `## Timeline` section after `<!-- timeline -->` sentinel | `timeline_entries` | `extract timeline` |
|
||||
| **Tags** | Frontmatter `tags:` YAML array | `tags` | `importFromFile` (reconciles per-page on import) |
|
||||
| **emotional_weight** | Recomputed from takes + tags | `pages.emotional_weight` (signal column) | `recompute_emotional_weight` cycle phase |
|
||||
| **synthesis_evidence** | FK into `takes` rows (`slug#N`) inside synthesis pages | `synthesis_evidence` | `extract takes` (transitively) |
|
||||
|
||||
### Derived from FS but not user-authored
|
||||
|
||||
These hold derived state that's automatically reconstructible from the
|
||||
markdown but not directly authored as markdown by the user. The
|
||||
chunker + embedder rebuild these on import.
|
||||
|
||||
| Table | Source | Notes |
|
||||
|---|---|---|
|
||||
| `pages` | The markdown file as a whole | One row per file; `compiled_truth` + `frontmatter` come from parse |
|
||||
| `content_chunks` | `pages.compiled_truth` after chunker strip | Re-chunked on content_hash change; embedded via configured model |
|
||||
| `page_versions` | Each `pages` UPDATE | Audit history; rebuildable in principle but not in practice |
|
||||
|
||||
### DB-only by design (named exceptions)
|
||||
|
||||
These hold runtime / infrastructure state that's intentionally not in
|
||||
the repo. The architectural rule still holds — these aren't
|
||||
"user knowledge" — but they're DB-only by design.
|
||||
|
||||
| Category | Why it's OK to be DB-only |
|
||||
|---|---|
|
||||
| `raw_data` | Webhook/transcript sidecars; not user-authored knowledge. |
|
||||
| `subagent_messages` / `subagent_tool_executions` / `subagent_rate_leases` | Runtime job state. Replay-only, not persistent knowledge. |
|
||||
| `oauth_clients` / `oauth_tokens` / `access_tokens` | Credentials. Not in source control by definition. |
|
||||
| `mcp_request_log` | Audit trail. Volatile by design. |
|
||||
| `minion_jobs` / `minion_inbox` / `minion_attachments` | Job queue. Restarts re-enqueue or drop. |
|
||||
| `eval_candidates` / `eval_capture_failures` | Contributor-mode dev loop; opt-in capture. |
|
||||
| `dream_verdicts` | Cheap verdict cache. Rebuildable by re-running Haiku. |
|
||||
| `gbrain_cycle_locks` / migration ledger | Infrastructure. |
|
||||
| `config` (some keys) | Site-local routing config (e.g. `sync.repo_path`). |
|
||||
|
||||
A new derived table that holds user-knowledge MUST land FS-first.
|
||||
If you're tempted to add one as "DB-only for now," the structural
|
||||
question is: does it belong in this DB-only-by-design list? If not,
|
||||
it's FS-canonical and needs a fence (or frontmatter field) plus a
|
||||
reconciler.
|
||||
|
||||
## The privacy boundary
|
||||
|
||||
Private knowledge in a fence still lives in the markdown file. If the
|
||||
user commits the page to git, the private data lands in git too. This
|
||||
is the existing operational model — we don't infer git policy.
|
||||
|
||||
For untrusted readers (remote MCP, subagent), the v0.32.2 release ships
|
||||
a 3-layer strip:
|
||||
|
||||
1. **Layer A (chunker):** `src/core/chunkers/recursive.ts` calls
|
||||
`stripFactsFence({keepVisibility: ['world']})` + `stripTakesFence`
|
||||
before chunking. Private fact text never reaches
|
||||
`content_chunks.chunk_text`, embeddings, or search results.
|
||||
2. **Layer B (get_page):** when `ctx.remote === true`, the response
|
||||
body has both fences stripped (private rows from facts; entire
|
||||
takes fence). Local CLI (`ctx.remote === false`) sees the full
|
||||
fence.
|
||||
3. **Layer C (git tracking):** the user decides whether to commit the
|
||||
entity page. `gbrain.yml` `db_only` paths are gitignored
|
||||
automatically; per-page choices via the user's normal git workflow.
|
||||
|
||||
For universally-private entities (a friend's name, an investor's
|
||||
internal notes), mark the entity page's directory as `db_only` in
|
||||
`gbrain.yml`. The file stays on disk but never lands in git.
|
||||
|
||||
## The forget contract
|
||||
|
||||
`gbrain forget <id>` and the MCP `forget_fact` op rewrite the fence
|
||||
row with strikethrough + `valid_until = today` + `context: "forgotten:
|
||||
<reason>"`. The DB's `expired_at = valid_until + now()` derivation
|
||||
reconstructs the forget state on every rebuild because the fence is
|
||||
canonical.
|
||||
|
||||
Strikethrough has two semantics distinguished by context:
|
||||
|
||||
- `~~claim~~` + `context: "superseded by #N"` → row was replaced by
|
||||
a newer row in the same fence
|
||||
- `~~claim~~` + `context: "forgotten: <reason>"` → row was retracted
|
||||
via the forget op
|
||||
|
||||
Both encodings keep the row in the markdown for audit history. To
|
||||
permanently delete a fact, edit the fence directly in markdown and
|
||||
remove the row. The next `extract_facts` cycle wipes the DB row.
|
||||
|
||||
## Disaster recovery
|
||||
|
||||
The promise the rule makes:
|
||||
|
||||
```bash
|
||||
# Snapshot what's there
|
||||
gbrain stats > /tmp/before.txt
|
||||
|
||||
# Wipe and rebuild
|
||||
gbrain rebuild --confirm-destructive # v0.32.3 — deletes derived tables
|
||||
# (pages + content_chunks survive
|
||||
# the CASCADE-safe design)
|
||||
# OR manually for v0.32.2:
|
||||
psql -c 'DELETE FROM facts; DELETE FROM takes; DELETE FROM links; DELETE FROM timeline_entries;'
|
||||
gbrain sync
|
||||
gbrain extract all
|
||||
|
||||
# Counts match
|
||||
gbrain stats > /tmp/after.txt
|
||||
diff /tmp/before.txt /tmp/after.txt
|
||||
```
|
||||
|
||||
The invariant E2E test at `test/e2e/system-of-record-invariant.test.ts`
|
||||
exercises this exact flow on every CI run.
|
||||
|
||||
## Rule for new code
|
||||
|
||||
When you add a new user-knowledge category:
|
||||
|
||||
1. **Define the markdown shape.** Fence (`<!--- gbrain:NAME:begin
|
||||
--> ... :end -->` table) or frontmatter field.
|
||||
2. **Build a parser** that produces structured data from markdown.
|
||||
See `src/core/fence-shared.ts` for the shared primitives.
|
||||
3. **Build a writer** that round-trips: parse + edit + render produces
|
||||
byte-identical markdown for identical input.
|
||||
4. **Add the engine method** that takes parsed data and stamps a
|
||||
derived table. The method gets an entry in the CI gate's
|
||||
banned-direct-call list.
|
||||
5. **Add a reconciler:** a cycle phase that walks pages, parses the
|
||||
fence, and rebuilds the derived table from scratch. The reconciler
|
||||
is the only legitimate call site for the engine method;
|
||||
`// gbrain-allow-direct-insert: <reason>` annotates it explicitly.
|
||||
6. **Add a round-trip test** in `test/e2e/system-of-record-invariant.test.ts`
|
||||
that proves DELETE + reconcile rebuilds the table byte-identically.
|
||||
|
||||
The CI gate at `scripts/check-system-of-record.sh` fails any PR that
|
||||
adds a new direct call to a derived-table writer outside the
|
||||
reconciler / migration layer without the explicit allow-list comment.
|
||||
|
||||
## Related
|
||||
|
||||
- `~/.claude/plans/system-instruction-you-are-working-expressive-pony.md`
|
||||
— the v0.32.2 design plan (decisions D1-D22 + Q1-Q8, Codex round 1
|
||||
and round 2 finds)
|
||||
- `skills/migrations/v0.32.2.md` — the agent-facing migration guide
|
||||
- `CHANGELOG.md` v0.32.2 entry — the release manifesto
|
||||
- `scripts/check-system-of-record.sh` — the CI gate that enforces
|
||||
the rule
|
||||
@@ -1,403 +0,0 @@
|
||||
# GBrain Deployment Topologies
|
||||
|
||||
GBrain supports three deployment shapes. They compose: a single user can mix
|
||||
all three on the same machine without conflict, because every shape resolves
|
||||
to "which `~/.gbrain/config.json` is active right now?" and `GBRAIN_HOME`
|
||||
controls that selection.
|
||||
|
||||
This page covers the three topologies, when each fits, and concrete setup
|
||||
recipes. Pair this doc with `docs/architecture/brains-and-sources.md` (which
|
||||
covers the in-brain organization axes) — that doc is about WHICH database;
|
||||
this doc is about WHERE that database lives.
|
||||
|
||||
## Quick decision tree
|
||||
|
||||
```
|
||||
"I'm setting up gbrain..."
|
||||
│
|
||||
▼
|
||||
Just for me, on one machine? ─── yes ───▶ Topology 1 (single brain)
|
||||
│
|
||||
no
|
||||
│
|
||||
▼
|
||||
Will a remote machine host the brain
|
||||
while my agent runs locally? ──── yes ───▶ Topology 2 (cross-machine thin client)
|
||||
│
|
||||
no
|
||||
│
|
||||
▼
|
||||
Multiple Conductor worktrees that
|
||||
shouldn't share a code index? ─── yes ───▶ Topology 3 (split-engine)
|
||||
```
|
||||
|
||||
Topologies 2 and 3 stack: a thin-client install can also host per-worktree
|
||||
code engines, and a per-worktree code engine can also point its artifact
|
||||
brain at a remote server.
|
||||
|
||||
## Topology 1 — Single brain (today's default)
|
||||
|
||||
```
|
||||
┌────────────────┐
|
||||
│ one machine │
|
||||
│ ┌──────────┐ │
|
||||
│ │ gbrain │──┼──→ ~/.gbrain/ → PGLite or Supabase
|
||||
│ │ CLI │ │
|
||||
│ └──────────┘ │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
What you get: one local DB (PGLite for small brains, Supabase for ~1000+
|
||||
files). All commands work directly against it. `gbrain serve` exposes it
|
||||
to a single agent over MCP.
|
||||
|
||||
When it fits: solo use, single machine, one agent, no Conductor parallelism.
|
||||
This is the default; `gbrain init` (no flags) gives you this.
|
||||
|
||||
Setup:
|
||||
|
||||
```
|
||||
gbrain init # interactive — defaults to PGLite
|
||||
gbrain init --pglite # explicit local
|
||||
gbrain init --supabase # remote Supabase (recommended for 1000+ files)
|
||||
```
|
||||
|
||||
Nothing else here is special. The other two topologies are variations on
|
||||
"who owns the DB" and "how does the agent talk to it."
|
||||
|
||||
## Topology 2 — Cross-machine thin client
|
||||
|
||||
```
|
||||
┌────────────┐ ┌──────────────────┐
|
||||
│ neuromancer│ │ brain-host │
|
||||
│ ┌────────┐ │ HTTP MCP / OAuth │ ┌────────────┐ │
|
||||
│ │ Hermes │─┼───────────────────→│ │ gbrain │──┼──→ Supabase
|
||||
│ │ agent │ │ │ │ serve --http│ │
|
||||
│ └────────┘ │ │ └────────────┘ │
|
||||
│ │ │ (with autopilot)│
|
||||
│ no local │ │ │
|
||||
│ gbrain DB │ │ │
|
||||
└────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
What you get: the agent on one machine ("neuromancer") consumes a brain
|
||||
hosted on another machine ("brain-host") over HTTP MCP with OAuth. The
|
||||
agent's machine has NO local engine. All queries, searches, embeddings,
|
||||
and indexing happen on the host.
|
||||
|
||||
When it fits:
|
||||
|
||||
- Heavy brain (Supabase + autopilot) lives on a beefy machine; agents
|
||||
elsewhere just consume it.
|
||||
- You want one source of truth across many machines.
|
||||
- Spinning up a parallel local install would create source-ID contention or
|
||||
duplicate work.
|
||||
|
||||
The thin client's `~/.gbrain/config.json` carries a `remote_mcp` field
|
||||
instead of a local DB connection:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"engine": "postgres", // ignored — never used
|
||||
"remote_mcp": {
|
||||
"issuer_url": "https://brain-host.local:3001",
|
||||
"mcp_url": "https://brain-host.local:3001/mcp",
|
||||
"oauth_client_id": "neuromancer-...",
|
||||
"oauth_client_secret": "..." // or set GBRAIN_REMOTE_CLIENT_SECRET
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The CLI dispatch guard refuses any DB-bound command (`sync`, `embed`,
|
||||
`extract`, `migrate`, `apply-migrations`, `repair-jsonb`, `orphans`,
|
||||
`integrity`, `serve`) on a thin-client install with a clear error pointing
|
||||
at the remote host. `gbrain doctor` runs a dedicated thin-client check set
|
||||
(OAuth discovery, token round-trip, MCP smoke).
|
||||
|
||||
### Setup
|
||||
|
||||
**Step 1 — On the host (brain-host):**
|
||||
|
||||
```bash
|
||||
gbrain init --supabase # or --pglite, doesn't matter
|
||||
gbrain serve --http --port 3001 --bind 0.0.0.0 # v0.34: bind explicitly for remote access
|
||||
# (defaults to 127.0.0.1 since v0.34)
|
||||
gbrain auth register-client neuromancer \
|
||||
--grant-types client_credentials \
|
||||
--scopes read,write,admin # admin needed for ping/doctor
|
||||
|
||||
# v0.34: source-scoped client (write to one source, federate reads across
|
||||
# multiple sources). Omit both flags for a v0.33-compatible super-client.
|
||||
gbrain auth register-client neuromancer-dept \
|
||||
--grant-types client_credentials \
|
||||
--scopes read,write \
|
||||
--source dept-x \
|
||||
--federated-read dept-x,shared,parent-canon
|
||||
```
|
||||
|
||||
The `register-client` command prints a `client_id` and `client_secret`.
|
||||
Note both. **Scope must include `admin`** — `submit_job` (used by
|
||||
`gbrain remote ping`) and `run_doctor` (used by `gbrain remote doctor`)
|
||||
both require it.
|
||||
|
||||
**Step 2 — On the thin client (neuromancer):**
|
||||
|
||||
```bash
|
||||
gbrain init --mcp-only \
|
||||
--issuer-url https://brain-host.local:3001 \
|
||||
--mcp-url https://brain-host.local:3001/mcp \
|
||||
--oauth-client-id <id> \
|
||||
--oauth-client-secret <secret>
|
||||
```
|
||||
|
||||
Pre-flight smoke runs three probes (OAuth discovery, token round-trip,
|
||||
MCP initialize). If any fails, init exits with an actionable error. On
|
||||
success, `~/.gbrain/config.json` gets `remote_mcp` set and NO local DB
|
||||
is created.
|
||||
|
||||
**Step 3 — Configure your agent's MCP client.**
|
||||
|
||||
For Claude Desktop / Hermes / openclaw, add a single MCP server entry
|
||||
pointing at the host's `mcp_url` with the bearer token from `register-client`.
|
||||
Example for Claude Desktop's `~/.config/claude/claude_desktop_config.json`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcpServers": {
|
||||
"gbrain": {
|
||||
"type": "url",
|
||||
"url": "https://brain-host.local:3001/mcp",
|
||||
"headers": { "Authorization": "Bearer <client_secret>" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4 — Verify.**
|
||||
|
||||
```bash
|
||||
gbrain doctor # runs thin-client checks (no local DB needed)
|
||||
gbrain remote ping # triggers an autopilot cycle on the host (Tier B)
|
||||
gbrain remote doctor # asks the host to run its own doctor (Tier B)
|
||||
```
|
||||
|
||||
`gbrain sync` and friends will refuse with a clear thin-client error
|
||||
naming the `mcp_url`. That's the correct behavior — those commands need
|
||||
a local engine that doesn't exist here.
|
||||
|
||||
### Re-run guard
|
||||
|
||||
Running `gbrain init` (no flags) on a machine that already has thin-client
|
||||
config set refuses without `--force`. This catches the scripted-setup-loop
|
||||
friction where an orchestrator keeps trying to create a local DB. Use
|
||||
`gbrain init --mcp-only --force` to refresh thin-client config.
|
||||
|
||||
### Storing the OAuth secret
|
||||
|
||||
Three storage paths in priority order:
|
||||
|
||||
1. **`GBRAIN_REMOTE_CLIENT_SECRET` env var** (preferred for headless agents).
|
||||
When set, overrides whatever's in the config file. The init flow doesn't
|
||||
persist a config-file copy when the env var was the source.
|
||||
2. **`~/.gbrain/config.json` with 0600 perms** (default for interactive
|
||||
setup; mirrors how Supabase keys are stored today).
|
||||
3. macOS Keychain integration is on the roadmap; not in v1.
|
||||
|
||||
## Topology 3 — Split-engine, per-worktree code + remote artifacts
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────┐
|
||||
│ one machine │
|
||||
│ │
|
||||
│ ┌─ worktree A ──────────────┐ │
|
||||
│ │ GBRAIN_HOME=A/.conductor │ │
|
||||
│ │ gbrain serve --port 3001 │── PGLite (code A) │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ worktree B ──────────────┐ │
|
||||
│ │ GBRAIN_HOME=B/.conductor │ │
|
||||
│ │ gbrain serve --port 3002 │── PGLite (code B) │
|
||||
│ └───────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─ default ~/.gbrain ───────┐ HTTP MCP / OAuth │
|
||||
│ │ gbrain serve --port 3000 │──────────────────────→ remote artifacts
|
||||
│ └───────────────────────────┘ (Supabase / brain-host)
|
||||
│ │
|
||||
│ Agent's MCP config (Hermes / Claude Desktop): │
|
||||
│ mcp__gbrain_code__* → http://localhost:3001 │
|
||||
│ mcp__gbrain_artifacts__* → http://brain-host/mcp │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
What you get: each Conductor worktree has its own per-worktree code index
|
||||
(local PGLite, disposable when the worktree dies). Artifacts (plans,
|
||||
learnings, transcripts) still live in a shared brain that all worktrees
|
||||
can see and write to.
|
||||
|
||||
When it fits:
|
||||
|
||||
- Multiple Conductor worktrees on one machine, all touching the same code
|
||||
repo.
|
||||
- You don't want each worktree's code-import to clobber the others'
|
||||
`last_commit`, source IDs, or symbol tables.
|
||||
- You DO want artifacts (plans, learnings, retros, transcripts) to be
|
||||
visible across worktrees.
|
||||
|
||||
### How it works
|
||||
|
||||
`GBRAIN_HOME` selects which `~/.gbrain` directory is active. Set per worktree:
|
||||
|
||||
```bash
|
||||
export GBRAIN_HOME=/path/to/worktree-A/.conductor/gbrain
|
||||
gbrain init --pglite
|
||||
gbrain serve --http --port 3001
|
||||
```
|
||||
|
||||
Each worktree's `gbrain serve` instance binds its own port and indexes its
|
||||
own DB. Multiple `gbrain serve` processes coexist fine — they're separate
|
||||
OS processes with separate config and separate connection pools.
|
||||
|
||||
The artifact brain runs as a separate `gbrain serve` instance with the
|
||||
default `~/.gbrain` (no GBRAIN_HOME override) — or remote, in which case
|
||||
it's a Topology 2 setup.
|
||||
|
||||
The agent's MCP client config lists multiple servers, each with a unique
|
||||
alias. Tool names are namespaced as `mcp__<alias>__<tool>`, so the agent
|
||||
calls `mcp__gbrain_code__search` for code lookups and `mcp__gbrain_artifacts__search`
|
||||
for artifact lookups.
|
||||
|
||||
### Recommended embedding model
|
||||
|
||||
Per-worktree code brains index source files only — no meeting notes,
|
||||
no people pages, no transcripts. Configure each code brain to use
|
||||
Voyage's code-tuned model at init time so the config can't be lost to a
|
||||
later `init` overwrite:
|
||||
|
||||
```bash
|
||||
export GBRAIN_HOME=/path/to/worktree-A/.conductor/gbrain
|
||||
gbrain init --pglite \
|
||||
--embedding-model voyage:voyage-code-3 \
|
||||
--embedding-dimensions 1024
|
||||
```
|
||||
|
||||
`voyage-code-3` is Voyage's code-specialized embedding model with
|
||||
head-to-head numbers above their general flagships on code retrieval
|
||||
([voyageai.com/blog](https://voyageai.com/blog)). For already-initialized
|
||||
brains, switch with the one-command wipe-and-reinit (preserves every
|
||||
other config field):
|
||||
|
||||
```bash
|
||||
gbrain reinit-pglite --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024
|
||||
gbrain reindex --code --yes
|
||||
```
|
||||
|
||||
(`gbrain config set embedding_model` is refused as of v0.37.11.0 because
|
||||
the schema column has to resize alongside the config.)
|
||||
|
||||
`gbrain reindex --code` prints a recommendation when the configured
|
||||
embedding model isn't code-tuned. Suppress with
|
||||
`GBRAIN_NO_CODE_MODEL_NUDGE=1` if you've intentionally chosen another
|
||||
provider (single-vendor procurement, compliance, no Voyage key).
|
||||
|
||||
### CRITICAL: alias-level routing is manual
|
||||
|
||||
Topology 3 has no smart per-tool routing inside gbrain. The agent picks
|
||||
which brain to query when it picks the alias. **A wrong alias writes (or
|
||||
queries) the wrong brain silently.** This is intentional (explicit beats
|
||||
magic) but real:
|
||||
|
||||
- If the agent calls `mcp__gbrain_artifacts__put_page` with code-shaped
|
||||
content, that page lands in the artifact brain forever.
|
||||
- If the agent calls `mcp__gbrain_code__search` for a question that
|
||||
actually wants artifact context, the search comes back empty.
|
||||
|
||||
Mitigations:
|
||||
|
||||
- Name aliases clearly. `gbrain_code` vs `gbrain_artifacts` is unambiguous;
|
||||
`gbrain` vs `gbrain_local` is not.
|
||||
- Document in your agent's system prompt or rules which alias goes where.
|
||||
Be explicit about "code questions → `gbrain_code`; everything else →
|
||||
`gbrain_artifacts`."
|
||||
- Pair Topology 3 with `gstack`'s per-worktree wiring (which sets the
|
||||
alias names + agent rules consistently across worktrees).
|
||||
|
||||
### Setup (manual; gstack automates this side)
|
||||
|
||||
The gbrain side requires zero new code — `GBRAIN_HOME` and `--port` already
|
||||
exist. Setup looks like:
|
||||
|
||||
```bash
|
||||
# Start the artifact brain (default ~/.gbrain) on port 3000
|
||||
gbrain serve --http --port 3000 &
|
||||
|
||||
# Start a per-worktree code brain on port 3001
|
||||
export GBRAIN_HOME=/path/to/worktree-A/.conductor/gbrain
|
||||
gbrain init --pglite
|
||||
gbrain serve --http --port 3001 &
|
||||
unset GBRAIN_HOME
|
||||
```
|
||||
|
||||
Then configure the agent's MCP config with two entries (different aliases,
|
||||
different ports). For Claude Desktop:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"mcpServers": {
|
||||
"gbrain_artifacts": {
|
||||
"type": "url",
|
||||
"url": "http://localhost:3000/mcp",
|
||||
"headers": { "Authorization": "Bearer <token-A>" }
|
||||
},
|
||||
"gbrain_code": {
|
||||
"type": "url",
|
||||
"url": "http://localhost:3001/mcp",
|
||||
"headers": { "Authorization": "Bearer <token-B>" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The gstack-side wiring (per-worktree home setup, port allocation, automatic
|
||||
MCP config generation, gitignore for the per-worktree DB) is in the gstack
|
||||
repo's setup-gbrain skill — it composes these primitives, gbrain doesn't
|
||||
have to know about Conductor.
|
||||
|
||||
## Combining topologies
|
||||
|
||||
The three shapes compose. A single machine can run:
|
||||
|
||||
- A thin-client default config pointing at a remote artifact brain
|
||||
(Topology 2).
|
||||
- Plus per-worktree code brains under their own `GBRAIN_HOME` (Topology 3).
|
||||
- Each worktree's `gbrain serve` instance is local; the agent's MCP config
|
||||
lists them alongside the remote artifact brain.
|
||||
|
||||
`GBRAIN_HOME` controls which config file is active for any one CLI
|
||||
invocation. `gbrain serve --port` controls which port a server listens on.
|
||||
The agent's MCP client picks the alias and thus the destination per tool
|
||||
call. There's no global gbrain orchestrator that knows about all of them
|
||||
simultaneously — that's by design.
|
||||
|
||||
## When NOT to use these topologies
|
||||
|
||||
- **Don't use Topology 2 if your agent only ever runs on the same machine
|
||||
as the brain.** A local `gbrain` install + `gbrain serve` (stdio) is
|
||||
simpler and faster.
|
||||
- **Don't use Topology 3 if you only have one Conductor worktree at a
|
||||
time.** Per-worktree engines exist to prevent contention; one-at-a-time
|
||||
use has no contention.
|
||||
- **Don't use a `remote_mcp` thin client AND a local engine on the same
|
||||
machine in the same `GBRAIN_HOME`.** The dispatch guard refuses DB-bound
|
||||
commands when `remote_mcp` is set. If you genuinely want both modes on
|
||||
one machine, use `GBRAIN_HOME` to separate them (one home for the thin
|
||||
client, another for the local engine).
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/architecture/brains-and-sources.md` — in-brain organization (brains
|
||||
vs sources axes).
|
||||
- `docs/mcp/CLAUDE_DESKTOP.md` and siblings — per-client MCP setup.
|
||||
- `gbrain init --help` and `gbrain auth --help` for command-level details.
|
||||
- [`docs/tutorials/`](../tutorials/) — end-to-end walkthroughs that combine
|
||||
these topologies into working setups (company brain, personal brain,
|
||||
agent integration, etc.).
|
||||
@@ -1,177 +0,0 @@
|
||||
# Type Taxonomy (v0.41.22: gbrain-base-v2)
|
||||
|
||||
> The 14-canonical-type DRY/MECE taxonomy shipped in v0.41.22. Predecessor
|
||||
> `gbrain-base` (24 types) stays bundled for back-compat; v0.42+ installs
|
||||
> default to `gbrain-base-v2`.
|
||||
|
||||
## Why
|
||||
|
||||
A production gbrain brain (186K pages) had accreted **94 distinct
|
||||
`pages.type` values** in 9 clusters of redundancy. The type system is
|
||||
the foundation for schema packs, search filtering, extract behavior,
|
||||
enrichment routing, and expert routing. When types are noisy, every
|
||||
downstream feature degrades:
|
||||
|
||||
- **Search filtering is ambiguous** — `--type article` misses 2.2K
|
||||
articles typed as `media/article`, `sources/article`, etc.
|
||||
- **Enrichment routing is incomplete** — `enrichable_types` could only
|
||||
list a few canonical types; 80+ legacy types meant most pages never
|
||||
got enriched.
|
||||
- **Agent confusion** — when ingesting a new article, should it be
|
||||
`article`, `media/article`, `sources/article`, or `source/article`?
|
||||
Four reasonable choices, none of them right.
|
||||
- **Orphan inflation** — 5,521 concept-redirect pages inflated orphan
|
||||
counts without adding knowledge value.
|
||||
|
||||
Issue #1479 catalogues the 9 clusters with exact counts. This doc is
|
||||
the response: a coherent 14-type taxonomy with subtypes/format/origin
|
||||
pushed to frontmatter, alias-table rows for redirects, real link-table
|
||||
rows for edge-shaped pages.
|
||||
|
||||
## The 14 canonical types (+ `note` catch-all)
|
||||
|
||||
| Type | Primitive | What it holds | Examples |
|
||||
|------|-----------|---------------|----------|
|
||||
| `person` | entity | People | Founders, partners, individuals |
|
||||
| `company` | entity | Companies, products, orgs (subtype-distinguished) | Companies, YC-companies, products |
|
||||
| `media` | media | Articles, videos, essays, books, podcasts (subtype-distinguished) | Substack posts, YouTube videos, books |
|
||||
| `tweet` | media | Twitter posts (single/bundle/stub subtype) | Single tweets, threads, bundles |
|
||||
| `social-digest` | temporal | Period-grouped social summaries (daily/monthly) | X account daily digests |
|
||||
| `analysis` | media | Research + competitive intel | Market analysis, pricing analysis |
|
||||
| `atom` | annotation | Knowledge units (extraction/manual/lore subtype) | Extracted facts, manual notes, lore |
|
||||
| `concept` | concept | Ideas + reference pages | Wiki concepts |
|
||||
| `source` | media | Transcripts, references | Interview transcripts |
|
||||
| `deal` | temporal | Investment deals | Term sheets, investments |
|
||||
| `email` | temporal | Email threads | Email correspondence |
|
||||
| `slack` | temporal | Slack messages + threads | Slack conversations |
|
||||
| `writing` | media | Original writing | Drafts, essays in progress |
|
||||
| `project` | concept | Initiatives, workstreams | Internal projects |
|
||||
| `note` | concept | **Catch-all** for one-offs (legacy_type preserved) | Memos, anecdotes, insights, etc. |
|
||||
|
||||
15 types total (14 canonical + `note`). The catch-all retype rule
|
||||
binds any uncovered legacy type to `note` with
|
||||
`frontmatter.legacy_type = <original>` preserved for rollback.
|
||||
|
||||
## Subtypes (declared in frontmatter post-unify)
|
||||
|
||||
| Canonical | Subtype field | Values |
|
||||
|-----------|---------------|--------|
|
||||
| `company` | `subtype` | `company` / `product` / `org` |
|
||||
| `media` | `subtype` | `video` / `article` / `essay` / `book` / `podcast` / `blog` |
|
||||
| `tweet` | `subtype` | `single` / `bundle` / `stub` |
|
||||
| `social-digest` | `subtype` | `daily` / `monthly` |
|
||||
| `atom` | `subtype` | `extraction` / `manual` / `lore` |
|
||||
|
||||
`subtype_field` for retype rules is restricted to an allowlist:
|
||||
`{subtype, legacy_type, origin, format, kind, period, domain}`. This
|
||||
prevents third-party packs from injecting `title`, `slug`, or `type`
|
||||
via mapping_rules (codex D9 security hardening).
|
||||
|
||||
## Migration flow
|
||||
|
||||
```
|
||||
gbrain onboard --check # surfaces pack_upgrade_available
|
||||
↓
|
||||
gbrain onboard --check --explain # per-cluster narrative dry-run
|
||||
↓
|
||||
gbrain jobs submit unify-types \ # PROTECTED + manual_only
|
||||
--allow-protected \
|
||||
--params '{"target_pack":"gbrain-base-v2"}'
|
||||
↓
|
||||
Handler runs 4 phases:
|
||||
┌─────────────────────────────────────┐
|
||||
│ Phase 1: Preflight + lock │ → gbrain-unify db-lock (60min TTL)
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 2: Retype explicit rules │ → chunked UPDATE 1000/batch
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 3: Retype catch-all sentinel │ → 'note' with legacy_type
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 4: Page-to-link conversions │ → insert links + soft-delete
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 5: Page-to-alias conversions │ → insert slug_aliases + soft-delete
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 6: Final sync (residual) │ → path-prefix typing
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 7: Flip active pack (D13) │ → engine.setConfig + saveConfig
|
||||
├─────────────────────────────────────┤
|
||||
│ Phase 8: Verify + celebrate │ → assert ≤16 types; stderr summary
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
gbrain onboard --check # pack_upgrade_available cleared
|
||||
# type_proliferation cleared
|
||||
```
|
||||
|
||||
## Rollback paths
|
||||
|
||||
Every primitive ships with a documented rollback:
|
||||
|
||||
| Operation | Rollback |
|
||||
|-----------|----------|
|
||||
| Retype | `frontmatter.legacy_type = <original>` preserved on every page (D8). One SQL UPDATE restores types: `UPDATE pages SET type = frontmatter->>'legacy_type' WHERE frontmatter ? 'legacy_type'`. |
|
||||
| Page-to-link | Source page soft-deleted with 72h TTL. `gbrain pages restore <slug>` within 72h. Link row stays harmless if source restored. |
|
||||
| Page-to-alias | Source page soft-deleted with 72h TTL. `gbrain pages restore <slug>` within 72h. Alias row stays harmless (or `DELETE FROM slug_aliases WHERE alias_slug = <slug>` to clean up). |
|
||||
| Active-pack flip | `gbrain schema use gbrain-base` reverses the flip. |
|
||||
|
||||
## What if my brain doesn't fit?
|
||||
|
||||
The catch-all retype rule (`from_type: '*unknown*'`) handles long-tail
|
||||
types automatically — any page whose type isn't covered by an explicit
|
||||
rule AND isn't a page_to_link / page_to_alias source gets retyped to
|
||||
`note` with `legacy_type` preserved. Guarantees ≤16 distinct types
|
||||
post-unify on ANY brain.
|
||||
|
||||
For brains with substantial custom types that deserve their own canonical
|
||||
(e.g. `researcher` for an academic brain), the right move is:
|
||||
|
||||
1. Fork gbrain-base-v2: `gbrain schema fork gbrain-base-v2 my-pack`
|
||||
2. Edit your fork to add page_types + mapping_rules covering your
|
||||
custom domain.
|
||||
3. Target your fork: `gbrain jobs submit unify-types --allow-protected
|
||||
--params '{"target_pack":"my-pack"}'`
|
||||
|
||||
Your fork can also declare `migration_from: {pack: gbrain-base-v2,
|
||||
version: "1.x"}` to register itself as a successor — future agents
|
||||
discovering your pack via `pack_upgrade_available` will offer the
|
||||
migration.
|
||||
|
||||
## Wikilink resolution post-unify
|
||||
|
||||
The slug_aliases table IS the resolver (D15: codex outside voice —
|
||||
don't rewrite body-text wikilinks; the alias table is the right
|
||||
primitive). Wikilinks like `[[old-redirect-slug]]` keep working post-
|
||||
unify because:
|
||||
|
||||
1. The wikilink resolver short-circuits through
|
||||
`engine.resolveSlugWithAlias(slug, sourceId)` BEFORE the existing
|
||||
fuzzy/prefix cascade.
|
||||
2. The lookup queries `slug_aliases` for any matching alias_slug in
|
||||
the provided source(s).
|
||||
3. If found, returns the canonical_slug. The renderer then resolves
|
||||
the wikilink to the canonical page.
|
||||
|
||||
Multi-source ambiguity (same alias_slug in two registered sources)
|
||||
emits a once-per-process `multi_match` stderr warning and returns the
|
||||
first match by source array order. Federated reads pass the full
|
||||
allowed-source array.
|
||||
|
||||
## Search ranking signal: alias_resolved_boost
|
||||
|
||||
Post-unify, search results whose slug is a canonical_slug in
|
||||
slug_aliases get a 1.05x score multiplier via the
|
||||
`applyAliasResolvedBoost` post-fusion stage. Semantic intent: "user
|
||||
explicitly disambiguated this as canonical, so it should outrank fuzzy
|
||||
matches that hit aliases by accident."
|
||||
|
||||
`SearchResult.alias_resolved_boost` is stamped on touched results for
|
||||
`--explain` formatter visibility. KNOBS_HASH_VERSION bumped 5→6 to
|
||||
invalidate pre-v0.42 cache rows that don't reflect the new stage.
|
||||
|
||||
## Reference
|
||||
|
||||
- Issue: https://github.com/garrytan/gbrain/issues/1479
|
||||
- Pack file: `src/core/schema-pack/base/gbrain-base-v2.yaml`
|
||||
- Pack-upgrade mechanism: `docs/architecture/pack-upgrade-mechanism.md`
|
||||
- Migration handler: `src/core/schema-pack/unify-types-handler.ts`
|
||||
- Onboard checks: `src/core/onboard/checks.ts`
|
||||
- Skill: `skills/schema-unify/SKILL.md`
|
||||
- Plan + decisions: `~/.claude/plans/system-instruction-you-are-working-transient-elephant.md`
|
||||
@@ -1,166 +0,0 @@
|
||||
# gbrain eval suspected-contradictions (v0.32.6)
|
||||
|
||||
The contradiction probe samples retrieval results, asks an LLM judge whether
|
||||
any pair contradicts on a factual claim relevant to the user's query, and
|
||||
aggregates into a calibrated report. The output is data — the operator
|
||||
decides what to act on. This doc covers the architecture, severity rubric,
|
||||
how to interpret the headline number, and when to act.
|
||||
|
||||
## Why this exists
|
||||
|
||||
gbrain handles contradictions for *curated* pages via compiled-truth-plus-
|
||||
timeline and source-boost: when `companies/acme.md` says MRR is $2M and a
|
||||
chat transcript from 2024 says MRR was $50K, the curated page outranks the
|
||||
chat. `takes.active` filtering hides explicitly-superseded takes. Recency
|
||||
decay biases ranking toward fresher content per source-tier.
|
||||
|
||||
What none of those mechanisms measure: how often do unmarked semantic
|
||||
contradictions actually surface in retrieval? Without a probe, every
|
||||
"should we build the bigger swing (chunk-level `revises` field + ranking
|
||||
change)" decision is vibes. The probe produces evidence.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ gbrain eval suspected-contradictions │
|
||||
└──────────────────┬───────────────────┘
|
||||
│
|
||||
┌──────────────────▼───────────────────┐
|
||||
│ For each query: hybridSearch top-K │
|
||||
│ → cross_slug_chunks + intra_page │
|
||||
│ chunk-vs-take pairs │
|
||||
└──────────────────┬───────────────────┘
|
||||
│
|
||||
┌──────────────────▼───────────────────┐
|
||||
│ Date pre-filter: skip pairs whose │
|
||||
│ dates are >30d apart (Codex fix: │
|
||||
│ same-paragraph-dual-date overrides) │
|
||||
└──────────────────┬───────────────────┘
|
||||
│
|
||||
┌──────────────────▼───────────────────┐
|
||||
│ Persistent cache lookup │
|
||||
│ (chunk_a_hash, chunk_b_hash, model, │
|
||||
│ prompt_version, truncation_policy) │
|
||||
└────────┬─────────┬────────────────────┘
|
||||
hit│ │miss
|
||||
│ ▼
|
||||
│ ┌─────────────────────────┐
|
||||
│ │ LLM judge call │
|
||||
│ │ → JudgeVerdict │
|
||||
│ │ confidence floor ≥ 0.7 │
|
||||
│ └─────────┬───────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ Aggregate per-query + global stats │
|
||||
│ Wilson 95% CI on headline % │
|
||||
│ source-tier breakdown │
|
||||
│ hot pages + resolution proposals │
|
||||
└──────────────────┬───────────────────┘
|
||||
│
|
||||
▼
|
||||
ProbeReport JSON
|
||||
│
|
||||
┌──────────────────┼──────────────────────┬───────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
doctor (M1) MCP (M3) synthesize (M2) trend (M5)
|
||||
surfaces find_contradictions informational persistent
|
||||
findings op for agents block in prompt tracking
|
||||
```
|
||||
|
||||
## Severity rubric
|
||||
|
||||
The judge assigns severity per finding:
|
||||
|
||||
| Level | Rubric | Example |
|
||||
|---|---|---|
|
||||
| `low` | naming/format differences | "Alice Smith" vs "A. Smith" |
|
||||
| `medium` | factual values that may be stale | revenue figure, headcount, valuation |
|
||||
| `high` | identity / structural claims | founder/CEO/CFO role, company status |
|
||||
|
||||
Doctor sorts findings by severity DESC. The MCP op accepts a severity filter
|
||||
so agents can fetch just the high-priority items.
|
||||
|
||||
## How to interpret the headline number
|
||||
|
||||
The probe outputs `queries_with_contradiction / queries_evaluated` with a
|
||||
Wilson 95% confidence interval:
|
||||
|
||||
```
|
||||
Queries with >=1 contradiction: 12 / 50 (24%) Wilson CI 95%: 14–37%
|
||||
```
|
||||
|
||||
What this says: with 95% confidence, the true rate is between 14% and 37%.
|
||||
The 24% point estimate is the most-likely-value but bounded by sampling
|
||||
noise. **`small_sample_note` fires when n < 30** — at that scale the CI is
|
||||
too wide to act on.
|
||||
|
||||
Decision criteria for the bigger swing (chunk-level `revises` field):
|
||||
|
||||
| Wilson CI lower bound | What it says | Action |
|
||||
|---|---|---|
|
||||
| < 5% | Source-boost + recency-decay + curated pages handle the load | Stop here; this is the right scope |
|
||||
| 5–15% | Real but bounded | Operator decides whether the cost justifies the swing |
|
||||
| > 15% | Real and substantial | Plan the bigger swing in v0.34+ |
|
||||
|
||||
## When to act on findings
|
||||
|
||||
Each finding ships with a `resolution_command` field — paste-ready:
|
||||
|
||||
- `gbrain takes supersede <slug> --row N` — newer take should replace
|
||||
the older chunk text on the same page (intra_page kind).
|
||||
- `gbrain dream --phase synthesize --slug <slug>` — compiled_truth for
|
||||
the curated entity needs an update (cross_slug curated-vs-bulk).
|
||||
- `gbrain takes mark-debate <slug> --row N` — intentional disagreement
|
||||
(e.g., two opinions you want to keep both of).
|
||||
- `# manual review: <a> vs <b>` — judge wasn't sure; operator decides.
|
||||
|
||||
Run `gbrain eval suspected-contradictions review --severity high` to
|
||||
inspect findings without re-running the probe.
|
||||
|
||||
## Cost model
|
||||
|
||||
Default judge is `claude-haiku-4-5` at ~$1/Mtok in, $5/Mtok out. With
|
||||
the v0.32.6 truncation at 1500 chars per pair, ~500 input + 80 output
|
||||
tokens per judge call. Budget cap defaults to $5 in TTY / $1 non-TTY.
|
||||
|
||||
- ~$0.0006 per judge call
|
||||
- ~$0.005 per query (after date pre-filter + cache hits)
|
||||
- ~$0.50 per 100 queries
|
||||
|
||||
The persistent cache means nightly runs against the same query set
|
||||
pay near-zero on re-runs (until you bump PROMPT_VERSION).
|
||||
|
||||
## Trust posture
|
||||
|
||||
- Probe never mutates the brain. Runs only read pages/takes/chunks.
|
||||
Writes go only to `eval_contradictions_runs` and `eval_contradictions_cache`.
|
||||
- MCP `find_contradictions` is read-scope. NOT in the subagent allowlist —
|
||||
user-initiated only, not autonomous-action surface.
|
||||
- Build-fixture script is local-only. The redactor + `isCleanForCommit`
|
||||
gate makes accidental private-data commits hard, but the operator MUST
|
||||
inspect every redaction before commit.
|
||||
|
||||
## See also
|
||||
|
||||
- Plan: `~/.claude/plans/system-instruction-you-are-working-hashed-dewdrop.md`
|
||||
- CHANGELOG: `## [0.32.6]` entry covers the whole release.
|
||||
- Cost discipline: `docs/eval-bench.md` for the recommended nightly cadence
|
||||
+ trend-tracking workflow.
|
||||
- **Temporal axis follow-on (v0.35.3.1 + v0.35.7):** v0.35.3.1 added a
|
||||
six-member verdict enum (`no_contradiction | contradiction |
|
||||
temporal_supersession | temporal_regression | temporal_evolution |
|
||||
negation_artifact`) and threaded `pages.effective_date` into the judge
|
||||
prompt so the probe stops crying wolf on legitimate change-over-time.
|
||||
v0.35.7 lands the trajectory substrate the probe pointed at:
|
||||
`gbrain eval trajectory <entity>` shows the chronological typed-claim
|
||||
history with regressions flagged inline; `gbrain founder scorecard
|
||||
<entity>` rolls up four signals (accuracy, consistency, growth
|
||||
direction, red flags) into a stable JSON contract. MCP op
|
||||
`find_trajectory` (read scope, visibility-filtered for remote callers)
|
||||
exposes the same data to agents. The probe's `temporal_supersession`
|
||||
verdict and the consolidate phase's `valid_until` writeback both
|
||||
preserve the `auto-supersession.ts:4` "NEVER auto-applies" invariant
|
||||
— the probe still emits paste-ready commands, only `consolidate`
|
||||
writes `valid_until` (R1+R8 grep guard pins this).
|
||||
@@ -1,580 +0,0 @@
|
||||
# Embedder Shootout — May 2026 Eval Plan
|
||||
|
||||
**Status:** approved, ready to execute
|
||||
**Owner:** Garry
|
||||
**Plan source:** `~/.claude/plans/system-instruction-you-are-working-linear-origami.md` (review log)
|
||||
**Target wallclock:** ~2 weeks
|
||||
**Target API spend:** ~$525 (hard cap $700)
|
||||
|
||||
## What this is
|
||||
|
||||
A head-to-head A/B/C comparison of three embedding providers under v0.35.0.0's new
|
||||
multi-vendor gateway routing:
|
||||
|
||||
- **OpenAI** `text-embedding-3-large` @ 1536 dims
|
||||
- **Voyage** `voyage-4-large` @ 2048 dims
|
||||
- **ZeroEntropy** `zembed-1` @ 2560 dims (also 1280 in a Matryoshka ablation)
|
||||
|
||||
Each tested with and without the `zerank-2` reranker. Two corpora: public LongMemEval
|
||||
(500q) and BrainBench in-house (145 relational queries + 50 newly-curated Cat 13
|
||||
embedder-sensitive queries).
|
||||
|
||||
The goal: produce a publishable comparison report that answers "which embedder wins,
|
||||
and does zerank-2 carry the win for ZeroEntropy" with bootstrap p-values, suitable
|
||||
for a v0.35.2.0 release-note headline.
|
||||
|
||||
## Why this design
|
||||
|
||||
Locked decisions from the planning review (see plan file + `GSTACK REVIEW REPORT` at
|
||||
the bottom of the linked plan):
|
||||
|
||||
- **Synthetic-only** — LongMemEval (public) + BrainBench (in-house). No `~/.gbrain` data.
|
||||
- **Answer-gen mode** — `gbrain eval longmemeval` runs the default answer-gen path
|
||||
(Anthropic Sonnet), then feeds the resulting hypothesis JSONL to LongMemEval's
|
||||
published `evaluate_qa.py` (OpenAI gpt-4o judge) for real correctness numbers.
|
||||
`--retrieval-only` is NOT used (would produce an attackable headline; the judge
|
||||
expects answer text, not retrieval text).
|
||||
- **`tokenmax` search mode** pinned across all cells (expansion + reranker slot active).
|
||||
- **Serial execution** in one workspace. Clean rate-limit profile; first-contact run on
|
||||
ZE wants debuggable signal.
|
||||
- **7-cell matrix** (no matched-dim cross-vendor row — no shared dim exists across
|
||||
all three vendors; honest framing is "each vendor at marketed sweet spot").
|
||||
|
||||
## Architectural facts that constrain the plan
|
||||
|
||||
- `content_chunks.embedding vector(N)` dim is fixed per brain. Per-question PGLite in
|
||||
LongMemEval makes this free; BrainBench needs separate brain per cell.
|
||||
- pgvector HNSW caps at **2000 dims** (`PGVECTOR_HNSW_VECTOR_MAX_DIMS` in
|
||||
`src/core/vector-index.ts:19`). Voyage 2048 and ZE 2560 fall back to exact vector
|
||||
scan. Helps quality (no HNSW approximation) but adds latency. Footnoted in writeup.
|
||||
- Reranker disable key is **`search.reranker.enabled false`**, NOT `reranker_model none`.
|
||||
`tokenmax` mode defaults reranker=true.
|
||||
- `gbrain/ai/gateway` is NOT exported in v0.35.0.0. PR α exposes it.
|
||||
|
||||
## Matrix
|
||||
|
||||
| Cell | Embedder | Dim | HNSW | Reranker | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| A0 | `openai:text-embedding-3-large` | 1536 | yes | none | OpenAI baseline |
|
||||
| A1 | `openai:text-embedding-3-large` | 1536 | yes | `zerank-2` | mixed-vendor |
|
||||
| B0 | `voyage:voyage-4-large` | 2048 | no (exact) | none | Voyage solo |
|
||||
| B1 | `voyage:voyage-4-large` | 2048 | no (exact) | `zerank-2` | mixed-vendor |
|
||||
| C0 | `zeroentropyai:zembed-1` | 2560 | no (exact) | none | ZE embedder solo |
|
||||
| C1 | `zeroentropyai:zembed-1` | 2560 | no (exact) | `zerank-2` | **ZE full stack** |
|
||||
| C2 | `zeroentropyai:zembed-1` | 1280 | yes | `zerank-2` | ZE-Matryoshka ablation |
|
||||
|
||||
## PR structure — as few as possible
|
||||
|
||||
**PR α — gbrain repo: v0.35.1.0 infra.** All gbrain changes bundled. Lands first.
|
||||
Bisect-friendly commits inside, ship at the very end.
|
||||
|
||||
**PR β — gbrain-evals repo: adapter + smoke + curation + eval receipts + writeup.** The
|
||||
big one. Includes the full eval-run output committed alongside the code that produced
|
||||
it, plus the comparison writeup. Lands when everything is done.
|
||||
|
||||
**PR γ (optional) — gbrain repo: v0.35.2.0 release** that cross-links the gbrain-evals
|
||||
benchmark in CHANGELOG. Small commit; no code changes.
|
||||
|
||||
Total: 2 substantive PRs + 1 optional release commit. **No mid-stream ships.**
|
||||
|
||||
## Conductor sessions
|
||||
|
||||
Each section below is a self-contained brief. Copy-paste into a fresh Conductor session
|
||||
to hand off. Each session ends with a clean deliverable.
|
||||
|
||||
---
|
||||
|
||||
## Session 1 — PR α: gbrain infra (v0.35.1.0)
|
||||
|
||||
**Repo:** `/Users/garrytan/conductor/workspaces/gbrain/<NEW-WORKSPACE>` (fresh from `master`)
|
||||
**Branch:** `garrytan/v0.35.1.0-infra`
|
||||
**Wallclock:** ~2h
|
||||
**API spend:** $0
|
||||
|
||||
### What this session ships
|
||||
Three changes in one PR, bundled so the embedder shootout in gbrain-evals (PR β) has a
|
||||
clean prereq baseline:
|
||||
|
||||
1. Add `voyage:voyage-4-large` ($0.18/M) and `zeroentropyai:zembed-1` ($0.05/M) to the
|
||||
embedding pricing table. Patch the `gbrain models doctor` cost estimator + test.
|
||||
2. Expose `gbrain/ai/gateway` in `package.json` exports map so the gbrain-evals
|
||||
adapters can call `configureGateway({embedding_model, embedding_dimensions, reranker_model})`
|
||||
from outside the gbrain process.
|
||||
3. Add `--resume-from <jsonl>` to `gbrain eval longmemeval` so a mid-run abort
|
||||
(rate-limit, cost-cap, OS interrupt) doesn't lose the cells we already paid for.
|
||||
|
||||
Ships at the end as v0.35.1.0.
|
||||
|
||||
### Prereqs (verify before starting)
|
||||
- On gbrain master at v0.35.0.0 baseline. `cat VERSION` shows `0.35.0.0`.
|
||||
- `bun test` and `bun run verify` both pass on master.
|
||||
|
||||
### Commits (bisect-friendly, one feature per commit)
|
||||
|
||||
```
|
||||
1. feat(pricing): add voyage-4-large + zembed-1 to EMBEDDING_PRICING
|
||||
- src/core/embedding-pricing.ts: add both entries
|
||||
- test/embedding-pricing.test.ts: pin both with $0.18 and $0.05
|
||||
- Verify: bun test test/embedding-pricing.test.ts
|
||||
|
||||
2. feat(exports): expose gbrain/ai/gateway with canary test
|
||||
- package.json: add "./ai/gateway" to exports map
|
||||
- test/public-exports.test.ts: add canary for configureGateway + embed
|
||||
- scripts/check-exports-count.sh: 17 -> 18
|
||||
- Verify: bun run verify
|
||||
|
||||
3. feat(eval): add --resume-from <jsonl> to longmemeval
|
||||
- src/commands/eval-longmemeval.ts: parse flag, skip questions already in input JSONL
|
||||
- test/eval-longmemeval.test.ts: simulated mid-run abort + resume regression
|
||||
- Verify: bun test test/eval-longmemeval.test.ts
|
||||
|
||||
4. chore: v0.35.1.0
|
||||
- VERSION: 0.35.1.0
|
||||
- package.json: 0.35.1.0
|
||||
- CHANGELOG.md: new entry
|
||||
- bun install (refresh lockfile)
|
||||
```
|
||||
|
||||
### Verify before /ship
|
||||
```bash
|
||||
bun run typecheck
|
||||
bun run verify
|
||||
bun test test/embedding-pricing.test.ts test/public-exports.test.ts test/eval-longmemeval.test.ts
|
||||
```
|
||||
|
||||
### Ship
|
||||
```bash
|
||||
/ship
|
||||
```
|
||||
|
||||
### Deliverable
|
||||
- `master` of gbrain at v0.35.1.0
|
||||
- `gbrain/ai/gateway` reachable from external consumers (verified by canary test)
|
||||
- `git tag eval-run-v0.35.1.0-baseline` (annotated, names this exact commit)
|
||||
- `gbrain --version` prints `0.35.1.0`
|
||||
|
||||
### Hand-off to Session 2
|
||||
- gbrain-evals can now `bun update gbrain` to v0.35.1.0
|
||||
- The tag preserves the exact commit for any future reproducibility need
|
||||
|
||||
---
|
||||
|
||||
## Session 2 — PR β setup: gbrain-evals adapter + smoke + subset flag
|
||||
|
||||
**Repo:** `/Users/garrytan/git/gbrain-evals` (or a fresh Conductor workspace cloned from it)
|
||||
**Branch:** `garrytan/embedder-shootout`
|
||||
**Wallclock:** ~3-4h
|
||||
**API spend:** ~$0.10 (smoke verification calls only)
|
||||
|
||||
### What this session ships into PR β (does NOT merge yet)
|
||||
Wire the harness to drive 3 embedding providers via the newly-exposed gbrain gateway:
|
||||
|
||||
1. New typed `EvalAdapterConfig {embedder, dim, reranker?}` passed into each adapter.
|
||||
2. Rewrite `vector.ts` + `hybrid-rrf.ts` to call `configureGateway()` from
|
||||
`gbrain/ai/gateway` instead of the hardcoded `gbrain/embedding` import.
|
||||
3. Critical: hybrid adapter must also route `search.reranker.enabled` (true/false) and
|
||||
`search.mode` (tokenmax) — codex flagged that the existing hybrid never sets these.
|
||||
4. New 3-phase smoke harness: wiring (5 queries × embed roundtrip + dim check) +
|
||||
long-haystack (1 query × 50K-token synthetic haystack) + rerank-payload (1 query
|
||||
× `topNIn=30`). Exit code is the gate.
|
||||
5. New `--include-subset <name>` flag on the BrainBench runner (Cat 13 wiring; subset
|
||||
itself comes in Session 3).
|
||||
|
||||
### Prereqs
|
||||
- Session 1 done. gbrain master at v0.35.1.0.
|
||||
- API keys present: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `VOYAGE_API_KEY`,
|
||||
`ZEROENTROPY_API_KEY`. Smoke fails-loud on missing key.
|
||||
|
||||
### Commits
|
||||
|
||||
```
|
||||
1. chore(deps): bump gbrain pin to v0.35.1.0
|
||||
- package.json + bun.lock
|
||||
- Verify: bun install && bun run typecheck
|
||||
|
||||
2. feat(adapter): typed EvalAdapterConfig + gateway swap
|
||||
- NEW: eval/runner/eval-adapter-config.ts (the type)
|
||||
- eval/runner/adapters/vector.ts: constructor takes EvalAdapterConfig,
|
||||
calls configureGateway({embedding_model, embedding_dimensions})
|
||||
- Drop hardcoded gbrain/embedding import
|
||||
- Verify: existing vector adapter unit tests still pass
|
||||
|
||||
3. feat(adapter): hybrid-rrf wires reranker_enabled + search.mode
|
||||
- eval/runner/adapters/hybrid-rrf.ts: constructor takes EvalAdapterConfig,
|
||||
plumbs search.reranker.enabled + search.mode = tokenmax through
|
||||
- Verify: bun test eval/
|
||||
|
||||
4. feat(smoke): 3-phase smoke harness
|
||||
- NEW: eval/runner/smoke.ts (CLI entry: bun run eval:smoke -- --embedder X --dim Y [--reranker Z])
|
||||
- Phase 1: 5 queries × embed roundtrip, assert vector dim matches config
|
||||
- Phase 2: 1 query × synthetic 50K-token haystack, assert no token-limit error
|
||||
- Phase 3: 1 query × topNIn=30 documents, assert no 5MB payload cap hit
|
||||
- Non-zero exit on any failure
|
||||
- Verify: bun run eval:smoke -- --embedder openai:text-embedding-3-large --dim 1536
|
||||
|
||||
5. feat(runner): --include-subset flag for BrainBench
|
||||
- eval/runner/multi-adapter.ts: parse flag, filter queries by subset tag
|
||||
- Subset itself comes in next commit (Session 3)
|
||||
- Verify: bun run eval:run -- --include-subset cat13-embedder (errors politely because subset file doesn't exist yet)
|
||||
```
|
||||
|
||||
### Smoke verification (run manually before opening PR)
|
||||
```bash
|
||||
bun run eval:smoke -- --embedder openai:text-embedding-3-large --dim 1536
|
||||
bun run eval:smoke -- --embedder voyage:voyage-4-large --dim 2048
|
||||
bun run eval:smoke -- --embedder zeroentropyai:zembed-1 --dim 2560
|
||||
bun run eval:smoke -- --embedder zeroentropyai:zembed-1 --dim 2560 --reranker zeroentropyai:zerank-2
|
||||
```
|
||||
|
||||
All four MUST exit 0. Reports should print the observed vector dim, matching the
|
||||
configured dim.
|
||||
|
||||
### Open PR β
|
||||
```bash
|
||||
gh pr create --base main --title "feat: embedder shootout (adapter + smoke + Cat 13 + eval receipts)" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
v0.35.0.0 shipped ZeroEntropy zembed-1 + zerank-2 reranker support. This PR runs a head-to-head A/B/C comparison across OpenAI, Voyage, and ZeroEntropy under the new gateway routing.
|
||||
|
||||
This first commit batch lands the harness. Cat 13 curation, Phase 1+2 evals, and the
|
||||
writeup follow in subsequent commits to this same PR.
|
||||
|
||||
## Test plan
|
||||
- [x] Adapter unit tests pass
|
||||
- [x] Smoke harness exits 0 against all 3 providers
|
||||
- [ ] Cat 13 subset committed (Session 3)
|
||||
- [ ] LongMemEval x 7 cells run (Session 4)
|
||||
- [ ] BrainBench x 7 cells run (Session 5)
|
||||
- [ ] Writeup committed (Session 5)
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### Deliverable
|
||||
- PR β open against gbrain-evals `main`, green CI
|
||||
- Smoke verified against all 3 providers (paste the smoke output in the PR body)
|
||||
- Branch ready for Session 3 (Cat 13 curation)
|
||||
|
||||
### Hand-off to Session 3
|
||||
- Branch `garrytan/embedder-shootout` exists on origin
|
||||
- The `--include-subset cat13-embedder` flag is wired but the subset file doesn't exist
|
||||
yet — that's Session 3
|
||||
|
||||
---
|
||||
|
||||
## Session 3 — PR β: Cat 13 conceptual-recall curation
|
||||
|
||||
**Repo:** `/Users/garrytan/git/gbrain-evals`, branch `garrytan/embedder-shootout` (same as Session 2)
|
||||
**Wallclock:** ~3-4h (heavily user-interactive; AI proposes, you review each)
|
||||
**API spend:** $0
|
||||
|
||||
### What this session ships into PR β
|
||||
Hand-curated 50 embedder-sensitive queries from BrainBench's Cat 13 (conceptual recall)
|
||||
corpus. These are the queries where a graph/keyword adapter would likely miss but a
|
||||
semantic adapter would find.
|
||||
|
||||
Codex flagged the existing 145-query relational corpus as graph/keyword-dominated and
|
||||
weak for embedder claims. Cat 13 is closer to the embedder-sensitive workload but
|
||||
needs hand-selection.
|
||||
|
||||
### Prereqs
|
||||
- Session 2 done. PR β open with adapter + smoke + subset flag.
|
||||
|
||||
### Workflow
|
||||
Interactive: Claude proposes queries in batches of 10, you accept/reject/edit each.
|
||||
|
||||
1. Claude reads the existing Cat 13 raw query pool:
|
||||
```bash
|
||||
ls eval/data/raw/ | grep -i cat13
|
||||
cat eval/data/raw/cat13-*.json | jq '.'
|
||||
```
|
||||
2. Claude proposes 10 candidate queries per batch, each tagged with the inclusion
|
||||
reasoning ("would a graph adapter miss this?")
|
||||
3. User accepts/rejects/edits inline. Target: 50 queries × ~5 batches.
|
||||
4. Claude commits to `eval/data/gold/brainbench-cat13-embedder-subset.json`:
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"subset": "cat13-embedder",
|
||||
"queries": [
|
||||
{
|
||||
"id": "cat13-emb-001",
|
||||
"query": "...",
|
||||
"relevant_chunk_ids": ["..."],
|
||||
"inclusion_reason": "paraphrase relationship; graph adapter wouldn't catch the synonym"
|
||||
}
|
||||
// ... 49 more
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Commit
|
||||
|
||||
```
|
||||
feat(eval): curate Cat 13 conceptual-recall subset (50 embedder-sensitive queries)
|
||||
- NEW: eval/data/gold/brainbench-cat13-embedder-subset.json
|
||||
- Each query tagged with inclusion_reason for future audit
|
||||
```
|
||||
|
||||
### Spot-check before commit
|
||||
- Pick 5 random queries, run them against a hypothetical graph adapter (e.g. grep on
|
||||
the relevant terms) and verify they would NOT surface the right chunk.
|
||||
- Run the same 5 against the existing hybrid adapter and verify they DO.
|
||||
|
||||
### Deliverable
|
||||
- `eval/data/gold/brainbench-cat13-embedder-subset.json` committed to PR β
|
||||
- Exactly 50 queries
|
||||
- Spot-check evidence in the commit message
|
||||
|
||||
### Hand-off to Session 4
|
||||
- PR β now has: adapter + smoke + Cat 13 subset
|
||||
- Ready for the actual eval runs
|
||||
|
||||
---
|
||||
|
||||
## Session 4 — PR β Phase 1: LongMemEval × 7 cells (overnight)
|
||||
|
||||
**Repo:** Same gbrain-evals branch
|
||||
**Wallclock:** ~10.5h (mostly hands-off, kick off and walk away)
|
||||
**API spend:** ~$476 (LongMemEval-heavy; 7 × $68/cell)
|
||||
|
||||
### What this session ships into PR β
|
||||
7 LongMemEval scored receipts (one per matrix cell). Each is a JSONL of 500
|
||||
hypotheses + a JSON file of correctness scores from `evaluate_qa.py`.
|
||||
|
||||
### Prereqs
|
||||
- Sessions 1+2+3 done. PR β has adapter + smoke + Cat 13.
|
||||
- LongMemEval dataset downloaded (gated HuggingFace; one-time setup).
|
||||
- `evaluate_qa.py` checked out somewhere (from
|
||||
https://github.com/xiaowu0162/LongMemEval) with its own venv set up.
|
||||
- API keys: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `VOYAGE_API_KEY`,
|
||||
`ZEROENTROPY_API_KEY`.
|
||||
|
||||
### Wrapper script
|
||||
Claude writes `scripts/run-shootout-phase1.sh` in the gbrain-evals branch. Single
|
||||
entry point that loops the 7 cells serially with smoke gating + cost-cap aborts.
|
||||
|
||||
```
|
||||
NEW: scripts/run-shootout-phase1.sh
|
||||
- Per cell: gbrain config set (embedder, dim, reranker, search.reranker.enabled, search.mode=tokenmax)
|
||||
- Per cell: bun run eval:smoke (abort cell on non-zero)
|
||||
- Per cell: gbrain eval longmemeval ... --output results/longmemeval-{cell}.jsonl
|
||||
- Per cell: cost-cap check ($90/cell hard stop)
|
||||
- Per cell: --resume-from existing results/longmemeval-{cell}.jsonl if present
|
||||
- Logs to results/phase1-run-log.txt
|
||||
```
|
||||
|
||||
### Run
|
||||
```bash
|
||||
# Kick off in background; check back in 10-12h
|
||||
bash scripts/run-shootout-phase1.sh 2>&1 | tee results/phase1-run-log.txt &
|
||||
```
|
||||
|
||||
Use `run_in_background: true` if running through Claude. Check back periodically.
|
||||
|
||||
### Scoring (after all 7 cells done)
|
||||
```bash
|
||||
for cell in A0 A1 B0 B1 C0 C1 C2; do
|
||||
python evaluate_qa.py \
|
||||
--input results/longmemeval-${cell}.jsonl \
|
||||
--output results/longmemeval-${cell}-scored.json
|
||||
done
|
||||
```
|
||||
|
||||
Each scored file has correctness %.
|
||||
|
||||
### Commits
|
||||
|
||||
```
|
||||
1. feat(scripts): Phase 1 LongMemEval wrapper with smoke gating + cost cap
|
||||
- NEW: scripts/run-shootout-phase1.sh
|
||||
|
||||
2. data(phase1): 7 LongMemEval cells (raw hypothesis JSONL)
|
||||
- results/longmemeval-{A0,A1,B0,B1,C0,C1,C2}.jsonl
|
||||
- results/phase1-run-log.txt (run timing + cost ledger)
|
||||
|
||||
3. data(phase1): evaluate_qa.py scoring results
|
||||
- results/longmemeval-{cell}-scored.json × 7
|
||||
```
|
||||
|
||||
### Verify
|
||||
- Each `longmemeval-{cell}.jsonl` has exactly 500 lines
|
||||
- Each `hypothesis` field is non-empty AND is actual answer text (NOT retrieval text)
|
||||
- Each `scored.json` has a `correctness_score` field
|
||||
|
||||
### Deliverable
|
||||
- 7 scored LongMemEval receipts committed to PR β
|
||||
- Real cost ledger committed alongside (compare against estimate)
|
||||
|
||||
### Hand-off to Session 5
|
||||
- Phase 1 done. Phase 2 (BrainBench, ~3.5h) and writeup remaining.
|
||||
|
||||
---
|
||||
|
||||
## Session 5 — PR β Phase 2 + writeup + ship
|
||||
|
||||
**Repo:** Same gbrain-evals branch
|
||||
**Wallclock:** ~7h (3.5h BrainBench + 3h writeup + /ship)
|
||||
**API spend:** ~$56 (BrainBench is cheap)
|
||||
|
||||
### What this session ships into PR β
|
||||
- 7 BrainBench cells (relational corpus + Cat 13 subset)
|
||||
- Final comparison writeup
|
||||
- PR β merged
|
||||
|
||||
### Prereqs
|
||||
- Session 4 done. PR β has Phase 1 receipts.
|
||||
|
||||
### Phase 2 wrapper script
|
||||
```
|
||||
NEW: scripts/run-shootout-phase2.sh
|
||||
- Per cell: configure provider (same as Phase 1)
|
||||
- Per cell: bun run eval:run -- --N 10 --include-subset cat13-embedder
|
||||
--output docs/benchmarks/2026-05-22-{cell}.md
|
||||
- Cost-cap check
|
||||
```
|
||||
|
||||
### Run
|
||||
```bash
|
||||
bash scripts/run-shootout-phase2.sh 2>&1 | tee results/phase2-run-log.txt
|
||||
```
|
||||
|
||||
### Writeup
|
||||
`docs/benchmarks/2026-05-22-embedder-shootout.md`. Structure:
|
||||
|
||||
1. **Headline table** — 7 cells × {LongMemEval correctness %, BrainBench relational MRR + P@5, Cat 13 correctness %, total cost}
|
||||
2. **Two questions answered:**
|
||||
- Which embedder wins solo? (A0 vs B0 vs C0)
|
||||
- Does zerank-2 carry ZE's win? (C0 vs C1 vs A1 vs B1)
|
||||
- Bonus: does dim matter for ZE? (C1 vs C2)
|
||||
3. **Paired-bootstrap p-values** per headline pair (methodology in
|
||||
`gbrain/docs/eval/SEARCH_MODE_METHODOLOGY.md`)
|
||||
4. **HNSW footnote** — Voyage 2048 and ZE 2560 used exact vector scan; OpenAI 1536
|
||||
and ZE 1280 used HNSW. Quality is primary, latency is secondary
|
||||
5. **What this does NOT prove** — synthetic-only, tokenmax-only, no real-brain replay
|
||||
6. **Recommendation:** explicit NON-recommendation to change `gbrain init` default;
|
||||
defer to a v0.36.x evidence pass with real-brain replay data
|
||||
|
||||
### Commits
|
||||
|
||||
```
|
||||
1. feat(scripts): Phase 2 BrainBench wrapper
|
||||
- NEW: scripts/run-shootout-phase2.sh
|
||||
|
||||
2. data(phase2): 7 BrainBench cells
|
||||
- docs/benchmarks/2026-05-22-{cell}.md × 7
|
||||
|
||||
3. docs(benchmark): embedder shootout comparison writeup
|
||||
- NEW: docs/benchmarks/2026-05-22-embedder-shootout.md
|
||||
- Bootstrap p-values, HNSW footnote, NOT-in-scope section
|
||||
```
|
||||
|
||||
### Ship
|
||||
```bash
|
||||
# Merge PR β to gbrain-evals main
|
||||
gh pr merge --squash --auto
|
||||
# Or non-auto if reviewing one more time:
|
||||
gh pr merge --squash
|
||||
```
|
||||
|
||||
### Deliverable
|
||||
- PR β merged to gbrain-evals `main`
|
||||
- Comparison report public at
|
||||
`gbrain-evals/docs/benchmarks/2026-05-22-embedder-shootout.md`
|
||||
|
||||
### Hand-off to Session 6 (optional)
|
||||
- gbrain-evals master has the full data + writeup
|
||||
- Ready for a v0.35.2.0 gbrain release that cross-links it
|
||||
|
||||
---
|
||||
|
||||
## Session 6 (optional) — PR γ: gbrain v0.35.2.0 release
|
||||
|
||||
**Repo:** `/Users/garrytan/conductor/workspaces/gbrain/<NEW-WORKSPACE>` (fresh from master)
|
||||
**Branch:** `garrytan/v0.35.2.0-benchmark-release`
|
||||
**Wallclock:** ~30min
|
||||
**API spend:** $0
|
||||
|
||||
### What this session ships
|
||||
A release-notes-only PR that bumps gbrain to v0.35.2.0 with a CHANGELOG entry
|
||||
cross-linking the embedder shootout benchmark. Optional — could be folded into the
|
||||
next routine release if no rush.
|
||||
|
||||
### Prereqs
|
||||
- Session 5 done. gbrain-evals merged with the comparison writeup.
|
||||
|
||||
### Commits
|
||||
|
||||
```
|
||||
1. docs(benchmark): mirror embedder shootout summary
|
||||
- NEW: docs/benchmarks/2026-05-22-embedder-shootout.md (slim mirror)
|
||||
- Cross-link to gbrain-evals canonical version
|
||||
|
||||
2. chore: v0.35.2.0
|
||||
- VERSION: 0.35.2.0
|
||||
- package.json: 0.35.2.0
|
||||
- CHANGELOG.md: new entry with the GStack-voice release summary
|
||||
+ "numbers that matter" table from the benchmark
|
||||
```
|
||||
|
||||
### Ship
|
||||
```bash
|
||||
/ship
|
||||
```
|
||||
|
||||
### Deliverable
|
||||
- gbrain v0.35.2.0 on master
|
||||
- CHANGELOG entry that drives the release-note headline
|
||||
|
||||
---
|
||||
|
||||
## Cost ledger (revised, post-review)
|
||||
|
||||
| Component | Per cell | × 7 cells |
|
||||
|---|---|---|
|
||||
| LongMemEval embed | <$0.05 | <$0.35 |
|
||||
| LongMemEval Sonnet answer-gen (500q × 2K tokens × $3/M) | $18 | $126 |
|
||||
| LongMemEval gpt-4o judge (500q × $0.10/q) | $50 | $350 |
|
||||
| BrainBench relational embed | $0.05-0.18 | <$1 |
|
||||
| BrainBench Cat 13 answer-gen + judge (50q × $0.14) | $7 | $49 |
|
||||
| Smoke harness (30 calls/cell) | <$0.10 | <$1 |
|
||||
| **Total** | **~$75/cell** | **~$525** |
|
||||
|
||||
**Hard cap: $700.** Per-cell hard cap: $90 (wrapper aborts cell if exceeded; partial
|
||||
JSONL preserved for resume).
|
||||
|
||||
## Failure modes and recovery
|
||||
|
||||
| Failure | Recovery |
|
||||
|---|---|
|
||||
| Voyage/ZE 429 rate-limit mid-cell | `gateway._shrinkState` halves safety_factor and retries. Cell continues. |
|
||||
| ZE 5MB rerank payload cap hit | `applyReranker` fail-opens, returns un-reranked results. Stderr warn. |
|
||||
| Mid-cell OS interrupt / cost-cap abort | Re-run with `gbrain eval longmemeval --resume-from results/longmemeval-{cell}.jsonl`. Picks up where it left off. |
|
||||
| `evaluate_qa.py` auth fail | OPENAI_API_KEY check in wrapper aborts before any spend. |
|
||||
| Adapter typo (bad dim) | `EvalAdapterConfig` runtime assertion at constructor throws AIConfigError. Cell aborts before API call. |
|
||||
|
||||
## NOT in scope (deliberate)
|
||||
|
||||
- **Real `~/.gbrain` replay** — adds 6-12h wallclock + $40-80 embed. Filed as v0.36.x.
|
||||
- **All 3 search modes** — pinned to tokenmax. `conservative` + `balanced` are v0.35.3.0
|
||||
follow-ups if reviewers push back.
|
||||
- **Matched-dim cross-vendor row** — no shared dim exists across all 3 vendors.
|
||||
Permanently out.
|
||||
- **`gbrain eval whoknows` / `cross-modal` / `takes-quality`** — embedding-invariant;
|
||||
rerunning across embedders produces noise.
|
||||
- **`gbrain eval code-retrieval`** — code corpus, separate concern.
|
||||
- **`gbrain eval suspected-contradictions`** — wants a real brain.
|
||||
- **`gbrain init --recommended` default change** — codex correctly flagged the evidence
|
||||
base as insufficient. Defer to v0.36.x with real-brain replay data.
|
||||
|
||||
## What already exists (reused, not rebuilt)
|
||||
|
||||
- `gbrain eval longmemeval` CLI (in-tree, answer-gen mode default)
|
||||
- gbrain-evals BrainBench runner (`eval:run`) — needs adapter parameterization but
|
||||
per-cell test plumbing is reused
|
||||
- Gateway routing for Voyage + ZE (shipped v0.35.0.0)
|
||||
- Reranker pipeline (`src/core/search/rerank.ts`, fail-open)
|
||||
- Pricing table (extended, not rebuilt)
|
||||
- Paired-bootstrap methodology (`docs/eval/SEARCH_MODE_METHODOLOGY.md`)
|
||||
- LongMemEval published `evaluate_qa.py` (invoked externally, not bundled)
|
||||
@@ -1,162 +0,0 @@
|
||||
# Code Cathedral II — v0.20.0 Design
|
||||
|
||||
**Status:** Accepted. CEO + Eng + 2 codex passes CLEARED (2026-04-24). 16 cross-model findings absorbed total: 7 codex pass 1 (structural prereqs) + 6 codex pass 2 (absorption errors including the CHUNKER_VERSION silent-no-op gate and inbound-edge invalidation) + 3 eng-review architectural decisions. DX review recommended post-Layer 8 (new CLI surfaces) before ship.
|
||||
**Supersedes:** Cathedral I (planned v0.18.0–v0.19.0 code indexing, shipped v0.19.0).
|
||||
**Mode:** SCOPE EXPANSION (user explicit: "I want the best code search in the world").
|
||||
**Scale:** 14 bisectable layers, ~20–25 CC hours, 3–5 human-weeks. One schema migration with split edge tables (`code_edges_chunk` + `code_edges_symbol`). Backfill via `CHUNKER_VERSION` bump (automatic on next sync) + explicit `gbrain reindex-code` command.
|
||||
|
||||
## Why v0.20.0
|
||||
|
||||
v0.19.0 shipped code indexing: tree-sitter chunker, 29 active languages, symbol columns, forward doc↔impl linking, incremental embed cache, BrainBench code category. Four cathedral-I items got deferred during shipping: `query --lang` filter, `sync --all` cost preview, markdown fence extraction, reverse-scan doc↔impl backfill.
|
||||
|
||||
Cathedral II is a promise-keeping release for those four, bundled with the leap that makes gbrain *the* code search: structural edges (call graph + references + imports + inheritance), parent-scope capture, doc-comment FTS binding, and two-pass retrieval. No more grep-class retrieval on code.
|
||||
|
||||
## The 10x leap
|
||||
|
||||
Today: agent asks "how does hybrid search handle N+1?" → gets 3 prose chunks of `hybrid.ts`.
|
||||
|
||||
Cathedral II: same query returns the anchor function + its 3 callers + its 2 callees + its JSDoc + the guide in `/docs` that cites it + the test file exercising it + parent scope chain. One walk. Code-aware brain.
|
||||
|
||||
## Scope (5 tiers + Layer 0 prerequisites, 14 bisectable layer commits)
|
||||
|
||||
### Tier 0 — Prerequisites (surfaced by codex outside voice)
|
||||
|
||||
**0a. File-classification widening.** `sync.ts:35` currently classifies only 9 extensions as code (TS, JS, Python, Go, Rust, Ruby, Java, C, C++). Cathedral II's B1 ships 165 lazy-loadable grammars, so the classifier needs to accept any extension the chunker can handle. Also reorders `detectCodeLanguage` so Magika (B2) runs as a fallback for extension-less files, not after a null-return gate.
|
||||
|
||||
**0b. Chunk-grain FTS.** Current keyword search lives on `pages.search_vector`. Adding doc-comments or two-pass anchoring at the chunk level has zero ranking effect against a page-grain primitive. Layer 0b adds `content_chunks.search_vector` with a trigger building from qualified symbol name + doc-comment (weight A) and chunk_text (weight B), plus rewrites `searchKeyword` to rank chunks directly. Page-level search_vector stays for title-heavy searches.
|
||||
|
||||
Both Layer 0 items are prerequisites for the 10x leap to actually move retrieval metrics.
|
||||
|
||||
### Tier A — Structural edges (the 10x leap)
|
||||
|
||||
**A1. Call-graph + reference extraction with qualified symbol identity.** Per-language tree-sitter queries at `importCodeFile` time capture:
|
||||
|
||||
- `calls` — function call-sites
|
||||
- `imports` — module deps
|
||||
- `extends` / `implements` — type hierarchies
|
||||
- `mixes_in` — Ruby `include`/`extend`/`prepend`
|
||||
- `type_refs` — parameter + return type usage
|
||||
- `declares` — chunk owns a symbol definition
|
||||
|
||||
**Qualified symbol identity across all 8 langs.** `parent_symbol_path` (A3) is the source of truth for scope; edges use qualified names built from it. Examples: `Admin::UsersController#render` (Ruby instance), `Admin::UsersController.find_all` (Ruby singleton), `admin.users_controller.UsersController.render` (Python), `(*UsersController).Render` (Go), `users::UsersController::render` (Rust), `com.acme.admin.UsersController.render` (Java). Per-lang delimiter + method/class-method distinction. Ruby ships fully in ranker (CLI + A2 two-pass) — no deferral.
|
||||
|
||||
**Split schema (two tables, not one polymorphic):**
|
||||
```sql
|
||||
CREATE TABLE code_edges_chunk (
|
||||
from_chunk_id INTEGER NOT NULL REFERENCES content_chunks(id) ON DELETE CASCADE,
|
||||
to_chunk_id INTEGER NOT NULL REFERENCES content_chunks(id) ON DELETE CASCADE,
|
||||
from_symbol_qualified TEXT NOT NULL,
|
||||
to_symbol_qualified TEXT NOT NULL,
|
||||
edge_type TEXT NOT NULL,
|
||||
source_id TEXT REFERENCES sources(id) ON DELETE CASCADE,
|
||||
UNIQUE (from_chunk_id, to_chunk_id, edge_type)
|
||||
);
|
||||
CREATE TABLE code_edges_symbol (
|
||||
from_chunk_id INTEGER NOT NULL REFERENCES content_chunks(id) ON DELETE CASCADE,
|
||||
from_symbol_qualified TEXT NOT NULL,
|
||||
to_symbol_qualified TEXT NOT NULL,
|
||||
edge_type TEXT NOT NULL,
|
||||
source_id TEXT REFERENCES sources(id) ON DELETE CASCADE,
|
||||
UNIQUE (from_chunk_id, to_symbol_qualified, edge_type)
|
||||
);
|
||||
```
|
||||
`code_edges_chunk` = resolved (both endpoints known). `code_edges_symbol` = unresolved (target symbol exists by qualified name, definition chunk not yet seen). Promotion from symbol→chunk table happens on later import. `source_id` is TEXT matching actual `sources.id` type.
|
||||
|
||||
**Shipped languages:** TypeScript, TSX, JavaScript, Ruby, Python, Go, Rust, Java (8 langs, ~85% of real brain code). Other languages chunk normally (via B1 lazy-load) but don't emit edges in v0.20.0 — extension is one query file + delimiter config per language, shippable as small follow-up PRs.
|
||||
|
||||
**A2. Two-pass retrieval.** Current: keyword + vector → RRF → dedup. New: keyword + vector → anchor set → expand 1–2 hops on `code_edges_chunk` with structural-distance decay → blend into RRF.
|
||||
|
||||
**Default OFF in all cases.** Opt-in only via `--walk-depth N` or `--near-symbol <name>`. Exact-symbol-match auto-on was unsafe (symbol names collide across files). Neighbor cap 50 per hop, depth cap 2. Dedup's per-page cap (currently 2) lifts to `min(10, walkDepth × 5)` when walking so structural neighbors from one file aren't clipped. Distance decay: `1/(1 + hop)` on expanded-neighbor RRF contributions.
|
||||
|
||||
**A3. Parent-scope capture + nested-chunk emission.** Two parts:
|
||||
|
||||
*Part 1:* Nested symbols get `parent_symbol_path text[]` on `content_chunks`. Embedded into chunk header: `[TypeScript] src/foo.ts:42-58 function formatResult (in BrainEngine.searchKeyword)`. Scope flows into embedding. Dual-use: drives A1's qualified symbol identity.
|
||||
|
||||
*Part 2:* Extend `splitLargeNode` to emit nested functions/methods/inner-classes as their own chunks. The current chunker is top-level-node oriented — a `class Foo { method1() {} method2() {} }` emits one chunk. Parent_symbol_path on top-level nodes is empty (no parent above top level), so A3 contributes nothing without sub-top-level chunks. Part 2 makes the scope annotation load-bearing.
|
||||
|
||||
**A4. Doc-comment → symbol binding.** Leading AST comment extracted to `doc_comment text`. Lands on **chunk-grain** search_vector (Layer 0b prerequisite) with FTS weight `'A'`. Natural-language queries rank docstring matches above body text and below title. `'A' > 'B' > 'C' > 'D'` per Postgres FTS weight convention.
|
||||
|
||||
### Tier B — Coverage (honest Chonkie parity)
|
||||
|
||||
**B1.** Lazy-load tree-sitter-language-pack (~165 languages). Replace 36 committed WASMs with a manifest + per-process parser cache. Cathedral I promised this and didn't deliver — Cathedral II does.
|
||||
|
||||
**B2.** Magika auto-detect for extension-less files (Dockerfile, Makefile, `.envrc`). ~1MB bundled asset. Falls back to null → recursive chunker if classifier fails to load.
|
||||
|
||||
### Tier C — Agent CLI surfaces
|
||||
|
||||
- `query --lang <lang>` — filter by `content_chunks.language`
|
||||
- `query --symbol-kind function|class|method|type|interface|enum` — filter by `symbol_type`
|
||||
- `query --near-symbol <name> --depth 1..2` — two-pass retrieval anchored at a known symbol
|
||||
- `code-callers <symbol>` — uses A1 `calls` edges, reversed
|
||||
- `code-callees <symbol>` — uses A1 `calls` edges, forward
|
||||
|
||||
All auto-JSON on non-TTY. `StructuredAgentError` envelopes on failure. `code-signature` deferred to v0.20.1 (needs per-language type captures).
|
||||
|
||||
### Tier D — Bridge items (cathedral I promises)
|
||||
|
||||
**D1.** `sync --all` cost preview. `estimateTokens` extracted from `chunkers/code.ts` to new `tokens.ts` module. Before per-source loop: walk sync-diff set, sum tokens, compute $ estimate. TTY + !json + !yes → interactive `[y/N]`. Non-TTY or `--json` or piped → emit `ConfirmationRequired` envelope, exit 2. `--yes` skips. `--dry-run` previews + exit 0. Preview on `--all` only, not single-source (DX review pain is first-time large-sync surprise bills).
|
||||
|
||||
**D2.** Markdown fence extraction in `importFromContent`. After `parseMarkdown`, iterate marked lexer tokens for `{type:'code', lang, text}`. Map fence tag → language. Chunk each fence through `chunkCodeText`. Persist as `chunk_source='fenced_code'`. Cap 100 fences per markdown page (DOS defense). Per-fence try/catch — one bad fence doesn't break the page import.
|
||||
|
||||
**D3.** `reconcile-links` batch command. Walks markdown pages, calls existing v0.19.0 `extractCodeRefs` per page, emits `addLink(md, code, ..., 'documents')` + reverse. `ON CONFLICT DO NOTHING` handles idempotency. Statement-timeout scoped via `sql.begin` + `SET LOCAL`. Progress reporter + final summary (edges added / existed / missing-target). Respects `auto_link` config.
|
||||
|
||||
### Tier E — Eval, backfill, honesty
|
||||
|
||||
**E1.** BrainBench code sub-categories: `call_graph_recall` (callers of X → expected set), `parent_scope_coverage` (nested-symbol queries return correct scope), `doc_comment_matching` (NL queries rank doc-comments above prose). Regression gates against A1/A3/A4 drift.
|
||||
|
||||
**E2.** Backfill: schema migrates automatically (zero cost). **`CHUNKER_VERSION` bumps 3 → 4** — that constant is folded into each code page's `content_hash`, so every code page's hash changes on upgrade. Next `gbrain sync` won't short-circuit on "git HEAD unchanged"; it re-chunks every code file. New `gbrain reindex-code [--source <id>] [--dry-run] [--yes] [--force]` provides explicit full backfill with cost preview (reuses D1 infra) and `--force` bypasses content_hash skip entirely. Users control when to pay; silent no-op path closed.
|
||||
|
||||
**E3.** Honest CHANGELOG. Retire "Chonkie superset" framing. Run BrainBench before/after for real numbers: 150+ languages loaded (after B1), MRR on NL→code queries, P@1 call-graph precision, P@k on symbol_name queries, sync cost preview on 5K-file repo. Back every claim with a runnable command.
|
||||
|
||||
## Implementation ordering (14 layers, post-codex)
|
||||
|
||||
1. **0a** — File-classification widening (sync.ts:35) + Magika reordered as fallback
|
||||
2. **0b** — Chunk-grain FTS (content_chunks.search_vector + trigger + searchKeyword chunk-level rewrite)
|
||||
3. **Foundation** — schema migration (split edge tables, qualified name columns on content_chunks) + engine method stubs + types
|
||||
4. **B1** — lazy-load grammar manifest + bun --compile guard
|
||||
5. **A1** — edge-extractor + 8 per-lang query files + qualified symbol identity + tests
|
||||
6. **A3** — parent-scope column + doc-comment column + splitLargeNode nested-chunk emission
|
||||
7. **A4** — doc-comment FTS weight A on chunk-grain search_vector
|
||||
8. **A2** — two-pass retrieval, default OFF, opt-in only; dedup cap lifts when walking
|
||||
9. **D tier bundled** — cost preview + fence extraction + reconcile-links
|
||||
10. **B2** — Magika auto-detect
|
||||
11. **C tier** — 5 CLI surfaces
|
||||
12. **E1** — BrainBench sub-categories + CHUNKER_VERSION 3→4 bump
|
||||
13. **E2** — `reindex-code` with `--force` + migration orchestrator with backfill-prompt phase
|
||||
14. **E3 + release** — honest CHANGELOG + docs + migration skill + `/ship`
|
||||
|
||||
## Size and cost
|
||||
|
||||
- Diff: ~5500–6500 lines (~2.5x v0.19.0 post-codex expansion)
|
||||
- Tests: ~2000 lines (8 langs × qualified-name + edge-extraction fixtures + Layer 0b FTS migration tests)
|
||||
- Files: ~36 new, ~25 modified
|
||||
- CC time: ~20–25 hours focused (was 14–18 pre-codex; +6h for Layer 0a/0b + qualified identity across 8 langs + nested-chunk emission + CHUNKER_VERSION bump layer)
|
||||
- Human-equivalent: 3–5 weeks
|
||||
- First-sync cost bump for upgraded v0.19.0 users: every code page re-chunks on first sync after upgrade (CHUNKER_VERSION bump forces invalidation). Users run `gbrain reindex-code --dry-run` for cost preview, then `--yes` or accept gradual backfill over time as files change.
|
||||
- Daily autopilot cost post-backfill: unchanged (edges extracted at chunk time, no per-query LLM)
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
1. **Schema migration on live Postgres.** Test against production-shape DB before ship. v0.12.0 JSONB incident is the canary.
|
||||
2. **Per-language tree-sitter queries are fiddly.** Hand-verified edge-set fixtures per language. Ruby gets extra coverage for dynamic-dispatch false negatives.
|
||||
3. **Two-pass retrieval regression.** Default off for prose. BrainBench Cat 1 MUST show no regression before shipping.
|
||||
4. **Backfill shape (G1 resolved).** Three composable layers: schema-auto migrates columns empty (zero cost). Lazy on-touch catches 80% over time (zero cost). Explicit `reindex-code` with cost preview for users wanting immediate full benefit. No surprise bills.
|
||||
5. **Magika bundle (G2 resolved).** +1MB asset, `bun --compile` guard extension. If bundling surfaces bugs late in implementation, B2 is the only tier that can fall back to v0.20.1 without blocking the cathedral — it's self-contained at Layer 8.
|
||||
6. **High-fan-out symbols.** `console.log`-style symbols have 100K callers. Neighbor cap 50, depth cap 2. Chaos test fixture required.
|
||||
|
||||
## Review gates
|
||||
|
||||
- CEO review (cathedral II) — CLEARED 2026-04-24
|
||||
- Outside voice (codex) — run during cathedral II CEO review
|
||||
- `/plan-devex-review` — up next (per user request, 5 new CLI surfaces + reindex-code need DX polish review before eng)
|
||||
- `/plan-eng-review` — required before implementation begins
|
||||
- `/review` + `/codex review` — required before `/ship`
|
||||
|
||||
## What's deferred to later cathedrals
|
||||
|
||||
- **C6** `code-signature "(A, B) => C"` — per-language type captures. v0.20.1.
|
||||
- **Call-graph langs beyond 8 shipped** — PHP, Swift, Kotlin, Scala, C#, C++, Elixir, etc. One small PR per language.
|
||||
- **LSP integration** for live precision. v0.22+ cathedral.
|
||||
- **Code-tour generator** (cathedral I T1).
|
||||
- **Private-code redaction pre-embed** (cathedral I T3).
|
||||
- **`gbrain doctor --chunker-debug`** AST dump.
|
||||
@@ -1,154 +0,0 @@
|
||||
# Homebrew for Personal AI Infrastructure
|
||||
|
||||
The 10-star vision for GBrain's integration system. Ship Approach B (v0.7.0),
|
||||
build toward this over subsequent releases.
|
||||
|
||||
## The Vision
|
||||
|
||||
GBrain becomes a personal infrastructure operating system where every signal in
|
||||
your life flows through the brain automatically. Integrations are **senses**
|
||||
(data inputs) and **reflexes** (automated responses to patterns). Users subscribe
|
||||
to the creator's actual operating system, then customize it.
|
||||
|
||||
```
|
||||
$ gbrain integrations
|
||||
|
||||
SENSES (data inputs) STATUS
|
||||
-------------------------------------------------------
|
||||
voice-to-brain Phone calls -> brain pages ACTIVE last call: 2h ago
|
||||
email-to-brain Gmail -> entity updates ACTIVE 47 emails today
|
||||
x-to-brain Twitter -> media pages ACTIVE 312 tweets tracked
|
||||
calendar-to-brain Google Cal -> meeting prep ACTIVE 3 meetings tomorrow
|
||||
photos-to-brain Camera roll -> visual mem AVAILABLE
|
||||
slack-to-brain Slack -> conversation index AVAILABLE
|
||||
rss-to-brain RSS feeds -> media pages AVAILABLE
|
||||
|
||||
REFLEXES (automated responses) STATUS
|
||||
-------------------------------------------------------
|
||||
meeting-prep Brief me before meetings ACTIVE next: 9am tomorrow
|
||||
entity-enrich Auto-enrich new contacts ACTIVE 12 enriched today
|
||||
dream-cycle Overnight brain maintenance ACTIVE last run: 3am
|
||||
deal-tracker Alert on deal changes AVAILABLE
|
||||
follow-up-nudge Remind on stale threads AVAILABLE
|
||||
|
||||
This week: 1,247 signals ingested. Top: email (47%), voice (23%), X (18%).
|
||||
34 new entity pages created. 7 calls transcribed.
|
||||
|
||||
Run 'gbrain integrations show <id>' for setup details.
|
||||
```
|
||||
|
||||
The user feels: "My brain is alive. It's watching everything I care about, and
|
||||
it's getting smarter every day. I didn't have to write any code. I just said yes
|
||||
when the agent asked."
|
||||
|
||||
## Architecture: Senses & Reflexes
|
||||
|
||||
### Recipe Format (YAML frontmatter + markdown body)
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: voice-to-brain
|
||||
name: Voice-to-Brain
|
||||
version: 0.7.0
|
||||
description: Phone calls create brain pages via Twilio + OpenAI Realtime + GBrain MCP
|
||||
category: sense
|
||||
requires: [credential-gateway]
|
||||
secrets:
|
||||
- name: TWILIO_ACCOUNT_SID
|
||||
description: Twilio account SID
|
||||
where: https://console.twilio.com
|
||||
- name: OPENAI_API_KEY
|
||||
description: OpenAI API key (for Realtime voice)
|
||||
where: https://platform.openai.com/api-keys
|
||||
health_checks:
|
||||
- curl -s https://api.twilio.com/2010-04-01 > /dev/null
|
||||
- curl -s https://api.openai.com/v1/models > /dev/null
|
||||
setup_time: 30 min
|
||||
---
|
||||
|
||||
[Opinionated setup instructions the agent executes...]
|
||||
```
|
||||
|
||||
### Dependency Graph
|
||||
|
||||
Recipes declare `requires` in frontmatter. The CLI resolves dependencies before
|
||||
setup. If voice-to-brain requires credential-gateway, the agent sets up
|
||||
credential-gateway first.
|
||||
|
||||
```
|
||||
credential-gateway
|
||||
├── voice-to-brain (requires credentials for Twilio)
|
||||
├── email-to-brain (requires credentials for Gmail)
|
||||
└── calendar-to-brain (requires credentials for Google Calendar)
|
||||
|
||||
x-to-brain (standalone, uses X API directly)
|
||||
```
|
||||
|
||||
### Health Dashboard
|
||||
|
||||
`gbrain integrations doctor` runs health_checks from every configured recipe:
|
||||
```
|
||||
$ gbrain integrations doctor
|
||||
voice-to-brain: ✓ Twilio reachable ✓ OpenAI key valid ✓ ngrok tunnel up
|
||||
email-to-brain: ✓ Gmail auth valid ✗ No emails in 48h (check cron)
|
||||
OVERALL: 1 warning
|
||||
```
|
||||
|
||||
### Sense Analytics
|
||||
|
||||
`gbrain integrations stats` aggregates heartbeat data:
|
||||
```
|
||||
$ gbrain integrations stats
|
||||
This week: 1,247 signals ingested
|
||||
Top sources: email (47%), voice (23%), X (18%), calendar (12%)
|
||||
34 new entity pages created
|
||||
7 calls transcribed
|
||||
Brain growth: 12,400 → 12,834 pages (+434)
|
||||
```
|
||||
|
||||
### Reflex Rules Engine (future)
|
||||
|
||||
Reflexes are recipes that trigger on brain state changes:
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: deal-tracker
|
||||
category: reflex
|
||||
triggers:
|
||||
- type: page_updated
|
||||
filter: {type: deal, field: status}
|
||||
- type: timeline_entry
|
||||
filter: {source: email, mentions: deal}
|
||||
action: alert
|
||||
---
|
||||
|
||||
When a deal page's status changes or a new email mentions a deal,
|
||||
alert the user with context from the brain.
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Version | What Ships | Key Recipe |
|
||||
|---------|-----------|------------|
|
||||
| v0.7.0 | Recipe format, CLI, SKILLPACK breakout | voice-to-brain |
|
||||
| v0.8.0 | 3 more senses, reflex format | email, X, calendar |
|
||||
| v0.9.0 | Community recipes, install executor | community submissions |
|
||||
| v1.0.0 | Full senses/reflexes, health dashboard | meeting-prep, dream-cycle |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **GBrain is deterministic infrastructure.** Cross-sense correlation, pattern
|
||||
detection, and intelligent responses are the agent's job (OpenClaw/Hermes).
|
||||
GBrain provides the plumbing.
|
||||
|
||||
2. **Agents ARE the runtime.** No npm packages, Docker images, or deterministic
|
||||
scripts. The recipe markdown IS the installer. The agent reads it and does
|
||||
the work.
|
||||
|
||||
3. **Very opinionated defaults.** Ship the creator's exact production setup as
|
||||
the default. Users customize from there. Unknown callers get screened. Quiet
|
||||
hours are enforced. Brain-first lookup happens on every call.
|
||||
|
||||
4. **Agent-readable outputs.** All CLI output must be parseable by agents (--json
|
||||
flag). Migration files include agent instructions. The agent is the primary
|
||||
consumer, not the human.
|
||||
@@ -1,717 +0,0 @@
|
||||
# GBrain Knowledge Runtime — Design Doc
|
||||
|
||||
**Status:** DRAFT for CEO review.
|
||||
**Date:** 2026-04-18.
|
||||
**Supersedes:** The earlier "Feynman Ideas Assessment + Phase A/B" plan.
|
||||
|
||||
---
|
||||
|
||||
## 0. Context
|
||||
|
||||
During a CEO review of a narrow two-feature plan (bare-tweet citation repair + completeness score, borrowed from Feynman), the scope was reframed. The narrow plan duplicated work Garry's OpenClaw already does and missed the real leverage point: **the bespoke abstractions hiding inside OpenClaw — resolvers, enrichment orchestration, scheduling, deterministic output — should live in GBrain as first-class primitives.**
|
||||
|
||||
North star: *"When Garry's OpenClaw's Claw upgrades to this version of GBrain, it should immediately recognize brilliance and completeness and say 'It's time to switch to these abstractions.'"*
|
||||
|
||||
That is the test this document is designed against. Everything else is downstream.
|
||||
|
||||
---
|
||||
|
||||
## 1. The Four Layers
|
||||
|
||||
The design is four layered abstractions. Each is independently useful; together they are the Knowledge Runtime.
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ KNOWLEDGE RUNTIME (new) │
|
||||
├───────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 4: Deterministic Output Builder │
|
||||
│ BrainWriter · Scaffolds · Back-link enforcer · Slug registry │
|
||||
│ Rule: LLM picks WHAT to write. Code guarantees WHERE and HOW. │
|
||||
├───────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 3: Scheduler │
|
||||
│ ScheduledResolver · TZ-aware quiet hours (enforced) · │
|
||||
│ Auto-stagger · Durable state · Retry/circuit-break │
|
||||
├───────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 2: Enrichment Orchestrator │
|
||||
│ Trigger convergence · Tier routing · Budget · Cascade · │
|
||||
│ Evidence-weighted completeness · Fail-safe transactions │
|
||||
├───────────────────────────────────────────────────────────────────┤
|
||||
│ Layer 1: Resolver SDK │
|
||||
│ Resolver<I,O> interface · Registry · Factory · Plugin recipes │
|
||||
│ Ported reference impls: X-API, Perplexity, Mistral, brain │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
REUSES (polished primitives already in GBrain) REPLACES (ad-hoc code)
|
||||
FailImproveLoop · backoff · storage factory · enrichment-service ·
|
||||
check-resolvable · operations validators · embedding · transcription ·
|
||||
engine interface · publish · backlinks 2 recipe formats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Why This Order (L1 → L4)
|
||||
|
||||
Every higher layer depends on the lower one. **L1 must land first or the rest leaks abstractions.**
|
||||
|
||||
- **L1 (Resolvers)** is the substrate. Without a uniform lookup interface, every orchestrator + writer has bespoke callers.
|
||||
- **L2 (Orchestrator)** uses L1 to fetch; without L1 it's still ad-hoc.
|
||||
- **L3 (Scheduler)** runs L2 periodically; without L2 it's scheduling nothing structured.
|
||||
- **L4 (Output Builder)** is what every layer ultimately writes through; without it we have 14 call sites doing `fs.writeFile` with hand-rolled citation discipline.
|
||||
|
||||
An earlier implementation could ship L1 + L4 first (the two "purest" layers) and have the most immediate integrity impact, then add L2 + L3. But the end-state must include all four.
|
||||
|
||||
---
|
||||
|
||||
## 3. Layer 1 — Resolver SDK
|
||||
|
||||
### 3.1 What's broken today
|
||||
|
||||
Garry's OpenClaw has **69 distinct external-lookup patterns** across X API (14 shapes), Perplexity, Mistral OCR, Gmail, Calendar, Slack, GitHub, YouTube, Diarize.io, YC tools, OSINT collectors, and brain-local lookups. Each one is a bespoke script under `scripts/` with its own error handling, retry logic, and output shape. GBrain has 3 ad-hoc wrappers (`embedding.ts`, `transcription.ts`, `enrichment-service.ts`) that don't share an interface.
|
||||
|
||||
Common consequences:
|
||||
- No uniform retry/backoff strategy (some scripts retry, most don't)
|
||||
- No cost tracking (Perplexity bills eaten silently when calls return no-substance results)
|
||||
- No confidence/provenance propagation (callers can't tell if an answer is verified or inferred)
|
||||
- Users can't add a resolver without forking GBrain
|
||||
|
||||
### 3.2 Interface
|
||||
|
||||
```typescript
|
||||
// src/core/resolvers/interface.ts
|
||||
|
||||
export type ResolverCost = 'free' | 'rate-limited' | 'paid';
|
||||
|
||||
export interface ResolverRequest<I> {
|
||||
input: I;
|
||||
context: ResolverContext;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface ResolverResult<O> {
|
||||
value: O;
|
||||
confidence: number; // 0.0–1.0; 1.0 = deterministic from ground-truth API
|
||||
source: string; // e.g. "x-api-v2", "perplexity-sonar", "brain-local"
|
||||
fetchedAt: Date;
|
||||
costEstimate?: number; // dollars; 0 if free
|
||||
raw?: unknown; // for sidecar preservation via put_raw_data
|
||||
}
|
||||
|
||||
export interface Resolver<I, O> {
|
||||
readonly id: string; // stable, slug-like: "x_handle_to_tweet"
|
||||
readonly cost: ResolverCost;
|
||||
readonly backend: string; // "x-api-v2", "perplexity", "brain-local"
|
||||
readonly inputSchema: JSONSchema;
|
||||
readonly outputSchema: JSONSchema;
|
||||
|
||||
available(ctx: ResolverContext): Promise<boolean>;
|
||||
resolve(req: ResolverRequest<I>): Promise<ResolverResult<O>>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Context
|
||||
|
||||
```typescript
|
||||
export interface ResolverContext {
|
||||
engine: BrainEngine;
|
||||
storage: StorageBackend;
|
||||
config: GBrainConfig;
|
||||
logger: Logger;
|
||||
metrics: MetricsRecorder;
|
||||
budget: BudgetLedger; // hard spend caps, queried pre-resolve
|
||||
requestId: string;
|
||||
remote: boolean; // trust boundary — untrusted callers get stricter validation
|
||||
deadline?: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Registry + Factory (mirrors `src/core/storage.ts`)
|
||||
|
||||
```typescript
|
||||
// src/core/resolvers/registry.ts
|
||||
export class ResolverRegistry {
|
||||
register<I, O>(r: Resolver<I, O>): void;
|
||||
get(id: string): Resolver<unknown, unknown>;
|
||||
list(filter?: { cost?: ResolverCost; backend?: string }): Resolver[];
|
||||
async resolve<I, O>(id: string, input: I, ctx: ResolverContext): Promise<ResolverResult<O>>;
|
||||
}
|
||||
|
||||
// src/core/resolvers/factory.ts (dynamic import like engine-factory)
|
||||
export async function createResolver(
|
||||
type: 'x-api' | 'perplexity' | 'mistral-ocr' | 'brain-local' | 'plugin',
|
||||
config: ResolverConfig,
|
||||
): Promise<Resolver>;
|
||||
```
|
||||
|
||||
### 3.5 Plugin format (unifies `recipes/` + `data-research` formats)
|
||||
|
||||
A plugin is YAML + JS module, discovered via filesystem scan of `~/.gbrain/resolvers/` and `recipes/`.
|
||||
|
||||
```yaml
|
||||
# Example: resolvers/x-api/handle-to-tweet.yaml
|
||||
id: x_handle_to_tweet
|
||||
version: 1
|
||||
category: lookup
|
||||
cost: rate-limited
|
||||
backend: x-api-v2
|
||||
module: ./handle-to-tweet.ts
|
||||
input_schema:
|
||||
type: object
|
||||
properties:
|
||||
handle: { type: string, pattern: "^[A-Za-z0-9_]{1,15}$" }
|
||||
keywords: { type: string }
|
||||
required: [handle]
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
url: { type: string, format: uri }
|
||||
tweet_id: { type: string }
|
||||
text: { type: string }
|
||||
created_at: { type: string, format: date-time }
|
||||
requires:
|
||||
env: [X_API_BEARER_TOKEN]
|
||||
health_check:
|
||||
kind: http
|
||||
url: https://api.twitter.com/2/tweets/1
|
||||
expect: { status: [200, 401] } # 401 = auth failure but endpoint reachable
|
||||
tests:
|
||||
- input: { handle: "garrytan" }
|
||||
expect: { url: { pattern: "^https://x\\.com/garrytan/status/\\d+$" } }
|
||||
```
|
||||
|
||||
Trust flagging follows the existing `src/commands/integrations.ts` pattern: only package-bundled resolvers are `embedded=true` and may run arbitrary commands; user-provided resolvers are restricted to `http` and validated schemas.
|
||||
|
||||
### 3.6 Wraps every resolver with `FailImproveLoop`
|
||||
|
||||
Existing `src/core/fail-improve.ts` is the deterministic-first/LLM-fallback pattern. Every resolver automatically gets wrapped: if the deterministic path (e.g. X API) returns a valid result, use it; if it fails, optionally fall back to an LLM-based resolver; log both paths for future pattern analysis and auto-test generation.
|
||||
|
||||
### 3.7 Reference implementations to ship
|
||||
|
||||
The OpenClaw survey inventoried 69 resolver shapes. Shipping all of them is wrong (over-scoped); shipping zero is under-scoped. The dogfood set:
|
||||
|
||||
| # | Resolver | Purpose | Used by |
|
||||
|---|---|---|---|
|
||||
| 1 | `x_handle_to_tweet` | Bare-tweet citation repair (original Phase A) | `gbrain integrity` |
|
||||
| 2 | `url_reachable` | Dead-link detection | `gbrain integrity` |
|
||||
| 3 | `brain_slug_lookup` | Name/email → slug (wraps existing `resolveSlugs`) | Output Builder |
|
||||
| 4 | `openai_embedding` | Refactor of `src/core/embedding.ts` into Resolver | Import pipeline |
|
||||
| 5 | `perplexity_query` | Query → synthesis + citations | Enrichment Orchestrator |
|
||||
| 6 | `text_to_entities` | LLM entity extraction (structured JSON) | Enrichment Orchestrator |
|
||||
|
||||
The remaining 63 OpenClaw patterns port incrementally, driven by user need. Each port is a new YAML + module under `recipes/` or `~/.gbrain/resolvers/` with no framework changes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Layer 2 — Enrichment Orchestrator
|
||||
|
||||
### 4.1 What's broken today
|
||||
|
||||
Garry's OpenClaw's enrichment is **polished at the data layer, hacky at the control layer**:
|
||||
|
||||
- **Completeness = "length > 500 chars + no `needs-enrichment` tag"** (`lib/enrich.mjs:351-355`). Naïve. A rich page of repetitive Perplexity summaries (see `brain/people/0interestrates.md` — 38 repeating blocks) passes this check.
|
||||
- **30-day auto-re-enrichment** runs forever. No "done" state. A person met once in 2023 still gets re-researched monthly.
|
||||
- **Cascade is convention-only.** Person→company stubs are created automatically; company→investors, company→employees traversals are documented but never implemented.
|
||||
- **No hard budget cap.** Cost is estimated per batch, never enforced across batches or per day.
|
||||
- **Failure is silent.** A bad Perplexity response logs and continues; partial writes can leave a page with a timeline entry but no raw-data sidecar.
|
||||
|
||||
### 4.2 The orchestrator
|
||||
|
||||
```typescript
|
||||
// src/core/enrichment/orchestrator.ts
|
||||
|
||||
export interface EnrichmentRequest {
|
||||
entitySlug: string;
|
||||
trigger: 'mention' | 'stub-creation' | 'cron-sweep' | 'manual' | 'cascade';
|
||||
tier?: 1 | 2 | 3; // optional override; auto-computed if absent
|
||||
cascadeDepth?: number; // 0 = no cascade; default 1
|
||||
}
|
||||
|
||||
export interface EnrichmentResult {
|
||||
entitySlug: string;
|
||||
completenessBefore: number;
|
||||
completenessAfter: number;
|
||||
resolversUsed: string[]; // e.g. ["perplexity_query", "x_handle_to_tweet"]
|
||||
costSpent: number;
|
||||
writtenTo: string[]; // page paths touched, for transaction audit
|
||||
cascadedTo: string[]; // related entities enriched
|
||||
status: 'enriched' | 'skipped' | 'failed' | 'budget-exhausted';
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export class EnrichmentOrchestrator {
|
||||
constructor(
|
||||
private registry: ResolverRegistry,
|
||||
private writer: BrainWriter,
|
||||
private budget: BudgetLedger,
|
||||
private scorer: CompletenessScorer,
|
||||
private graph: EntityGraph,
|
||||
) {}
|
||||
|
||||
async enrich(req: EnrichmentRequest): Promise<EnrichmentResult>;
|
||||
async enrichBatch(reqs: EnrichmentRequest[]): Promise<EnrichmentResult[]>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Evidence-weighted completeness (replaces length heuristic)
|
||||
|
||||
Completeness is a per-entity-type rubric, stored in frontmatter on write and recomputed on demand.
|
||||
|
||||
```typescript
|
||||
// src/core/enrichment/completeness.ts
|
||||
export interface CompletenessRubric<Page> {
|
||||
entityType: PageType;
|
||||
dimensions: {
|
||||
name: string;
|
||||
weight: number; // sum must = 1.0
|
||||
check: (page: Page) => number; // 0.0–1.0
|
||||
}[];
|
||||
}
|
||||
|
||||
// Example rubric for persons:
|
||||
// - has_role_and_company 0.20
|
||||
// - has_source_urls 0.20 (≥1 URL with resolver-verified reachability)
|
||||
// - has_timeline_entries 0.15 (≥1)
|
||||
// - has_citations 0.15 (every claim has [Source: ...])
|
||||
// - has_backlinks 0.10 (every linked page links back)
|
||||
// - recency_score 0.10 (last_verified within 90 days)
|
||||
// - non_redundancy 0.10 (no repeated blocks; distinct-lines/total-lines > 0.8)
|
||||
```
|
||||
|
||||
**Key property:** `non_redundancy` + `recency_score` explicitly kill the two brain pathologies observed in the audit (Wilco-style repeating blocks; stale pages without `last_verified`).
|
||||
|
||||
The `completeness` field goes in frontmatter as `0.0–1.0`. It becomes queryable via `list_pages(where: completeness < 0.5)`.
|
||||
|
||||
### 4.4 Tier routing with hard budget
|
||||
|
||||
Two-dimensional routing: **importance** (tier 1/2/3 from person-score) × **budget state**.
|
||||
|
||||
```typescript
|
||||
// src/core/enrichment/tiers.ts
|
||||
export const TIER_CONFIG = {
|
||||
1: { models: ['opus', 'sonar-deep'], maxCostUsd: 0.10, cascadeDepth: 2 },
|
||||
2: { models: ['sonar'], maxCostUsd: 0.02, cascadeDepth: 1 },
|
||||
3: { models: ['sonar'], maxCostUsd: 0.005, cascadeDepth: 0 },
|
||||
};
|
||||
|
||||
// src/core/enrichment/budget.ts
|
||||
export class BudgetLedger {
|
||||
// Hard caps. Queryable pre-resolve.
|
||||
dailyCapUsd: number;
|
||||
perEntityCapUsd: number;
|
||||
perResolverCapUsd: Map<string, number>;
|
||||
|
||||
async reserve(resolverId: string, estimateUsd: number): Promise<Reservation | 'exhausted'>;
|
||||
async commit(reservation: Reservation, actualUsd: number): Promise<void>;
|
||||
async rollback(reservation: Reservation): Promise<void>;
|
||||
async state(): Promise<{ spent: number; remaining: number; perResolver: Record<string, number> }>;
|
||||
}
|
||||
```
|
||||
|
||||
**Property:** if the daily cap is reached, `orchestrator.enrich()` returns `status: 'budget-exhausted'` immediately. No silent overages. Circuit-breaker resets at midnight in the user's configured TZ.
|
||||
|
||||
### 4.5 Cascade (entity graph traversal)
|
||||
|
||||
```typescript
|
||||
// src/core/enrichment/cascade.ts
|
||||
export class EntityGraph {
|
||||
// Deterministic, no LLM. Uses engine.getLinks() + engine.getBacklinks().
|
||||
async neighbors(slug: string, depth: number): Promise<string[]>;
|
||||
async cascadeFrom(trigger: string, depth: number): Promise<EnrichmentRequest[]>;
|
||||
}
|
||||
```
|
||||
|
||||
If person X is enriched and gains a new `company: Acme` field, cascade checks: does `companies/acme` exist? If not, create stub + enqueue at tier 2. Does `companies/acme` link back to X? If not, write the back-link. **Iron Law is machine-enforced, not skill-enforced.**
|
||||
|
||||
### 4.6 Fail-safe transactions
|
||||
|
||||
Every enrichment is wrapped in a BrainWriter transaction (Layer 4). Partial writes are rolled back. No asymmetric state like timeline-entry-without-raw-sidecar.
|
||||
|
||||
```typescript
|
||||
await writer.transaction(async (tx) => {
|
||||
const research = await registry.resolve('perplexity_query', {...}, ctx);
|
||||
await tx.appendTimeline(slug, {...});
|
||||
await tx.putRawData(slug, 'perplexity', research.raw);
|
||||
await tx.setFrontmatterField(slug, 'completeness', score);
|
||||
// All-or-nothing commit on exit.
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Layer 3 — Scheduler
|
||||
|
||||
### 5.1 What's broken today
|
||||
|
||||
Garry's OpenClaw's cron is **externally-driven JSON** (`cron/jobs.json`) with ~30 jobs manually stagger-offset at different minutes. GBrain has **zero native scheduling** — `src/commands/autopilot.ts` is a single daemon loop, and `docs/guides/cron-schedule.md` is architectural guidance, not code.
|
||||
|
||||
Failures observed in Garry's OpenClaw's actual state:
|
||||
- `X OAuth2 Token Refresh`: 11 consecutive timeouts (critical-path silent failure)
|
||||
- `flight-tracker daily scan`: 5 consecutive timeouts
|
||||
- `morning-briefing`: 4 consecutive timeouts
|
||||
- Quiet hours are checked at runtime in skills, so a skill that forgets to check will DM at 3 a.m.
|
||||
- Staggering is manual convention; no protection against two jobs colliding after a config edit.
|
||||
|
||||
### 5.2 ScheduledResolver interface
|
||||
|
||||
```typescript
|
||||
// src/core/scheduling/scheduler.ts
|
||||
export interface Schedule {
|
||||
kind: 'cron' | 'interval';
|
||||
expr?: string; // cron string
|
||||
intervalMs?: number;
|
||||
tz: string; // IANA: "America/Los_Angeles"
|
||||
quietHours?: {
|
||||
startHour: number; // 22 = 10 PM local
|
||||
endHour: number; // 7 = 7 AM local
|
||||
policy: 'skip' | 'defer' | 'silent-run';
|
||||
};
|
||||
staggerKey?: string; // jobs with same key auto-offset
|
||||
maxConcurrent?: number; // global concurrency cap
|
||||
maxDurationMs?: number; // timeout
|
||||
}
|
||||
|
||||
export interface ScheduledResolver extends Resolver<void, ScheduledResult> {
|
||||
schedule: Schedule;
|
||||
retryPolicy: { maxRetries: number; backoffMs: number };
|
||||
circuitBreaker: { failureThreshold: number; cooldownMs: number };
|
||||
state: DurableState; // watermark, content-hash, idempotency key
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Enforcement vs convention (the key delta from Garry's OpenClaw)
|
||||
|
||||
| Concern | Garry's OpenClaw today | Knowledge Runtime |
|
||||
|---|---|---|
|
||||
| Quiet hours | Checked inside each skill (trust-based) | Enforced at scheduler, skill cannot override |
|
||||
| Staggering | Manual minute-offset in `jobs.json` | Scheduler assigns slots via hashed staggerKey |
|
||||
| Concurrency | `MAX_BATCH_PROCESSES=2` in backoff, ignored by cron | Global semaphore in scheduler |
|
||||
| Timeout | Per-job string in JSON, not always respected | Enforced via `AbortController`, timeout raises `TimeoutError` caught by orchestrator |
|
||||
| Retry | None at cron level | `retryPolicy` with exponential backoff |
|
||||
| Silent failure | "11 consecutive timeouts" unnoticed | Circuit breaker opens at threshold → escalation to user |
|
||||
| Idempotency | State files per job, no framework | `DurableState` primitive: watermark/ID/content-hash |
|
||||
|
||||
### 5.4 Native engine + OS cron adapter
|
||||
|
||||
The scheduler runs as either:
|
||||
1. **Embedded** (default for `gbrain autopilot`): native event loop inside the daemon process. One process, many ScheduledResolvers.
|
||||
2. **OS-driven** (for Railway/launchd/systemd): `gbrain schedule run <id>` invoked by OS cron, scheduler state is durable so cross-invocation dedup still works.
|
||||
|
||||
Both modes share the same `Schedule` config + state.
|
||||
|
||||
### 5.5 Observability
|
||||
|
||||
Every scheduled run emits structured events: `started`, `skipped-quiet-hours`, `deferred-to-active-hours`, `failed-retrying`, `circuit-opened`, `completed`. Events go to:
|
||||
- `~/.gbrain/scheduler/events.jsonl` (local, always)
|
||||
- `engine.logIngest` (audit trail in brain DB)
|
||||
- Optional webhook (Slack/Telegram for the user)
|
||||
|
||||
`gbrain doctor` reads the event log and reports: current circuit-breaker state, any resolver with > 3 consecutive failures, any resolver that hasn't fired within 3× its interval (freshness SLA like Garry's OpenClaw's `freshness-check.mjs` but built-in).
|
||||
|
||||
---
|
||||
|
||||
## 6. Layer 4 — Deterministic Output Builder
|
||||
|
||||
### 6.1 The anti-hallucination invariant
|
||||
|
||||
**Iron Law: LLM picks WHAT. Code guarantees WHERE and HOW.**
|
||||
|
||||
Garry's OpenClaw's existing `lib/enrich.mjs:buildTweetEntry` is close to this — tweet URLs are built from `tweet.id` returned by the X API, never from LLM memory. But:
|
||||
|
||||
- A past incident: *"Sub-agent test #2 FAILED — hallucinated 'Philip Leung' entity links across all daily files. LLM rewriting of daily files is too error-prone."* (Garry's OpenClaw memory log, 2026-04-13.)
|
||||
- Back-links depend on `appendTimeline` being called everywhere; skips are silent.
|
||||
- Slug collisions are unchecked (no conflict detection on `slugify`).
|
||||
- Citation format is post-hoc linted weekly, not pre-write enforced.
|
||||
|
||||
### 6.2 BrainWriter
|
||||
|
||||
```typescript
|
||||
// src/core/output/writer.ts
|
||||
export class BrainWriter {
|
||||
constructor(
|
||||
private engine: BrainEngine,
|
||||
private slugRegistry: SlugRegistry,
|
||||
private scaffolder: Scaffolder,
|
||||
) {}
|
||||
|
||||
async transaction<T>(fn: (tx: WriteTx) => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
export interface WriteTx {
|
||||
// High-level typed operations; never raw string writes.
|
||||
createEntity(input: EntityInput): Promise<string>; // returns slug, conflict-checked
|
||||
appendTimeline(slug: string, entry: TimelineInput): Promise<void>;
|
||||
setCompiledTruth(slug: string, body: CompiledTruthInput): Promise<void>;
|
||||
setFrontmatterField(slug: string, key: string, value: unknown): Promise<void>;
|
||||
putRawData(slug: string, source: string, data: object): Promise<void>;
|
||||
addLink(from: string, to: string, context: string): Promise<void>; // auto-creates reverse back-link
|
||||
|
||||
// Validators (called implicitly on commit)
|
||||
validate(): Promise<ValidationReport>;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Scaffolder — deterministic link + citation construction
|
||||
|
||||
Every user-visible URL/link/citation is built by code from resolver outputs, not from LLM text.
|
||||
|
||||
```typescript
|
||||
// src/core/output/scaffold.ts
|
||||
export class Scaffolder {
|
||||
tweetCitation(handle: string, tweetId: string, dateISO: string): string {
|
||||
// "[Source: [X/garrytan, 2026-04-18](https://x.com/garrytan/status/123456)]"
|
||||
}
|
||||
emailCitation(account: string, messageId: string, subject: string): string {
|
||||
// deterministic Gmail URL per OpenClaw pattern
|
||||
}
|
||||
sourceCitation(resolverResult: ResolverResult<unknown>): string {
|
||||
// pulls .source, .fetchedAt, .raw from the result
|
||||
}
|
||||
entityLink(slug: string): string {
|
||||
// slugRegistry checks existence; returns resolvable wikilink
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 SlugRegistry — conflict detection
|
||||
|
||||
```typescript
|
||||
// src/core/output/slug-registry.ts
|
||||
export class SlugRegistry {
|
||||
async create(desiredSlug: string, displayName: string, type: PageType): Promise<CreatedSlug>;
|
||||
// Throws SlugCollision if another entity already occupies desiredSlug and isn't
|
||||
// confirmed as the same person (via email / x_handle / disambiguator).
|
||||
// Auto-resolves near-collisions by appending disambiguator.
|
||||
|
||||
async confirmSame(slugA: string, slugB: string, confidence: number): Promise<void>;
|
||||
async merge(canonical: string, duplicate: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Pre-write validators (fail-closed for integrity)
|
||||
|
||||
On `WriteTx.validate()` before commit:
|
||||
|
||||
1. **Citation validator.** Every factual sentence in `compiled_truth` must have an inline `[Source: ...]` within N lines. Non-compliant paragraphs are flagged. Configurable: strict-mode rejects the transaction, lint-mode warns.
|
||||
2. **Link validator.** Every `[text](path)` must point to a page that exists OR to a URL the Scaffolder built (so it's guaranteed-valid). No raw LLM-composed URLs.
|
||||
3. **Back-link validator.** Every outbound link must have a reverse link written in the same transaction.
|
||||
4. **Triple-HR validator.** Compiled truth / timeline split enforced at the schema level.
|
||||
|
||||
**Fails closed**: the default is strict-mode. Loosening requires explicit `writer.transaction({ strictMode: false }, ...)` and logs a warning to the ingest log.
|
||||
|
||||
### 6.6 LLM output sanitization
|
||||
|
||||
Any LLM output destined for a brain page passes through a JSON-Schema-validated parser first. No free-form markdown goes to disk.
|
||||
|
||||
- Entity extraction: JSON array of `{ name, type, context }` per existing `extractEntities` pattern — strict validation.
|
||||
- Compiled-truth synthesis: LLM emits structured `{ sections: [{heading, paragraphs: [{text, sources: [...]}]}]}`, scaffolder renders to markdown.
|
||||
- Timeline entries: LLM emits `{ date, summary, detail, sources }`, scaffolder renders.
|
||||
|
||||
LLM never sees file paths, never writes files, never emits finished markdown.
|
||||
|
||||
---
|
||||
|
||||
## 7. Integration with existing GBrain
|
||||
|
||||
### 7.1 Reuse (already polished)
|
||||
|
||||
| Existing | Used by | Change |
|
||||
|---|---|---|
|
||||
| `src/core/fail-improve.ts` (9/10) | Wraps every Resolver in L1 | None; becomes default wrapper |
|
||||
| `src/core/backoff.ts` (9/10) | ResolverContext.backoff | None |
|
||||
| `src/core/storage.ts` (9/10) | Template for Resolver factory pattern | None; serves as pattern reference |
|
||||
| `src/core/check-resolvable.ts` (9/10) | Extend to validate Resolver plugins | Add `checkResolvers()` mode |
|
||||
| `src/commands/publish.ts` (9/10) | Uses BrainWriter under the hood | Minor: route through L4 |
|
||||
| `src/commands/backlinks.ts` (8/10) | Folded into L4 validator | Keep as CLI-facing lint entry point |
|
||||
| `src/core/operations.ts` validators | Reused in ResolverContext trust enforcement | None |
|
||||
| `src/core/engine.ts` BrainEngine (35 methods) | ResolverContext.engine | Extend with `getResolverRegistry()` |
|
||||
|
||||
### 7.2 Replace (ad-hoc today)
|
||||
|
||||
| Existing | Replace with |
|
||||
|---|---|
|
||||
| `src/core/enrichment-service.ts` (5/10) | `src/core/enrichment/orchestrator.ts` (L2) |
|
||||
| `src/core/embedding.ts` (monolithic) | `src/core/resolvers/builtin/embedding/openai.ts` |
|
||||
| `src/core/transcription.ts` (monolithic) | `src/core/resolvers/builtin/transcription/{groq,openai}.ts` |
|
||||
| `src/commands/integrations.ts` recipe format | Unified Resolver plugin format (§3.5) |
|
||||
| `src/core/data-research.ts` recipe format | Same unified format |
|
||||
| `src/commands/autopilot.ts` hard-coded daemon loop | Wraps a set of ScheduledResolvers |
|
||||
|
||||
### 7.3 Extend
|
||||
|
||||
- `src/core/engine.ts`: add `getResolverRegistry()`, `getWriter()`, `getScheduler()`. Engine becomes the runtime's root container.
|
||||
- `src/core/operations.ts`: `OperationContext` inherits from `ResolverContext` (or vice-versa). Trust flags unified.
|
||||
- `src/core/types.ts`: add `completeness: number` to `Page`, `sourcedBy: string[]` for provenance.
|
||||
|
||||
---
|
||||
|
||||
## 8. Migration Path (phased, shippable)
|
||||
|
||||
Each phase ships independently, passes full E2E, is feature-flagged, and is reversible. No big-bang.
|
||||
|
||||
### Phase 0 — Foundation (human: ~1 wk / CC: ~4 h)
|
||||
- Define `Resolver<I,O>`, `ResolverContext`, `ResolverRegistry`, `ResolverResult` (§3.2–3.4).
|
||||
- Add `src/core/resolvers/index.ts` wiring + tests for registry (register/get/list).
|
||||
- No behavioral change; ship as `v0.11.0-alpha` with feature flag.
|
||||
|
||||
### Phase 1 — Three reference resolvers (human: ~1 wk / CC: ~4 h)
|
||||
- Port `src/core/embedding.ts` → `resolvers/builtin/embedding/openai.ts`.
|
||||
- Implement `resolvers/builtin/brain-local/slug-lookup.ts` (wraps `engine.resolveSlugs`).
|
||||
- Implement `resolvers/builtin/url-reachable.ts` (HEAD-check).
|
||||
- Prove the interface: old callers swap to `registry.resolve('openai_embedding', ...)`.
|
||||
|
||||
### Phase 2 — BrainWriter + Slug Registry (human: ~1.5 wk / CC: ~6 h)
|
||||
- L4 core: `BrainWriter.transaction`, `Scaffolder`, `SlugRegistry` with conflict detection.
|
||||
- Pre-write validators: citation, link, back-link, triple-HR.
|
||||
- Migrate `src/commands/publish.ts` + `src/commands/backlinks.ts` to route through BrainWriter.
|
||||
- **Now** Garry's OpenClaw's "Philip Leung" hallucination is structurally impossible — LLM output passes through JSON-Schema validator before reaching Scaffolder.
|
||||
|
||||
### Phase 3 — `gbrain integrity` command (human: ~0.5 wk / CC: ~2 h)
|
||||
- Ship the originally-scoped user-facing feature on top of the new foundation.
|
||||
- Uses Resolver SDK: `x_handle_to_tweet` + `url_reachable`.
|
||||
- Uses BrainWriter: all auto-repairs go through validated writes.
|
||||
- `--auto --confidence 0.8` mode as user approved in cherry-pick #1.
|
||||
- **User-visible value ships in Phase 3, not Phase 7.**
|
||||
|
||||
### Phase 4 — Enrichment Orchestrator (human: ~2 wk / CC: ~8 h)
|
||||
- L2 core: `EnrichmentOrchestrator`, `BudgetLedger`, `CompletenessScorer`, `EntityGraph.cascadeFrom`.
|
||||
- Migrate `src/core/enrichment-service.ts` callers (deprecate the old file after).
|
||||
- Completeness score in frontmatter on every write (dogfooding cascades).
|
||||
|
||||
### Phase 5 — Scheduler (human: ~2 wk / CC: ~8 h)
|
||||
- L3 core: `Scheduler`, `ScheduledResolver`, `DurableState`, circuit breaker, quiet-hours enforcer.
|
||||
- Migrate `src/commands/autopilot.ts` to a ScheduledResolver set.
|
||||
- Ship `gbrain schedule list|run|pause|tail` CLI for observability.
|
||||
|
||||
### Phase 6 — Port 5–8 OpenClaw resolvers (human: ~1.5 wk / CC: ~6 h)
|
||||
- `perplexity_query`, `text_to_entities`, `mistral_ocr_pdf`, `x_search_all`, `x_user_to_tweets`, `gmail_query_to_threads`, `calendar_date_to_events`.
|
||||
- Each ships as YAML + TS module under `resolvers/builtin/` — **proof of the plugin format.**
|
||||
|
||||
### Phase 7 — OpenClaw Adoption Integration (human: ~1 wk / CC: ~4 h)
|
||||
- Write `docs/openclaw/ADOPTION.md` showing your OpenClaw how to replace its 69 bespoke scripts with calls to `gbrain registry.resolve(...)`.
|
||||
- Ship a `gbrain claw-bridge` subcommand that proxies Garry's OpenClaw's current script invocations to the resolver registry — zero-edit adoption path.
|
||||
- **This is the test of the north star.** If your OpenClaw can stand up a 1-line shim and drop `scripts/x-api-client.mjs`, the abstraction succeeded.
|
||||
|
||||
Total: human: ~10 weeks / CC: ~42 hours / calendar with single implementer: ~3–4 weeks.
|
||||
|
||||
---
|
||||
|
||||
## 9. Critical Files
|
||||
|
||||
### New directories / files
|
||||
|
||||
```
|
||||
src/core/
|
||||
runtime/
|
||||
index.ts # RuntimeContext (engine, storage, config, logger, metrics, budget)
|
||||
registry.ts # ResolverRegistry
|
||||
factory.ts # createResolver()
|
||||
resolvers/
|
||||
interface.ts # Resolver<I, O>
|
||||
fail-improve-wrapper.ts # auto-wraps every resolver in FailImproveLoop
|
||||
builtin/
|
||||
x-api/
|
||||
handle-to-tweet.ts
|
||||
handle-to-tweet.yaml
|
||||
perplexity/
|
||||
query.ts
|
||||
query.yaml
|
||||
brain-local/
|
||||
slug-lookup.ts
|
||||
url-reachable.ts
|
||||
embedding/
|
||||
openai.ts # refactored from src/core/embedding.ts
|
||||
transcription/
|
||||
groq.ts
|
||||
openai.ts
|
||||
enrichment/
|
||||
orchestrator.ts # EnrichmentOrchestrator
|
||||
tiers.ts # TIER_CONFIG
|
||||
budget.ts # BudgetLedger
|
||||
completeness.ts # CompletenessScorer + per-type rubrics
|
||||
cascade.ts # EntityGraph
|
||||
scheduling/
|
||||
scheduler.ts # Scheduler + ScheduledResolver
|
||||
schedule.ts # Schedule type, cron expr parser
|
||||
state.ts # DurableState primitives
|
||||
quiet-hours.ts # TZ-aware enforcement
|
||||
stagger.ts # deterministic slot assignment
|
||||
output/
|
||||
writer.ts # BrainWriter
|
||||
scaffold.ts # Scaffolder (typed URL builders)
|
||||
slug-registry.ts # SlugRegistry (conflict detection)
|
||||
validators/
|
||||
citation.ts
|
||||
link.ts
|
||||
back-link.ts
|
||||
triple-hr.ts
|
||||
|
||||
src/commands/
|
||||
integrity.ts # ships in Phase 3, replaces Feynman Phase A/B
|
||||
schedule.ts # gbrain schedule list|run|pause|tail (Phase 5)
|
||||
|
||||
docs/openclaw/
|
||||
ADOPTION.md # written in Phase 7
|
||||
```
|
||||
|
||||
### Replaced / removed
|
||||
- `src/core/enrichment-service.ts` — folded into `enrichment/orchestrator.ts`
|
||||
- `src/core/embedding.ts` — moved into `resolvers/builtin/embedding/openai.ts`
|
||||
- `src/core/transcription.ts` — moved into `resolvers/builtin/transcription/`
|
||||
|
||||
### Extended
|
||||
- `src/core/engine.ts` — add `getResolverRegistry()`, `getWriter()`, `getScheduler()`
|
||||
- `src/core/operations.ts` — unify with ResolverContext; every operation validator reusable by resolvers
|
||||
- `src/core/types.ts` — add `completeness: number`, `sourcedBy: string[]`, `lastVerified: Date`
|
||||
|
||||
---
|
||||
|
||||
## 10. Testing Strategy
|
||||
|
||||
### Contract tests
|
||||
Every Resolver implementation tested against the interface spec. Table-driven: run the same suite against `openai_embedding`, `x_handle_to_tweet`, etc. Ensures plugin authors can't ship broken resolvers.
|
||||
|
||||
### Property tests
|
||||
- **Idempotency:** running a ScheduledResolver twice with the same state produces the same output and doesn't double-write.
|
||||
- **Atomicity:** a BrainWriter transaction that throws mid-flight leaves the brain bit-for-bit identical to pre-transaction.
|
||||
- **Deterministic scaffolds:** given the same resolver outputs, the Scaffolder produces byte-identical citations/links.
|
||||
|
||||
### Integration tests
|
||||
- `EnrichmentOrchestrator` end-to-end against PGLite (in-memory, no API keys) with mocked resolver registry.
|
||||
- `Scheduler` with fake clock + quiet-hours scenarios.
|
||||
- BrainWriter transaction rollback on validator failure.
|
||||
|
||||
### Chaos tests
|
||||
- Kill the process mid-enrichment; next run must resume cleanly.
|
||||
- Simulate API timeout mid-transaction; transaction must roll back completely.
|
||||
- Corrupted state file; scheduler must escalate, not silently skip.
|
||||
|
||||
### Regression tests vs. Garry's OpenClaw behavior
|
||||
For each OpenClaw pattern we port (e.g. X-handle → tweet URL), a regression test proves the new resolver produces the same answer on real-world inputs from the brain audit. This is the "your OpenClaw would adopt" proof.
|
||||
|
||||
---
|
||||
|
||||
## 11. Open Questions (flagged for CEO re-review)
|
||||
|
||||
1. **Scope shape.** Is this the right four-layer decomposition, or are some layers better left to OpenClaw (e.g. Scheduling lives above GBrain, not in it)?
|
||||
2. **Phase 3 user-value break.** Does Phase 3 (user-visible `gbrain integrity`) ship early enough, or do we need an even smaller MVP?
|
||||
3. **LLM-as-resolver.** Should `text_to_entities` be a Resolver, or does that blur the "code vs LLM" line the invariant relies on?
|
||||
4. **Plugin format.** YAML + TS module (§3.5) vs. pure TS module with decorator-style metadata. Latter is more type-safe; former is more discoverable.
|
||||
5. **Cross-resolver transactions.** Do we support "atomic fetch-from-Perplexity + write-to-brain" at the L2 layer? Current design says yes; implementation is tricky (Perplexity call isn't rollbackable).
|
||||
6. **OpenClaw bridge scope.** Phase 7 `gbrain claw-bridge` — is that worth a phase of its own, or should adoption be documentation-only?
|
||||
7. **Completeness rubric coverage.** Do we define rubrics for all 9 PageTypes upfront, or ship people/company/meeting first and extend incrementally?
|
||||
8. **Budget config UX.** Hard daily cap is strict; should we also expose a soft-cap warning mode, and how is the cap set (env var? config file? prompt on first use?)
|
||||
9. **Backwards compat.** `src/commands/publish.ts` and `src/commands/backlinks.ts` have been running cleanly for weeks. Refactoring through BrainWriter carries migration risk. Acceptable?
|
||||
10. **Existing TODOS alignment.** `TODOS.md` has P0 "Runtime MCP access control" and P2 security hardening. The new RuntimeContext.remote flag interacts with both — do we fold MCP access control into Phase 0 or keep separate?
|
||||
|
||||
---
|
||||
|
||||
## 12. Verification (the "your OpenClaw would adopt" test)
|
||||
|
||||
The design succeeds iff:
|
||||
|
||||
- [ ] A user can add a new resolver by dropping a YAML + TS module in `~/.gbrain/resolvers/` without editing GBrain source.
|
||||
- [ ] Your OpenClaw can delete `scripts/x-api-client.mjs` and replace all callers with 1-line `await registry.resolve('x_handle_to_tweet', ...)`.
|
||||
- [ ] No brain page can be written with a bare tweet reference, a missing back-link, or an unverified URL (validators catch it pre-commit).
|
||||
- [ ] Running `gbrain integrity --auto --confidence 0.8` over a real brain fixes ≥1,000 of the 1,424 known bare-tweet citations without human review.
|
||||
- [ ] Full E2E test suite passes on both PGLite + Postgres engines.
|
||||
- [ ] The Knowledge Runtime ships across 7 phases with each phase individually shippable and reversible.
|
||||
@@ -1,448 +0,0 @@
|
||||
---
|
||||
status: ACTIVE
|
||||
---
|
||||
# CEO Plan: Minions as Universal Agent Orchestration Protocol
|
||||
Generated by /plan-ceo-review on 2026-04-15
|
||||
Branch: garrytan/minions-jobs | Mode: SCOPE EXPANSION
|
||||
Repo: garrytan/gbrain
|
||||
|
||||
## Vision
|
||||
|
||||
### 10x Check
|
||||
Instead of "GBrain has a queue, OpenClaw uses it," make Minions a universal agent
|
||||
orchestration protocol. Any platform (OpenClaw, Hermes, Claude Code, Codex, custom
|
||||
scripts) submits, monitors, steers, and composes agents through the same Postgres-native
|
||||
protocol. GBrain IS the agent control plane.
|
||||
|
||||
### Platonic Ideal (aspirational North Star, NOT in v1 scope)
|
||||
Open a terminal, type `gbrain jobs dashboard`. See every agent across every platform.
|
||||
Their progress, tool calls, token spend. Click any agent for full execution trace.
|
||||
Type a message to redirect a running agent mid-flight. See the governor's decisions
|
||||
visualized. Run A/B tests between agent configurations. The feeling: complete
|
||||
situational awareness of your AI workforce.
|
||||
|
||||
**Note:** The dashboard, A/B testing, and visual governor are future phases. This plan
|
||||
builds the primitives they would sit on top of: real-time events, structured progress,
|
||||
token accounting, inbox with ack, and session transcripts.
|
||||
|
||||
## Scope Decisions
|
||||
|
||||
| # | Proposal | Effort | Decision | Reasoning |
|
||||
|---|----------|--------|----------|-----------|
|
||||
| 1 | pg LISTEN/NOTIFY real-time events | S | ACCEPTED | Sub-second event delivery vs 5s polling. Every platform benefits. |
|
||||
| 2 | Structured progress protocol | S | ACCEPTED | Standard progress makes unified dashboard possible. |
|
||||
| 3 | Job cost tracking (token accounting) | M | ACCEPTED | Token cost is #1 thing users want to know about agent work. |
|
||||
| 4 | Job replay | S | ACCEPTED | Small surface area, high utility for debugging failures. |
|
||||
| 5 | Job groups / waves | M | DEFERRED | Parent-child already provides grouping. Overlap concern. |
|
||||
| 6 | Inbox acknowledgment (read receipts) | S | ACCEPTED | Without it, inbox is fire-and-forget — same problem we're fixing. |
|
||||
| 7 | Universal agent protocol | S | ACCEPTED | Design framing, not extra code. Platform-agnostic naming/docs. |
|
||||
| 8 | Session transcript capture | M | ACCEPTED | Full audit trail of every agent run. |
|
||||
|
||||
## Accepted Scope — Implementation Detail
|
||||
|
||||
### 0a. Pause/resume (from base plan)
|
||||
|
||||
**Schema:** Add `'paused'` to `MinionJobStatus` (already in migration v6 constraint).
|
||||
|
||||
**New methods:**
|
||||
- `MinionQueue.pauseJob(id): MinionJob | null`
|
||||
Transitions `waiting` or `active` → `paused`. For `active` jobs, clears `lock_token`
|
||||
and `lock_until` (worker will detect lock loss and stop). Returns null if job not
|
||||
in pausable state.
|
||||
- `MinionQueue.resumeJob(id): MinionJob | null`
|
||||
Transitions `paused` → `waiting`. Resets for claiming. Returns null if not paused.
|
||||
|
||||
**Worker integration:** Worker's lock renewal loop checks `isActive()`. When a job
|
||||
is paused, the lock is cleared, so `renewLock()` returns false and the worker stops
|
||||
execution gracefully (same path as stall detection). The job's progress and state
|
||||
are preserved in the DB for when it resumes.
|
||||
|
||||
**MCP operations:** `pause_job`, `resume_job` (added in Step 3 of implementation plan).
|
||||
|
||||
**PGLite compatibility:** Full.
|
||||
|
||||
### 0b. Resource governor (from base plan)
|
||||
|
||||
**New file:** `src/core/minions/governor.ts`
|
||||
|
||||
```typescript
|
||||
interface GovernorConfig {
|
||||
maxConcurrency: number; // ceiling
|
||||
minConcurrency: number; // floor (default 1)
|
||||
checkIntervalMs: number; // default 10000
|
||||
cpuThreshold: number; // default 0.80 (80%)
|
||||
memoryThreshold: number; // default 0.85 (85%)
|
||||
circuitBreakerMemory: number; // default 0.90 (90%)
|
||||
}
|
||||
|
||||
class ResourceGovernor {
|
||||
getEffectiveConcurrency(): number; // current allowed concurrency
|
||||
start(): void; // begin polling system metrics
|
||||
stop(): void; // stop polling
|
||||
onCircuitBreak(cb: (jobId) => void): void; // kill callback
|
||||
}
|
||||
```
|
||||
|
||||
**System metrics:** Reuse `getSystemLoad()` from `src/core/backoff.ts` (already
|
||||
implements CPU and memory checks). Add event loop lag measurement via
|
||||
`perf_hooks.monitorEventLoopDelay()`.
|
||||
|
||||
**Worker integration:** `MinionWorker.start()` consults `governor.getEffectiveConcurrency()`
|
||||
before claiming new jobs. If current in-flight count >= effective concurrency, skip claim.
|
||||
|
||||
**Circuit breaker:** If memory > 90%, governor calls `onCircuitBreak` with the
|
||||
lowest-priority active job ID. Worker cancels that job via `failJob()` with
|
||||
`UnrecoverableError("circuit breaker: memory pressure")`.
|
||||
|
||||
**Prerequisite:** Concurrent job processing must be implemented first (see
|
||||
Concurrency Note below).
|
||||
|
||||
**PGLite compatibility:** Full (governor is app-level, not DB-level).
|
||||
|
||||
### 1. pg LISTEN/NOTIFY (real-time events)
|
||||
|
||||
**Schema:** No new columns. Add NOTIFY triggers to state transitions.
|
||||
|
||||
**SQL trigger:**
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION notify_minion_job_change() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('minion_jobs', json_build_object(
|
||||
'id', NEW.id, 'status', NEW.status, 'name', NEW.name,
|
||||
'queue', NEW.queue, 'prev_status', COALESCE(OLD.status, 'new')
|
||||
)::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER minion_job_notify AFTER INSERT OR UPDATE OF status ON minion_jobs
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_minion_job_change();
|
||||
```
|
||||
|
||||
**New method:** `MinionQueue.subscribe(callback: (event) => void): () => void`
|
||||
Returns unsubscribe function. Requires direct Postgres connection (NOT pooled).
|
||||
|
||||
**PGLite compatibility:** PGLite does NOT support LISTEN/NOTIFY. Fallback: polling
|
||||
via `getJob()` at configurable interval (default 2s). The `subscribe()` method
|
||||
detects engine type and uses polling fallback automatically.
|
||||
|
||||
**Supabase constraint:** Requires direct connection (port 5432), not pgBouncer
|
||||
pooler (port 6543). Document in skill file and setup guide.
|
||||
|
||||
### 2. Structured progress protocol
|
||||
|
||||
**TypeScript interface (convention, not enforced at DB level):**
|
||||
```typescript
|
||||
interface AgentProgress {
|
||||
step: number; // current step (1-based)
|
||||
total: number; // total expected steps (0 = unknown)
|
||||
message: string; // human-readable status
|
||||
tokens_in: number; // cumulative input tokens
|
||||
tokens_out: number; // cumulative output tokens
|
||||
last_tool: string; // name of last tool called
|
||||
started_at: string; // ISO 8601 when this step started
|
||||
}
|
||||
```
|
||||
|
||||
**Storage:** Existing `progress JSONB` column. No schema change needed.
|
||||
Handlers use `ctx.updateProgress(agentProgress)`. Non-agent jobs can use
|
||||
any JSONB shape (backward compatible).
|
||||
|
||||
**Validation:** `updateProgress()` accepts any JSONB. The `AgentProgress`
|
||||
interface is a convention enforced by the agent handler, not by the queue.
|
||||
|
||||
### 3. Job cost tracking (token accounting)
|
||||
|
||||
**Schema changes (migration v6):**
|
||||
```sql
|
||||
ALTER TABLE minion_jobs ADD COLUMN tokens_input INTEGER DEFAULT 0;
|
||||
ALTER TABLE minion_jobs ADD COLUMN tokens_output INTEGER DEFAULT 0;
|
||||
ALTER TABLE minion_jobs ADD COLUMN tokens_cache_read INTEGER DEFAULT 0;
|
||||
ALTER TABLE minion_jobs ADD COLUMN cost_usd NUMERIC(10,6) DEFAULT 0;
|
||||
```
|
||||
|
||||
**New method:** `MinionQueue.updateTokens(id, lockToken, { input, output, cache_read, cost_usd })`
|
||||
Accumulates (adds to existing values, does not replace).
|
||||
|
||||
**Parent rollup:** When `completeJob()` is called, if `parent_job_id` is set,
|
||||
add this job's token counts to the parent's via:
|
||||
```sql
|
||||
UPDATE minion_jobs SET
|
||||
tokens_input = tokens_input + $child_input,
|
||||
tokens_output = tokens_output + $child_output,
|
||||
tokens_cache_read = tokens_cache_read + $child_cache,
|
||||
cost_usd = cost_usd + $child_cost
|
||||
WHERE id = $parent_id;
|
||||
```
|
||||
|
||||
**PGLite compatibility:** Full support (standard columns).
|
||||
|
||||
### 4. Job replay
|
||||
|
||||
**New method:** `MinionQueue.replayJob(id, dataOverrides?: Record<string, unknown>): MinionJob`
|
||||
|
||||
Implementation: Read the completed/failed/dead job. Create a NEW job with:
|
||||
- Same `name`, `queue`, `priority`, `max_attempts`, `backoff_type`, `backoff_delay`
|
||||
- `data` = deep merge of original data + overrides
|
||||
- Fresh `attempts_made: 0`, `status: 'waiting'`
|
||||
- `parent_job_id` = null (replay is a new top-level job, not a child)
|
||||
- Does NOT clone children (replay is a single job, not a DAG)
|
||||
|
||||
**Constraint:** Only works on terminal statuses (completed/failed/dead).
|
||||
Returns the new job record.
|
||||
|
||||
**Idempotency:** Each replay creates a distinct new job. No deduplication.
|
||||
If the original had side effects, the replay may repeat them. Document this
|
||||
in the skill file as a user responsibility.
|
||||
|
||||
### 5. Inbox (sidechannel messaging)
|
||||
|
||||
**Schema changes (migration v6):**
|
||||
```sql
|
||||
ALTER TABLE minion_jobs ADD COLUMN inbox JSONB DEFAULT '[]';
|
||||
```
|
||||
|
||||
**Inbox message format:**
|
||||
```typescript
|
||||
interface InboxMessage {
|
||||
id: string; // UUIDv4
|
||||
sent_at: string; // ISO 8601
|
||||
read_at: string | null; // null until worker reads it
|
||||
sender: string; // 'parent' | 'user' | job ID
|
||||
payload: unknown; // arbitrary directive
|
||||
}
|
||||
```
|
||||
|
||||
**New methods:**
|
||||
- `MinionQueue.sendMessage(jobId, payload, sender?): InboxMessage`
|
||||
Appends message to inbox array via atomic JSONB append
|
||||
(`inbox = inbox || $1::jsonb`), not read-modify-write. Returns the message with id + sent_at.
|
||||
- `MinionQueue.readInbox(jobId, lockToken): InboxMessage[]`
|
||||
Returns unread messages (read_at = null). Marks them as read (sets read_at).
|
||||
Token-fenced: only the worker holding the lock can read.
|
||||
|
||||
**Worker integration:** Agent handler calls `readInbox()` on each iteration.
|
||||
If messages exist, injects them into the agent's context as system messages.
|
||||
|
||||
**PGLite compatibility:** Full support (standard JSONB column).
|
||||
|
||||
### 6. Inbox acknowledgment (read receipts)
|
||||
|
||||
Built into the inbox design above. The `read_at` field on each `InboxMessage`
|
||||
provides the receipt. `sendMessage()` returns the message ID; the sender can
|
||||
later check `getJob(id)` and inspect `inbox` to see which messages have been
|
||||
read.
|
||||
|
||||
No additional schema or methods needed beyond what's in #5.
|
||||
|
||||
### 7. Universal agent protocol (platform-agnostic framing)
|
||||
|
||||
**This is a design decision, not code.** It means:
|
||||
|
||||
1. The skill file (`skills/minion-orchestrator/SKILL.md`) is written for ANY
|
||||
agent platform, not just OpenClaw. Examples show MCP tool calls, not
|
||||
OpenClaw-specific commands.
|
||||
|
||||
2. The agent handler (`agent-handler.ts`) accepts a generic interface:
|
||||
```typescript
|
||||
interface AgentJobData {
|
||||
prompt: string;
|
||||
tools?: string[]; // MCP tool names
|
||||
model?: string; // e.g., 'claude-opus-4-6', 'gpt-4o'
|
||||
context?: string; // additional context
|
||||
platform?: string; // 'openclaw' | 'hermes' | 'claude-code' | 'custom'
|
||||
max_iterations?: number; // agent loop budget
|
||||
}
|
||||
```
|
||||
|
||||
3. The OpenClaw plugin is ONE consumer. Hermes, Claude Code extensions,
|
||||
or custom scripts can submit `agent` jobs through the same MCP operations.
|
||||
|
||||
4. **NOT in v1 scope:** Multi-tenant auth, cross-network connectivity,
|
||||
protocol versioning, API key isolation. These are Phase 2 concerns when
|
||||
actual multi-platform usage materializes. v1 is single-user, single-brain.
|
||||
|
||||
### Agent Handler Architecture (critical design decision)
|
||||
|
||||
The agent handler does NOT live in GBrain. GBrain provides the queue infrastructure
|
||||
and a clean handler contract. The actual agent execution lives in the platform plugin.
|
||||
|
||||
```
|
||||
GBrain (this repo):
|
||||
MinionQueue — queue/claim/complete/inbox/tokens/NOTIFY
|
||||
MinionWorker — poll/lock/stall/governor framework
|
||||
Handler contract — AgentJobData interface + MinionJobContext
|
||||
|
||||
OpenClaw plugin (separate repo):
|
||||
Registers "agent" handler with MinionWorker
|
||||
Handler calls OpenClaw's PI agent core (the actual LLM loop)
|
||||
Each iteration: readInbox → inject as system message, updateProgress, updateTokens
|
||||
Completion: store result + session transcript in job.result + job.stacktrace
|
||||
|
||||
GBrain ships a test/echo handler for unit testing only.
|
||||
```
|
||||
|
||||
**Handler contract (GBrain side):**
|
||||
```typescript
|
||||
// The handler receives this context (already exists in worker.ts)
|
||||
interface MinionJobContext {
|
||||
id: number;
|
||||
name: string;
|
||||
data: Record<string, unknown>; // AgentJobData when name="agent"
|
||||
attempts_made: number;
|
||||
updateProgress(progress: unknown): Promise<void>;
|
||||
updateTokens(tokens: TokenUpdate): Promise<void>; // NEW
|
||||
log(message: string | TranscriptEntry): Promise<void>;
|
||||
isActive(): Promise<boolean>;
|
||||
readInbox(): Promise<InboxMessage[]>; // NEW
|
||||
}
|
||||
```
|
||||
|
||||
**Why this is right:** GBrain is orchestration, not execution. OpenClaw has the
|
||||
PI agent core. Hermes has AIAgent. Claude Code has its own loop. Each platform
|
||||
brings its own engine and registers a handler. GBrain manages lifecycle, progress,
|
||||
steering, cost tracking, and persistence around it.
|
||||
|
||||
### 8. Session transcript capture
|
||||
|
||||
**Extends existing stacktrace mechanism.** The `stacktrace` field (JSONB array
|
||||
of strings) already captures log messages. Session transcripts use the same
|
||||
field with structured entries:
|
||||
|
||||
```typescript
|
||||
type TranscriptEntry =
|
||||
| { type: 'log'; message: string; ts: string }
|
||||
| { type: 'tool_call'; tool: string; args_size: number; result_size: number; ts: string }
|
||||
| { type: 'llm_turn'; model: string; tokens_in: number; tokens_out: number; ts: string }
|
||||
| { type: 'error'; message: string; stack?: string; ts: string };
|
||||
```
|
||||
|
||||
**Storage:** Existing `stacktrace JSONB` column. No schema change.
|
||||
The agent handler appends `TranscriptEntry` objects instead of plain strings.
|
||||
Backward compatible: non-agent jobs continue appending strings.
|
||||
|
||||
**Size concern:** Long agent runs could generate large transcripts. Add a
|
||||
`max_transcript_entries` option (default 1000) that rotates oldest entries
|
||||
when exceeded (FIFO). The full transcript for forensic analysis can be
|
||||
stored as a brain file via `gbrain files upload-raw`.
|
||||
|
||||
## Schema Migration v6
|
||||
|
||||
All schema changes are additive (ALTER TABLE ADD COLUMN). No backfill needed.
|
||||
Existing jobs continue to work with default values.
|
||||
|
||||
```sql
|
||||
-- Migration v6: Agent orchestration primitives
|
||||
ALTER TABLE minion_jobs ADD COLUMN IF NOT EXISTS tokens_input INTEGER DEFAULT 0;
|
||||
ALTER TABLE minion_jobs ADD COLUMN IF NOT EXISTS tokens_output INTEGER DEFAULT 0;
|
||||
ALTER TABLE minion_jobs ADD COLUMN IF NOT EXISTS tokens_cache_read INTEGER DEFAULT 0;
|
||||
|
||||
-- Separate inbox table (not JSONB on job row)
|
||||
CREATE TABLE IF NOT EXISTS minion_inbox (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_id INTEGER NOT NULL REFERENCES minion_jobs(id) ON DELETE CASCADE,
|
||||
sender TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
read_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_minion_inbox_unread
|
||||
ON minion_inbox (job_id) WHERE read_at IS NULL;
|
||||
|
||||
-- Status constraint update: add 'paused'
|
||||
ALTER TABLE minion_jobs DROP CONSTRAINT IF EXISTS minion_jobs_status_check;
|
||||
ALTER TABLE minion_jobs ADD CONSTRAINT minion_jobs_status_check
|
||||
CHECK (status IN ('waiting','active','completed','failed','delayed','dead','cancelled','waiting-children','paused'));
|
||||
|
||||
-- NOTIFY trigger for real-time events (Postgres only, not PGLite)
|
||||
CREATE OR REPLACE FUNCTION notify_minion_job_change() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('minion_jobs', json_build_object(
|
||||
'id', NEW.id, 'status', NEW.status, 'name', NEW.name,
|
||||
'queue', NEW.queue, 'prev_status', COALESCE(OLD.status, 'new')
|
||||
)::text);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER minion_job_notify AFTER INSERT OR UPDATE OF status ON minion_jobs
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_minion_job_change();
|
||||
```
|
||||
|
||||
## PGLite Compatibility Matrix
|
||||
|
||||
| Feature | Postgres | PGLite | Fallback |
|
||||
|---|---|---|---|
|
||||
| Pause/resume | Full | Full | — |
|
||||
| Inbox + ack | Full | Full | — |
|
||||
| Token accounting | Full | Full | — |
|
||||
| Job replay | Full | Full | — |
|
||||
| LISTEN/NOTIFY | Full | NO | Polling (2s interval) |
|
||||
| NOTIFY trigger | Full | NO | Skipped in PGLite schema |
|
||||
| Structured progress | Full | Full | — |
|
||||
| Session transcripts | Full | Full | — |
|
||||
| Resource governor | Full | Full | — |
|
||||
| Worker daemon | Full | NO (existing limitation) | — |
|
||||
|
||||
## Concurrency Note
|
||||
|
||||
The current `MinionWorker.start()` processes jobs sequentially (one at a time)
|
||||
despite `concurrency` being declared in `MinionWorkerOpts`. Implementing actual
|
||||
concurrent job processing (Promise pool) is a prerequisite for the resource
|
||||
governor to be meaningful. The governor adjusts effective concurrency, which
|
||||
requires actual concurrent processing to exist.
|
||||
|
||||
**Action:** Implement concurrent job processing in `worker.ts` before or as
|
||||
part of the governor step. Use a semaphore pattern: maintain up to N in-flight
|
||||
promises, claim new jobs as slots free up.
|
||||
|
||||
## Outside Voice Decisions (from adversarial review)
|
||||
|
||||
1. **AbortController for pause/resume** — Handler contract gets `signal: AbortSignal`.
|
||||
Pause clears lock AND signals abort. Handler must check `signal.aborted` on each
|
||||
iteration. Without this, pausing active jobs creates duplicate execution.
|
||||
|
||||
2. **Drop cost_usd column** — Token counts (input/output/cache_read) are stable facts.
|
||||
USD pricing is volatile. Compute cost at display/read time from a pricing table,
|
||||
not at write time. Removes `cost_usd NUMERIC(10,6)` from migration v6.
|
||||
|
||||
3. **Separate minion_inbox table** — Instead of JSONB array on job row, use a dedicated
|
||||
table for inbox messages. Avoids row bloat from rewriting entire inbox on every send.
|
||||
Properly concurrent-safe with standard INSERT (no JSONB append concerns).
|
||||
```sql
|
||||
CREATE TABLE minion_inbox (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_id INTEGER NOT NULL REFERENCES minion_jobs(id) ON DELETE CASCADE,
|
||||
sender TEXT NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
read_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX idx_minion_inbox_unread ON minion_inbox (job_id) WHERE read_at IS NULL;
|
||||
```
|
||||
|
||||
4. **One release, not two** — Ship all features in one migration (v6). User prefers
|
||||
cohesive release over incremental delivery for this feature set.
|
||||
|
||||
5. **Selective column projection** — Fix SELECT * queries in getJobs(), claim(),
|
||||
handleStalled() to exclude stacktrace column. Include stacktrace only in getJob()
|
||||
detail view. Prevents transcript bloat from affecting query performance.
|
||||
|
||||
## Future Phases (accepted trajectory)
|
||||
|
||||
- **Phase 2: Dashboard CLI** — `gbrain jobs dashboard` live TUI showing all agents.
|
||||
Enabled by: LISTEN/NOTIFY, structured progress, token accounting.
|
||||
- **Phase 3: Multi-tenant auth** — Runtime MCP access control, per-platform API keys.
|
||||
Enabled by: platform-agnostic framing, sender validation on inbox.
|
||||
- **Phase 4: Agent composition patterns** — Map-reduce, pipeline, approval gates as
|
||||
first-class primitives. Enabled by: parent-child DAGs, inbox sidechannel.
|
||||
|
||||
## Deferred to TODOS.md
|
||||
- Job groups / waves (parent-child covers this; revisit if real grouping need emerges)
|
||||
- cost_usd column (compute from pricing table at read time when pricing API exists)
|
||||
|
||||
## Key Premises Confirmed
|
||||
1. GBrain is intentionally evolving from knowledge brain to agent infrastructure (user confirmed)
|
||||
2. Coupling between OpenClaw and GBrain's Postgres is acceptable (OpenClaw already depends on GBrain)
|
||||
3. Full Infrastructure approach (all 8+ steps) selected over Minimal Viable or Sidecar Tracking
|
||||
4. Prior learning [agent-dx-instruction-layer] validates that the teaching layer (skill + evals) is mandatory
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,221 +0,0 @@
|
||||
---
|
||||
status: ACTIVE
|
||||
---
|
||||
# CEO Plan: v0.38 Schema Packs — Bring Your Own Shape
|
||||
|
||||
Generated by /plan-ceo-review on 2026-05-19
|
||||
Branch: garrytan/houston-v1 | Mode: EXPANSION
|
||||
Repo: garrytan/gbrain
|
||||
|
||||
## Definitions (terms used throughout)
|
||||
|
||||
- **Primitive** — a named bundle of (default link verbs, default
|
||||
frontmatter fields, expert-routing flag, enrichment rubric slot).
|
||||
Five built-in: `entity`, `media`, `temporal`, `annotation`,
|
||||
`concept`. A pack type extends one primitive by name, inheriting
|
||||
its defaults, then optionally overriding specific fields. Not a
|
||||
table shape, not a schema in the SQL sense — a behavioral
|
||||
template the engine consults at inference and search time.
|
||||
- **Alias closure** — for read paths, when a pack declares type
|
||||
`researcher` aliases base type `person`, queries for `researcher`
|
||||
expand the WHERE clause to `type IN ('researcher','person', + any
|
||||
other type aliasing person)`. The closure is computed once at
|
||||
pack load, cached on the pack object, and inlined into search
|
||||
SQL. Aliasing is one-directional (researcher → person; querying
|
||||
`person` does NOT surface `researcher` rows unless the inverse is
|
||||
declared).
|
||||
- **Pack resolution chain (7 tiers)** — extends model-config's
|
||||
6-tier pattern. Order: (1) per-call `schema_pack` opt, (2)
|
||||
`GBRAIN_SCHEMA_PACK` env, (3) per-source `--source <id>` override
|
||||
via DB config key `schema_pack:source:<id>`, (4) brain-wide DB
|
||||
config key `schema_pack`, (5) `gbrain.yml schema:` section,
|
||||
(6) `~/.gbrain/config.json schema_pack`, (7) default `gbrain-base`.
|
||||
Tier 3 is the new tier introduced in v0.38; tiers 1, 2, 4-7
|
||||
mirror existing patterns.
|
||||
|
||||
## Vision
|
||||
|
||||
### 10x Check
|
||||
The plan as accepted ships a self-EXPANDING engine, not just a
|
||||
self-describing one. The differences from the baseline plan:
|
||||
|
||||
- The brain watches what you create and proposes schema refinements
|
||||
you didn't think to ask for (`schema suggest`)
|
||||
- Schema is per-source (ISOLATED reads), so ~/git/brain and
|
||||
~/git/zion-brain hold different mental models in the same engine
|
||||
without renames. Cross-source federated reads still see per-source
|
||||
packs in isolation — a query joining results across mounts does
|
||||
NOT compute a closure across both packs. Federation (closure
|
||||
across mounts) is explicitly deferred to v0.39.
|
||||
- The pack is inspectable: ASCII graph, plain-English explanation,
|
||||
consistency lint against actual content
|
||||
- First unknown-type write asks "Add to pack?" with a primitive
|
||||
inference, instead of silently logging
|
||||
- Schema packs distribute as `.gbrain-schema` tarballs through the
|
||||
v0.37 skillpack pipeline; skillpacks rename to `.gbrain-skillpack`
|
||||
for symmetry. Community schema packs propagate the same way
|
||||
community skillpacks do.
|
||||
|
||||
### Platonic Ideal
|
||||
A new user clones gbrain and types `gbrain init`. Within 30 seconds
|
||||
gbrain has read their existing markdown anywhere on disk, proposed a
|
||||
schema matching their organic shape, asked 3-5 yes/no questions to
|
||||
refine, and the brain is live. They never author YAML unless they
|
||||
want to. They can publish their pack as a `.gbrain-schema` tarball
|
||||
for anyone to install and fork.
|
||||
|
||||
The 12-month state: `gbrain init` runs `schema detect` automatically,
|
||||
proposes a primitive structure, and 90% of users never see the
|
||||
manifest format. The 10% who want to customize see a clean YAML they
|
||||
can edit. Community packs cover the long tail of domains.
|
||||
|
||||
## Scope Decisions
|
||||
|
||||
| # | Proposal | Effort | Decision | Reasoning |
|
||||
|---|----------|--------|----------|-----------|
|
||||
| 0C-bis | Approach C (Full Cathedral) | ~4 weeks | ACCEPTED | User explicitly chose the most ambitious of three approaches; ecosystem + engine in one ship |
|
||||
| D2 | Per-source schema packs | ~1 week | ACCEPTED | User owns two brains today; v0.34.1.0 source-isolation makes the seam architecturally clean |
|
||||
| D3 | `gbrain schema suggest` (LLM-powered) | ~3-5 days | ACCEPTED | Closes the gap from "what exists" to "what your brain implies"; bounded cost via sampling |
|
||||
| D4 | `schema graph` + `lint` + `explain` | ~2 days | ACCEPTED | Schema becomes legible and self-documenting; tiny effort, large UX delta |
|
||||
| D5 | Auto-prompt on first unknown type | ~1-2 days | ACCEPTED | TTY-gated + per-type silenceable; turns lenient-mode from fallback to feature |
|
||||
| D6-orig | `fork-from <brain-path>` (live-brain) | ~3-5 days | REJECTED | Privacy hazard (read access to whole repo); unclear value vs published tarballs |
|
||||
| D6-reframed | Skillpack tarball reuse + extension expansion | ~3-5 days | ACCEPTED | Schema packs ship as `.gbrain-schema`; skillpacks gain `.gbrain-skillpack` extension alongside existing `.tgz`; both ride v0.37 pipeline parameterized on manifest discriminator. Extension is the install-time type detector — lets validation route to the right manifest validator before extraction. |
|
||||
|
||||
Total budget: **revised 9-11 weeks** (vs ~6.5-7 initial estimate;
|
||||
spec review surfaced LLM prompt-tuning loops for `schema suggest`,
|
||||
primitive-inference heuristics for auto-prompt, 7-tier × federated-
|
||||
read interaction edges, full rename-migration surface, and 400-600
|
||||
test cases at v0.36/v0.37 scope precedent). If budget pressure
|
||||
emerges, the safest cuts in order are: D5 auto-prompt (~2 days),
|
||||
D4 inspect triad (~2 days), reduce examples 7→3 (~3 days), defer
|
||||
suggest LLM polish to v0.38.1 (~1 week).
|
||||
|
||||
## Accepted Scope (added to this plan)
|
||||
|
||||
- **Engine layer:** gbrain-base universal starter pack; 5 composable
|
||||
primitives (entity, media, temporal, annotation, concept); alias
|
||||
closure for read paths; lenient-by-default with audit for write
|
||||
paths; strict mode opt-in.
|
||||
- **Detect layer:** `gbrain schema detect` SQL-driven heuristic
|
||||
clustering proposing a pack manifest matching brain shape.
|
||||
- **Suggest layer:** `gbrain schema suggest` LLM-powered refinement
|
||||
via gateway.chat() over a bounded sample.
|
||||
- **Inspect layer:** `gbrain schema graph` (ASCII viz),
|
||||
`gbrain schema lint` (consistency check), `gbrain schema explain
|
||||
<type>` (plain English).
|
||||
- **Author layer:** `gbrain schema init/use/fork/edit/validate/
|
||||
diff/review-candidates` CLI.
|
||||
- **Source layer:** per-source schema-pack resolution; pack
|
||||
resolution gets a 7th tier (per-source override before per-brain);
|
||||
`--source <id>` flag on every relevant command.
|
||||
- **Auto-prompt layer:** TTY-gated interrupt on first unknown-type
|
||||
`put_page` with primitive inference; per-type "always silent"
|
||||
escape hatch.
|
||||
- **Distribution layer:** `.gbrain-schema` tarball format; rename
|
||||
skillpacks to `.gbrain-skillpack`; v0.37 skillpack pipeline
|
||||
parameterized on artifact type (manifest discriminator drives
|
||||
type-specific validation); both extensions accepted on install
|
||||
for back-compat.
|
||||
- **Examples:** 7 example packs in-tree (minimal, person-first,
|
||||
media-archive, temporal-archive, research-notebook, founder-ops,
|
||||
personal-archive) explicitly framed as sketches not products.
|
||||
- **gbrain-base:** byte-for-byte reproduces today's hardcoded
|
||||
behavior so existing brains see zero change after upgrade.
|
||||
- **Migrations:** v76 drops `takes.kind` CHECK constraint;
|
||||
validation moves to runtime against active pack's declared kinds.
|
||||
- **Doctor checks:** schema_pack_active, schema_pack_consistency,
|
||||
per-source pack drift.
|
||||
- **Engine refactor coverage:** the v0.38 plan parameterizes EVERY
|
||||
hardcoded type-coupling site listed in the original exploration,
|
||||
not just `takes.kind`. Concretely: `inferType` path-prefix table,
|
||||
`inferLinkType` regex bank, `FRONTMATTER_FIELD_OVERRIDES` table,
|
||||
`find_experts` SQL (`type IN (…)`), `whoknows` `DEFAULT_TYPES`,
|
||||
`enrichment-service` person/company restriction,
|
||||
`completeness.ts` rubric map, dream-cycle entity-type prompts.
|
||||
gbrain-base reproduces today's values for each.
|
||||
- **Cache + rollback story:**
|
||||
- `query_cache.knobs_hash` (v0.32.3 column) folds `schema_pack`
|
||||
name + version into the hash so a cache row written under
|
||||
`vc` is unreachable when `research-state` is active. Cross-
|
||||
pack contamination structurally impossible.
|
||||
- `eval_candidates` rows (v0.25.0) gain a `schema_pack` column
|
||||
so `gbrain eval replay` reproduces the same retrieval space.
|
||||
Migration v77 adds the column NULL-tolerant; pre-v0.38 rows
|
||||
fall back to active pack during replay.
|
||||
- HNSW indexes are pack-agnostic (vector columns don't change
|
||||
shape across packs); no reindex needed on pack switch.
|
||||
- Rollback: every `gbrain schema use` operation writes the
|
||||
previous pack name to `~/.gbrain/schema-pack-history.jsonl`
|
||||
so `gbrain schema use --previous` is one keystroke. Strict-
|
||||
mode failures on switch surface the offending pages with
|
||||
paste-ready "rename type to X" hints before any data
|
||||
mutation runs. Soft-deletes from autopilot purge are NOT
|
||||
triggered by pack changes.
|
||||
- **Test budget:** ~400-600 cases across unit + e2e per the
|
||||
v0.36/v0.37 precedent. Specifically: ~150 cases for engine layer
|
||||
+ alias closure, ~50 for detect heuristic accuracy, ~50 for
|
||||
suggest LLM prompts (hermetic via stubbed gateway), ~30 for per-
|
||||
source resolution × 7-tier matrix, ~40 for auto-prompt UX
|
||||
states, ~30 for inspect triad output stability, ~30 for tarball
|
||||
type-detection + parameterized install, ~50 for migration v76 +
|
||||
v77 + bootstrap parity, ~50 for examples × byte-for-byte
|
||||
gbrain-base equivalence regression. **gbrain-base byte-for-byte
|
||||
parity is a CI gate**, not a hope — pinned by
|
||||
`test/regressions/gbrain-base-equivalence.test.ts` asserting the
|
||||
pre-v0.38 hardcoded behavior reproduces from the pack-driven
|
||||
paths on a fixture brain.
|
||||
|
||||
## Deferred to TODOS.md (v0.39+)
|
||||
|
||||
- Live-brain `fork-from <brain-path>` (rejected for privacy; revisit
|
||||
if a sandboxed schema-only extraction path is designed)
|
||||
- Per-source pack FEDERATION across mounts (a query crossing
|
||||
multiple sources can use closure over each source's schema; right
|
||||
now per-source is isolated reads only)
|
||||
- Schema versioning + semver compatibility checks between pack
|
||||
versions
|
||||
- Skillpack ↔ schema-pack cross-reference (a skillpack can declare
|
||||
"I work best with these primitives present in your pack")
|
||||
- Live schema migration helpers (when you add a type, auto-suggest
|
||||
backfill of existing pages)
|
||||
- Schema diff in PR review (rendering pack changes as human-readable
|
||||
diffs for community pack PRs)
|
||||
|
||||
## Reviewer Concerns (from spec review loop, partially addressed)
|
||||
|
||||
- Quality score on first pass: 6.5/10. Issues addressed in this
|
||||
revision: definitions block (primitive, alias closure, 7-tier
|
||||
resolution chain), per-source isolation vs federation contradiction
|
||||
clarified, skillpack extension framing changed from rename to
|
||||
expansion, full hardcoded-site coverage enumerated, cache +
|
||||
rollback story added, test budget enumerated, budget revised to
|
||||
9-11 weeks honestly.
|
||||
- Issues NOT fully addressed, surfaced for the 11-section review:
|
||||
- `schema suggest` LLM prompt-tuning iteration budget remains a
|
||||
range estimate, not a measured number. The 11-section review
|
||||
should pin a specific eval fixture set (size + diversity) and
|
||||
a target accuracy threshold before code lands.
|
||||
- 7-tier resolution × v0.34.1 federated_read OAuth scoping has
|
||||
edge cases at the intersection that the 11-section review must
|
||||
enumerate (specifically: an OAuth client with read scope across
|
||||
federated sources but no source-specific pack override — which
|
||||
pack drives the alias closure for cross-source queries?).
|
||||
- The 7→3 example pack reduction is a real cut consideration. The
|
||||
11-section review should decide whether 7 examples is the right
|
||||
number or whether 3 + community-derived is more honest.
|
||||
|
||||
## Cathedral risks worth surfacing in 11-section review
|
||||
|
||||
1. The 7-week budget vs 4-week original ask. If pressure emerges,
|
||||
D5 (auto-prompt) and D4 (inspect triad) are the safest cuts.
|
||||
2. v0.37 skillpack registry currently has zero published packs.
|
||||
The `.gbrain-schema` rename and tarball reuse doubles down on a
|
||||
distribution layer with no usage signal.
|
||||
3. Per-source pack resolution adds a 7th tier to the resolution
|
||||
chain. The model-config 6-tier pattern is already cognitively
|
||||
dense; tier 7 is an inflection point.
|
||||
4. `schema suggest` introduces ongoing LLM cost per invocation.
|
||||
Bounded by sampling, but sets a precedent for "gbrain commands
|
||||
that cost money."
|
||||
5. Auto-prompt UX is novel. TTY gate + per-type silencing helps,
|
||||
but bulk-import flows could hit unexpected interruption patterns.
|
||||
@@ -1,151 +0,0 @@
|
||||
# Switching embedding models or dimensions on an existing brain
|
||||
|
||||
GBrain stores embeddings in a fixed-dimension `vector(N)` column on
|
||||
`content_chunks`. If you switch to a model with a different dimension
|
||||
(e.g. `openai:text-embedding-3-large` 1536 → `zeroentropyai:zembed-1`
|
||||
1280, or `voyage:voyage-4-large` 2048), the on-disk column type doesn't
|
||||
change automatically.
|
||||
|
||||
`gbrain init`, `gbrain doctor`, and `gbrain embed --stale` all detect
|
||||
this mismatch and refuse to silently proceed. This doc is the recipe
|
||||
they point at.
|
||||
|
||||
## Why we don't do this automatically
|
||||
|
||||
Switching dimensions requires:
|
||||
|
||||
1. Dropping the HNSW vector index (pgvector won't survive an `ALTER COLUMN TYPE`).
|
||||
2. Altering the column type (Postgres only — PGLite cannot do this).
|
||||
3. Wiping every existing embedding (the old vectors are unusable in the new space).
|
||||
4. Re-embedding the entire corpus (can take hours on a 50K-page brain and costs $1-100 in API calls depending on model).
|
||||
5. Conditionally recreating the index (HNSW supports up to 2000 dimensions per pgvector; above that you must use exact scans).
|
||||
|
||||
That's not an upgrade-time auto-run. It's a deliberate, expensive
|
||||
operation. Run it when you've decided you actually want the new model.
|
||||
|
||||
## PGLite (default install)
|
||||
|
||||
**PGLite cannot `ALTER COLUMN TYPE vector(N)`.** pgvector ships as
|
||||
embedded WASM, not a native extension, and the WASM build rejects the
|
||||
column-type alter with `could not access file "$libdir/vector"`. The
|
||||
SQL recipe below works against Postgres only.
|
||||
|
||||
The path that works on PGLite is **wipe-and-reinit**. v0.37 ships a
|
||||
single-command wrapper:
|
||||
|
||||
```bash
|
||||
gbrain reinit-pglite \
|
||||
--embedding-model zeroentropyai:zembed-1 \
|
||||
--embedding-dimensions 1280
|
||||
```
|
||||
|
||||
This backs up the existing brain to `<path>.bak`, runs `gbrain init`
|
||||
with the new flags (preserving every other field in
|
||||
`~/.gbrain/config.json`), and re-syncs the brain repo. Add `--no-sync`
|
||||
to skip the resync, `--yes` to skip the TTY confirmation, `--json` for
|
||||
structured output.
|
||||
|
||||
Equivalent by hand:
|
||||
|
||||
```bash
|
||||
# 1. Back up the existing brain (in case you want to roll back).
|
||||
mv ~/.gbrain/brain.pglite ~/.gbrain/brain.pglite.bak
|
||||
|
||||
# 2. Re-init with the new model + dimensions. `gbrain init` writes
|
||||
# the schema sized to the new dim, and (as of v0.37) preserves
|
||||
# every other field in ~/.gbrain/config.json (chat model,
|
||||
# expansion model, API keys).
|
||||
gbrain init --pglite \
|
||||
--embedding-model zeroentropyai:zembed-1 \
|
||||
--embedding-dimensions 1280
|
||||
|
||||
# 3. Re-import your brain repo. `gbrain sync` reads the brain repo
|
||||
# from disk and re-creates the page rows.
|
||||
gbrain sync
|
||||
|
||||
# 4. Re-embed. The embed pipeline now uses the new model and the
|
||||
# column accepts the new dim.
|
||||
gbrain embed --stale
|
||||
```
|
||||
|
||||
If your brain repo is large enough that re-syncing from disk is
|
||||
expensive (>50K pages), see the Postgres section below — migrating to
|
||||
Postgres temporarily lets you run the SQL recipe, then migrate back to
|
||||
PGLite.
|
||||
|
||||
`GBRAIN_HOME` users: substitute the active database path (or use
|
||||
`gbrain config get database_path` to find it).
|
||||
|
||||
## Postgres (Supabase / self-hosted)
|
||||
|
||||
Postgres supports the in-place column alter. Replace `<NEW_DIMS>` with
|
||||
your target dimension count.
|
||||
|
||||
```sql
|
||||
BEGIN;
|
||||
|
||||
-- 1. Drop the HNSW index. It can't survive the column type change.
|
||||
DROP INDEX IF EXISTS idx_chunks_embedding;
|
||||
|
||||
-- 2. Alter the column type.
|
||||
ALTER TABLE content_chunks ALTER COLUMN embedding TYPE vector(<NEW_DIMS>);
|
||||
|
||||
-- 3. Clear stale embeddings so they don't survive into the new space.
|
||||
UPDATE content_chunks SET embedding = NULL, embedded_at = NULL;
|
||||
|
||||
-- 4. Recreate the HNSW index ONLY IF dims <= 2000. Above that, leave it
|
||||
-- indexless and rely on exact scans (gbrain searchVector handles this
|
||||
-- automatically — search just gets slower, not broken).
|
||||
-- For dims <= 2000 (e.g. 1024, 1280, 1536, 768):
|
||||
CREATE INDEX IF NOT EXISTS idx_chunks_embedding
|
||||
ON content_chunks USING hnsw (embedding vector_cosine_ops);
|
||||
-- For dims > 2000 (e.g. 2048 Voyage 4 Large): skip step 4.
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
Then re-init config with the new model:
|
||||
|
||||
```bash
|
||||
gbrain init --supabase \
|
||||
--embedding-model <provider:model> \
|
||||
--embedding-dimensions <NEW_DIMS>
|
||||
```
|
||||
|
||||
And re-embed:
|
||||
|
||||
```bash
|
||||
gbrain embed --stale
|
||||
```
|
||||
|
||||
## A note on `gbrain config set`
|
||||
|
||||
Pre-v0.37 docs recommended `gbrain config set embedding_model X` to
|
||||
switch models. **This is a no-op for the embed pipeline.** `config set`
|
||||
writes the DB plane; the embed gateway reads the file plane
|
||||
(`~/.gbrain/config.json`). The pre-v0.37 recipe shipped the lie because
|
||||
the contract wasn't surfaced.
|
||||
|
||||
As of v0.37, `gbrain config set embedding_model` and `gbrain config set
|
||||
embedding_dimensions` REFUSE and print the wipe-and-reinit recipe.
|
||||
|
||||
To change schema-sizing fields, use `gbrain init` (PGLite) or the SQL
|
||||
recipe (Postgres). Both update the file plane AND the schema together.
|
||||
|
||||
## Verify
|
||||
|
||||
After the recipe lands, `gbrain doctor --fast` should report green and
|
||||
`gbrain doctor` should pass the `embedding_width_consistency` check:
|
||||
|
||||
```
|
||||
✓ embedding_width_consistency dim parity: config 1280 / column vector(1280)
|
||||
```
|
||||
|
||||
If it doesn't, file an issue with the doctor output and the steps you
|
||||
ran.
|
||||
|
||||
## v0.37+ followups
|
||||
|
||||
- Auto-fallback to alternative embedding providers when the primary
|
||||
fails quota/auth. Tracked; requires explicit `--try-fallback`
|
||||
consent because mixing provider vectors silently corrupts retrieval.
|
||||
@@ -1,188 +0,0 @@
|
||||
---
|
||||
type: essay
|
||||
title: "Homebrew for Personal AI"
|
||||
subtitle: "Why Markdown is Code and Your Agent is a Package Manager"
|
||||
author: Garry Tan
|
||||
created: 2026-04-11
|
||||
updated: 2026-04-11
|
||||
tags: [ai, gbrain, gstack, markdown-is-code, open-source, software-distribution, agents, openclaw]
|
||||
status: draft-v2
|
||||
prior: "Thin Harness, Fat Skills"
|
||||
---
|
||||
|
||||
# Homebrew for Personal AI
|
||||
|
||||
`brew install` gives you someone else's binary. `npm install` gives you someone else's source code. Both require you to understand the tool, configure it, integrate it, maintain it.
|
||||
|
||||
What if software distribution worked differently? What if you could describe a capability in plain English, hand that description to an AI agent, and the agent built a native implementation tailored to your setup?
|
||||
|
||||
That's what happens when markdown is code.
|
||||
|
||||
## Markdown is code
|
||||
|
||||
Here's a real skill file. This one teaches an AI agent to screen phone calls:
|
||||
|
||||
```markdown
|
||||
# Voice Agent — Your Phone Number
|
||||
|
||||
Caller → Twilio → <Stream> WebSocket → Voice Server (port 8765)
|
||||
↕ audio
|
||||
OpenAI Realtime API
|
||||
↓ tool calls
|
||||
Brain / Calendar / Telegram
|
||||
|
||||
## Call Routing
|
||||
|
||||
Every inbound call routes based on caller phone number + brain lookup:
|
||||
|
||||
### Owner → Authenticated Mode
|
||||
- Send crypto-random 6-digit code to secure channel
|
||||
- Caller reads it back
|
||||
- Match → full assistant mode (brain, calendar, scheduling)
|
||||
- No match → treated as unknown caller
|
||||
|
||||
### Known Person, Inner Circle (brain score ≥ 4) → Forward
|
||||
- Greet by name with brain context
|
||||
- Transfer to cell
|
||||
- If no answer (30s timeout), take message
|
||||
- Text Telegram with who called and context
|
||||
|
||||
### Unknown Caller → Screen
|
||||
- Get their name, look them up in brain
|
||||
- If inner circle → offer to transfer
|
||||
- Otherwise → take message
|
||||
- Create brain entry with phone number (marked UNVERIFIED)
|
||||
```
|
||||
|
||||
That's not pseudocode. That's not documentation. That's a working specification that a model like Claude Opus 4.6 with a million-token context window can read and implement. The architecture diagram tells it the components. The routing table tells it the logic. The security model tells it the constraints. The agent reads this file, understands it, and builds the Twilio integration, the WebSocket server, the Telegram bot hooks, the brain lookup, all of it, shaped to whatever infrastructure the user already has.
|
||||
|
||||
A skill file is a method call. It takes parameters (your phone number, your brain, your preferred messaging app). Same skill, different arguments, different implementation. The procedure is the package. The model is the runtime.
|
||||
|
||||
## The distribution mechanism
|
||||
|
||||
Traditional package managers distribute artifacts: compiled binaries, source tarballs, container images. The consumer runs someone else's code.
|
||||
|
||||
GBrain distributes recipes: markdown files that describe capabilities with enough specificity that an AI agent can implement them from scratch. The consumer gets a native implementation. No dependency hell. No version conflicts. No transitive vulnerability chains. Because there is no upstream code. There's a description of what to build and why.
|
||||
|
||||
Here's how it works:
|
||||
|
||||
1. **Build a feature.** Implement a voice agent, meeting ingestion pipeline, email triage system, investment diligence workflow, whatever.
|
||||
|
||||
2. **GBrain captures the recipe.** Not just the code. The architecture, the integration points, the failure modes, the judgment calls. A markdown file that encodes the full capability.
|
||||
|
||||
3. **Push to the repo.** Open source. Anyone can read it.
|
||||
|
||||
4. **Someone else's agent pulls the recipe.** Reads the markdown. Says: "New recipe available: AI voice agent with caller screening. Want it?" User says yes. The agent reads the spec and builds it.
|
||||
|
||||
No installation. No configuration wizard. No README. The agent read a document and figured it out.
|
||||
|
||||
## Why this works now
|
||||
|
||||
This didn't work two years ago. Two things changed.
|
||||
|
||||
**Context windows hit a million tokens.** A real skill file for meeting ingestion is 200+ lines. The enrichment skill that calls it references a brain schema, a resolver, a citation standard, five external APIs, and a cross-linking protocol. An agent implementing this recipe needs to hold all of that in working memory simultaneously while also understanding the user's existing setup. At 8K tokens, impossible. At 128K, marginal. At 1M, comfortable.
|
||||
|
||||
**Models crossed the judgment threshold.** Here's a snippet from a real enrichment recipe:
|
||||
|
||||
```markdown
|
||||
## Philosophy
|
||||
|
||||
A brain page should read like an intelligence dossier crossed
|
||||
with a therapist's notes, not a LinkedIn scrape. We want:
|
||||
|
||||
- What they believe — ideology, worldview, first principles
|
||||
- What they're building — current projects, what's next
|
||||
- What motivates them — ambition drivers, career arc
|
||||
- What makes them emotional — angry, excited, defensive, proud
|
||||
- Their trajectory — ascending, plateauing, pivoting, declining?
|
||||
- Hard facts — role, company, funding, location, contact info
|
||||
|
||||
Facts are table stakes. Texture is the value.
|
||||
```
|
||||
|
||||
A model implementing this recipe has to understand the difference between a LinkedIn scrape and an intelligence dossier. That's a judgment call about what information is worth capturing and how to weight it. GPT-3 couldn't do this. GPT-4 could sort of do it. Opus 4.6 does it well. The enabling technology is models that are smart enough to interpret intent, not just follow instructions.
|
||||
|
||||
## What a recipe actually contains
|
||||
|
||||
A good recipe has five sections:
|
||||
|
||||
**Architecture.** The component diagram. What talks to what, over what protocol, with what data flow. This is the skeleton the agent builds first.
|
||||
|
||||
**Routing logic.** The decision tree. When X happens, do Y. When Z fails, fall back to W. This is where domain knowledge lives. A voice agent recipe encodes call routing. A diligence recipe encodes how to process pitch decks vs. financial models vs. cap tables. A meeting ingestion recipe encodes how to turn a raw transcript into actionable intelligence.
|
||||
|
||||
**Integration points.** What external systems does this touch? Twilio, Telegram, Gmail, Circleback, Slack, GitHub, Supabase, whatever. The recipe names the integrations; the agent figures out how to connect them given what the user already has configured.
|
||||
|
||||
**Judgment calls.** The hard part. Not "send an email" but "decide whether this email is worth surfacing to the user based on sender importance, time sensitivity, and whether it requires a decision." Recipes that skip the judgment calls produce shallow implementations. The judgment calls are the actual value.
|
||||
|
||||
**Failure modes.** What goes wrong and what to do about it. "If Circleback token expires, message the user and ask them to reconnect. Don't silently skip." "If caller ID is spoofed, never trust it for authentication. Use a challenge-response code via a separate channel." Recipes without failure modes produce brittle systems.
|
||||
|
||||
Here's a real example. This is the diligence recipe's detection logic:
|
||||
|
||||
```markdown
|
||||
## Detection
|
||||
|
||||
Recognize data room materials by:
|
||||
- PDF filenames: "Data Deck", "Intro Deck", "Cap Table",
|
||||
"Financial Model", "Pitch Deck", "Series [A-D]"
|
||||
- Spreadsheets with tabs: Revenue, Retention, Cohorts,
|
||||
CAC, Gross Margin, Unit Economics, ARR
|
||||
- User saying: "data room", "diligence", "deck", "pitch"
|
||||
- Context: shared in the Diligence topic
|
||||
```
|
||||
|
||||
That's a pattern matcher expressed in English. An agent reads this and knows how to classify incoming documents. No regex. No file type configuration. Just a description of the pattern and the model's judgment about whether a given document matches.
|
||||
|
||||
## Pick and choose
|
||||
|
||||
GBrain is not monolithic. Recipes are independent. Take what you want:
|
||||
|
||||
- **Voice agent** — phone screening, caller ID, brain lookup, message routing
|
||||
- **Meeting ingestion** — transcript processing, entity extraction, action item capture, timeline updates
|
||||
- **Email triage** — inbox sweep, priority classification, draft replies, scheduling extraction
|
||||
- **Enrichment pipeline** — people and company research from multiple data sources, diarized into brain pages
|
||||
- **Diligence processing** — data room ingestion, PDF extraction, financial model analysis
|
||||
- **Social monitoring** — X/Twitter timeline analysis, mention tracking, narrative detection
|
||||
- **Content pipeline** — idea capture, link ingestion, article summarization
|
||||
|
||||
Each recipe is self-contained. Your agent knows what you already have. GBrain pings daily: "Three new recipes since last sync. Want any?" You pick. It builds.
|
||||
|
||||
And because the source code is English, forking is trivial. Don't like how the voice agent handles unknown callers? Edit the markdown. Change "take a message" to "ask three screening questions first." The behavior changes because the spec changed.
|
||||
|
||||
## The thin harness, fat skills connection
|
||||
|
||||
This essay is a sequel. The prequel was "Thin Harness, Fat Skills," which argued that the secret to 100x AI productivity isn't better models but better context management. Keep the harness thin (the program running the model). Make the skills fat (markdown procedures encoding judgment and process).
|
||||
|
||||
"Markdown is code" is the distribution corollary. If the skills are fat markdown files, and if models are smart enough to implement from markdown, then the skills are distributable software. The skill file is simultaneously:
|
||||
|
||||
- **Documentation** for humans reading it
|
||||
- **Specification** for the implementing agent
|
||||
- **Package** for the distribution system
|
||||
- **Source code** for the resulting capability
|
||||
|
||||
Four artifacts collapsed into one. That's why this is different from every previous package manager. `brew install` separates the formula from the binary from the docs from the source. GBrain collapses them. The markdown is all four.
|
||||
|
||||
## The architecture underneath
|
||||
|
||||
Three layers, same as the talk:
|
||||
|
||||
**Fat skills** on top. Markdown recipes encoding judgment, process, failure modes, and domain knowledge. This is where 90% of the value lives. This is what gets distributed.
|
||||
|
||||
**Thin harness** in the middle. The program running the model. File operations, tool dispatch, context management, safety enforcement. About 200 lines. OpenClaw or any equivalent. The less the harness constrains, the more the recipes can express.
|
||||
|
||||
**Deterministic foundation** on the bottom. Databases, APIs, CLIs. Same input, same output, every time. SQL queries, HTTP calls, file reads. The skills describe WHEN to call these; the harness executes them.
|
||||
|
||||
Push intelligence UP into skills. Push execution DOWN into deterministic tooling. Distribute the skills. That's the whole system.
|
||||
|
||||
## What this means
|
||||
|
||||
When implementation cost approaches zero, the bottleneck shifts. It's no longer "can we build this?" It's "should we build this?" and "what exactly should it do?"
|
||||
|
||||
Taste, vision, and domain knowledge become the scarce resources. The person who deeply understands call screening and writes a precise recipe creates more value than the person who can implement a Twilio integration from scratch. The recipe IS the implementation.
|
||||
|
||||
This also means the best AI agent setups will be open source by default. Closed, proprietary agent configurations are competing against a world where someone publishes a recipe and a thousand agents implement it overnight. The recipe propagates at the speed of a git push. The moat is taste, not code.
|
||||
|
||||
Software distribution reimagined: the package is a markdown file, the runtime is a sufficiently smart model, the package manager is your AI agent, and the app store is a git repo.
|
||||
|
||||
`gbrain install voice-agent`
|
||||
|
||||
That's it.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Origin story
|
||||
|
||||
GBrain came out of building OpenClaw — Garry's personal AI agent fork. The first version had skills and a brain, but the brain was a flat directory of markdown files. Search was ripgrep. Memory was vibes.
|
||||
|
||||
Two problems surfaced almost immediately.
|
||||
|
||||
First, the agent forgot things between conversations. Every new session re-asked basic questions. Names of people Garry had introduced last week were gone. Decisions made on Tuesday didn't survive to Thursday. The brain existed but the agent couldn't actually use it.
|
||||
|
||||
Second, the agent kept duplicating work. Two different signals about the same company became two different people pages. Three meetings with the same person became three uncorrelated timeline entries. The signal-to-noise ratio decayed in real time.
|
||||
|
||||
GBrain is what you build when you decide both of those are unacceptable.
|
||||
|
||||
The fix wasn't one big idea. It was many small ones layered together:
|
||||
|
||||
- Brain-first lookup before any external API call.
|
||||
- Auto-linking on every page write so the graph grows for free.
|
||||
- Typed edges so "who works at Acme AI?" actually returns something.
|
||||
- Hybrid search because vector alone underdelivers.
|
||||
- Reranker on top because hybrid alone is locally optimal but globally suboptimal.
|
||||
- Nightly cron to dedup, enrich, fix citations, surface contradictions.
|
||||
- An agent that reads `skills/RESOLVER.md` once and knows what to do.
|
||||
|
||||
None of those are novel ideas. The contribution is shipping all of them together, on Postgres + pgvector that runs in WASM (no server), with skills that are markdown (not code), routed by a small text file (not a router LLM).
|
||||
|
||||
The production brain has been running for months now. 17,888 pages. 4,383 people. 723 companies. 21 cron jobs running autonomously. It wakes Garry up smarter than the day before.
|
||||
|
||||
GBrain is what happens when you write the brain you actually wanted to have.
|
||||
|
||||
The reason the brain is worth building is `gbrain think`. Without it, the brain is just a place that holds your notes. With it, the brain is a thing you can query about itself: what does it know, what does it not know yet, where does it contradict itself, where are the holes. The 24/7 cron cycle keeps the brain sharp. `think` is what makes a sharp brain useful.
|
||||
@@ -1,209 +0,0 @@
|
||||
---
|
||||
type: essay
|
||||
title: "Thin Harness, Fat Skills"
|
||||
subtitle: "How to Make AI Agents Actually Understand Your Data"
|
||||
author: Garry Tan
|
||||
created: 2026-04-09
|
||||
updated: 2026-04-11
|
||||
tags: [ai, agents, gstack, harness-engineering, skills, architecture]
|
||||
status: draft-v4
|
||||
talk: "YC Spring 2026 -- Thin Harness, Fat Skills"
|
||||
thread: https://x.com/garrytan/status/2042925773300908103
|
||||
---
|
||||
|
||||
# Thin Harness, Fat Skills
|
||||
|
||||
Steve Yegge says people using AI coding agents are "10x to 100x as productive as engineers using Cursor and chat today, and roughly 1000x as productive as Googlers were back in 2005."
|
||||
|
||||
That's a real number. I've seen it. I've lived it. But when people hear 100x, they think: better models. Smarter Claude. More parameters.
|
||||
|
||||
That's the wrong frame entirely. The 2x people and the 100x people are using the same models. The difference is five concepts that fit on an index card.
|
||||
|
||||
## The harness is the secret sauce
|
||||
|
||||
On March 31, 2026, Anthropic accidentally shipped the entire source code for Claude Code to the npm registry. 512,000 lines. When I read it, it confirmed everything I'd been teaching at YC. The secret sauce isn't the model. It's the thing wrapping the model: the harness. Live repo context. Prompt caching. Purpose-built tools. Context bloat minimization. Structured session memory. Parallel sub-agents.
|
||||
|
||||
None of that is about making the model smarter. All of it is about giving the model the right context, at the right time, without drowning it in noise.
|
||||
|
||||
That's the only question that matters. And the answer has a specific shape. I call it **thin harness, fat skills**.
|
||||
|
||||
## Five definitions
|
||||
|
||||
The bottleneck is never the model's intelligence. The bottleneck is whether the model understands your schema. Models already know how to reason, synthesize, and write code. They fail because they don't know your data. Five definitions fix this.
|
||||
|
||||
### Definition 1: Skill File
|
||||
|
||||
A skill file is a reusable markdown procedure that teaches the model HOW to do something. Not WHAT to do. The user supplies the specifics. The skill supplies the process.
|
||||
|
||||
**Markdown is actually code.** A skill file is a more perfect encapsulation of capability than rigid source code, because it describes process, judgment, and context in the language the model already thinks in.
|
||||
|
||||
On the left is a skill called `/investigate`. Seven steps: scope the dataset, build a timeline, diarize every document, synthesize, argue both sides, cite sources. It takes three parameters: TARGET, QUESTION, and DATASET.
|
||||
|
||||
On the right are two completely different invocations of the same skill. One points at Dr. Sarah Chen and 2.1 million discovery emails, asking whether a safety scientist was silenced. The other points at Pacific Corporate Services and FEC filings, asking whether shell companies are coordinating campaign donations.
|
||||
|
||||
Same skill. Same seven steps. Same markdown file. In one case it's a medical research analyst. In the other it's a forensic investigator. The skill describes a process of judgment. The invocation supplies the world.
|
||||
|
||||
**This is the key insight most people miss: a skill file works like a method call.** It takes parameters. You invoke it with different arguments. The same procedure produces radically different capabilities depending on what you pass in. This is not prompt engineering. This is software design, using markdown as the programming language and human judgment as the runtime.
|
||||
|
||||
### Definition 2: Harness
|
||||
|
||||
The harness is the program that runs the LLM. It does four things: runs the model in a loop, reads and writes your files, manages context, and enforces safety. That's the "thin."
|
||||
|
||||
The anti-pattern is a fat harness with thin skills: 40+ tool definitions eating half the context window. God tools with 2 to 5 second MCP round-trips. REST API wrappers that turn every endpoint into a tool. 3x the tokens, 3x the latency, 3x the failure rate.
|
||||
|
||||
What you should build instead: a Playwright CLI that does each browser operation in 100 milliseconds. Compare: Chrome MCP takes 15 seconds for screenshot + find + click + wait + read. Playwright CLI takes 200 milliseconds for screenshot + assert. 75x faster. Software doesn't have to be precious anymore. Build exactly what you need.
|
||||
|
||||
### Definition 3: Resolver
|
||||
|
||||
A resolver is a routing table for context. When task type X appears, load document Y first.
|
||||
|
||||
Skills say HOW. Resolvers say WHAT to load WHEN. A developer changes a prompt. Without the resolver, they ship it. With the resolver, the model reads `docs/EVALS.md` first, which says: run the eval suite, compare scores, if accuracy drops more than 2%, revert and investigate. The developer didn't know the eval suite existed. The resolver loaded the right context at the right moment.
|
||||
|
||||
Claude Code has a built-in resolver. Every skill has a description field, and the model matches user intent to skill descriptions automatically. You never have to remember `/ship` exists. The description IS the resolver. It's like Clippy. Except it actually works.
|
||||
|
||||
A confession: my CLAUDE.md was 20,000 lines. Every single thing I ran across went in there. Every quirk, every pattern, every lesson. Completely ridiculous. The model's attention degraded. Claude Code literally told me to cut it back. The fix: about 200 lines. Just pointers to documents. The resolver loads the right one when it matters.
|
||||
|
||||
### Definition 4: Latent vs. Deterministic
|
||||
|
||||
Every step in your system is one or the other.
|
||||
|
||||
**Latent space** is where intelligence lives. The model reads, interprets, decides. Judgment. Synthesis. Pattern recognition.
|
||||
|
||||
**Deterministic** is where trust lives. Same input, same output. Every time. SQL. Code. Numbers.
|
||||
|
||||
An LLM can seat 8 people at a dinner table. Ask it to seat 800 and it will hallucinate a seating chart that looks plausible but is completely wrong. That's a deterministic problem forced into latent space. The worst systems put the wrong work on the wrong side.
|
||||
|
||||
### Definition 5: Diarization
|
||||
|
||||
The model reads everything about a subject and writes a structured profile. Read 50 documents, produce 1 page of judgment.
|
||||
|
||||
No SQL query produces this. No RAG pipeline produces this. The model has to actually read, hold contradictions in mind, notice what changed and when, and write structured intelligence. This is what makes AI useful for real knowledge work.
|
||||
|
||||
## The architecture
|
||||
|
||||
Three layers:
|
||||
|
||||
**Fat skills** on top. Markdown procedures that encode judgment, process, and domain knowledge. This is where 90% of the value lives.
|
||||
|
||||
**Thin CLI harness** in the middle. About 200 lines. JSON in, text out. Read-only by default. CLI first, add MCP later.
|
||||
|
||||
**Your app** on the bottom. QueryDB. ReadDoc. Search. Timeline. The deterministic foundation.
|
||||
|
||||
Push intelligence UP into skills. Push execution DOWN into deterministic tooling. Keep the harness THIN.
|
||||
|
||||
## The system that learns: YC Startup School
|
||||
|
||||
Let me show you all five definitions working together. Not in theory. In an actual system we're building at YC.
|
||||
|
||||
Chase Center. July 2026. 6,000 founders. Each one has a structured application, questionnaire answers, transcripts from 1:1 advisor chats, and public signals: X posts, GitHub commits, Claude Code transcripts showing how fast they ship.
|
||||
|
||||
The traditional approach: a program team of 15 reads applications, makes gut calls, updates a spreadsheet. It works at 200 founders. It breaks at 6,000.
|
||||
|
||||
No human can hold 6,000 profiles in working memory and notice that the three best candidates for the infrastructure-for-AI-agents cohort are a dev tools founder in Lagos, a compliance founder in Singapore, and a CLI-tooling founder in Brooklyn who all described the same pain point in different words during their 1:1 chats.
|
||||
|
||||
The model can.
|
||||
|
||||
**Step 1: Enrich every founder.**
|
||||
|
||||
The `/enrich-founder` skill: pull all sources, run enrichments, diarize, highlight what they SAY vs what they're ACTUALLY BUILDING. On the right, the deterministic calls: SQL to find stale profiles, GitHub stats, browser test on the demo URL, social signal pulls, CrustData for company intel.
|
||||
|
||||
Cron runs nightly at 2am. 6,000 profiles, every night, always fresh.
|
||||
|
||||
The diarization output catches things no keyword search would find:
|
||||
|
||||
```
|
||||
FOUNDER: Maria Santos
|
||||
COMPANY: Contrail (contrail.dev)
|
||||
SAYS: "Datadog for AI agents"
|
||||
ACTUALLY BUILDING: 80% of commits are in billing module.
|
||||
She's building a FinOps tool disguised as observability.
|
||||
```
|
||||
|
||||
"SAYS" vs "ACTUALLY BUILDING." That requires reading the GitHub commit history, the application, and the advisor transcript and holding all three in mind at once.
|
||||
|
||||
**Step 2: Match 6,000 founders. Make judgment calls.**
|
||||
|
||||
This is where skill-as-method-call really shines. Three invocations:
|
||||
|
||||
`/match-breakout`: 1,200 founders, cluster by sector affinity, 30 per room. Embed + deterministic assign.
|
||||
|
||||
`/match-lunch`: 600 founders, serendipity matching (cross-sector), 8 per table, no repeats. The LLM invents the themes, then assigns.
|
||||
|
||||
`/match-live`: whoever is in the zone, nearest-neighbor embedding, real-time at 200ms, 1:1 pairs, not already met.
|
||||
|
||||
Same skill. Three invocations. Three completely different matching strategies. Different parameters, different strategies, different group sizes. The skill describes the process. The arguments shape the output.
|
||||
|
||||
And the model's judgment calls: "Santos and Oram are both AI infra, but they're not competitors. Santos is cost attribution, Oram is orchestration. Put them in the same group." And: "Kim applied as 'developer tools' but his 1:1 transcript reveals he's building compliance automation for SOC2. Move him to FinTech/RegTech."
|
||||
|
||||
No embedding captures the Kim reclassification. No algorithm can do it. The model has to read the entire profile.
|
||||
|
||||
**Step 3: The self-learning loop.**
|
||||
|
||||
After the event, the `/improve` skill reads NPS surveys, diarizes the "OK" responses (not the bad ones, the mediocre ones), and extracts patterns. Then it proposes new rules and writes them back into the matching skills:
|
||||
|
||||
```
|
||||
When attendee says "AI infrastructure"
|
||||
but startup is 80%+ billing code:
|
||||
-> Classify as FinTech, not AI Infra.
|
||||
|
||||
When two attendees in same group
|
||||
already know each other:
|
||||
-> Penalize proximity.
|
||||
Prioritize novel introductions.
|
||||
```
|
||||
|
||||
These rules get written back into the skill file. Next run uses them automatically. The skill rewrites itself.
|
||||
|
||||
July event: 12% "OK" ratings. Next event: 4%. The skill file learned what "OK" actually meant.
|
||||
|
||||
Same pattern as every other domain: retrieve, read, diarize, count, synthesize. Then: survey, investigate, diarize, rewrite the skill. It transfers everywhere.
|
||||
|
||||
## OpenClaw: where the skills live
|
||||
|
||||
I want to tell you about one more harness. Not for coding. For everything else.
|
||||
|
||||
I run a personal AI agent on OpenClaw. It has a persona, knows who I am, and maintains a knowledge base of thousands of interconnected files. But the thing that makes it work is the exact same principle. Thin harness, fat skills.
|
||||
|
||||
I tweeted about this a few days ago:
|
||||
|
||||
> *You are not allowed to do one-off work. If I ask you to do something and it's the kind of thing that will need to happen again, you must: do it manually the first time on 3 to 10 items. Show me the output. If I approve, codify it into a skill file. If it should run automatically, put it on a cron.*
|
||||
|
||||
> *The test: if I have to ask you for something twice, you failed.*
|
||||
|
||||
That resonated: a thousand likes, twenty-five hundred bookmarks. People thought it was a prompt engineering trick. It's not. It's the same architecture.
|
||||
|
||||
Claude Code is the best harness for coding. OpenClaw is the best harness for everything else: email, calendar, meetings, people, research, alerts. Same principle. Thin harness. Fat skills. The skills are method calls. The parameters change. The process stays.
|
||||
|
||||
Every skill I write is a permanent upgrade. It never degrades. It never forgets. It runs at 3 AM while I sleep. And when the next model drops, every skill instantly gets better, because the judgment in the latent steps improves while the deterministic steps stay perfectly reliable.
|
||||
|
||||
That's how you get Yegge's 10x to 100x. Not a smarter model. Fat skills, thin harness, and the discipline to codify everything.
|
||||
|
||||
The system compounds. Build it once. It runs forever.
|
||||
|
||||
Five definitions. Three layers. One principle. Thin harness, fat skills.
|
||||
|
||||
---
|
||||
|
||||
## Agent Decision Guide: Skill or Code?
|
||||
|
||||
When building GBrain features, use this decision guide:
|
||||
|
||||
| Question | If YES | If NO |
|
||||
|----------|--------|-------|
|
||||
| Does the agent need to think, adapt, or ask questions? | **Skill** (recipe markdown) | Code |
|
||||
| Same input always produces same output? | **Code** (CLI command) | Skill |
|
||||
| Does it require judgment about the user's environment? | **Skill** | Code |
|
||||
| Is it a lookup, list, or status check? | **Code** | Probably skill |
|
||||
| Does it change behavior based on conversation context? | **Skill** | Code |
|
||||
|
||||
**GBrain examples:**
|
||||
- `gbrain integrations list` = **Code** (reads files, checks env vars, deterministic)
|
||||
- `gbrain integrations status` = **Code** (checks env vars + heartbeat, deterministic)
|
||||
- `gbrain integrations doctor` = **Code** (runs health checks, deterministic)
|
||||
- `gbrain integrations stats` = **Code** (aggregates JSONL, deterministic)
|
||||
- Recipe setup flow = **Skill** (asks for API keys, adapts to environment, validates)
|
||||
- Recipe changelog surfacing = **Skill** (agent describes changes conversationally)
|
||||
- Entity detection = **Skill** (reads message, decides what's important, creates pages)
|
||||
- Meeting ingestion = **Skill** (reads transcript, extracts entities, updates pages)
|
||||
|
||||
**The rule:** If it's a lookup table, it's code. If the agent needs to think, it's a skill.
|
||||
@@ -1,598 +0,0 @@
|
||||
# Running real-world eval benchmarks against your gbrain changes
|
||||
|
||||
Audience: gbrain maintainers and contributors. If you're touching retrieval
|
||||
(search, ranking, embeddings, intent classification, query expansion, source
|
||||
boost, hybrid fusion), this is the doc.
|
||||
|
||||
For the **NDJSON wire format** consumed by gbrain-evals, see
|
||||
[`eval-capture.md`](./eval-capture.md). This doc is the human dev loop
|
||||
that lives on top of that format.
|
||||
|
||||
## v0.41 update — the LOOP is now real
|
||||
|
||||
Before v0.41, you could capture eval rows and replay them but nothing
|
||||
stitched them into a gate. `gbrain bench publish` + `gbrain eval gate`
|
||||
close the loop. Two gates:
|
||||
|
||||
- **Regression gate** (`--baseline X.baseline.ndjson`): replays a baseline
|
||||
you captured against your current brain. Catches: "did my refactor break
|
||||
search?" Compares jaccard / top-1 stability / latency multiplier.
|
||||
- **Correctness gate** (`--qrels Y.qrels.json`): runs known-right queries
|
||||
against your current brain via bare `hybridSearch`. Catches: "is my
|
||||
retrieval actually any good?" Computes recall@K, first-relevant-hit-rate,
|
||||
expected_top1-hit-rate.
|
||||
|
||||
Both can be passed together; both must pass for verdict `pass`. At least
|
||||
one is required.
|
||||
|
||||
### The full LOOP for your own brain
|
||||
|
||||
```bash
|
||||
# 1. Capture (one-time; uses queries already in eval_candidates)
|
||||
gbrain eval export --limit 200 --tool query > /tmp/captured.ndjson
|
||||
|
||||
# 2. Publish a baseline
|
||||
mkdir -p ~/.gbrain/baselines
|
||||
gbrain bench publish --from /tmp/captured.ndjson --to ~/.gbrain/baselines/personal.baseline.ndjson --label "personal-$(date +%Y%m%d)"
|
||||
|
||||
# 3. Gate against it
|
||||
gbrain eval gate --baseline ~/.gbrain/baselines/personal.baseline.ndjson
|
||||
```
|
||||
|
||||
### Privacy posture (D9)
|
||||
|
||||
**Public baselines in `gbrain-evals` are hermetic-synthetic ONLY.** Real
|
||||
user captures stay local in `~/.gbrain/baselines/`. The boundary is
|
||||
enforced at the file source, not by post-hoc scrubbing. If you publish a
|
||||
baseline to `gbrain-evals`, generate it from a fixture-seeded test brain
|
||||
(placeholder names like `alice-example`, `widget-co-example`) — never
|
||||
from a real user's `eval_candidates` table.
|
||||
|
||||
### Deterministic-pipeline disclosure
|
||||
|
||||
`gbrain eval gate --qrels` uses bare `hybridSearch` (not the production
|
||||
`query` op handler). This is deliberate: gates need to be deterministic in
|
||||
CI. Production retrieval differs via the query cache, salience freshness,
|
||||
expansion, etc. The gate measures retrieval quality with a fixed pipeline;
|
||||
your users may see different results when the cache is warm.
|
||||
|
||||
### `.qrels.json` shape
|
||||
|
||||
Two equivalent representations per entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"queries": [
|
||||
{
|
||||
"query_id": "q1",
|
||||
"query": "fintech founder",
|
||||
"relevant_slugs": ["people/alice-example"],
|
||||
"first_relevant_slug": "people/alice-example"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For federated / multi-source brains, use the explicit shape (no defaults
|
||||
to `source_id='default'`):
|
||||
|
||||
```json
|
||||
{
|
||||
"query_id": "q2",
|
||||
"query": "anything",
|
||||
"relevant": [
|
||||
{"source_id": "host", "slug": "people/alice"},
|
||||
{"source_id": "team-a", "slug": "people/alice"}
|
||||
],
|
||||
"expected_top1": {"source_id": "host", "slug": "people/alice"}
|
||||
}
|
||||
```
|
||||
|
||||
Without `source_id`, a hit from the wrong source could false-pass the
|
||||
gate. The compare everywhere is `${source_id}::${slug}` strings.
|
||||
|
||||
### Example GitHub Actions workflow
|
||||
|
||||
```yaml
|
||||
name: gbrain-eval-gate
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun install
|
||||
- run: |
|
||||
# Run both gates; CI fails on any breach.
|
||||
gbrain eval gate \
|
||||
--baseline gbrain-evals/baselines/v0.41-launch.baseline.ndjson \
|
||||
--qrels gbrain-evals/qrels/v0.41-launch.qrels.json \
|
||||
--json | tee /tmp/gate.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prerequisite: turn on contributor mode
|
||||
|
||||
Capture is **off by default** for production users (privacy-positive — no
|
||||
surprise data accumulation). Contributors flip it on with one line:
|
||||
|
||||
```bash
|
||||
# In ~/.zshrc or ~/.bashrc:
|
||||
export GBRAIN_CONTRIBUTOR_MODE=1
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
gbrain query "anything" >/dev/null
|
||||
psql $DATABASE_URL -c 'SELECT count(*) FROM eval_candidates' # should be > 0
|
||||
```
|
||||
|
||||
To override (force on/off regardless of env var), edit `~/.gbrain/config.json`:
|
||||
|
||||
```json
|
||||
{"eval": {"capture": true}} // force on
|
||||
{"eval": {"capture": false}} // force off
|
||||
```
|
||||
|
||||
Explicit config beats the env var both directions.
|
||||
|
||||
## The 4-command loop
|
||||
|
||||
```bash
|
||||
# ① Capture: writes to eval_candidates whenever CONTRIBUTOR_MODE is set.
|
||||
# Inspect what's been collected:
|
||||
gbrain doctor # surfaces capture failures
|
||||
psql $DATABASE_URL -c 'SELECT count(*) FROM eval_candidates'
|
||||
|
||||
# ② Snapshot: freeze a baseline before your code change.
|
||||
gbrain eval export --since 7d > baseline.ndjson
|
||||
|
||||
# ③ Code change: do whatever you want — tune RRF_K, swap embed model, edit
|
||||
# hybrid.ts, add a new boost source, change the intent classifier.
|
||||
|
||||
# ④ Replay: re-run every captured query against the current build.
|
||||
gbrain eval replay --against baseline.ndjson
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Replaying 247 captured queries…
|
||||
...25/247
|
||||
...50/247
|
||||
...
|
||||
Replayed 247 of 247 captured queries (0 skipped, 0 errored)
|
||||
Mean Jaccard@k: 0.927
|
||||
Top-1 stability: 91.5%
|
||||
Mean latency Δ: +14ms (current vs captured)
|
||||
|
||||
Top 5 regression(s):
|
||||
jaccard=0.20 captured=12 current=3 "find every reference to widget-co"
|
||||
jaccard=0.43 captured=14 current=8 "show me everything tagged for review"
|
||||
jaccard=0.50 captured=8 current=4 "what did alice say about the spec"
|
||||
...
|
||||
```
|
||||
|
||||
Three numbers tell you whether the change is safe to land:
|
||||
|
||||
| Metric | What it means | Healthy range |
|
||||
|---|---|---|
|
||||
| **Mean Jaccard@k** | Average overlap between captured retrieved slugs and current run's slugs. 1.0 = identical sets. | ≥0.85 for "neutral" changes. <0.7 means major retrieval shift. |
|
||||
| **Top-1 stability** | Fraction of queries whose #1 result didn't change. | ≥85% for tuning passes. <70% means top-of-funnel broke. |
|
||||
| **Mean latency Δ** | Current minus captured. Positive = slower now. | Within ±50ms of captured. >2× anywhere = regression alarm. |
|
||||
|
||||
## What it actually does
|
||||
|
||||
`gbrain eval replay` reads your NDJSON snapshot and, for each row:
|
||||
|
||||
1. Re-executes the same op (`searchKeyword` for `tool_name='search'`,
|
||||
`hybridSearch` for `tool_name='query'`) with the captured `detail` and
|
||||
`expand_enabled` values threaded back in.
|
||||
2. Captures the current `retrieved_slugs` (deduped, in result order).
|
||||
3. Computes set-Jaccard between captured and current slug sets.
|
||||
4. Records top-1 match (was the #1 result the same slug?).
|
||||
5. Records latency delta vs captured `latency_ms`.
|
||||
|
||||
It does NOT compute MRR or nDCG — those need ground-truth relevance labels,
|
||||
not a baseline comparison. For metric-against-truth eval, use
|
||||
`gbrain eval --qrels <path>` (the legacy IR-eval path, still supported). The
|
||||
replay tool answers a different question: "did my code change move
|
||||
retrieval, and which queries did it move most?"
|
||||
|
||||
For a third evaluation axis — public benchmark, ground-truth labels, full
|
||||
question-answer pipeline (not just retrieval) — `gbrain eval longmemeval
|
||||
<dataset.jsonl>` (v0.28.8) runs the LongMemEval benchmark against gbrain's
|
||||
hybrid retrieval. Each question gets a clean in-memory PGLite, its haystack
|
||||
imported, the question asked, the hypothesis emitted as JSONL — exactly the
|
||||
shape LongMemEval's `evaluate_qa.py` consumes. Your `~/.gbrain` brain is
|
||||
never opened. See `## Public benchmarks: LongMemEval` below.
|
||||
|
||||
## Best-effort by design
|
||||
|
||||
Replay is not pure. Three things can drift between capture and replay:
|
||||
|
||||
1. **Brain state** — your brain probably has more pages now than when the
|
||||
snapshot was taken. Unless you explicitly seed a fixed corpus, mean
|
||||
Jaccard will drop simply because new pages are eligible.
|
||||
2. **Embedding source** — if you changed `OPENAI_API_KEY` between capture
|
||||
and replay (or the embedding model rotated), vector-path results drift
|
||||
even with identical code.
|
||||
3. **Capture cap** — captured `retrieved_slugs` is a deduped set; it doesn't
|
||||
preserve internal ranking metadata. Two tools can return the same slug
|
||||
set with different scores — Jaccard will say 1.0, but a downstream
|
||||
consumer that orders by score may behave differently.
|
||||
|
||||
The metrics are **regression alarms on real queries**, not a hash check.
|
||||
Pair them with manual inspection of the top regressions.
|
||||
|
||||
## Cost
|
||||
|
||||
Every `query` row in the snapshot embeds the query string via OpenAI to run
|
||||
the vector half of `hybridSearch`. Cost is identical to a normal `gbrain
|
||||
query` invocation — text-embedding-3-large at OpenAI list price, batched
|
||||
inside a single replay row.
|
||||
|
||||
If you're iterating locally and don't want to pay per change, use
|
||||
`--limit 50` to cap rows replayed. The 50 most recent rows are usually
|
||||
enough to catch direction; expand for the final pre-merge run.
|
||||
|
||||
```bash
|
||||
# Iteration mode — 50 most recent queries
|
||||
gbrain eval replay --against baseline.ndjson --limit 50
|
||||
|
||||
# Pre-merge — full snapshot
|
||||
gbrain eval replay --against baseline.ndjson --top-regressions 20
|
||||
```
|
||||
|
||||
## CI integration
|
||||
|
||||
```bash
|
||||
gbrain eval replay --against baseline.ndjson --json > replay.json
|
||||
jq -e '.summary.mean_jaccard >= 0.85' replay.json || exit 1
|
||||
jq -e '.summary.top1_stability_rate >= 0.85' replay.json || exit 1
|
||||
```
|
||||
|
||||
Stable JSON shape (schema_version: 1):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"summary": {
|
||||
"rows_total": 247,
|
||||
"rows_replayed": 247,
|
||||
"rows_skipped": 0,
|
||||
"rows_errored": 0,
|
||||
"mean_jaccard": 0.927,
|
||||
"top1_stability_rate": 0.915,
|
||||
"mean_latency_delta_ms": 14,
|
||||
"rows_over_2x_latency": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`--verbose` adds a `results: [...]` array with one entry per replayed row
|
||||
(useful for piping into jq or a notebook for deeper analysis).
|
||||
|
||||
## When to run this
|
||||
|
||||
Before merging anything that touches:
|
||||
|
||||
- `src/core/search/hybrid.ts` (RRF, fusion, dedup, two-pass retrieval)
|
||||
- `src/core/search/source-boost.ts` / `sql-ranking.ts` (per-source ranking)
|
||||
- `src/core/search/intent.ts` (auto-detail classification)
|
||||
- `src/core/search/expansion.ts` (Haiku query expansion)
|
||||
- `src/core/search/dedup.ts` (cross-page result collapse)
|
||||
- `src/core/embedding.ts` or any embedding model swap
|
||||
- `src/core/operations.ts` `query` or `search` op handlers (capture surface)
|
||||
- `src/core/postgres-engine.ts` / `pglite-engine.ts` `searchKeyword` /
|
||||
`searchVector` SQL
|
||||
|
||||
Skip for: schema-only migrations, doc changes, tests-only PRs, CLI ergonomics
|
||||
that don't touch retrieval.
|
||||
|
||||
## Building your own corpus
|
||||
|
||||
If you don't have captured traffic yet (fresh install, can't dogfood for a
|
||||
week before merging), you can hand-author an NDJSON file:
|
||||
|
||||
```jsonl
|
||||
{"schema_version":1,"id":1,"tool_name":"query","query":"who is alice","retrieved_slugs":["people/alice","people/alice-bio"],"expand_enabled":false,"detail":null,"latency_ms":0,"remote":false}
|
||||
{"schema_version":1,"id":2,"tool_name":"search","query":"acme deal","retrieved_slugs":["deals/acme-seed","companies/acme"],"latency_ms":0,"remote":false}
|
||||
```
|
||||
|
||||
Then run `gbrain eval replay --against handcrafted.ndjson` to confirm the
|
||||
authoritative slugs come back. This is the seam between the BrainBench-Real
|
||||
pipeline (replay against live captures) and the BrainBench fixed-fixture
|
||||
pipeline (`gbrain eval --qrels` with the sibling
|
||||
[gbrain-evals](https://github.com/garrytan/gbrain-evals) corpus).
|
||||
|
||||
## Off-switch
|
||||
|
||||
Two ways to disable capture:
|
||||
|
||||
```bash
|
||||
unset GBRAIN_CONTRIBUTOR_MODE # easy: just unset the env var
|
||||
```
|
||||
|
||||
Or force off regardless of the env var via `~/.gbrain/config.json`:
|
||||
|
||||
```json
|
||||
{"eval": {"capture": false}}
|
||||
```
|
||||
|
||||
Existing `eval_candidates` rows stay until you `gbrain eval prune
|
||||
--older-than 0d` (or just drop the table).
|
||||
|
||||
## Failure modes
|
||||
|
||||
| What you see | What it means |
|
||||
|---|---|
|
||||
| `Mean Jaccard@k: 0.4`, top regressions all in one source dir | Source boost or hard-exclude regression on that prefix |
|
||||
| `Top-1 stability: 30%`, mean Jaccard still high | RRF tuning shifted the rank order without changing the set — re-tune `rrfK` |
|
||||
| `Mean latency Δ: +500ms`, jaccard high | Vector path got slower; check embedding API or HNSW probes |
|
||||
| `rows_errored > 0` | One or more queries threw. Inspect first 3 in human output, or `--json` to see all `error_message` fields |
|
||||
| Many `skipped: empty query` | Capture ran on rows where someone passed empty `query` — check why those were captured |
|
||||
|
||||
## Public benchmarks: LongMemEval (v0.28.8)
|
||||
|
||||
`gbrain eval longmemeval` runs the public [LongMemEval](https://huggingface.co/datasets/xiaowu0162/longmemeval)
|
||||
benchmark directly against gbrain's hybrid retrieval. Different evaluation
|
||||
axis from `eval replay`: public dataset with ground-truth labels, end-to-end
|
||||
question-answer pipeline, hermetic per-question brains.
|
||||
|
||||
```bash
|
||||
# Download the dataset (visit the HF page in a browser; gated/manual download).
|
||||
# Place longmemeval_oracle.json (or _s.json) somewhere local.
|
||||
|
||||
# Retrieval-only (no LLM answer-gen, fastest path, no Anthropic key needed):
|
||||
gbrain eval longmemeval ./longmemeval_oracle.json --limit 50 --retrieval-only \
|
||||
> /tmp/hypothesis.jsonl
|
||||
|
||||
# Full pipeline (Anthropic key required for answer-gen):
|
||||
gbrain eval longmemeval ./longmemeval_oracle.json --limit 50 \
|
||||
> /tmp/hypothesis.jsonl
|
||||
|
||||
# Score with LongMemEval's published evaluate_qa.py (not bundled — needs
|
||||
# OpenAI gpt-4o per their spec):
|
||||
python evaluate_qa.py /tmp/hypothesis.jsonl
|
||||
```
|
||||
|
||||
### Architecture (read this if you're touching the harness)
|
||||
|
||||
- One in-memory PGLite per benchmark run via `createBenchmarkBrain` +
|
||||
`withBenchmarkBrain`. Your `~/.gbrain` is never opened.
|
||||
- Between questions: `TRUNCATE` over runtime-enumerated `pg_tables`, NOT a
|
||||
hardcoded list — schema migrations don't silently leak data across
|
||||
questions. Infrastructure tables (`sources`, `config`,
|
||||
`gbrain_cycle_locks`, `subagent_rate_leases`) are preserved across resets.
|
||||
- Sanitization parity: re-uses `INJECTION_PATTERNS` from
|
||||
`src/core/think/sanitize.ts` so adding a new injection pattern
|
||||
automatically covers takes AND benchmarks. One source of truth.
|
||||
- Retrieved chat content is wrapped in `<chat_session id="..." date="...">`
|
||||
framing; the answer-gen system prompt declares the content UNTRUSTED.
|
||||
Same posture as `<take>` framing.
|
||||
- LLM injection seam: `runEvalLongMemEval(args, {client?: ThinkLLMClient})`.
|
||||
Tests stub the client so the full pipeline runs hermetically without any
|
||||
API key.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--limit N` | run all | Cap question count (iterate fast) |
|
||||
| `--retrieval-only` | off | Emit retrieved chunks; no LLM answer-gen |
|
||||
| `--keyword-only` | off | Disable vector path (debug retrieval issues) |
|
||||
| `--expansion` | **off** | Multi-query expansion. Off by default for determinism (no per-query Haiku call). Pass to opt in. |
|
||||
| `--top-k K` | 10 | Retrieval depth |
|
||||
| `--model M` | resolved | Default resolves through `resolveModel()` 6-tier chain (`models.eval.longmemeval` config key) |
|
||||
| `--output FILE` | stdout | Write hypothesis JSONL to file instead of stdout |
|
||||
|
||||
### Numbers
|
||||
|
||||
p50 25.9ms / p99 30.3ms warm reset+import+search on Apple Silicon (per the
|
||||
`test/eval-longmemeval.test.ts` perf gate). Per-question cost well under the
|
||||
500ms speed gate. 500 questions = ~13s of overhead plus your retrieval and
|
||||
LLM latency.
|
||||
|
||||
## Measuring brain consistency over time (v0.32.6)
|
||||
|
||||
`gbrain eval suspected-contradictions` is a complementary measurement
|
||||
instrument: it samples retrieval results for unmarked semantic
|
||||
contradictions (e.g., compiled_truth vs chat content, intra-page chunk
|
||||
vs active take). Where LongMemEval measures retrieval correctness on a
|
||||
fixed labeled set, the contradiction probe measures how often a real
|
||||
brain surfaces conflicting answers.
|
||||
|
||||
### Recommended nightly cadence
|
||||
|
||||
```bash
|
||||
# Once a day, against your top 50 most-frequent queries:
|
||||
gbrain eval suspected-contradictions \
|
||||
--queries-file ~/.gbrain/queries.jsonl \
|
||||
--top-k 5 \
|
||||
--budget-usd 5 \
|
||||
--output ~/.gbrain/probe-runs/$(date +%Y-%m-%d).json
|
||||
```
|
||||
|
||||
Persistent cache (`eval_contradictions_cache`) makes re-runs near-zero
|
||||
cost until you bump `PROMPT_VERSION`. Trend-track via:
|
||||
|
||||
```bash
|
||||
gbrain eval suspected-contradictions trend --days 30
|
||||
```
|
||||
|
||||
The ASCII bar chart shows total flagged per day. Headline % surfaces in
|
||||
`gbrain doctor`'s `contradictions` check with paste-ready resolution
|
||||
commands per high-severity finding.
|
||||
|
||||
### See also
|
||||
|
||||
- `docs/contradictions.md` — architecture, severity rubric, action criteria.
|
||||
- CHANGELOG `## [0.32.6]` — full release notes including the bigger-swing
|
||||
decision criteria gated on Wilson CI lower-bound.
|
||||
|
||||
## v0.40.1.0 Track D — Eval infrastructure
|
||||
|
||||
Three eval surfaces grew non-trivial capabilities in v0.40.1.0. This section
|
||||
covers the dev loop that uses them and the gates they enforce.
|
||||
|
||||
### `gbrain eval longmemeval --by-type` — per-question-type R@k breakdown
|
||||
|
||||
LongMemEval has always computed per-question-type recall internally; v0.40.1.0
|
||||
surfaces it in machine-readable form. Two additive changes:
|
||||
|
||||
1. Every per-question JSONL row now includes a `question: string` field so the
|
||||
`gbrain eval cross-modal --batch` consumer (below) can read it without
|
||||
joining back against the source dataset.
|
||||
2. New `--by-type` flag emits a final aggregate line keyed by `question_type`:
|
||||
|
||||
```json
|
||||
{"schema_version": 1, "kind": "by_type_summary",
|
||||
"recall_by_type": {"single-session-user": {"hit": 18, "total": 19, "rate": 0.947}},
|
||||
"aggregate": {"hit": 110, "total": 120, "rate": 0.917}}
|
||||
```
|
||||
|
||||
**Resume-safe.** When `--resume-from` is the same path as `--output`, the
|
||||
summary is rebuilt from the file (each per-row includes `question_type` and
|
||||
`recall_hit`) so the final aggregate covers all resumed questions, not just
|
||||
this run's slice. The prior summary at the file tail is replaced, not
|
||||
appended — a brain that resumes 5 times across a 500-question run ends with
|
||||
exactly ONE summary at the tail.
|
||||
|
||||
**Optional gate.** `--by-type-floor 0.85` exits non-zero when any
|
||||
`question_type`'s rate falls below 0.85. Default: informational only.
|
||||
|
||||
```bash
|
||||
# Diagnose per-type ranking quality after a search-touching change.
|
||||
gbrain eval longmemeval ~/datasets/longmemeval_s.jsonl \
|
||||
--by-type --output /tmp/run.jsonl
|
||||
tail -1 /tmp/run.jsonl | jq . # summary line
|
||||
|
||||
# Strict gate in a CI script.
|
||||
gbrain eval longmemeval test/fixtures/longmemeval-mini.jsonl \
|
||||
--by-type --by-type-floor 0.80 --output /tmp/run.jsonl
|
||||
echo "exit=$?" # 1 if any type fell below 0.80
|
||||
```
|
||||
|
||||
### Hermetic retrieval gate — `test/eval-replay-gate.test.ts`
|
||||
|
||||
The v0.40.1.0 Track D structural fix for "PRs touching `src/core/search/`
|
||||
silently regress retrieval." Replaces the original "replay against captured
|
||||
eval_candidates" design (which Codex caught as non-functional in CI — see
|
||||
the `v0.41+: contributor-mode CI capture` TODO in `TODOS.md` for the deferred
|
||||
real-query version).
|
||||
|
||||
How it works:
|
||||
- Hand-curated qrels fixture at `test/fixtures/eval-baselines/qrels-search.json`
|
||||
with PLACEHOLDER names only (no real people / companies per CLAUDE.md privacy
|
||||
rule).
|
||||
- The test seeds a PGLite engine with synthetic pages whose embeddings are
|
||||
basis vectors (the same `basisEmbedding(idx)` pattern as
|
||||
`test/e2e/search-quality.test.ts`). No API keys, no DATABASE_URL.
|
||||
- For each qrels query, calls `engine.searchVector(basisEmbedding(dim))` and
|
||||
computes `top1_match_rate` and `recall@10`. Asserts both meet floors
|
||||
(`>= 0.80` and `>= 0.85` by default).
|
||||
- Lives in the unit-shard test matrix (`.github/workflows/test.yml`) so it
|
||||
runs on every PR via `bun test`, NOT in the E2E fixed-file workflow.
|
||||
|
||||
#### Refreshing the qrels fixture (the `Why:` discipline, D4)
|
||||
|
||||
When CI fails because a legitimate ranking change moved expected slugs, the
|
||||
fix is to edit `qrels-search.json` directly. **Always include a `Why:` line
|
||||
in the commit body** so future maintainers can read the audit trail. Without
|
||||
the `Why:`, the gate degrades to a rubber stamp within months. The convention
|
||||
is informational (not a commit-hook block), but enforce it in PR review.
|
||||
|
||||
Example commit body:
|
||||
|
||||
```
|
||||
chore(eval): refresh qrels for new source-boost ordering
|
||||
|
||||
Why: v0.40.x source-boost now weights originals/ over concepts/, so
|
||||
q12 (founder-mode) now correctly surfaces originals/founder-mode-example
|
||||
top-1. Manual verification: ran the production query; new ranking is
|
||||
clearly better-aligned with the query intent.
|
||||
```
|
||||
|
||||
#### Env-overrides for floors
|
||||
|
||||
```bash
|
||||
GBRAIN_REPLAY_GATE_TOP1_FLOOR=0.85 \
|
||||
GBRAIN_REPLAY_GATE_RECALL_FLOOR=0.90 \
|
||||
bun test test/eval-replay-gate.test.ts
|
||||
```
|
||||
|
||||
Use to tighten or loosen the gate as the qrels fixture matures.
|
||||
|
||||
### `gbrain eval cross-modal --batch` — batch quality scoring
|
||||
|
||||
Single-task cross-modal eval scores one (task, output) pair. Batch mode runs
|
||||
the same scoring over an entire LongMemEval JSONL output, with cost guardrails.
|
||||
|
||||
```bash
|
||||
# Step 1: produce LongMemEval hypotheses (real cost: depends on model + N).
|
||||
gbrain eval longmemeval ~/datasets/longmemeval_s.jsonl \
|
||||
--limit 10 --output /tmp/run.jsonl
|
||||
|
||||
# Step 2: batch-score those hypotheses (real cost: ~$0.70 for 10 questions,
|
||||
# 1 cycle, 3 model slots at default --max-usd 5 budget cap).
|
||||
gbrain eval cross-modal --batch /tmp/run.jsonl \
|
||||
--limit 10 --cycles 1 --concurrent 3 --max-usd 5 --json
|
||||
echo "exit=$?" # 0=all-pass, 1=any-fail, 2=any-error-or-inconclusive
|
||||
```
|
||||
|
||||
**Key behaviors:**
|
||||
- Default `--cycles 1` in batch mode (single-task default is 3 in TTY) to bound
|
||||
cost. Pass `--cycles 3` to match single-task strictness.
|
||||
- `--concurrent 3` runs up to 3 questions in parallel x 3 model slots each =
|
||||
9 simultaneous API calls. Below tier-1 rate limits for all three providers.
|
||||
- `--max-usd FLOAT` refuses to start if the pre-flight cost estimate exceeds
|
||||
the cap, unless `--yes` bypasses (required for non-interactive cron / CI).
|
||||
- Filters `kind: "by_type_summary"` rows automatically (the LongMemEval
|
||||
`--by-type` summary line is metadata, not a question).
|
||||
- `--batch` is mutually exclusive with `--task`; fail-fast usage error if both
|
||||
are set.
|
||||
- Exit precedence (fail-loud): ERROR > FAIL > INCONCLUSIVE > PASS.
|
||||
- Per-question receipts land in a tempdir and are deleted at end of batch; the
|
||||
summary inlines per-question verdicts so the audit trail is self-contained.
|
||||
|
||||
### Nightly cross-modal quality probe (opt-in, autopilot)
|
||||
|
||||
`src/core/cycle/nightly-quality-probe.ts` ships a phase that runs the longmemeval
|
||||
+ cross-modal pipeline once per 24h. **Disabled by default** to avoid surprise
|
||||
API spend. Enable per-host:
|
||||
|
||||
```bash
|
||||
gbrain config set autopilot.nightly_quality_probe.enabled true
|
||||
gbrain config set autopilot.nightly_quality_probe.max_usd 5.00 # optional override
|
||||
```
|
||||
|
||||
Note: `--phase nightly_quality_probe` wiring into the autopilot scheduler is
|
||||
deferred to a v0.41+ follow-up (see TODOS.md). For now the phase is callable
|
||||
in isolation; the test harness exercises it via DI stubs.
|
||||
|
||||
```bash
|
||||
# Manual smoke (exercises the path via DI stubs, no real API spend).
|
||||
bun test test/nightly-quality-probe.test.ts
|
||||
```
|
||||
|
||||
Observability:
|
||||
- `~/.gbrain/audit/quality-probe-YYYY-Www.jsonl` — one event per run with
|
||||
outcome (pass / fail / inconclusive / error / budget_exceeded /
|
||||
rate_limited / no_embedding_key), pass/fail/inconclusive/error counts,
|
||||
est_cost_usd, fixture_sha8. ISO-week rotation (mirrors slug-fallback
|
||||
audit).
|
||||
- `gbrain doctor` surfaces `nightly_quality_probe_health`:
|
||||
- SKIPPED (disabled) — with paste-ready enable command.
|
||||
- OK (enabled, no events yet) — autopilot hasn't fired its first run.
|
||||
- OK (last 7d all PASS) — with timestamp of latest run.
|
||||
- WARN — any FAIL / ERROR / BUDGET_EXCEEDED in the window, with outcome
|
||||
counts and the latest run's reason.
|
||||
|
||||
Real expected cost: ~$0.35 per nightly run (5 questions x 3 slots x 1 cycle
|
||||
x ~$0.02/call) ≈ $10.50/month. Worst-case under the default budget cap:
|
||||
$150/month. Opt-in default prevents discovering this in your card statement.
|
||||
@@ -1,160 +0,0 @@
|
||||
# Eval capture — NDJSON schema reference
|
||||
|
||||
**Status:** stable from v0.21.0. Schema versioning via `schema_version`
|
||||
on every row; additive changes increment the minor version; removals
|
||||
are breaking-schema-v2.
|
||||
|
||||
**Audience:** downstream consumers (primarily the sibling
|
||||
[gbrain-evals](https://github.com/garrytan/gbrain-evals) repo) that
|
||||
replay captured real-world queries as a BrainBench-Real fixture.
|
||||
|
||||
## The pipeline
|
||||
|
||||
```
|
||||
MCP / CLI / subagent tool-bridge caller
|
||||
│
|
||||
▼
|
||||
src/core/operations.ts — query + search op handlers
|
||||
│
|
||||
│ (hybridSearch or searchKeyword)
|
||||
│
|
||||
▼
|
||||
{results, meta: HybridSearchMeta} ┌── captureEvalCandidate
|
||||
│ │ (fire-and-forget)
|
||||
▼ │
|
||||
return to caller ▼
|
||||
scrubPii(query) ←── src/core/eval-capture-scrub.ts
|
||||
│
|
||||
▼
|
||||
buildEvalCandidateInput
|
||||
│
|
||||
▼
|
||||
engine.logEvalCandidate
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ success │ fail
|
||||
▼ ▼
|
||||
INSERT into eval_candidates engine.logEvalCaptureFailure
|
||||
(reason: db_down | rls_reject |
|
||||
check_violation |
|
||||
scrubber_exception | other)
|
||||
```
|
||||
|
||||
## `gbrain eval export` — the consumer contract
|
||||
|
||||
```sh
|
||||
gbrain eval export [--since DUR] [--limit N] [--tool query|search]
|
||||
```
|
||||
|
||||
Emits NDJSON to **stdout**. One JSON object per `\n`-terminated line.
|
||||
stderr receives progress heartbeats. Every line starts with
|
||||
`"schema_version": 1` so a forward-compat parser can fail loudly on
|
||||
schema v2 instead of silently misparsing.
|
||||
|
||||
Typical usage from gbrain-evals:
|
||||
|
||||
```sh
|
||||
# Snapshot the last week of real traffic for replay
|
||||
gbrain eval export --since 7d > brainbench-real.ndjson
|
||||
```
|
||||
|
||||
```sh
|
||||
# Stream through jq for ad-hoc analysis
|
||||
gbrain eval export --tool query | jq -c 'select(.latency_ms > 500)'
|
||||
```
|
||||
|
||||
## Row schema (v1)
|
||||
|
||||
Every exported row has this shape. Field order in JSON output is not
|
||||
guaranteed; consumers MUST key by name, not position.
|
||||
|
||||
| Field | Type | Notes |
|
||||
|---|---|---|
|
||||
| `schema_version` | number | Always `1` on v1 rows. Forward-compat gate. |
|
||||
| `id` | number | Autoincrement primary key. Stable across exports. |
|
||||
| `tool_name` | `"query"` \| `"search"` | Which MCP operation captured this row. |
|
||||
| `query` | string | **Already PII-scrubbed** by `scrubPii` unless `eval.scrub_pii: false`. Emails / phones / SSN / Luhn-verified credit cards / JWTs / bearer tokens replaced with `[REDACTED]`. Max length 50KB (CHECK-enforced). |
|
||||
| `retrieved_slugs` | string[] | Deduplicated slugs that came back in `SearchResult[]`. |
|
||||
| `retrieved_chunk_ids` | number[] | Every chunk id in result order (duplicates preserved — one per hit). |
|
||||
| `source_ids` | string[] | Distinct `sources.id` values across the result set (v0.18 multi-source). Empty for pre-v0.18 rows that lacked the column. |
|
||||
| `expand_enabled` | boolean \| null | Whether the caller **requested** Haiku expansion. `null` for `search` (no expansion concept). |
|
||||
| `detail` | `"low"` \| `"medium"` \| `"high"` \| null | Detail level the caller **requested**. `null` when omitted. |
|
||||
| `detail_resolved` | `"low"` \| `"medium"` \| `"high"` \| null | What `hybridSearch` **actually used** after auto-detect. `null` when neither caller nor heuristic classified. |
|
||||
| `vector_enabled` | boolean | True iff vector search actually ran. `false` when `OPENAI_API_KEY` was missing or the embed call failed. **Replay MUST respect this** — rows with `false` only exercised the keyword path. |
|
||||
| `expansion_applied` | boolean | True iff Haiku expansion actually produced variants (not just "was requested"). |
|
||||
| `latency_ms` | number | Wall-clock duration of the op handler (includes capture itself — negligible since it's fire-and-forget). |
|
||||
| `remote` | boolean | `true` for MCP callers (untrusted), `false` for local CLI. Partitions "real agent traffic" from "operator probing." |
|
||||
| `job_id` | number \| null | `OperationContext.jobId` when the caller was a subagent tool-bridge. Null for MCP + CLI. |
|
||||
| `subagent_id` | number \| null | `OperationContext.subagentId` for subagent-owned runs. |
|
||||
| `created_at` | string (ISO 8601) | UTC timestamp of insert. |
|
||||
|
||||
## Ordering + determinism
|
||||
|
||||
`listEvalCandidates` orders by `created_at DESC, id DESC`. Same-
|
||||
millisecond inserts tie on `created_at`; `id DESC` is the stable
|
||||
tiebreaker. Replay tools can consume rows in order and assume:
|
||||
- no duplicate rows across calls with non-overlapping `--since` windows
|
||||
- no missed rows across calls that chain `--since` windows (window end
|
||||
of run 1 is the strict upper bound, not a soft cursor)
|
||||
|
||||
## Schema versioning promise
|
||||
|
||||
- **v1 (shipped v0.21.0)** — this document. All fields listed above.
|
||||
- **Additive changes** increment gbrain minor version (v0.25.0, v0.23.0
|
||||
…) and ship with new optional fields. Consumers keyed on known fields
|
||||
ignore unknown keys and keep working.
|
||||
- **Breaking changes** (rename, type change, removal) increment
|
||||
`schema_version` to 2. Consumers MUST branch on `schema_version` to
|
||||
stay compatible.
|
||||
|
||||
## `eval_capture_failures` — companion audit table
|
||||
|
||||
Not exported by `gbrain eval export`. Surfaced via `gbrain doctor`:
|
||||
|
||||
```sh
|
||||
gbrain doctor # warns when failures in last 24h > 0
|
||||
```
|
||||
|
||||
Reason enum (stable): `db_down` | `rls_reject` | `check_violation` |
|
||||
`scrubber_exception` | `other`. Cross-process visibility is the whole
|
||||
point — `gbrain doctor` runs in its own process and reads the table
|
||||
directly, so in-process counters wouldn't work.
|
||||
|
||||
## Config + CONTRIBUTOR_MODE
|
||||
|
||||
Capture is **off by default** as of v0.25.0 (was on for everyone in
|
||||
earlier drafts). Two paths to turn it on:
|
||||
|
||||
**Path A — env var (contributor opt-in, the common case):**
|
||||
|
||||
```bash
|
||||
export GBRAIN_CONTRIBUTOR_MODE=1 # in ~/.zshrc or ~/.bashrc
|
||||
```
|
||||
|
||||
**Path B — explicit config (`~/.gbrain/config.json`, file-plane only):**
|
||||
|
||||
```json
|
||||
{
|
||||
"engine": "postgres",
|
||||
"database_url": "...",
|
||||
"eval": {
|
||||
"capture": true,
|
||||
"scrub_pii": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Resolution order (most explicit wins):
|
||||
|
||||
1. `eval.capture: true` in config → on
|
||||
2. `eval.capture: false` in config → off (overrides CONTRIBUTOR_MODE=1)
|
||||
3. `GBRAIN_CONTRIBUTOR_MODE === '1'` → on
|
||||
4. otherwise → off
|
||||
|
||||
`scrub_pii` defaults to `true` independent of capture. Set
|
||||
`eval.scrub_pii: false` to preserve raw query text (only if you control
|
||||
the brain's distribution).
|
||||
|
||||
`gbrain config set eval.capture false` does **not** work — that
|
||||
command writes the DB-plane config, and the MCP server reads the
|
||||
file-plane. Edit the JSON directly or use the env var.
|
||||
@@ -1,159 +0,0 @@
|
||||
# `gbrain eval takes-quality` — reproducible cross-modal quality eval
|
||||
|
||||
v0.32+ ships a CI-able quality gate for the takes layer. Three frontier models
|
||||
score a sample of takes against a 5-dimension rubric, the runner aggregates to
|
||||
PASS / FAIL / INCONCLUSIVE, and the receipt persists to `eval_takes_quality_runs`
|
||||
so a follow-up `trend` or `regress` can compare against history.
|
||||
|
||||
This doc is the consumer contract. The sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals)
|
||||
repo and any future CI gate read receipts shaped exactly like the JSON below.
|
||||
Fields are additive-stable at `schema_version: 1`. A breaking shape change
|
||||
bumps the version.
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Brain required? | Exit codes |
|
||||
|---|---|---|
|
||||
| `gbrain eval takes-quality run [flags]` | yes (samples takes) | 0 PASS, 1 FAIL, 2 INCONCLUSIVE |
|
||||
| `gbrain eval takes-quality replay <receipt>` | **no** (disk-only) | 0 PASS, 1 FAIL, 2 INCONCLUSIVE |
|
||||
| `gbrain eval takes-quality trend [flags]` | yes (reads runs table) | 0 |
|
||||
| `gbrain eval takes-quality regress --against <receipt>` | yes | 0 OK, 1 regression |
|
||||
|
||||
`replay` is the only mode that runs without `DATABASE_URL` — it reads the
|
||||
receipt file from disk and re-renders it. The other modes need the brain.
|
||||
|
||||
## `run` flags
|
||||
|
||||
| Flag | Default | Notes |
|
||||
|---|---|---|
|
||||
| `--limit N` | 100 | Random sample of N takes from the brain. |
|
||||
| `--cycles N` | 3 (TTY) / 1 (non-TTY) | Up to N panel calls before giving up; early-stop on PASS or INCONCLUSIVE. |
|
||||
| `--budget-usd N` | unset | Abort before next call's projected cost would exceed cap. Models without a `pricing.ts` entry fail loud (codex #4). |
|
||||
| `--source db|fs` | `db` | `fs` is reserved for v0.33+. |
|
||||
| `--slug-prefix P` | unset | Filter takes to pages whose slug starts with P. |
|
||||
| `--models a,b,c` | `openai:gpt-4o,anthropic:claude-opus-4-7,google:gemini-1.5-pro` | Comma-separated panel. |
|
||||
| `--json` | off | Emit the full receipt to stdout. |
|
||||
|
||||
## Receipt JSON shape (`schema_version: 1`)
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"ts": "2026-05-09T22:00:00.000Z",
|
||||
"rubric_version": "v1.0",
|
||||
"rubric_sha8": "abcd1234",
|
||||
"corpus": {
|
||||
"source": "db",
|
||||
"n_takes": 100,
|
||||
"slug_prefix": null,
|
||||
"corpus_sha8": "abcd1234"
|
||||
},
|
||||
"prompt_sha8": "abcd1234",
|
||||
"models_sha8": "abcd1234",
|
||||
"models": ["openai:gpt-4o", "anthropic:claude-opus-4-7", "google:gemini-1.5-pro"],
|
||||
"cycles_run": 3,
|
||||
"successes_per_cycle": [3, 3, 2],
|
||||
"verdict": "pass",
|
||||
"scores": {
|
||||
"accuracy": { "mean": 7.8, "min": 7, "max": 9, "scores": [9,7,7], "per_model": {...} },
|
||||
"attribution": { "mean": 7.0, "min": 7, "max": 7, "scores": [7,7,7], "per_model": {...} },
|
||||
"weight_calibration": { "mean": 7.5, "min": 7, "max": 8, "scores": [8,7,7], "per_model": {...} },
|
||||
"kind_classification": { "mean": 7.2, "min": 7, "max": 8, "scores": [7,8,7], "per_model": {...} },
|
||||
"signal_density": { "mean": 7.0, "min": 6, "max": 8, "scores": [8,7,6], "per_model": {...} }
|
||||
},
|
||||
"overall_score": 7.3,
|
||||
"cost_usd": 1.85,
|
||||
"improvements": ["..."],
|
||||
"errors": [],
|
||||
"verdictMessage": "PASS: every dim mean >=7 and min >=5 ..."
|
||||
}
|
||||
```
|
||||
|
||||
### Field reference
|
||||
|
||||
- `schema_version` — locks the contract. Adding optional fields is additive
|
||||
and compatible. Renaming, removing, or changing semantics bumps the version.
|
||||
- `rubric_version` + `rubric_sha8` — segregate trend rows by rubric epoch
|
||||
(codex review #3). When the rubric definition changes, both fields update,
|
||||
and trend mode groups runs accordingly so a stricter rubric doesn't
|
||||
silently look like a quality drop.
|
||||
- `corpus.corpus_sha8` — fingerprint over the joined takes-text the judge
|
||||
saw. Determines whether two runs are over the "same" sample.
|
||||
- `models_sha8` — fingerprint over the sorted model id list. Re-ordering
|
||||
models in `--models` doesn't change the sha (sort is stable).
|
||||
- `successes_per_cycle` — count of contributing models per cycle. A model
|
||||
contributes when (a) its JSON parsed AND (b) every declared rubric dim
|
||||
has a finite score (codex review #5 — missing-dim drops the contribution).
|
||||
- `verdict` — `pass` if every dim mean >= 7 AND every dim min across
|
||||
contributing models >= 5; `fail` otherwise; `inconclusive` if fewer than
|
||||
2/3 models contributed complete scores.
|
||||
- `cost_usd` — sum of per-call cost via `pricing.ts`. Unknown models when
|
||||
`--budget-usd` is set produce a `PricingNotFoundError` before any call
|
||||
fires.
|
||||
|
||||
## Receipt persistence
|
||||
|
||||
Receipts persist to **`eval_takes_quality_runs`** (DB-authoritative per
|
||||
codex review #6) AND to disk at `~/.gbrain/eval-receipts/takes-quality-<corpus>-<prompt>-<models>-<rubric>.json`
|
||||
as a best-effort artifact. The DB row carries the full receipt JSON in the
|
||||
`receipt_json` JSONB column, so when the disk artifact is gone, `replay`
|
||||
can still reconstruct via `loadReceiptFromDb` (v0.33+ flag wiring).
|
||||
|
||||
The 4-sha primary key is unique (`UNIQUE` constraint) so re-running an
|
||||
identical eval is `INSERT ... ON CONFLICT DO NOTHING` — idempotent.
|
||||
|
||||
## Trend output
|
||||
|
||||
Plain text (default):
|
||||
|
||||
```
|
||||
ts rubric verdict overall cost corpus
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
2026-05-09T22:00:00 v1.0 pass 7.3 $1.85 abcd1234
|
||||
2026-05-08T18:30:00 v1.0 fail 6.8 $1.92 ef567890
|
||||
```
|
||||
|
||||
JSON shape (`--json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"rows": [
|
||||
{ "id": 42, "ts": "...", "rubric_version": "v1.0", "verdict": "pass",
|
||||
"overall_score": 7.3, "cost_usd": 1.85, "corpus_sha8": "abcd1234" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Regress: gating CI on quality
|
||||
|
||||
```bash
|
||||
# Capture a baseline.
|
||||
gbrain eval takes-quality run --limit 100 --json \
|
||||
> .ci/takes-quality-baseline.json
|
||||
|
||||
# Later, after changing the extraction prompt:
|
||||
gbrain eval takes-quality regress --against .ci/takes-quality-baseline.json \
|
||||
--threshold 0.5
|
||||
# exit 0 → no regression past threshold
|
||||
# exit 1 → some dim dropped > 0.5; CI fails
|
||||
```
|
||||
|
||||
The threshold is the per-dim-mean drop counting as regression. Default 0.5.
|
||||
Regress reuses the **same** model panel + slug prefix + source as the prior
|
||||
receipt for an apples-to-apples compare. Diffs in `corpus_sha8` /
|
||||
`prompt_sha8` / `rubric_sha8` are surfaced as informational warnings (the
|
||||
runner doesn't refuse — that's the caller's call).
|
||||
|
||||
## Contract stability
|
||||
|
||||
The shape above is the read contract for downstream consumers. Anything
|
||||
not listed (e.g. internal aggregator state, gateway providerMetadata) is
|
||||
**not** in the receipt and may change without notice.
|
||||
|
||||
When you need to evolve the schema:
|
||||
1. Additive optional field → no version bump; old consumers ignore the
|
||||
new key, new consumers read it.
|
||||
2. Renamed or removed field, or changed semantics → bump
|
||||
`schema_version` to `2`; runner emits both shapes for one release as
|
||||
a deprecation runway.
|
||||
@@ -1,124 +0,0 @@
|
||||
# Evaluation Metric Glossary
|
||||
|
||||
**Auto-generated from `src/core/eval/metric-glossary.ts`. Do not edit by hand.** Run `bun run scripts/generate-metric-glossary.ts` to regenerate.
|
||||
|
||||
Every metric `gbrain eval *` and `gbrain search stats` reports has a plain-English explanation here. Industry terms are preserved verbatim so users searching the literature find what we report.
|
||||
|
||||
## Retrieval Metrics
|
||||
|
||||
### Precision at k (P@k)
|
||||
|
||||
**Key:** `precision@k`
|
||||
|
||||
**Plain English:** Of the top k results the engine returned, what fraction were actually relevant? High precision means few junk results in the top of the list.
|
||||
|
||||
**Range:** 0..1, higher is better. P@10 = 0.7 means 7 of the top 10 results were on-topic.
|
||||
|
||||
### Recall at k (R@k)
|
||||
|
||||
**Key:** `recall@k`
|
||||
|
||||
**Plain English:** Of all the relevant results that exist in the brain, what fraction did the engine find in its top k? High recall means few missed answers.
|
||||
|
||||
**Range:** 0..1, higher is better. R@10 = 0.81 means out of every 100 questions, the right answer was in the top 10 for 81 of them.
|
||||
|
||||
### Mean Reciprocal Rank (MRR)
|
||||
|
||||
**Key:** `mrr`
|
||||
|
||||
**Plain English:** On average, how far down the list is the FIRST relevant result? An MRR of 1.0 means the first hit is always right; an MRR of 0.5 means it's typically at rank 2.
|
||||
|
||||
**Range:** 0..1, higher is better. Computed as the average of 1/rank-of-first-relevant-result across all test queries.
|
||||
|
||||
### Normalized Discounted Cumulative Gain at k (nDCG@k)
|
||||
|
||||
**Key:** `ndcg@k`
|
||||
|
||||
**Plain English:** Like precision@k, but the engine gets MORE credit for putting good results near the top than near rank k. A perfect ordering scores 1.0; a totally random ordering scores near 0.
|
||||
|
||||
**Range:** 0..1, higher is better. nDCG@10 above 0.65 is the common "ship it" threshold for hybrid retrieval on technical corpora.
|
||||
|
||||
## Set-Similarity / Stability Metrics
|
||||
|
||||
### Jaccard similarity at k (set Jaccard @k)
|
||||
|
||||
**Key:** `jaccard@k`
|
||||
|
||||
**Plain English:** How much do two result lists overlap? Compare the top k slugs from the captured baseline against the current run; Jaccard@10 = 1.0 means perfect agreement, 0.0 means zero overlap.
|
||||
|
||||
**Range:** 0..1, higher = more stable. Below 0.5 on a stable corpus means retrieval changed significantly.
|
||||
|
||||
### Top-1 stability rate
|
||||
|
||||
**Key:** `top1_stability`
|
||||
|
||||
**Plain English:** Fraction of queries where the #1 result is the same between two runs. The most aggressive stability check — small ranking shifts that don't change the top answer don't hurt it.
|
||||
|
||||
**Range:** 0..1, higher = more stable. Above 0.85 typically means safe-to-merge for retrieval changes.
|
||||
|
||||
## Statistical-Significance Metrics
|
||||
|
||||
### p-value (paired bootstrap)
|
||||
|
||||
**Key:** `p_value`
|
||||
|
||||
**Plain English:** How likely the observed difference between two modes is just noise. Lower = stronger evidence the difference is real. We compute paired bootstrap with 10,000 resamples and Bonferroni correction across the 12 comparisons (3 modes × 4 metrics).
|
||||
|
||||
**Range:** 0..1, lower = stronger signal. Below 0.05 is the common "statistically significant" threshold; below 0.01 is strong evidence.
|
||||
|
||||
### 95% Confidence Interval (CI)
|
||||
|
||||
**Key:** `confidence_interval`
|
||||
|
||||
**Plain English:** The range we're 95% sure the true value falls inside, given the sample we measured. Narrower CI = more reliable estimate. Computed via bootstrap resampling.
|
||||
|
||||
**Range:** Two-tuple [low, high]. If 0 is inside the CI for a Δ, the difference isn't statistically significant.
|
||||
|
||||
## Operational / Cost Metrics
|
||||
|
||||
### Cache hit rate
|
||||
|
||||
**Key:** `cache_hit_rate`
|
||||
|
||||
**Plain English:** Fraction of searches that reused a recent cached answer instead of running fresh. Higher hit rate = lower latency + lower LLM spend, but stale results may slip through if the threshold is too loose.
|
||||
|
||||
**Range:** 0..1, higher generally better. 0.7-0.9 is the sweet spot for a busy brain; above 0.9 may indicate the similarity threshold is too loose.
|
||||
|
||||
### Average results returned
|
||||
|
||||
**Key:** `avg_results`
|
||||
|
||||
**Plain English:** Mean number of search-result rows the engine returned per call. Should be near the active mode's searchLimit unless the brain is small or the budget is dropping results.
|
||||
|
||||
**Range:** 0..searchLimit. Far below searchLimit suggests budget pressure or sparse retrieval.
|
||||
|
||||
### Average tokens delivered
|
||||
|
||||
**Key:** `avg_tokens`
|
||||
|
||||
**Plain English:** Estimated tokens (chars / 4) in the chunk text returned per search call. The direct measure of how much context an agent loop is paying for each search.
|
||||
|
||||
**Range:** 0..tokenBudget. Approximates OpenAI tiktoken count for English; off by ~5-10% for Anthropic and worse for non-English.
|
||||
|
||||
### Cost per query (USD)
|
||||
|
||||
**Key:** `cost_per_query_usd`
|
||||
|
||||
**Plain English:** Sum of LLM + embedding API charges for one search call. Includes Haiku expansion call (tokenmax mode only) + embedding cost + downstream answer-model cost if measured.
|
||||
|
||||
**Range:** 0..unbounded. Conservative mode is typically <\$0.001 per call; tokenmax with answer-gen can exceed \$0.01.
|
||||
|
||||
### p99 latency (ms)
|
||||
|
||||
**Key:** `p99_latency_ms`
|
||||
|
||||
**Plain English:** 99th percentile wall-clock time per search call. The latency that 1% of users see — long-tail experience, not the average.
|
||||
|
||||
**Range:** 0..unbounded. Warm-cache hits should be <50ms; tokenmax with expansion can exceed 200ms due to the Haiku call.
|
||||
|
||||
---
|
||||
|
||||
## Coverage
|
||||
|
||||
Every metric printed by any `gbrain eval *` or `gbrain search stats` command resolves through `getMetricGloss()` in `src/core/eval/metric-glossary.ts`. Adding a new metric to the glossary REQUIRES updating this doc; the CI guard catches drift.
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
# Search Mode Evaluation Methodology
|
||||
|
||||
_How v0.32.3 measures the difference between `conservative`, `balanced`, and `tokenmax`. Written haters-immune: every claim is reproducible from the committed dataset + raw outputs._
|
||||
|
||||
## 1. What this measures and what it doesn't
|
||||
|
||||
**Measures:** retrieval quality and operational cost on fixed public datasets, under each named search mode, against the same brain content.
|
||||
|
||||
**Does NOT measure:**
|
||||
- Your specific brain content (this is a benchmark, not your bill).
|
||||
- Your specific query distribution.
|
||||
- End-user satisfaction or downstream task success.
|
||||
- Latency under concurrent load.
|
||||
- Production cost (the cost numbers are model-pricing estimates × dataset size, not your actual API spend).
|
||||
|
||||
If you want to know how a mode behaves on YOUR brain, run `gbrain search stats --days 30` after a real usage window, then run `gbrain search tune` for actionable recommendations.
|
||||
|
||||
## 2. Datasets and sizes
|
||||
|
||||
- **LongMemEval** — public split, `n=500` questions. Downloaded from [Hugging Face](https://huggingface.co/datasets/xiaowu0162/longmemeval). The corpus + answer keys are pinned to a specific commit; recorded in every per-run record.
|
||||
- **Replay captures** — NDJSON from the sibling `gbrain-evals` repo, `n=200` queries. Each query carries a `retrieved_slugs` baseline + a `latency_ms` measurement from the original production run.
|
||||
- **BrainBench v1** — `n=1240` documents / `n=350` qrels (binary relevance judgments). Lives in the sibling [`gbrain-evals`](https://github.com/garrytan/gbrain-evals) repo, SHA-pinned at every run.
|
||||
|
||||
No private brain content is used in any reported result. The committed NDJSON dumps under `<repo>/.gbrain-evals/` contain only the LongMemEval question IDs + the rank-ordered retrieved session IDs.
|
||||
|
||||
## 3. Sample selection
|
||||
|
||||
- **Random seed:** `42` throughout. Set via `--seed N` on `gbrain eval run-all`; recorded in every per-run record.
|
||||
- **No per-question curation.** Splits are taken whole; no question is filtered for reporting.
|
||||
- **No mode-specific tuning.** The same dataset + same seed feeds every mode. The mode is the only independent variable.
|
||||
- **Stability across re-runs:** with `--seed 42` and the same dataset SHA, two runs of the same (mode, suite) produce identical retrieval orderings (modulo the optional Haiku expansion call, which is non-deterministic). Persisted in `eval_results` so anyone can re-score from the committed dumps.
|
||||
|
||||
## 4. Run procedure
|
||||
|
||||
The command is the doc. Anyone can reproduce.
|
||||
|
||||
```bash
|
||||
# Setup: in your gbrain working tree, with OPENAI_API_KEY + ANTHROPIC_API_KEY exported.
|
||||
git rev-parse HEAD # record the commit for the methodology footer
|
||||
|
||||
# Sweep all 3 modes × 2 retrieval-focused suites with seed 42.
|
||||
gbrain eval run-all \
|
||||
--modes conservative,balanced,tokenmax \
|
||||
--suites longmemeval,replay \
|
||||
--seed 42 \
|
||||
--limit 500 \
|
||||
--budget-usd-retrieval 5 \
|
||||
--budget-usd-answer 20 \
|
||||
--output docs/eval/results/v0.32.3/
|
||||
|
||||
# Render the comparison.
|
||||
gbrain eval compare --md > docs/eval/results/v0.32.3/README.md
|
||||
gbrain eval compare --json > docs/eval/results/v0.32.3/comparison.json
|
||||
```
|
||||
|
||||
The orchestrator writes per-run records to `<repo>/.gbrain-evals/eval-results.jsonl`. Every record carries: `run_id`, `ran_at`, `suite`, `mode`, `commit`, `seed`, `limit`, `params`, `status`, `duration_ms`. The dumps under `docs/eval/results/v0.32.3/` carry the raw question-level outputs so a reviewer can re-score with their own metric implementation.
|
||||
|
||||
## 5. Threats to validity
|
||||
|
||||
Honest list. We name what would let a critic dismiss the numbers.
|
||||
|
||||
- **LongMemEval skews English + technical.** The questions are software-engineering and consumer-product flavored. Performance on a brain rich in non-English / non-technical content (writing, art history, etc.) may differ.
|
||||
- **BrainBench is small** (1240 docs) relative to a production brain (10K-100K pages). Absolute scores aren't predictive of your hit rate; the _delta_ between modes is.
|
||||
- **char/4 token heuristic.** Token-budget enforcement and cost estimates use a character-count / 4 heuristic. Accurate within ~5-10% for English with the OpenAI tiktoken family; off worse for Voyage (we don't use Voyage in chat retrieval, so it doesn't bias the reported numbers, but if you do, your budget caps will be approximate).
|
||||
- **Expansion's quality lift varies by query distribution.** The eval data shows ~97.6% relative quality with LLM expansion vs without (i.e., barely measurable lift) on the LongMemEval corpus. On rarer-entity / longer-tail queries, the lift can be larger. We report the corpus we measured; YMMV.
|
||||
- **Paired bootstrap assumes question-level independence.** Multi-hop questions within the same conversation thread aren't independent; the bootstrap CI is slightly tighter than reality.
|
||||
- **Single brain instance per benchmark.** The benchmark spins up an in-memory PGLite per question. Cache hit rate measured here doesn't reflect a long-running production brain's cache state.
|
||||
|
||||
## 6. Per-question raw outputs
|
||||
|
||||
Every reported metric is reproducible from the NDJSON dumps committed at `docs/eval/results/v0.32.3/`. The commit SHA in the methodology footer pins the code version.
|
||||
|
||||
**Examples per mode:** the auto-generated `README.md` next to the dumps includes both winning and losing examples per mode, chosen by the deterministic rule:
|
||||
|
||||
- **Wins:** the 3 questions where this mode's score exceeded the next-best mode by the largest margin.
|
||||
- **Losses:** the 3 questions where this mode's score fell short of the next-best mode by the largest margin.
|
||||
|
||||
Picked by the score delta, NOT cherry-picked by hand. The README documents the rule so a critic can verify.
|
||||
|
||||
## 7. Pre-registered expectations
|
||||
|
||||
Before running, we expect:
|
||||
|
||||
1. **tokenmax wins Recall@10** by 5-15 percentage points over conservative. LLM expansion + 50-result ceiling helps rare-entity surface forms.
|
||||
2. **conservative wins cost-per-query** by 5-15× over tokenmax. No Haiku expansion + tight 4K budget cap = single-digit-cent queries.
|
||||
3. **balanced lands within 3pp of tokenmax** on Recall@10. Intent weighting (zero-LLM cost) closes most of the expansion gap on common queries.
|
||||
4. **No mode breaks nDCG@10 ≥ 0.65** — the published "ship it" threshold for hybrid retrieval on technical corpora.
|
||||
|
||||
Then we publish whether the data agrees. **If a hypothesis fails, that's documented honestly** in the release README, not buried. Pre-registration is what makes the comparison defensible — without it, a "we expected X and got X" outcome is observation, not prediction.
|
||||
|
||||
## 8. Re-run cadence
|
||||
|
||||
This document + the eval results are regenerated on every release that touches retrieval-affecting code. The `gbrain doctor eval_drift` check surfaces changes to the curated watch-list in `src/core/eval/drift-watch.ts`:
|
||||
|
||||
- `src/core/search/**`
|
||||
- `src/core/embedding.ts`
|
||||
- `src/core/chunkers/**`
|
||||
- `src/core/ai/recipes/anthropic.ts`
|
||||
- `src/core/ai/recipes/openai.ts`
|
||||
- `src/core/operations.ts`
|
||||
|
||||
Additions to the watch-list require a CHANGELOG line.
|
||||
|
||||
## Statistical-significance discipline
|
||||
|
||||
When `gbrain eval compare --md` reports a Δ between two modes, it computes:
|
||||
|
||||
- **Paired bootstrap** with 10,000 resamples per metric. Each resample draws _question-level_ pairs (same question, mode A vs mode B), so question-level variance is differenced out.
|
||||
- **Bonferroni correction** across the 12 comparisons (3 modes × 4 metrics). The reported p-value is the comparison's raw p-value × 12 (clamped at 1.0).
|
||||
- **95% confidence intervals** computed from the bootstrap distribution.
|
||||
|
||||
If the CI for a Δ includes 0 OR the Bonferroni-adjusted p-value exceeds 0.05, the difference is **not** statistically significant. The MD report says "not significant" verbatim.
|
||||
|
||||
## Glossary
|
||||
|
||||
Every metric the report prints has a plain-English entry in `docs/eval/METRIC_GLOSSARY.md`, auto-generated from `src/core/eval/metric-glossary.ts`. The CI guard at `scripts/check-eval-glossary-fresh.sh` regenerates and diffs against the committed file on every test run; a stale doc fails the build.
|
||||
|
||||
## Cost anchors
|
||||
|
||||
The mode-picker prompt at `gbrain init` and the CLAUDE.md `## Search Mode` table both surface these rough cost anchors. Working through the math so they're auditable:
|
||||
|
||||
**Variables:**
|
||||
- `T` = avg tokens per search-result chunk. The recursive chunker targets 300 words / chunk → ~400 tokens (English, OpenAI tiktoken approx).
|
||||
- `N` = chunks delivered per query (capped by the mode's `searchLimit`).
|
||||
- `R` = downstream model input rate. Sonnet 4.6 = \$3/M. Opus 4.7 = \$5/M. Haiku 4.5 = \$1/M.
|
||||
- `Q` = queries per month.
|
||||
|
||||
**Per-query input cost** (downstream agent reads the chunks):
|
||||
|
||||
cost_per_query = T × N × R
|
||||
|
||||
| Mode | T (tokens) | N (chunks) | Sonnet (\$3/M) | Opus (\$5/M) | Haiku (\$1/M) |
|
||||
|---|---|---|---|---|---|
|
||||
| conservative (4K cap, 10 max) | ~400 | 10 (or fewer if budget hits) | \$0.012 | \$0.020 | \$0.004 |
|
||||
| balanced (12K cap, 25 max) | ~400 | ~25 | \$0.030 | \$0.050 | \$0.010 |
|
||||
| tokenmax (no cap, 50 max) | ~400 | ~50 | \$0.060 | \$0.100 | \$0.020 |
|
||||
|
||||
**Monthly cost** (Q × per-query):
|
||||
|
||||
| Mode @ Sonnet | 1K Q/mo | 10K Q/mo | 100K Q/mo |
|
||||
|---|---|---|---|
|
||||
| conservative | \$12 | \$120 | \$1,200 |
|
||||
| balanced | \$30 | \$300 | \$3,000 |
|
||||
| tokenmax | \$60 | \$600 | \$6,000 |
|
||||
|
||||
| Mode @ Opus | 1K Q/mo | 10K Q/mo | 100K Q/mo |
|
||||
|---|---|---|---|
|
||||
| conservative | \$20 | \$200 | \$2,000 |
|
||||
| balanced | \$50 | \$500 | \$5,000 |
|
||||
| tokenmax | \$100 | \$1,000 | \$10,000 |
|
||||
|
||||
**gbrain's own cost** on top:
|
||||
- Query embedding (text-embedding-3-large @ \$0.13/M tokens): ~\$0.00001 per query. Negligible at every scale.
|
||||
- Tokenmax Haiku expansion call (\$1/M input, \$5/M output, ~500 input + 200 output per call): ~\$0.0015 per query, or \$150/mo at 100K queries. Cache hits cut this in half.
|
||||
- Per-page indexing (one-time): bounded by your import volume, not query volume. Not modeled here.
|
||||
|
||||
**Cache hit adjustment.** A warmed brain typically sees 30-50% cache hits on repeat-query traffic. Cache hits skip the downstream input cost entirely (the cached result was already in the agent's context once). So real-world costs run ~50-70% of the table above on a busy brain.
|
||||
|
||||
**Why these numbers DRIFT from your actual bill:**
|
||||
- Your agent's system prompt + reasoning tokens add input that gbrain doesn't see.
|
||||
- Compaction reduces input over a long session.
|
||||
- Most agents make 1-5 searches per turn; cost-per-turn is what bills you, not cost-per-query.
|
||||
- The model price column drifts as providers reprice; pin the rate via `src/core/anthropic-pricing.ts` for a current snapshot.
|
||||
|
||||
The picker copy + CLAUDE.md table are the canonical user-facing source. Update them in lockstep when the underlying chunker size or default `searchLimit` changes.
|
||||
|
||||
## Mode × Model matrix (the 25x spread)
|
||||
|
||||
The per-query math above assumes Sonnet 4.6 downstream. In reality, the
|
||||
downstream model tier is the BIGGER cost lever. Per-query cost at 10K
|
||||
queries/month (typical single-user volume), search payload only (no cache
|
||||
savings):
|
||||
|
||||
| Mode (search tokens) | 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** (cheap model + tight mode → frontier model + loose
|
||||
mode). **Mismatches waste capacity:**
|
||||
|
||||
- `tokenmax + Haiku`: Haiku gets 20K of search results stuffed into its
|
||||
context per query. Haiku's reasoning is weaker; more chunks = more noise,
|
||||
not more signal. You pay Haiku rates but get sub-Haiku quality. Wrong
|
||||
direction.
|
||||
- `conservative + Opus`: Opus has 200K context window and can synthesize
|
||||
across many chunks. Capping at 10 chunks / 4K tokens leaves Opus
|
||||
reasoning underfed. You pay Opus rates but get conservative-shape
|
||||
retrieval. Wasted spend.
|
||||
|
||||
**Right-sizing rule:** match the mode's `searchLimit` to the downstream
|
||||
model's "useful context depth":
|
||||
|
||||
- Haiku struggles past ~5-10 chunks of cross-referenced content → conservative
|
||||
- Sonnet handles ~25-40 chunks well → balanced
|
||||
- Opus benefits from 50+ chunks for multi-hop reasoning → tokenmax
|
||||
|
||||
## Realistic-scale anchor (single power-user agent loop)
|
||||
|
||||
The per-query math above is honest but theoretical: it treats each search as an isolated billable event. Real agent loops amortize a lot of context across turns via Anthropic prompt caching. Here's what one heavy power-user loop actually looks like in production, anonymized + scaled so the numbers represent a representative power user rather than any specific deployment.
|
||||
|
||||
**Reference shape — tokenmax in production at a single-user scale:**
|
||||
|
||||
| Quantity | Approximate value |
|
||||
|---|---|
|
||||
| 30-day total agent spend | ~\$700/mo |
|
||||
| 30-day total tokens billed | ~800M |
|
||||
| Turns per month | ~860 (~29/day; one active agent loop) |
|
||||
| Average tokens per turn | ~900K |
|
||||
| Average cost per turn | ~\$0.85 |
|
||||
| Anthropic prompt-cache hit rate | ~88% |
|
||||
|
||||
A "turn" here is one agent loop iteration: read user message, plan, execute tool calls (including gbrain searches), generate response. Each turn typically includes 2-4 gbrain searches.
|
||||
|
||||
**Per-mode scaling from the tokenmax anchor:**
|
||||
|
||||
The cost difference between modes is concentrated in the search-attributable fraction of per-turn cost. System prompt, tool definitions, conversation history, and reasoning tokens don't change with mode — only the chunks gbrain delivers do. Assume 3 searches per turn at the mode's `searchLimit`:
|
||||
|
||||
| Mode | Search tokens/turn | Search cost/turn (at \$3/M effective) | Search-attributable @ 860 turns | Δ vs tokenmax |
|
||||
|---|---|---|---|---|
|
||||
| tokenmax | ~60K (3 × 20K) | ~\$0.18 | ~\$155/mo | — |
|
||||
| balanced | ~30K (3 × 10K) | ~\$0.09 | ~\$77/mo | -\$78 |
|
||||
| conservative | ~12K (3 × 4K) | ~\$0.036 | ~\$31/mo | -\$124 |
|
||||
|
||||
**Implied total agent spend by NATURAL PAIRING** (mode + matched
|
||||
downstream model). Per-turn cost scales with the downstream model's
|
||||
per-token rate, since the cached prefix + uncached portion + reasoning
|
||||
tokens all bill at that rate:
|
||||
|
||||
| Pairing | Per-turn cost | Total @ 860 turns/mo |
|
||||
|---|---|---|
|
||||
| tokenmax + Opus (frontier, max quality) | ~\$0.85 | ~\$700/mo |
|
||||
| balanced + Sonnet (the sweet spot) | ~\$0.50 | ~\$430/mo |
|
||||
| conservative + Haiku (cost-sensitive) | ~\$0.20 | ~\$170/mo |
|
||||
|
||||
**4x spread across natural pairings.** The model tier dominates because
|
||||
the per-token rate applies to the WHOLE per-turn payload (system + tools
|
||||
+ history + reasoning + search), not just gbrain's chunks. Mode choice
|
||||
contributes ~10-20% on top of that base.
|
||||
|
||||
**Mismatched pairings push you off the curve:**
|
||||
|
||||
| Pairing | Per-turn estimate | Total @ 860 turns/mo | Compared to natural |
|
||||
|---|---|---|---|
|
||||
| tokenmax + Haiku | ~\$0.20 | ~\$170/mo | Same cost as conservative+Haiku, worse quality |
|
||||
| conservative + Opus | ~\$0.75 | ~\$640/mo | 92% of tokenmax+Opus spend, conservative-shape retrieval |
|
||||
|
||||
The mismatch math says: a tokenmax+Haiku user pays the same as
|
||||
conservative+Haiku but gets a noisier context (Haiku can't filter signal
|
||||
from 50 chunks). A conservative+Opus user pays nearly the same as
|
||||
tokenmax+Opus but starves Opus on retrieval depth. Both burn budget for
|
||||
no improvement.
|
||||
|
||||
**What this anchor tells us that the per-query math doesn't:**
|
||||
|
||||
1. **At realistic agent-loop scale with disciplined prompt caching, mode choice saves 10-20% of total agent spend** — meaningful, but smaller than the per-query 5x ratio implies. Disciplined prompt-cache layouts blunt the mode delta because most of the per-turn cost is the cached prefix, not the search payload.
|
||||
|
||||
2. **Without that prompt-cache discipline, the per-query framing reasserts itself.** Setups that churn the prompt prefix on every turn (frequent system-prompt edits, untemplated tool defs, no prompt-cache structuring) see search payload contribute a much larger fraction of total cost. Those setups should care about mode choice more, not less.
|
||||
|
||||
3. **The cache hit rate quoted here (~88%) is achievable but not automatic.** It requires structuring the prompt so the cached prefix stays stable across turns: system prompt + tool defs first, history compacted but cache-aware, retrieved chunks appended LAST (where their volatility doesn't invalidate the prefix). Agents that interleave search results inside the cached region pay the prefix-rebuild tax on every turn.
|
||||
|
||||
**Caveats stacked here:**
|
||||
|
||||
- The anchor represents ONE power-user loop. Multi-user fleets aggregate proportionally; the per-user shape doesn't change.
|
||||
- The "3 searches per turn" assumption varies wildly. A code-review agent might issue 10+ searches per turn; a chat-only loop might do 0.
|
||||
- The 88% cache hit rate is the high end of what's achievable. Half that is closer to a default agent without cache-aware prompt layout.
|
||||
- The "Δ vs tokenmax" math assumes the OTHER cost components (system, tools, history, reasoning) stay constant. In practice, conservative's smaller per-turn payload also leaves more room in the context window for history → which can change agent behavior in either direction.
|
||||
|
||||
This anchor + the per-query math both live in this doc on purpose. The per-query framing is what an isolated benchmark would measure (and what `gbrain eval run-all` will produce). The realistic-scale anchor is what an operator actually pays. Both are honest; neither is the whole truth.
|
||||
|
||||
## Reproducibility footer
|
||||
|
||||
Every release that publishes eval numbers includes a footer with:
|
||||
|
||||
- Code commit SHA
|
||||
- Dataset SHA (LongMemEval, BrainBench, Replay)
|
||||
- `--seed N`
|
||||
- Run commands verbatim
|
||||
- API model identifiers used (Anthropic + OpenAI + judge model)
|
||||
|
||||
Without these, the numbers are unfalsifiable. With them, anyone with API keys can re-score.
|
||||
@@ -1,199 +0,0 @@
|
||||
# How a downstream agent should talk to gbrain
|
||||
|
||||
This guide is for authors of downstream agents (hermes, openclaw, future
|
||||
forks) that need to call gbrain operations from their own runtime. Reading
|
||||
this first will save you a debugging cycle: gbrain has **two distinct
|
||||
surfaces**, and which one you pick depends on the operation.
|
||||
|
||||
## The two surfaces
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ gbrain process │
|
||||
│ │
|
||||
Agent (hermes, │ ┌──────────────────┐ ┌────────────────┐ │
|
||||
openclaw, fork) ────┼──▶ MCP ops surface │ │ localOnly │ │
|
||||
│ │ (HTTP + OAuth) │ │ admin ops │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ search, query, │ │ sync, embed, │ │
|
||||
│ │ put_page, │ │ dream, doctor,│ │
|
||||
│ │ get_page, │ │ autopilot, │ │
|
||||
│ │ find_experts, │ │ init, secrets │ │
|
||||
│ │ ... │ │ │ │
|
||||
│ └──────────────────┘ └────────────────┘ │
|
||||
│ ▲ ▲ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ thin-client OAuth shell-job `inherit:`│
|
||||
│ (preferred for (only path for │
|
||||
│ MCP-equivalent ops) localOnly ops) │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The two surfaces are **not interchangeable**. Pick by op, not by preference.
|
||||
|
||||
## Surface 1 — MCP ops over HTTP (thin-client + OAuth)
|
||||
|
||||
Use for any operation that has an MCP equivalent: `search`, `query`,
|
||||
`put_page`, `get_page`, `find_experts`, `find_orphans`, `find_anomalies`,
|
||||
`get_recent_salience`, `find_trajectory`, and so on. The canonical list is
|
||||
the set of ops in `src/core/operations.ts` whose `localOnly` flag is unset
|
||||
(or `false`).
|
||||
|
||||
### Setup
|
||||
|
||||
The host runs gbrain as a long-lived HTTP server:
|
||||
|
||||
```bash
|
||||
GBRAIN_ALLOW_SHELL_JOBS=1 gbrain serve --http --port 3131
|
||||
```
|
||||
|
||||
The agent registers as an OAuth client (one-time):
|
||||
|
||||
```bash
|
||||
gbrain auth register-client hermes \
|
||||
--grant-types client_credentials \
|
||||
--scopes read,write
|
||||
# Prints client_id + client_secret one-time. Store securely.
|
||||
```
|
||||
|
||||
The agent's runtime calls `/mcp` with a bearer token from `client_credentials`
|
||||
grant. Secrets stay in the gbrain serve process; the agent never sees
|
||||
DATABASE_URL or API keys.
|
||||
|
||||
Thin-client mode (`gbrain init --mcp-only`) gives the agent the same
|
||||
client-credentials wiring, plus the `gbrain` CLI itself routes MCP-eligible
|
||||
commands through the configured remote MCP. The agent can call
|
||||
`gbrain search` / `gbrain query` directly and the CLI does the OAuth dance.
|
||||
|
||||
### Why this is preferred for MCP ops
|
||||
|
||||
- Secrets never leave the server process.
|
||||
- OAuth scopes give you `read`, `write`, `admin` separation — agent only gets
|
||||
what it needs.
|
||||
- Source-scoped tokens (`--source dept-x` on `register-client`) confine the
|
||||
agent to a specific source within a federated brain.
|
||||
- One audit surface (`mcp_request_log`) covers every op call uniformly.
|
||||
|
||||
## Surface 2 — localOnly admin ops via shell-job `inherit:`
|
||||
|
||||
Some operations are flagged `localOnly: true` in `src/core/operations.ts` and
|
||||
are **refused** in thin-client mode at `src/cli.ts:isThinClient`. The full
|
||||
list (as of v0.36.5.0) includes:
|
||||
|
||||
- `sync` (filesystem walks need local FS access)
|
||||
- `embed` (orchestrates the embed pipeline)
|
||||
- `extract` (walks markdown files)
|
||||
- `dream` (synthesis cycle)
|
||||
- `doctor` (filesystem hygiene checks)
|
||||
- `autopilot` (background daemon orchestration)
|
||||
- `init` (creates `~/.gbrain/`)
|
||||
- `secrets` (config management)
|
||||
|
||||
For these, the agent cannot route through HTTP MCP. The only path is to run
|
||||
`gbrain` as a CLI subprocess. The recommended pattern is to submit the
|
||||
subprocess as a shell job to the gbrain Minions worker so retry / backoff /
|
||||
DLQ / audit trail all come for free.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
gbrain jobs submit shell --params '{
|
||||
"cmd": "gbrain sync --skip-failed && gbrain embed --stale",
|
||||
"cwd": "/data/gbrain",
|
||||
"inherit": ["database_url"]
|
||||
}'
|
||||
```
|
||||
|
||||
The `inherit: ["database_url"]` field tells the worker to look up
|
||||
`database_url` from its `loadConfig()` and inject the value into the child
|
||||
env as `GBRAIN_DATABASE_URL`. The DB row in `minion_jobs.data` carries the
|
||||
names only — `inherit: ["database_url"]` — never the value. See
|
||||
[minions-shell-jobs.md#secrets](./minions-shell-jobs.md#secrets) for the
|
||||
full validation rules and error catalog.
|
||||
|
||||
### Why this is preferred over writing secrets into `env:` per-job
|
||||
|
||||
- Pre-v0.36.5.0 callers passed `env: { GBRAIN_DATABASE_URL: "postgresql://..." }`
|
||||
per job. The URL landed plaintext in `minion_jobs.data` and the shell-audit
|
||||
JSONL. Anyone with brain-DB read access (or a brain dump, or a shared brain
|
||||
via mounts) saw the URL. As of v0.36.5.0, this is rejected at pre-enqueue
|
||||
validation. The error message names `inherit: ["database_url"]` as the
|
||||
replacement.
|
||||
|
||||
### Worker setup (one-time, per host)
|
||||
|
||||
The agent's host needs a worker that processes shell jobs:
|
||||
|
||||
```bash
|
||||
# One-shot inline execution (PGLite or Postgres):
|
||||
gbrain jobs submit shell --params '{...}' --follow
|
||||
|
||||
# Persistent worker (Postgres only — PGLite uses --follow inline):
|
||||
GBRAIN_ALLOW_SHELL_JOBS=1 gbrain jobs work
|
||||
```
|
||||
|
||||
`GBRAIN_ALLOW_SHELL_JOBS=1` is the worker-side opt-in. Without it, shell jobs
|
||||
sit in `waiting` indefinitely. Set it on the worker process env (or in your
|
||||
deploy unit / launchd plist), not per-submission — submitter env is a weak
|
||||
proxy for worker env.
|
||||
|
||||
## Decision table
|
||||
|
||||
| Operation | Surface | Why |
|
||||
|---|---|---|
|
||||
| `search` / `query` | HTTP MCP via thin-client | Has MCP op; OAuth-scoped. |
|
||||
| `get_page` / `list_pages` | HTTP MCP | Same. |
|
||||
| `put_page` | HTTP MCP | Same; respects subagent allow-list when applicable. |
|
||||
| `find_experts` / `find_orphans` | HTTP MCP | Same. |
|
||||
| `sync` / `embed` / `extract` | Shell job + `inherit:` | `localOnly: true`. |
|
||||
| `dream` | Shell job + `inherit:` | `localOnly: true`. |
|
||||
| `doctor` | Shell job + `inherit:` (or no inherit if no DB) | `localOnly: true`. |
|
||||
| `autopilot` | Run as a daemon directly on the host | Long-lived, not job-shaped. |
|
||||
| `init` / `secrets` | One-time host setup | Operator action, not agent action. |
|
||||
|
||||
## Recommended patterns
|
||||
|
||||
- **Prefer `inherit:` for secrets you don't want in the row.** Names land in
|
||||
`minion_jobs.data`; values resolve at child-spawn from the worker's config.
|
||||
If a brain DB ever traverses a trust boundary, secrets stay out.
|
||||
- **Free-form names.** `inherit:` accepts any snake_case config-key on your
|
||||
worker — `database_url`, `anthropic_api_key`, `openai_api_key`,
|
||||
`voyage_api_key`, `groq_api_key`, `zeroentropy_api_key`, or any custom
|
||||
field you stuff into `~/.gbrain/config.json`. The agent picks what it
|
||||
needs.
|
||||
- **`env:` still works** for non-secret values, or for cases where you
|
||||
WANT the value in the row (e.g. an opaque correlation token your audit
|
||||
flow needs to read back later). The validator doesn't second-guess you.
|
||||
- **Never try to route a `localOnly` op through thin-client MCP.** It will
|
||||
fail with `localOnly op refused in thin-client mode`. Use shell-job +
|
||||
`inherit:` (for secrets) or `env:` (for non-secrets).
|
||||
|
||||
## Migration: from pre-v0.36.5.0
|
||||
|
||||
If your agent submits shell jobs that pass secrets via `env:`:
|
||||
|
||||
```jsonc
|
||||
// Pre-v0.36.5.0: works but URL persists in minion_jobs.data plaintext.
|
||||
{
|
||||
"cmd": "gbrain sync --skip-failed",
|
||||
"cwd": "/data/gbrain",
|
||||
"env": { "GBRAIN_DATABASE_URL": "postgresql://..." }
|
||||
}
|
||||
```
|
||||
|
||||
Switch to (recommended):
|
||||
|
||||
```jsonc
|
||||
// v0.36.5.0+: name in row, value resolved at child-spawn from worker config.
|
||||
{
|
||||
"cmd": "gbrain sync --skip-failed",
|
||||
"cwd": "/data/gbrain",
|
||||
"inherit": ["database_url"]
|
||||
}
|
||||
```
|
||||
|
||||
Make sure the worker host has `database_url` configured (either via
|
||||
`gbrain config set database_url <value>` or via `GBRAIN_DATABASE_URL` /
|
||||
`DATABASE_URL` env on the worker process). If the worker can't resolve the
|
||||
key, the validator rejects the job at submit time with a paste-ready hint.
|
||||
@@ -1,129 +0,0 @@
|
||||
# The Brain-Agent Loop
|
||||
|
||||
## Goal
|
||||
|
||||
Every conversation makes the brain smarter. Every brain lookup makes responses
|
||||
better. The loop compounds daily.
|
||||
|
||||
## What the User Gets
|
||||
|
||||
Without this: the agent answers from stale context. You discuss a deal on Monday,
|
||||
and by Friday the agent has forgotten. Every conversation starts from zero.
|
||||
|
||||
With this: six months in, the agent knows more about your world than you can hold
|
||||
in working memory. It never forgets. It never stops indexing.
|
||||
|
||||
## The Loop
|
||||
|
||||
```
|
||||
Signal arrives (message, meeting, email, tweet, link)
|
||||
│
|
||||
▼
|
||||
DETECT entities (people, companies, concepts, original thinking)
|
||||
│ → spawn sub-agent (see entity-detection.md)
|
||||
│
|
||||
▼
|
||||
READ: check brain FIRST (before responding)
|
||||
│ → gbrain search "{entity name}"
|
||||
│ → gbrain get {slug} (if you know it)
|
||||
│ → gbrain query "what do we know about {topic}"
|
||||
│
|
||||
▼
|
||||
RESPOND with brain context (every answer is better with context)
|
||||
│
|
||||
▼
|
||||
WRITE: update brain pages (new info → compiled truth + timeline)
|
||||
│ → gbrain put {slug} (update page)
|
||||
│ → add_timeline_entry (append to timeline)
|
||||
│ → add_link (cross-reference to other entities)
|
||||
│
|
||||
▼
|
||||
SYNC: gbrain indexes changes
|
||||
│ → gbrain sync --no-pull --no-embed
|
||||
│
|
||||
▼
|
||||
(next signal arrives — agent is now smarter)
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### On Every Inbound Message
|
||||
|
||||
```
|
||||
on_message(text):
|
||||
// 1. DETECT (async, don't block)
|
||||
spawn_entity_detector(text)
|
||||
|
||||
// 2. READ (before composing response)
|
||||
entities = extract_entity_names(text) // quick regex/NER
|
||||
context = []
|
||||
for name in entities:
|
||||
results = gbrain_search(name)
|
||||
if results:
|
||||
page = gbrain_get(results[0].slug)
|
||||
context.append(page.compiled_truth)
|
||||
|
||||
// 3. RESPOND (with brain context injected)
|
||||
response = compose_response(text, context)
|
||||
|
||||
// 4. WRITE (after responding, if new info emerged)
|
||||
if response_contains_new_info(response):
|
||||
for entity in mentioned_entities:
|
||||
gbrain_add_timeline_entry(entity.slug, {
|
||||
date: today,
|
||||
summary: "Discussed {topic}",
|
||||
source: "[Source: User, conversation, {date}]"
|
||||
})
|
||||
|
||||
// 5. SYNC
|
||||
gbrain_sync()
|
||||
```
|
||||
|
||||
### The Two Invariants
|
||||
|
||||
1. **Every READ improves the response.** If you answered a question about a
|
||||
person without checking their brain page first, you gave a worse answer
|
||||
than you could have. The brain almost always has something. External APIs
|
||||
fill gaps, they don't start from scratch.
|
||||
|
||||
2. **Every WRITE improves future reads.** If a meeting transcript mentioned
|
||||
new information about a company and you didn't update the company page,
|
||||
you created a gap that will bite you later.
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Read BEFORE responding, not after.** The temptation is to respond first
|
||||
and update the brain later. But the brain context makes the response better.
|
||||
Read first.
|
||||
|
||||
2. **Don't skip the write step.** "I'll update the brain later" means never.
|
||||
Write immediately after the conversation, while the context is fresh.
|
||||
|
||||
3. **Sync after every write batch.** Without sync, the brain search index is
|
||||
stale. The next query won't find what you just wrote.
|
||||
|
||||
4. **External APIs are fallback, not primary.** `gbrain search` before
|
||||
Brave Search. `gbrain get` before Crustdata. The brain has relationship
|
||||
history, your own assessments, meeting transcripts, cross-references.
|
||||
No external API can provide that.
|
||||
|
||||
## How to Verify It Works
|
||||
|
||||
1. **Mention a person the brain knows.** Ask "what do we know about {name}?"
|
||||
The agent should search the brain and return compiled truth, not hallucinate
|
||||
or do a web search.
|
||||
|
||||
2. **Discuss something new about a known entity.** Say "I heard Acme Corp
|
||||
just raised Series B." After the conversation, check: does Acme Corp's
|
||||
brain page have a new timeline entry?
|
||||
|
||||
3. **Ask about the same person a day later.** The agent should immediately
|
||||
pull brain context without you asking. If it doesn't reference the brain
|
||||
page, the loop isn't running.
|
||||
|
||||
4. **Check the sync.** After a conversation, run `gbrain search "{topic}"`
|
||||
from the CLI. The new information should be searchable.
|
||||
|
||||
---
|
||||
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md). See also: [Entity Detection](entity-detection.md), [Brain-First Lookup](brain-first-lookup.md)*
|
||||
@@ -1,85 +0,0 @@
|
||||
# Brain-First Lookup Protocol
|
||||
|
||||
## Goal
|
||||
|
||||
Check the brain before calling ANY external API. The brain almost always has
|
||||
something. External APIs fill gaps, they don't start from scratch.
|
||||
|
||||
## What the User Gets
|
||||
|
||||
Without this: the agent calls Brave Search for someone you've had 12 meetings with.
|
||||
You get a LinkedIn summary instead of your relationship history.
|
||||
|
||||
With this: the agent pulls your compiled truth, recent timeline entries, and
|
||||
shared context before doing anything else. External APIs only fill gaps.
|
||||
|
||||
## Implementation
|
||||
|
||||
```
|
||||
lookup(name_or_topic):
|
||||
// STEP 1: Keyword search (fast, works day one, no embeddings needed)
|
||||
results = gbrain search "{name_or_topic}"
|
||||
if results.length > 0:
|
||||
page = gbrain get {results[0].slug}
|
||||
return page // done, brain had it
|
||||
|
||||
// STEP 2: Hybrid search (needs embeddings, finds semantic matches)
|
||||
results = gbrain query "what do we know about {name_or_topic}"
|
||||
if results.length > 0:
|
||||
page = gbrain get {results[0].slug}
|
||||
return page
|
||||
|
||||
// STEP 3: Direct slug (if you know or can guess the slug)
|
||||
page = gbrain get "people/{slugify(name_or_topic)}"
|
||||
if page: return page
|
||||
|
||||
// STEP 4: External API (FALLBACK ONLY)
|
||||
// Only reach here if brain has nothing
|
||||
return external_search(name_or_topic)
|
||||
```
|
||||
|
||||
**This is mandatory.** An agent that calls Brave Search before checking the brain
|
||||
is wasting money and giving worse answers.
|
||||
|
||||
## Why Brain First
|
||||
|
||||
The brain has context no external API can provide:
|
||||
- Relationship history (how you know them, what you discussed)
|
||||
- Your own assessments (what you think of them, not their LinkedIn bio)
|
||||
- Meeting transcripts (what was said, what was decided)
|
||||
- Cross-references (who they know, what companies they're connected to)
|
||||
- Timeline (what changed recently, what's trending)
|
||||
|
||||
A LinkedIn scrape gives you their job title. The brain gives you: "co-founded
|
||||
Brex, you had coffee with him 3 times, last discussed the payments infrastructure
|
||||
thesis, he's interested in your take on AI agents."
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Try keyword first, then hybrid.** Keyword search works without embeddings
|
||||
(day one). Hybrid search needs embeddings but finds semantic matches. Try
|
||||
both in sequence.
|
||||
|
||||
2. **Fuzzy slug matching.** `gbrain get` supports fuzzy matching. If the exact
|
||||
slug doesn't exist, it suggests alternatives. Use this for name variants
|
||||
("Pedro" → "pedro-franceschi").
|
||||
|
||||
3. **Don't skip for "simple" questions.** Even "what's Acme Corp's address?"
|
||||
should check the brain first. The brain might have it, and the lookup adds
|
||||
no latency (< 100ms for keyword search).
|
||||
|
||||
4. **Load compiled truth + recent timeline.** The compiled truth gives you the
|
||||
state of play in 30 seconds. The timeline gives you what changed recently.
|
||||
Both together = full context.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Ask about someone in the brain. Verify the agent searched the brain FIRST
|
||||
(check tool call order in the response).
|
||||
2. Ask about someone NOT in the brain. Verify the agent searched the brain,
|
||||
found nothing, THEN fell back to external search.
|
||||
3. Ask the same question twice. Second time should be instant (brain has it).
|
||||
|
||||
---
|
||||
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md). See also: [Brain-Agent Loop](brain-agent-loop.md), [Search Modes](search-modes.md)*
|
||||
@@ -1,75 +0,0 @@
|
||||
# Brain vs Memory vs Session
|
||||
|
||||
## Goal
|
||||
Know what goes in GBrain, what goes in agent memory, and what stays in session context -- so every piece of information lands in the right layer.
|
||||
|
||||
## What the User Gets
|
||||
Without this: people dossiers get stored in agent memory (lost on agent reset), user preferences get stored in GBrain (cluttering knowledge pages), and the agent re-asks questions it already knows the answer to. With this: world knowledge persists in the brain, operational state persists in agent memory, and the agent never puts information in the wrong layer.
|
||||
|
||||
## Implementation
|
||||
|
||||
```
|
||||
on new_information(info):
|
||||
# Three layers, three purposes -- route to the right one
|
||||
|
||||
if info.is_about_the_world:
|
||||
# GBRAIN: people, companies, deals, meetings, concepts, ideas
|
||||
# This is world knowledge -- facts about entities external to the agent
|
||||
gbrain put <slug> --content "..."
|
||||
# Examples:
|
||||
# "Pedro is CEO of Brex" -> gbrain (person page)
|
||||
# "Brex raised Series D at $12B" -> gbrain (company page)
|
||||
# "Tuesday's meeting covered Q2" -> gbrain (meeting page)
|
||||
# "The meatsuit maintenance tax" -> gbrain (originals page)
|
||||
|
||||
elif info.is_about_operations:
|
||||
# AGENT MEMORY: preferences, decisions, tool config, session continuity
|
||||
# This is how the agent operates -- not facts about the world
|
||||
memory_write(info)
|
||||
# Examples:
|
||||
# "User prefers concise formatting" -> agent memory
|
||||
# "Deploy to staging before prod" -> agent memory
|
||||
# "Use dark mode in code blocks" -> agent memory
|
||||
# "API key for Crustdata goes in .env" -> agent memory
|
||||
|
||||
elif info.is_current_conversation:
|
||||
# SESSION CONTEXT: what was just said, current task, immediate state
|
||||
# This is automatic -- already in the conversation window
|
||||
# No storage action needed
|
||||
# Examples:
|
||||
# "We were just discussing the board deck" -> session
|
||||
# "You asked me to review this PR" -> session
|
||||
# "The file I just shared" -> session
|
||||
|
||||
# Lookup routing:
|
||||
on user_asks(question):
|
||||
if question.about_person or question.about_company or question.about_meeting:
|
||||
gbrain search "{entity}" # -> world knowledge
|
||||
gbrain get <slug>
|
||||
|
||||
elif question.about_preference or question.about_how_to_operate:
|
||||
memory_search("{topic}") # -> operational state
|
||||
|
||||
elif question.about_current_context:
|
||||
# Already in session -- just reference conversation history
|
||||
pass
|
||||
```
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Don't store people in agent memory.** "Pedro prefers email over Slack" feels like a preference, but it's a fact about Pedro -- it goes in GBrain on Pedro's page. Agent memory is for the agent's own operational state, not facts about people in the world.
|
||||
2. **Don't store user preferences in GBrain.** "User likes bullet points over paragraphs" is about how the agent should behave, not about the world. It goes in agent memory. GBrain pages are for entities, not for agent configuration.
|
||||
3. **Synthesis of external ideas goes in GBrain.** "User's take on Peter Thiel's zero-to-one framework" is the user's original thinking -- it goes in GBrain under originals/, not in agent memory.
|
||||
4. **Agent memory doesn't survive agent resets on some platforms.** Critical world knowledge MUST be in GBrain, which is durable. If the agent loses memory, the brain still has everything.
|
||||
5. **When in doubt, ask: is this about the world or about how to operate?** World -> GBrain. Operations -> agent memory. Current conversation -> session.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Ask the agent "Who is Pedro?" -- confirm it runs `gbrain search` or `gbrain get`, not `memory_search`. Person lookup should hit GBrain.
|
||||
2. Ask the agent "How should I format responses?" -- confirm it checks agent memory, not GBrain. Preferences are operational state.
|
||||
3. Check that no person or company pages exist in agent memory storage. Run `memory_search "person"` -- it should return preferences, not dossiers.
|
||||
4. Check that GBrain doesn't contain pages about agent behavior. Run `gbrain search "user prefers"` -- it should return nothing (preferences belong in agent memory).
|
||||
5. After an agent reset, confirm GBrain knowledge is still accessible. Run `gbrain get <any_slug>` -- world knowledge should survive the reset.
|
||||
|
||||
---
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
@@ -1,137 +0,0 @@
|
||||
# Compiled Truth + Timeline Pattern
|
||||
|
||||
## Goal
|
||||
|
||||
Every brain page has two zones: compiled truth (current synthesis, rewritten as
|
||||
evidence changes) and timeline (append-only evidence trail, never edited).
|
||||
|
||||
## What the User Gets
|
||||
|
||||
Without this: brain pages are append-only logs. To understand a person, you read
|
||||
200 timeline entries. The answer is buried in entry #147.
|
||||
|
||||
With this: the compiled truth gives you the state of play in 30 seconds. The
|
||||
timeline is the proof. Six months of entries compress into a one-paragraph
|
||||
assessment that's always current.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Page Structure
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: person
|
||||
title: Sarah Chen
|
||||
tags: [engineering, acme-corp]
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
One paragraph. How you know them, why they matter.
|
||||
|
||||
## State
|
||||
VP Engineering at Acme Corp. Managing 45-person team. Reports to CEO.
|
||||
|
||||
## What They Believe
|
||||
Strong opinions on test coverage. "Ship it when the tests pass, not before."
|
||||
|
||||
## What They're Building
|
||||
Leading the API migration from REST to GraphQL. Target: Q3 completion.
|
||||
|
||||
## Assessment
|
||||
Sharp technical leader. Under-appreciated internally. Watch for signs of burnout.
|
||||
|
||||
## Trajectory
|
||||
Ascending. Likely CTO track if the migration succeeds.
|
||||
|
||||
## Relationship
|
||||
Met through Pedro. Had coffee 3x. Last: discussed API architecture thesis.
|
||||
|
||||
## Contact
|
||||
sarah@acmecorp.com | @sarahchen | linkedin.com/in/sarahchen
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
- **2026-04-07** | Met at team sync. Discussed API migration timeline.
|
||||
Seemed energized about GraphQL pivot.
|
||||
[Source: Meeting notes, 2026-04-07 2:00 PM PT]
|
||||
- **2026-04-03** | Mentioned in email re Q2 planning. Taking lead on ops.
|
||||
[Source: Gmail, sarah@acmecorp.com, 2026-04-03 10:30 AM PT]
|
||||
- **2026-03-15** | First meeting. Intro from Pedro. Strong technical background.
|
||||
[Source: User, direct conversation, 2026-03-15 3:00 PM PT]
|
||||
```
|
||||
|
||||
### Updating a Page
|
||||
|
||||
```
|
||||
update_brain_page(slug, new_info, source):
|
||||
page = gbrain get {slug}
|
||||
|
||||
// TIMELINE: always APPEND (never edit existing entries)
|
||||
gbrain add_timeline_entry {slug} {
|
||||
date: today,
|
||||
summary: new_info.summary,
|
||||
detail: new_info.detail,
|
||||
source: format_source(source) // [Source: who, channel, date time tz]
|
||||
}
|
||||
|
||||
// COMPILED TRUTH: REWRITE (not append)
|
||||
// Read the existing compiled truth
|
||||
// Integrate new information
|
||||
// Write the updated synthesis
|
||||
updated_truth = rewrite_compiled_truth(page.compiled_truth, new_info)
|
||||
gbrain put {slug} {
|
||||
compiled_truth: updated_truth,
|
||||
// timeline is NOT passed — it's managed by add_timeline_entry
|
||||
}
|
||||
```
|
||||
|
||||
### The Rules
|
||||
|
||||
| Zone | Action | Explanation |
|
||||
|------|--------|-------------|
|
||||
| Compiled truth | **REWRITE** | Current synthesis. Changes when evidence changes. |
|
||||
| Timeline | **APPEND** | Evidence trail. Never edited, only added to. |
|
||||
|
||||
**Every compiled truth claim must trace to timeline entries.** If the Assessment
|
||||
says "under-appreciated internally," there should be timeline entries that
|
||||
support that claim.
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **REWRITE means rewrite, not append.** Don't add a new paragraph to compiled
|
||||
truth. Rewrite the entire section with the new information integrated. Old
|
||||
assessments that are no longer accurate should be updated, not kept alongside
|
||||
contradictory new ones.
|
||||
|
||||
2. **Timeline entries are immutable.** Never edit a timeline entry. If information
|
||||
turns out to be wrong, add a NEW entry correcting it:
|
||||
`- 2026-04-10 | Correction: Sarah is VP Eng, not CTO. Previous entry was wrong.`
|
||||
|
||||
3. **GBrain search weights compiled truth higher.** `gbrain query` returns compiled
|
||||
truth chunks with higher relevance than timeline chunks. This means the freshest
|
||||
synthesis surfaces first in search results.
|
||||
|
||||
4. **The --- separator matters.** GBrain uses the first standalone `---` after
|
||||
frontmatter to split compiled_truth from timeline. Everything above is compiled
|
||||
truth, everything below is timeline.
|
||||
|
||||
5. **Don't skip the Assessment section.** The assessment is the value. "Strong
|
||||
technical leader" is something no API can provide. It's YOUR read on this
|
||||
person. That's what makes the brain page better than LinkedIn.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. **Update a person page.** Add new meeting info. Check: compiled truth was
|
||||
REWRITTEN (not appended), timeline has new entry at the top.
|
||||
2. **Search for the person.** `gbrain query "Sarah Chen"`. The compiled truth
|
||||
(current synthesis) should appear first, not a random timeline entry.
|
||||
3. **Check traceability.** Every claim in compiled truth should have a
|
||||
corresponding timeline entry. Read both sections and verify.
|
||||
4. **Check immutability.** After update, old timeline entries should be unchanged.
|
||||
Dates, sources, and content should match the originals exactly.
|
||||
|
||||
---
|
||||
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md). See also: [Source Attribution](source-attribution.md), [Entity Detection](entity-detection.md)*
|
||||
@@ -1,136 +0,0 @@
|
||||
# Content and Media Ingestion
|
||||
|
||||
## Goal
|
||||
YouTube videos, social media, PDFs, and documents become searchable brain pages with the agent's own analysis and full cross-references to every entity mentioned.
|
||||
|
||||
## What the User Gets
|
||||
Without this: media links are bookmarks that decay -- you remember watching a video but can't find what was said, who said it, or why it mattered. With this: every piece of media is a permanent brain page with the agent's analysis layered on top, every mentioned entity gets a back-link, and the full content is searchable forever.
|
||||
|
||||
## Implementation
|
||||
|
||||
```
|
||||
on user_shares_media(url_or_file):
|
||||
|
||||
# PATTERN 1: YouTube Video Ingestion
|
||||
if media.type == "youtube":
|
||||
# Step 1: Get FULL transcript with speaker diarization
|
||||
# WHO said WHAT -- not just a wall of text
|
||||
# Use Diarize.io or equivalent service
|
||||
transcript = diarize(video_url) # speaker-attributed transcript
|
||||
# NEVER use YouTube's auto-generated summary or AI summary
|
||||
|
||||
# Step 2: Agent writes OWN analysis (this is the value)
|
||||
# NOT a summary. NOT regurgitation. The agent's TAKE:
|
||||
# - What matters and why (given the user's worldview)
|
||||
# - Key quotes attributed to specific speakers
|
||||
# - Connections to existing brain pages
|
||||
# - Implications and follow-up angles
|
||||
analysis = agent_analyze(transcript, user_context)
|
||||
|
||||
# Step 3: Create brain page
|
||||
slug = f"media/youtube/{video_slug}"
|
||||
gbrain put <slug> --content """
|
||||
# {title}
|
||||
**Channel:** {channel} | **Date:** {date} | **Link:** {url}
|
||||
|
||||
## Analysis
|
||||
{agent_analysis}
|
||||
|
||||
## Key Quotes
|
||||
- **{Speaker}** ({timestamp}): "{quote}" -- {why_it_matters}
|
||||
|
||||
---
|
||||
## Full Transcript
|
||||
{diarized_transcript}
|
||||
"""
|
||||
|
||||
# Step 4: Extract and cross-reference entities
|
||||
for person in transcript.mentioned_people:
|
||||
gbrain add_link <slug> <person_slug>
|
||||
gbrain add_link <person_slug> <slug>
|
||||
gbrain add_timeline_entry <person_slug> \
|
||||
--entry "Discussed in {video_title}: {what_was_said}" \
|
||||
--source "YouTube: {url}"
|
||||
|
||||
# PATTERN 2: Social Media Bundles
|
||||
elif media.type == "tweet" or media.type == "social":
|
||||
# Don't just save a tweet -- reconstruct FULL context
|
||||
bundle = {
|
||||
"original": fetch_tweet(url),
|
||||
"thread": reconstruct_thread(url), # quoted tweets, replies
|
||||
"linked_articles": fetch_linked_urls(), # fetch and summarize
|
||||
"engagement": get_engagement_data(), # what resonated
|
||||
}
|
||||
|
||||
slug = f"media/social/{platform}-{author}-{date}"
|
||||
gbrain put <slug> --content """
|
||||
# {author}: {topic}
|
||||
{agent_analysis_of_full_bundle}
|
||||
|
||||
## Thread
|
||||
{reconstructed_thread}
|
||||
|
||||
## Linked Articles
|
||||
{article_summaries}
|
||||
|
||||
---
|
||||
## Raw
|
||||
{original_tweet_text}
|
||||
"""
|
||||
|
||||
# Extract entities and cross-reference
|
||||
for entity in bundle.mentioned_entities:
|
||||
gbrain add_link <slug> <entity_slug>
|
||||
gbrain add_link <entity_slug> <slug>
|
||||
|
||||
# PATTERN 3: PDFs and Documents
|
||||
elif media.type == "pdf" or media.type == "document":
|
||||
# OCR if needed (scanned PDFs)
|
||||
content = ocr_if_needed(file) or extract_text(file)
|
||||
|
||||
# For books and long-form:
|
||||
slug = f"sources/{document_slug}"
|
||||
gbrain put <slug> --content """
|
||||
# {title}
|
||||
**Author:** {author} | **Date:** {date}
|
||||
|
||||
## Chapter Summaries
|
||||
{per_chapter_summary}
|
||||
|
||||
## Key Quotes
|
||||
- p.{page}: "{quote}" -- {why_it_matters}
|
||||
|
||||
## Cross-References
|
||||
{links_to_brain_pages_for_people_and_concepts}
|
||||
|
||||
---
|
||||
## Source
|
||||
{full_text_or_key_sections}
|
||||
"""
|
||||
|
||||
for entity in document.mentioned_entities:
|
||||
gbrain add_link <slug> <entity_slug>
|
||||
gbrain add_link <entity_slug> <slug>
|
||||
|
||||
# Always sync after ingestion
|
||||
gbrain sync
|
||||
```
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Always FULL transcript, never AI summary.** YouTube's auto-summary and AI-generated summaries lose the texture: who said what, exact phrasing, tone, what was left unsaid. The full diarized transcript is the evidence base. The agent's analysis goes above it.
|
||||
2. **The agent's OWN analysis is the value, not regurgitation.** "The video discussed AI safety" is worthless. "Dario made a specific claim about compute scaling that contradicts what Ilya said in the NeurIPS talk -- see media/youtube/ilya-neurips-2025" is useful. The analysis connects the new media to the existing brain.
|
||||
3. **Social media is a bundle, not a single tweet.** A tweet without its thread, quoted tweets, linked articles, and engagement context is a fragment. Reconstruct the full context before creating the brain page.
|
||||
4. **Cross-references make media pages alive.** A YouTube page without back-links to the people and companies mentioned is a dead archive. Every mentioned entity gets a link and a timeline entry.
|
||||
5. **Over time, `media/` becomes a searchable archive.** Every video, podcast, talk, interview, article, and tweet the user has consumed, with the agent's commentary layered on top. This is the memex at full power.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Ingest a YouTube video. Run `gbrain get media/youtube/{slug}`. Confirm the page has: the agent's analysis (not just a summary), key quotes with speaker attribution, and the full diarized transcript.
|
||||
2. Run `gbrain get_links media/youtube/{slug}`. Confirm back-links exist to brain pages for every person and company mentioned in the video.
|
||||
3. Pick a person mentioned in the video. Run `gbrain get <person_slug>`. Confirm their timeline has a new entry referencing the video with specific context.
|
||||
4. Ingest a tweet. Confirm the brain page includes the thread context, linked article summaries, and entity cross-references -- not just the tweet text.
|
||||
5. Run `gbrain search "{topic_from_video}"`. Confirm the media page appears in search results (verifies the content is indexed and searchable).
|
||||
|
||||
---
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
@@ -1,193 +0,0 @@
|
||||
# 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)*
|
||||
@@ -1,146 +0,0 @@
|
||||
# Deterministic Collectors: Code for Data, LLMs for Judgment
|
||||
|
||||
## Goal
|
||||
|
||||
Separate mechanical work (100% reliable code) from analytical work (LLM judgment) so that deterministic tasks never fail probabilistically.
|
||||
|
||||
## What the User Gets
|
||||
|
||||
Without this: the LLM generates Gmail links, formats tables, and tracks state.
|
||||
It follows the rule for the first 10 items, then drops a link on item 11. You
|
||||
write "NO EXCEPTIONS" in the prompt. It still fails. 90% reliability over 20
|
||||
items means visible failures twice per day. Trust is destroyed.
|
||||
|
||||
With this: code handles URLs, formatting, and state (100% reliable). The LLM
|
||||
reads pre-formatted data and adds judgment, classification, and enrichment.
|
||||
Links are never wrong because the LLM never generates them.
|
||||
|
||||
## Implementation
|
||||
|
||||
```
|
||||
// The pattern: code collects, LLM analyzes
|
||||
|
||||
// STEP 1: Deterministic collector (script, no LLM calls)
|
||||
collector_run():
|
||||
messages = gmail_api.fetch_unread()
|
||||
for msg in messages:
|
||||
structured = {
|
||||
id: msg.id,
|
||||
from: msg.sender,
|
||||
subject: msg.subject,
|
||||
snippet: msg.snippet,
|
||||
gmail_link: f"https://mail.google.com/mail/u/?authuser={account}#inbox/{msg.id}",
|
||||
gmail_markdown: f"[Open in Gmail]({gmail_link})",
|
||||
is_signature: regex_match(msg, DOCUSIGN_PATTERNS),
|
||||
is_noise: regex_match(msg, NOISE_PATTERNS),
|
||||
is_new: msg.id not in state.seen_ids
|
||||
}
|
||||
store(structured)
|
||||
state.seen_ids.add(msg.id)
|
||||
generate_markdown_digest(structured_messages)
|
||||
|
||||
// STEP 2: LLM reads the pre-formatted digest
|
||||
llm_analyze():
|
||||
digest = read("data/digests/today.md") // links already baked in
|
||||
classify_urgency(digest) // judgment call
|
||||
add_commentary(digest) // contextual analysis
|
||||
run_brain_enrichment(notable_entities) // gbrain search + update
|
||||
draft_replies(urgent_items) // creative work
|
||||
surface_to_user(final_output) // delivery
|
||||
|
||||
// STEP 3: Wire into cron
|
||||
cron_job():
|
||||
collector_run() // fast, cheap, deterministic
|
||||
llm_analyze() // slower, expensive, creative
|
||||
```
|
||||
|
||||
### The Architecture
|
||||
|
||||
```
|
||||
+-----------------------------+ +------------------------------+
|
||||
| Deterministic Collector |---->| LLM Agent |
|
||||
| (Node.js / Python script) | | |
|
||||
| | | - Read the pre-formatted |
|
||||
| - Pull data from API | | digest |
|
||||
| - Store structured JSON | | - Classify items |
|
||||
| - Generate links/URLs | | - Add commentary |
|
||||
| - Detect patterns (regex) | | - Run brain enrichment |
|
||||
| - Track state (seen/new) | | - Draft replies |
|
||||
| - Output markdown digest | | - Surface to user |
|
||||
| | | |
|
||||
| CODE — deterministic, | | AI — judgment, context, |
|
||||
| never forgets | | creativity |
|
||||
+-----------------------------+ +------------------------------+
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
scripts/email-collector/
|
||||
├── email-collector.mjs # No LLM calls, no external deps
|
||||
├── data/
|
||||
│ ├── state.json # Last pull timestamp, known IDs, pending signatures
|
||||
│ ├── messages/ # Structured JSON per day
|
||||
│ │ └── 2026-04-09.json
|
||||
│ └── digests/ # Pre-formatted markdown
|
||||
│ └── 2026-04-09.md
|
||||
```
|
||||
|
||||
### Where the Pattern Applies
|
||||
|
||||
| Signal Source | Collector Generates | LLM Adds |
|
||||
|--------------|-------------------|----------|
|
||||
| **Email** | Gmail links, sender metadata, signature detection | Urgency classification, enrichment, reply drafts |
|
||||
| **X/Twitter** | Tweet links, engagement metrics, deletion detection | Sentiment analysis, narrative detection, content ideas |
|
||||
| **Calendar** | Event links, attendee lists, conflict detection | Prep briefings, meeting context from brain |
|
||||
| **Slack** | Channel links, thread links, mention detection | Priority classification, action item extraction |
|
||||
| **GitHub** | PR/issue links, diff stats, CI status | Code review context, priority assessment |
|
||||
|
||||
### The Principle
|
||||
|
||||
If a piece of output MUST be present and MUST be formatted correctly every
|
||||
time, generate it in code. If a piece of output requires judgment, context,
|
||||
or creativity, generate it with the LLM. Don't ask the LLM to do both in
|
||||
the same pass.
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **LLMs forget links -- bake them in code.** The LLM will follow the
|
||||
"include a Gmail link" rule for the first 10 items, then silently drop
|
||||
it on item 11. No amount of prompt engineering fixes probabilistic
|
||||
formatting over long outputs. The fix: generate every link in the
|
||||
collector script. The LLM reads pre-formatted markdown where links are
|
||||
already embedded. It can't forget what it didn't generate.
|
||||
|
||||
2. **Noise filtering must be deterministic.** Regex-based noise detection
|
||||
(newsletters, automated receipts, marketing) belongs in the collector,
|
||||
not the LLM. The LLM might classify a newsletter as "possibly important"
|
||||
on one run and "noise" on the next. Code classifies the same input the
|
||||
same way every time.
|
||||
|
||||
3. **Atomic writes prevent corruption.** The collector writes to a state
|
||||
file (`state.json`) that tracks which messages have been seen. If the
|
||||
script crashes mid-write, the state file can be corrupted. Write to a
|
||||
temp file first, then rename atomically. This also prevents the LLM
|
||||
from reading a partial digest if the cron fires during a collection run.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. **Run the collector and check every link.** Execute the collector script
|
||||
manually. Open the generated digest. Click every `[Open in Gmail]` link
|
||||
(or equivalent). Every single link must resolve to the correct item. If
|
||||
any link is broken or missing, the collector has a bug.
|
||||
|
||||
2. **Verify noise filtering is consistent.** Run the collector twice on the
|
||||
same input data. The noise classification (is_noise field) must be
|
||||
identical both times. If it varies, a probabilistic element leaked into
|
||||
the deterministic layer.
|
||||
|
||||
3. **Verify the LLM reads structured output.** Run the full pipeline
|
||||
(collector then LLM). Check that the LLM's analysis references data
|
||||
from the structured digest, not from its own generation. The links in
|
||||
the final output should be identical to the links in the digest file.
|
||||
|
||||
---
|
||||
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
@@ -1,151 +0,0 @@
|
||||
# Diligence Ingestion: Data Room to Brain Pages
|
||||
|
||||
## Goal
|
||||
|
||||
Turn pitch decks, financial models, and data room materials into searchable, cross-referenced brain pages with bull/bear analysis.
|
||||
|
||||
## What the User Gets
|
||||
|
||||
Without this: pitch decks sit in email attachments. Financial models in Google
|
||||
Drive. No cross-reference to the company brain page. You can't search "what
|
||||
were the key metrics from Acme Corp's Series A deck?"
|
||||
|
||||
With this: every data room document is extracted, diarized, cross-referenced to
|
||||
the company page, and searchable. Index.md gives you the bull/bear case at a
|
||||
glance. `gbrain query "Acme Corp revenue growth"` finds the exact chart.
|
||||
|
||||
## Implementation
|
||||
|
||||
Recognize data room materials by PDF filenames containing "Data Deck", "Intro
|
||||
Deck", "Data Room", "Cap Table", "Financial Model", "Investor Memo", "Pitch
|
||||
Deck", or series round names. Spreadsheet tabs with Revenue, Retention, Cohorts,
|
||||
CAC, Gross Margin, Unit Economics, ARR. User language like "data room",
|
||||
"diligence", "deck", "pitch", "fundraise materials".
|
||||
|
||||
### The 9-Step Pipeline
|
||||
|
||||
**Step 1: Identify the Company.**
|
||||
From the document content or filename, identify the company name.
|
||||
Check if `brain/companies/{slug}.md` exists.
|
||||
|
||||
**Step 2: Create Diligence Directory.**
|
||||
|
||||
```bash
|
||||
mkdir -p brain/diligence/{company-slug}/.raw
|
||||
```
|
||||
|
||||
**Step 3: Extract Content.**
|
||||
|
||||
- **PDFs:** Use PDF extraction tool. For scanned/image-heavy PDFs,
|
||||
use OCR (e.g., Mistral OCR or similar).
|
||||
- **Spreadsheets:** Export each sheet as CSV. For Google Sheets:
|
||||
```
|
||||
https://docs.google.com/spreadsheets/d/{ID}/gviz/tq?tqx=out:csv&sheet={Sheet Name}
|
||||
```
|
||||
|
||||
**Step 4: Diarize and Save.**
|
||||
Write extracted content to `brain/diligence/{company}/{doc-name}.md`:
|
||||
- Document title and type
|
||||
- Section-by-section breakdown with key metrics
|
||||
- Notable footnotes or caveats
|
||||
- Raw data tables where relevant
|
||||
|
||||
**Step 5: Save Raw Files.**
|
||||
Copy original PDFs/files to `brain/diligence/{company}/.raw/`
|
||||
Preserve originals for reference. The diarized version is for search.
|
||||
|
||||
**Step 6: Create or Update index.md.**
|
||||
Every diligence directory needs an `index.md`:
|
||||
|
||||
```markdown
|
||||
# {Company Name} — Diligence
|
||||
|
||||
## Round Details
|
||||
- Stage: Series A
|
||||
- Amount: $10M
|
||||
- Date: 2026-04
|
||||
|
||||
## Document Inventory
|
||||
- [Pitch Deck](pitch-deck.md) — 25 slides, company overview + traction
|
||||
- [Financial Model](financial-model.md) — 5 tabs, 3-year projections
|
||||
- [Cap Table](cap-table.md) — current ownership + option pool
|
||||
|
||||
## Key Findings
|
||||
- Revenue growing 30% MoM for last 6 months
|
||||
- CAC payback period: 4 months
|
||||
- Net retention: 135%
|
||||
|
||||
## Bull Case
|
||||
- Strong product-market fit signal (NPS 72)
|
||||
- Expanding into adjacent vertical
|
||||
|
||||
## Bear Case
|
||||
- Single customer represents 40% of revenue
|
||||
- Burn rate increased 3x last quarter
|
||||
|
||||
## Open Questions
|
||||
- What's the path to profitability?
|
||||
- How defensible is the moat?
|
||||
```
|
||||
|
||||
**Step 7: Enrich Company Brain Page.**
|
||||
Update `brain/companies/{slug}.md`:
|
||||
- Add document sources to frontmatter
|
||||
- Update compiled truth with key findings
|
||||
- Add "See Also" link to diligence directory
|
||||
- If no company page exists, create one via the enrich skill
|
||||
|
||||
**Step 8: Commit.**
|
||||
|
||||
```bash
|
||||
cd brain/ && git add -A && git commit -m "diligence: {Company} — {doc type} ingestion" && git push
|
||||
```
|
||||
|
||||
**Step 9: Publish (if asked).**
|
||||
When the user wants a shareable brief, create a password-protected
|
||||
published version. Strip internal notes and raw assessment language.
|
||||
|
||||
### Quality Bar
|
||||
|
||||
A good diligence page reads like an intelligence assessment:
|
||||
- **What they say** vs **what the data shows** (the gap is the insight)
|
||||
- Explicit bull/bear case (not just a summary)
|
||||
- Key metrics highlighted, not buried
|
||||
- Open questions that need answers before decision
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **PDF extraction is lossy.** Scanned decks and image-heavy PDFs lose
|
||||
tables and charts during extraction. Always check the diarized output
|
||||
against the original `.raw/` file. If key metrics are missing, re-extract
|
||||
with OCR or transcribe manually.
|
||||
|
||||
2. **Idempotency on re-ingestion.** If the user sends an updated deck for
|
||||
the same company, don't create a duplicate directory. Check for an existing
|
||||
`brain/diligence/{company-slug}/` and update in place. Append a version
|
||||
suffix to the document file if the old version should be preserved.
|
||||
|
||||
3. **index.md completeness.** The index.md is the entry point for the entire
|
||||
diligence package. If it's missing the bull/bear case or open questions,
|
||||
the diligence is incomplete. Always generate all sections even if some
|
||||
require judgment calls -- flag uncertain assessments explicitly.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. **Search for key metrics.** After ingestion, run
|
||||
`gbrain search "revenue growth"` or `gbrain search "{company name} CAC"`.
|
||||
The diarized content should appear in results. If it doesn't, the sync
|
||||
or embedding step was missed.
|
||||
|
||||
2. **Check the company page cross-reference.** Open
|
||||
`brain/companies/{slug}.md` and verify it links to the diligence directory.
|
||||
The compiled truth section should include key findings from the deck.
|
||||
|
||||
3. **Verify index.md has all sections.** Open
|
||||
`brain/diligence/{company}/index.md` and confirm it has Round Details,
|
||||
Document Inventory, Key Findings, Bull Case, Bear Case, and Open Questions.
|
||||
Missing sections mean the pipeline stopped early.
|
||||
|
||||
---
|
||||
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
@@ -1,103 +0,0 @@
|
||||
# Enrichment Pipeline
|
||||
|
||||
## Goal
|
||||
Enrich brain pages from external APIs with tiered spend -- full pipeline for key people, light touch for passing mentions, raw data preserved for auditability.
|
||||
|
||||
## What the User Gets
|
||||
Without this: brain pages are thin shells with only what the user manually typed, API calls are wasted on nobodies, and enrichment data vanishes after the agent session ends. With this: key people have rich, multi-source portraits; spend scales to importance; raw API responses are preserved for re-processing; and cross-references connect the entire graph.
|
||||
|
||||
## Implementation
|
||||
|
||||
```
|
||||
on enrich(entity, trigger):
|
||||
# trigger: meeting mention, email thread, social interaction, user request
|
||||
|
||||
# Step 1: Identify entities from the incoming signal
|
||||
entities = extract_entities(signal)
|
||||
# people names, company names, associations
|
||||
|
||||
# Step 2: Check brain state -- UPDATE or CREATE path?
|
||||
for entity in entities:
|
||||
existing = gbrain search "{entity.name}"
|
||||
if existing:
|
||||
page = gbrain get <entity_slug>
|
||||
path = "UPDATE"
|
||||
else:
|
||||
path = "CREATE"
|
||||
|
||||
# Step 3: Determine tier -- scale spend to importance
|
||||
tier = classify_tier(entity):
|
||||
# Tier 1 (10-15 API calls): key people, inner circle, business partners,
|
||||
# portfolio companies. Full pipeline, ALL data sources.
|
||||
# Tier 2 (3-5 API calls): notable people, occasional interactions.
|
||||
# Web search + social + brain cross-reference.
|
||||
# Tier 3 (1-2 API calls): minor mentions, everyone else worth tracking.
|
||||
# Brain cross-reference + social lookup if handle known.
|
||||
|
||||
# Step 4: Run external lookups (priority order, stop when enough signal)
|
||||
data = {}
|
||||
data["brain"] = gbrain search "{entity.name}" # Always first (free)
|
||||
if tier <= 2:
|
||||
data["web"] = brave_search("{entity.name}") # Background, press, talks
|
||||
if tier <= 2:
|
||||
data["twitter"] = twitter_lookup(entity.handle) # Beliefs, building, network
|
||||
if tier == 1:
|
||||
data["linkedin"] = crustdata_enrich(entity.name) # Career, connections
|
||||
data["research"] = happenstance_research(entity) # Career arcs, web presence
|
||||
data["funding"] = captain_api(entity.company) # Funding, valuation, team
|
||||
data["meetings"] = circleback_search(entity.name) # Transcript search
|
||||
data["contacts"] = google_contacts(entity.email) # Contact data
|
||||
|
||||
# Step 5: Store raw data (auditable, re-processable)
|
||||
gbrain put_raw_data <entity_slug> \
|
||||
--data '{"sources": {"crustdata": {"fetched_at": "...", "data": {...}}, ...}}'
|
||||
# Overwrite on re-enrichment, don't append
|
||||
|
||||
# Step 6: Write to brain page
|
||||
if path == "CREATE":
|
||||
gbrain put <entity_slug> --content "<compiled_truth_from_all_sources>"
|
||||
gbrain add_timeline_entry <entity_slug> --entry "Page created via enrichment"
|
||||
elif path == "UPDATE":
|
||||
# Append timeline, update compiled truth ONLY if materially new
|
||||
gbrain add_timeline_entry <entity_slug> --entry "Enriched: {new_signal}"
|
||||
# Flag contradictions -- don't silently resolve them
|
||||
|
||||
# Step 7: Cross-reference the graph
|
||||
gbrain add_link <person_slug> <company_slug> # person -> company
|
||||
gbrain add_link <company_slug> <person_slug> # company -> person
|
||||
gbrain add_link <person_slug> <deal_slug> # person -> deal
|
||||
# Every entity page links to every other entity page that references it
|
||||
|
||||
# People page sections (not a LinkedIn profile -- a living portrait):
|
||||
# Executive Summary, State, What They Believe, What They're Building,
|
||||
# What Motivates Them, Assessment, Trajectory, Relationship, Contact, Timeline
|
||||
# Facts are table stakes. TEXTURE is the value.
|
||||
|
||||
# Extract texture, not just facts:
|
||||
# Opinion expressed? -> What They Believe
|
||||
# Building or shipping? -> What They're Building
|
||||
# Emotion expressed? -> What Makes Them Tick
|
||||
# Who did they engage with? -> Network / Relationship
|
||||
# Recurring topic? -> Hobby Horses
|
||||
# Committed to something? -> Open Threads
|
||||
# Energy level? -> Trajectory
|
||||
```
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Don't overwrite human-written assessments.** If the user wrote an Assessment section with their own read on someone, API enrichment NEVER overwrites it. API data goes into State, Contact, Timeline. The user's assessment is sacrosanct.
|
||||
2. **Don't re-enrich the same page more than once per week.** Check `put_raw_data` timestamps before running the pipeline again. Enrichment is expensive and data doesn't change that fast.
|
||||
3. **LinkedIn connection count < 20 means wrong person.** Crustdata sometimes returns a different person with the same name. If the LinkedIn profile has fewer than 20 connections, it's almost certainly a false match. Discard it.
|
||||
4. **X/Twitter is the most underrated data source.** When you have someone's handle, their tweets reveal beliefs, what they're building, hobby horses, network (reply patterns), and trajectory (posting frequency, tone shifts). This is richer than LinkedIn for "What They Believe" and "What Makes Them Tick."
|
||||
5. **Cross-references are not optional.** After enriching a person, update their company page. After enriching a company, update founder pages. An enriched page without cross-links is a dead end in the graph.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Enrich a Tier 1 person. Run `gbrain get <slug>` and confirm the page has Executive Summary, State, What They Believe, Contact, and Timeline sections populated from multiple sources.
|
||||
2. Run `gbrain get_raw_data <slug>`. Confirm raw API responses are stored with `sources.{provider}.fetched_at` timestamps.
|
||||
3. Run `gbrain get_links <slug>`. Confirm cross-reference links exist to the person's company page, deal pages, and related entities.
|
||||
4. Check a page that was enriched AND has a user-written Assessment. Confirm the Assessment section was preserved, not overwritten by API data.
|
||||
5. Try to re-enrich the same person. Confirm the system checks the `fetched_at` timestamp and skips if less than a week old.
|
||||
|
||||
---
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
@@ -1,190 +0,0 @@
|
||||
# Entity Detection: Run It on Every Message
|
||||
|
||||
## Goal
|
||||
|
||||
Every inbound message gets scanned for original thinking AND entity mentions so the brain grows on every conversation, automatically.
|
||||
|
||||
## What the User Gets
|
||||
|
||||
Without this: the agent answers questions but forgets everything. You mention
|
||||
Pedro in a meeting, and next week the agent doesn't know who Pedro is.
|
||||
|
||||
With this: every person, company, and idea mentioned in conversation gets a
|
||||
brain page. Next time Pedro comes up, the agent already has context. The
|
||||
brain compounds.
|
||||
|
||||
## Implementation
|
||||
|
||||
Spawn a lightweight sub-agent on EVERY inbound message. Do NOT wait for it
|
||||
to finish before responding. It runs in parallel.
|
||||
|
||||
```
|
||||
on_every_message(message_text, source_context):
|
||||
|
||||
// 1. SPAWN ASYNC — don't block the response
|
||||
spawn_subagent({
|
||||
model: "sonnet-class", // cheap + fast, not opus
|
||||
timeout: 120, // seconds
|
||||
task: build_detection_prompt(message_text, source_context)
|
||||
})
|
||||
|
||||
// 2. RESPOND TO USER NORMALLY
|
||||
// The sub-agent runs in the background
|
||||
```
|
||||
|
||||
### The Detection Prompt
|
||||
|
||||
```
|
||||
build_detection_prompt(text, source):
|
||||
return `
|
||||
SIGNAL DETECTION — scan this message for ideas AND entities:
|
||||
|
||||
Message: "${text}"
|
||||
Source: [Source: User, ${source.topic}, ${source.platform}, ${source.timestamp}]
|
||||
|
||||
STEP 1 — IDEAS FIRST (highest priority):
|
||||
Is the user expressing an original thought, observation, thesis, or framework?
|
||||
|
||||
If yes:
|
||||
- Create or update brain/originals/{slug}.md
|
||||
- Use the user's EXACT phrasing (the language IS the insight)
|
||||
- "The ambition-to-lifespan ratio has never been more broken" is better
|
||||
than "tension between ambition and mortality"
|
||||
- Include [Source: ...] citation with full context
|
||||
|
||||
If the idea references a world concept: brain/concepts/{slug}.md
|
||||
If it's a product/business idea: brain/ideas/{slug}.md
|
||||
|
||||
STEP 2 — ENTITIES:
|
||||
Extract all person names, company names, media titles.
|
||||
|
||||
For each entity:
|
||||
a. Run: gbrain search "{name}"
|
||||
b. If page exists AND new info: append timeline entry
|
||||
Format: - YYYY-MM-DD | {what happened} [Source: {who}, {context}, {date}]
|
||||
c. If no page AND entity is notable: create page with web enrichment
|
||||
d. If page is thin (< 5 lines compiled truth): spawn background enrichment
|
||||
|
||||
STEP 3 — BACK-LINKING (mandatory):
|
||||
For every entity mentioned, add a back-link FROM their page TO this source.
|
||||
An unlinked mention is a broken brain.
|
||||
Format: - **YYYY-MM-DD** | Referenced in [{page title}]({path}) — {context}
|
||||
|
||||
STEP 4 — SYNC:
|
||||
Run: gbrain sync --no-pull --no-embed
|
||||
|
||||
If nothing to capture, reply "No signals detected" and exit.
|
||||
`
|
||||
```
|
||||
|
||||
### Notability Filtering
|
||||
|
||||
Before creating a new entity page, check notability:
|
||||
|
||||
```
|
||||
is_notable(entity):
|
||||
// CREATE a page for:
|
||||
- People the user knows or discusses with specificity
|
||||
- Companies the user is evaluating, working with, or investing in
|
||||
- Media the user mentions with personal reaction
|
||||
- Anyone the user has explicitly engaged with
|
||||
|
||||
// DON'T create a page for:
|
||||
- Generic references or passing examples
|
||||
- Low-engagement accounts who mentioned the user once
|
||||
- Pure metaphors ("like the Roman Empire...")
|
||||
- One-off encounters with no follow-up
|
||||
|
||||
// If notable AND no page: create FULL page (not a stub)
|
||||
// If not notable: skip silently
|
||||
```
|
||||
|
||||
### What Counts as Original Thinking
|
||||
|
||||
| Capture | Don't Capture |
|
||||
|---------|---------------|
|
||||
| Original observations about how the world works | "ok", "do it", "sure" |
|
||||
| Novel connections between disparate things | Pure questions without observations |
|
||||
| Frameworks and mental models | Echoing back what the agent said |
|
||||
| Pattern recognition ("I keep seeing X in every Y") | Acknowledgments and reactions |
|
||||
| Hot takes with reasoning | Routine operational messages |
|
||||
| Metaphors that reveal new angles | Requests without embedded insight |
|
||||
|
||||
### Filing Rules
|
||||
|
||||
| Signal | Destination |
|
||||
|--------|-------------|
|
||||
| User generated the idea | `brain/originals/{slug}.md` |
|
||||
| User's synthesis of others' ideas | `brain/originals/` (the synthesis is original) |
|
||||
| World concept someone else coined | `brain/concepts/{slug}.md` |
|
||||
| Product or business idea | `brain/ideas/{slug}.md` |
|
||||
| Person mentioned | `brain/people/{slug}.md` |
|
||||
| Company mentioned | `brain/companies/{slug}.md` |
|
||||
| Media referenced | `brain/media/{type}/{slug}.md` |
|
||||
|
||||
### The Iron Law of Back-Linking
|
||||
|
||||
Every entity mention MUST create a back-link FROM the entity page TO the
|
||||
source. This is not optional.
|
||||
|
||||
```
|
||||
// When message mentions "Pedro" and creates a meeting page:
|
||||
|
||||
// 1. Update the meeting page (normal)
|
||||
brain/meetings/2026-04-10-board-sync.md:
|
||||
- Pedro presented Q1 numbers
|
||||
|
||||
// 2. ALSO update Pedro's page (back-link)
|
||||
brain/people/pedro-franceschi.md:
|
||||
## Timeline
|
||||
- **2026-04-10** | Presented Q1 numbers at board sync
|
||||
[Source: User, board meeting, 2026-04-10]
|
||||
```
|
||||
|
||||
Without back-links, you can't traverse the graph. "Show me everything related
|
||||
to Pedro" only works if Pedro's page links back to every mention.
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Don't block the conversation.** Entity detection runs async. The user
|
||||
should see a response immediately, not wait 2 minutes while the sub-agent
|
||||
enriches 5 entity pages.
|
||||
|
||||
2. **Sonnet, not Opus.** Entity detection is pattern matching, not deep
|
||||
reasoning. Sonnet is 5-10x cheaper and fast enough. Use Opus for the
|
||||
main conversation.
|
||||
|
||||
3. **Exact phrasing matters.** "Markdown is actually code" is an insight.
|
||||
"Markdown can be used as code" is a summary. Capture the first version.
|
||||
|
||||
4. **Don't create stubs.** If you create a page, make it good. Run a web
|
||||
search, build out the compiled truth, add context. A stub page with just
|
||||
a name is worse than no page (it gives false confidence).
|
||||
|
||||
5. **Dedup before creating.** Always `gbrain search` before creating a page.
|
||||
Variant spellings, nicknames, and company abbreviations cause duplicates.
|
||||
"Pedro Franceschi" and "Pedro" might be the same person.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. **Send a message mentioning a person.** Say "I had coffee with Sarah Chen
|
||||
from Acme Corp today." Verify: brain/people/sarah-chen.md was created or
|
||||
updated, brain/companies/acme-corp.md was created or updated, both have
|
||||
timeline entries with today's date.
|
||||
|
||||
2. **Send a message with an original idea.** Say "What if we could distribute
|
||||
software as markdown files that agents execute?" Verify:
|
||||
brain/originals/{slug}.md was created with your exact phrasing.
|
||||
|
||||
3. **Check back-links.** Open Sarah Chen's page. It should have a timeline
|
||||
entry linking back to today's conversation. Open Acme Corp's page. Same.
|
||||
|
||||
4. **Send a boring message.** Say "ok sounds good." Verify: nothing was
|
||||
created. The detector should report "No signals detected."
|
||||
|
||||
5. **Check for duplicates.** Mention "Pedro" then later "Pedro Franceschi."
|
||||
Verify: one page, not two.
|
||||
|
||||
---
|
||||
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
@@ -1,109 +0,0 @@
|
||||
# Executive Assistant Pattern
|
||||
|
||||
## Goal
|
||||
Email triage, meeting prep, and scheduling powered by brain context -- so every interaction is informed by the full history of the relationship.
|
||||
|
||||
## What the User Gets
|
||||
Without this: the agent triages email mechanically ("you have 12 unread"), preps for meetings with generic LinkedIn bios, and schedules without relationship context. With this: the agent knows who every sender is before reading their email, surfaces shared history before every meeting, and nudges scheduling based on relationship temperature and open threads.
|
||||
|
||||
## Implementation
|
||||
|
||||
```
|
||||
# WORKFLOW 1: Email Triage
|
||||
on email_batch(emails):
|
||||
for email in emails:
|
||||
# Step 1: Search sender BEFORE reading the email body
|
||||
# Brain context makes triage 10x better
|
||||
sender_page = gbrain search "{email.sender_name}"
|
||||
if sender_page:
|
||||
context = gbrain get <sender_slug>
|
||||
# Now you know: who they are, relationship history,
|
||||
# what they care about, open threads
|
||||
|
||||
# Step 2: Read the email WITH brain context loaded
|
||||
# Classification is now informed, not mechanical
|
||||
|
||||
# Step 3: Classify with context
|
||||
if context.relationship == "inner_circle" or context.has_open_threads:
|
||||
priority = "urgent"
|
||||
elif context.is_known_entity:
|
||||
priority = "normal"
|
||||
else:
|
||||
priority = "noise" # unknown sender, no brain page
|
||||
|
||||
# Step 4: Draft reply with relationship context
|
||||
if needs_reply(email):
|
||||
draft = compose_reply(
|
||||
email,
|
||||
context=context, # their brain page
|
||||
open_threads=context.open_threads, # what you're working on together
|
||||
relationship=context.relationship # tone calibration
|
||||
)
|
||||
|
||||
# WORKFLOW 2: Meeting Prep
|
||||
on upcoming_meeting(meeting):
|
||||
briefing = {}
|
||||
for attendee in meeting.attendees:
|
||||
# Search brain for each attendee
|
||||
results = gbrain search "{attendee.name}"
|
||||
if results:
|
||||
page = gbrain get <attendee_slug>
|
||||
briefing[attendee] = {
|
||||
"compiled_truth": page.compiled_truth,
|
||||
"last_interaction": page.timeline[0], # most recent
|
||||
"open_threads": page.open_threads,
|
||||
"relationship_temperature": page.relationship,
|
||||
"relevant_deals": gbrain get_links <attendee_slug>,
|
||||
}
|
||||
else:
|
||||
briefing[attendee] = "No brain page -- consider enriching"
|
||||
|
||||
# Surface: shared history, what to follow up on, what to watch for
|
||||
# "Last time you discussed the Series B timeline. Pedro was concerned
|
||||
# about burn rate. Here's the latest from his company page."
|
||||
|
||||
# WORKFLOW 3: Post-Inbox Brain Updates
|
||||
on inbox_cleared():
|
||||
for email in processed_emails:
|
||||
if email.contained_new_information:
|
||||
# Update the sender's brain page with new signal
|
||||
gbrain add_timeline_entry <sender_slug> \
|
||||
--entry "Email re: {subject}. Key info: {extracted_signal}" \
|
||||
--source "email from {sender} re {subject}, {date}"
|
||||
|
||||
# Update any mentioned entity pages too
|
||||
for entity in email.mentioned_entities:
|
||||
gbrain add_timeline_entry <entity_slug> \
|
||||
--entry "{what_was_said_about_them}" \
|
||||
--source "email from {sender}, {date}"
|
||||
|
||||
# WORKFLOW 4: Scheduling Nudges
|
||||
on schedule_request(meeting):
|
||||
for attendee in meeting.attendees:
|
||||
page = gbrain get <attendee_slug>
|
||||
if page.last_interaction > 6_weeks_ago:
|
||||
nudge("You haven't met with {attendee} in {weeks} weeks")
|
||||
if page.has_open_threads:
|
||||
nudge("{attendee} has an open thread about {topic}")
|
||||
if page.relationship_temperature == "cooling":
|
||||
nudge("Relationship with {attendee} may need attention")
|
||||
```
|
||||
|
||||
## Tricky Spots
|
||||
|
||||
1. **Search sender BEFORE reading the email.** This is counterintuitive but critical. Loading brain context first means you know who they are, what you're working on together, and what they care about -- before you even see the subject line. The triage is informed, not mechanical.
|
||||
2. **Unknown senders with no brain page are almost always noise.** If `gbrain search` returns nothing for a sender, they're probably not important. Classify as low priority unless the email content signals otherwise.
|
||||
3. **Meeting prep is the highest-leverage EA workflow.** The user walks into every meeting already briefed on each attendee: last interaction, open threads, relationship history. This is the difference between "you have a meeting at 3" and "you have a meeting at 3 with Pedro -- last time you discussed the Series B, he was concerned about burn rate."
|
||||
4. **Post-inbox brain updates are where the brain compounds.** Every email is signal. If you clear the inbox without updating brain pages, the information is lost. This is the step most agents skip.
|
||||
5. **Scheduling nudges require timeline data.** "You haven't met with Diana in 6 weeks" only works if meeting pages have been ingested with proper entity propagation (see meeting-ingestion guide).
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Run meeting prep for tomorrow's calendar. For each attendee, confirm the agent ran `gbrain search` and loaded their brain page before generating the briefing.
|
||||
2. Triage 5 emails. Confirm the agent searched for each sender in the brain before classifying the email.
|
||||
3. After clearing an inbox, check 2 sender brain pages with `gbrain get <slug>`. Confirm new timeline entries were added with information from the emails.
|
||||
4. Check a scheduling suggestion. Confirm the agent referenced the attendee's brain page (last interaction date, open threads) in the nudge.
|
||||
5. Send a test email from someone with a brain page. Confirm the triage response references their relationship context, not just the email content.
|
||||
|
||||
---
|
||||
*Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user