Compare commits

..
Author SHA1 Message Date
Garry Tan 739c8f21a7 Merge origin/master into garrytan/single-wave-ship
Master shipped v0.40.4.0 (selective graph signals + per-stage attribution
+ audit-writer unification). No migration collision this time — my v92
remains the highest.

Resolved conflicts:
- VERSION: kept 0.40.5.0 (still higher than master's 0.40.4.0)
- package.json: kept 0.40.5.0
- CHANGELOG.md: preserved both entries (mine on top + master's v0.40.4.0)
- src/commands/doctor.ts: auto-merged cleanly

Refreshed llms.txt + llms-full.txt against the merged tree.

Verification:
- bun run typecheck: clean
- bun run verify: clean (all 19 checks pass)
- Wave smoke tests (db-lock-per-source + embed-backfill-submit +
  doctor-federation-health): 23/23 pass; migration v92 lands cleanly
2026-05-23 10:03:35 -07:00
Garry Tan 9a397cdb33 Merge origin/master into garrytan/single-wave-ship
Master shipped v0.40.3.0 (contextual retrieval + cache invalidation gate)
claiming migrations v90 (contextual_retrieval_columns) + v91
(pages_generation_trigger_and_bookmark) + a new doctor check
(contextual_retrieval_coverage) + a new sources subcommand (set-cr-mode).

Resolved conflicts:
- VERSION: kept 0.40.5.0 (mine higher than master's 0.40.3.0)
- package.json: kept 0.40.5.0
- CHANGELOG.md: preserved both entries (mine on top + master's v0.40.3.0)
- src/core/migrate.ts: renumbered sources_github_repo_index from v90 → v92
  (v90 + v91 now taken by master's contextual retrieval work)
- src/commands/doctor.ts: kept BOTH check pushes —
  contextual_retrieval_coverage (master, #11) + federation_health (mine, #12)
- src/commands/sources.ts: kept BOTH subcommands —
  status/webhook/tracked-branch (mine) + set-cr-mode (master)

Bumped migration version refs from v90 → v92 in CHANGELOG, migration
walkthrough (skills/migrations/v0.40.5.md), and pglite-schema.ts comment.

Refreshed llms.txt + llms-full.txt against the merged tree.

Verification:
- bun run typecheck: clean
- bun run verify: clean (all 19 checks pass including the leak-guard fix
  from the previous push)
- Wave smoke tests (db-lock-per-source + embed-backfill-submit +
  doctor-federation-health): 23/23 pass; migration v92 lands as expected
2026-05-23 08:57:55 -07:00
Garry Tan 36f354989b fix(check-source-config-leak): tighten regex to source-row patterns only
The v0.40.5.0 wave added scripts/check-source-config-leak.sh with a
too-broad pattern (JSON\.stringify\(.*config) that flagged any variable
named 'config' — catching the GLOBAL gbrain config.json serializers in
src/commands/init.ts (status envelopes) and src/core/config.ts (the
config-file write site). On the CI runner without rg installed, the
grep -rE fallback fired correctly and produced 4 false positives that
broke the `verify` script.

Tightened the patterns to specifically match `(source|src|row|s).config`
property access — the actual risk shape (a sources-table row being
serialized whole). The global gbrain config has a different shape and
threat model (file-mode 0o600 at the write site), so it's safe to
exempt at the regex level rather than per-file whitelist.

Also fixed a latent bug: the rg branch used `--include='*.ts'` (grep's
flag, not rg's). rg silently rejected it and CANDIDATES came back empty,
so the local-dev runs (which have rg) would never have caught a real
leak. Now branches on tool availability: `-g '*.ts'` for rg, `--include`
for grep -rE. Both branches verified against a synthetic leak fixture.

Also added init.ts + config.ts to the whitelist as a belt-and-suspenders
since they handle gbrain-global config (not source rows) and could
otherwise reflect-back via regex iteration.

CI: `bun run verify` exit 0 locally with both the original false-positive
fixture (clean repo) and a synthetic leak fixture (correctly caught,
exit 1).
2026-05-23 07:33:26 -07:00
Garry Tan 5b0943ec96 Merge origin/master into garrytan/single-wave-ship
Resolve VERSION → 0.40.5.0 (master shipped v0.40.2.0 trajectory routing).
Resolve package.json verify script — no-op (master's verify line unchanged).
Resolve CHANGELOG (preserve all entries; my v0.40.5.0 stays on top).
Resolve src/core/migrate.ts: master claimed v89 (facts_event_type_column);
renumber my sources_github_repo_index v89 → v90.

Update CHANGELOG + skills/migrations/v0.40.5.md migration version refs from
v89 to v90.

Refresh llms.txt + llms-full.txt against the merged tree.

All wave tests still green (23/23 across db-lock-per-source +
embed-backfill-submit + doctor-federation-health smoke).
2026-05-23 07:29:34 -07:00
Garry TanandClaude Opus 4.7 02e8da8c73 v0.40.5.0 Federated Sync v2 — parallel source sync + push triggers + per-source health
Bump VERSION + package.json + CHANGELOG header + migration walkthrough filename
to v0.40.5.0 (claiming the next free slot in the v0.40.x patch series after
master's v0.40.1.0).

What ships (6 components, all behind sync.federated_v2 feature flag default-on):
1. Per-source sync lock — syncLockId(sourceId), phantom-redirect parity
2. Parallel sync --all — pMapAllSettled fan-out, --max-sources N cap
3. embed-backfill minion handler — D2 per-source lock + D6 $10/job budget + D15.1
   fire-and-forget submission + D19 source-level cooldown + 24h $25 rolling cap
4. sync trigger CLI + POST /webhooks/github — HMAC-verified (60 req/min/IP),
   X-GitHub-Event=push + ref filter against tracked_branch
5. sources status + federation_health doctor — batched GROUP BY pipeline
   (4 queries instead of 6×N per-source roundtrips)
6. sources federate/unfederate hook — auto-submit embed-backfill on flip

Correctness fixes (unconditional):
- D21: sync.ts:959 facts backstop now passes sourceId to engine.getPage
- D15.4: redactSourceConfig + CI guard prevent webhook_secret leak
- D15.5: safeHexEqual extracted to src/core/timing-safe.ts

Schema:
- Migration v89 (sources_github_repo_index): partial expression index on
  config->>'github_repo' for fast webhook source-lookup

Tests:
- 14 new test files, 112 cases. 4 IRON-RULE regressions pinned (SYNC_LOCK_ID
  back-compat, phantom per-source lock, embed-backfill kill+resume,
  webhook HMAC prefix-strip). All 9449 unit tests pass.

Caught at test-write time: the webhook handler had a Buffer.from('sha256=...',
'hex') truncation bug — without the prefix-strip, every signature would have
"matched" empty buffers. Pinned by a test/sources-webhook.test.ts IRON-RULE.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 07:25:56 -07:00
Garry Tan 9fb2ea09f0 Merge origin/master into garrytan/single-wave-ship
Resolve VERSION → 0.41.0.0 (master shipped v0.40.x cathedral).
Resolve package.json verify script (keep both check:source-config-leak + check:no-pii-agent-voice).
Resolve CHANGELOG (renumber wave entry from 0.40.0.0 → 0.41.0.0; preserve all master entries).
Resolve migrate.ts (renumber sources_github_repo_index migration v87 → v89; master added v87+v88).
Rename skills/migrations/v0.40.0.md → v0.41.0.md.
Refresh bun.lock + llms-full.txt.
Bump beforeAll timeouts to 30s on 9 new PGLite-using test files
(89 migrations now exceed the 5s default cold-start budget).

Auto-merged cleanly: autopilot.ts (D17 freshness gate coexists with
master's v0.39.2 per-source fanout; different job names + idempotency-
key prefixes), doctor.ts, jobs.ts, serve-http.ts, sync.ts.
2026-05-22 22:24:15 -07:00
Garry Tan c12b14ced1 wip: federated sync v2 pre-merge snapshot 2026-05-22 22:17:17 -07:00
843 changed files with 8042 additions and 133412 deletions
+15 -235
View File
@@ -5,72 +5,12 @@ on:
branches: [master]
pull_request:
branches: [master]
# Manual dispatch lets a local dev/agent offload the suite to GitHub's
# on-demand runners from ANY branch (see scripts/ship-remote-tests.sh).
# Frees a load-saturated local machine (e.g. many Conductor agents running
# their own bun-test suites at once — load avg 120 on 16 cores).
workflow_dispatch:
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
@@ -80,189 +20,29 @@ jobs:
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'
# ubuntu-latest is free 2-core/7GB. Larger runners (16-cores, etc.) require
# a provisioned runner pool in repo settings. Falling back to default keeps
# the matrix shard speedup (~5-6x via parallelism) at zero cost.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shard: [1, 2, 3, 4]
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"
- name: Pre-test gates (shard 1 only — they're not test files)
if: matrix.shard == 1
run: bun run verify
- name: Run test shard ${{ matrix.shard }}/4
run: scripts/test-shard.sh ${{ matrix.shard }} 4
- name: Run *.serial.test.ts at --max-concurrency=1 (shard 1 only)
# Serial files share file-wide state (top-level mock.module, module
# singletons) that leaks across files in the same bun-test process.
# test-shard.sh excludes them; this step runs them at concurrency=1.
if: matrix.shard == 1
run: bun run test:serial
+3 -9
View File
@@ -32,12 +32,8 @@ start here.
## Read this order
1. `./AGENTS.md` (this file) — install + operating protocol.
2. [`./CLAUDE.md`](./CLAUDE.md) — orientation + resolver: architecture, cross-cutting
invariants, the reference map, inline ship rules. It routes to on-demand detail docs:
[`./docs/architecture/KEY_FILES.md`](./docs/architecture/KEY_FILES.md) (per-file index —
read a file's entry before editing it), [`./docs/TESTING.md`](./docs/TESTING.md) (test
tiers + isolation lint + E2E lifecycle), and
[`./docs/architecture/thin-client.md`](./docs/architecture/thin-client.md) (remote-MCP seam).
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.
@@ -112,9 +108,7 @@ diff-aware subset during fast iteration on a focused branch. Requires Docker
Manual path: `bun test` plus the E2E lifecycle described in `./CLAUDE.md` (spin
up the test Postgres container, run `bun run test:e2e`, tear it down).
Ship via the `/ship` skill, not by hand. The full release + contributor process
(CHANGELOG voice, version-locations sync, PR conventions, community-PR-wave) lives in
[`./docs/RELEASING.md`](./docs/RELEASING.md); read it before shipping.
Ship via the `/ship` skill, not by hand.
## Privacy
+19 -6865
View File
File diff suppressed because it is too large Load Diff
+1322 -138
View File
File diff suppressed because one or more lines are too long
+4 -7
View File
@@ -57,7 +57,7 @@ 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
bun run verify # privacy + jsonb + progress + test-isolation + wasm + admin-build + typecheck
# Pre-merge sanity (everything CI runs)
bun run test:full # verify + parallel unit + slow + smart e2e
@@ -81,12 +81,9 @@ 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.
(`scripts/check-wasm-embedded.sh`), and stale admin-dashboard build artifacts
(`scripts/check-admin-build.sh`). `bun run check:all` runs the full historical
sweep including the trailing-newline and exports-count checks.
### Writing tests that survive the parallel loop
-78
View File
@@ -161,29 +161,6 @@ After this step:
If a user has a very large brain (>10K pages), `extract --source db` is idempotent
and supports `--since YYYY-MM-DD` for incremental runs.
### Obsidian-style bare wikilinks (opt-in)
If the user imported an Obsidian or Notion vault that uses **bare** `[[note-name]]`
wikilinks — where `[[struktura]]` written in one folder means the page that lives
at `projects/struktura.md` in another — GBrain does NOT connect those by default.
Out of the box it only resolves path-qualified refs like `[[projects/struktura]]`,
so a vault full of bare links shows up as a thin, broken graph. Turn on basename
resolution so the cross-folder links connect:
```bash
gbrain config set link_resolution.global_basename true
gbrain extract links --source db # re-run so the new edges land
```
`gbrain doctor` surfaces a `link_resolution_opportunity` hint with the exact count
("47 of 60 bare wikilinks would resolve") so you know whether it's worth enabling
before you flip it. When a bare name matches more than one page (`[[struktura]]`
both `projects/struktura` and `archive/struktura`), GBrain emits one edge to each
rather than guessing a winner — review and prune the duplicates with
`gbrain graph-query <slug>`. The mode is also honored on the filesystem-walk path
(`gbrain extract links` with no `--source db`) and by auto-link on every future
`put_page`.
## Step 5: Load Skills
If you're running an agent platform (OpenClaw, Hermes, or any repo with a workspace),
@@ -297,58 +274,3 @@ 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.
+40 -301
View File
@@ -1,176 +1,71 @@
# GBrain
**Search gives you raw pages. GBrain gives you the answer.** It's the brain layer your AI agent has been missing — the only one that does synthesis, graph traversal, and gap analysis in one box. Run a full autonomous agent on top of it, or just wire it into Claude Code or Codex as a supercharged retrieval layer in one command; either way your coding agent stops being amnesiac about everything that isn't code.
Your AI agent is smart but forgetful. GBrain gives it a brain.
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.
Built by the President and CEO of Y Combinator to run his actual AI agents. The production brain behind his OpenClaw and Hermes deployments: **146,646 pages, 24,585 people, 5,339 companies**, 66 cron jobs running autonomously. The agent ingests meetings, emails, tweets, voice calls, and original ideas while you sleep. It enriches every person and company it encounters. It fixes its own citations and consolidates memory overnight. You wake up smarter than when you went to bed.
**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)**
The brain wires itself. Every page write extracts entity references and creates typed links (`attended`, `works_at`, `invested_in`, `founded`, `advises`) with zero LLM calls. Hybrid search. Self-wiring knowledge graph. Structured timeline. Backlink-boosted ranking. Ask "who works at Acme AI?" or "what did Bob invest in this quarter?" and get answers vector search alone can't reach. Benchmarked side-by-side: gbrain lands **P@5 49.1%, R@5 97.9%** on a 240-page Opus-generated rich-prose corpus, beating its graph-disabled variant by **+31.4 points P@5** and ripgrep-BM25 + vector-only RAG by a similar margin. Full BrainBench scorecards live in the sibling [gbrain-evals](https://github.com/garrytan/gbrain-evals) repo.
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:
**New default in v0.36.2.0: ZeroEntropy** for both embedding (`zembed-1` at 1280d via Matryoshka) and reranker (`zerank-2`). On a real-corpus benchmark vs OpenAI and Voyage: **2.2× faster** (442ms vs OpenAI 973ms), **2.6× cheaper at regular pricing** ($0.05/M vs OpenAI $0.13), wins 11 of 20 queries head-to-head, reshuffles 60% of top-1 results when used as a second-pass reranker. Bring your own key from [zeroentropy.dev](https://dashboard.zeroentropy.dev), or switch to OpenAI/Voyage at install time via `gbrain init --pglite --embedding-model <provider:model> --embedding-dimensions <N>` — your choice is sticky. To switch an existing brain, run `gbrain reinit-pglite --embedding-model <provider:model> --embedding-dimensions <N>` (PGLite) or follow the SQL recipe in `docs/embedding-migrations.md` (Postgres). `gbrain config set embedding_model` is refused as of v0.37.11.0 because the schema column has to resize too.
- **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.
GBrain is those patterns, generalized. Install in 30 minutes. Your agent does the work. As Garry's personal agent gets smarter, so does yours.
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.
**New in v0.40.2.0 — `gbrain think` grounds temporal answers in the typed-claim timeline.** Ask "when did Marco last switch jobs" or "what was the ARR in March" and the answer comes back rooted in a real chronological timeline of the metric + event facts your brain already extracted via the `extract_facts` cycle phase. Default ON. The intent classifier (`temporal` / `knowledge_update` / `other`) is a regex pass with zero LLM cost; the `'other'` fast path short-circuits with zero extra SQL. Migration v82 adds a nullable `facts.event_type` column so the same plumbing carries event-shaped rows (`'meeting'`, `'job_change'`, `'location_change'`) alongside metric rows. Flip `think.trajectory_enabled=false` to opt out. Debug with `GBRAIN_THINK_DEBUG=1 gbrain think "..."` to see the spliced prompt. The same trajectory plumbing also lands in the LongMemEval benchmark with a methodology change disclosed in `methodology_note: extractor=haiku-preprocess-full-haystack-v1` — published scores are "gbrain + Haiku-preprocess pipeline" vs "gbrain alone", NOT directly comparable to baseline LongMemEval numbers without that note.
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.
**New in v0.36.4.0 — Your agent drives the brain to 90/100 by itself.** One command does the loop you used to run by hand: `gbrain doctor --remediate --yes --target-score 90 --max-usd 5`. It computes a dependency-ordered plan (sync before extract, embed after consolidate), submits each step as a Minion job, re-checks score between every step, and refuses to spend past your cost cap. Cron can drive it unattended. `gbrain doctor --remediation-plan --json` previews what would run. Autopilot now does the same thing on its 5-minute tick: small problems get targeted handlers, big problems get the full cycle, a healthy brain sleeps for 60 minutes instead of grinding through synthesize+patterns+embed every tick. Eleven new things you can submit as background jobs (`reindex`, `repair-jsonb`, `orphans`, `integrity`, `purge`, plus six cycle phases); three of them (synthesize, patterns, consolidate) are PROTECTED so an MCP-connected agent can't silently burn Anthropic credits. New `--background` flag on `gbrain embed` submits the job and exits with `job_id=N` for shell composition.
**New in v0.35.7 — Temporal trajectory + founder scorecard.** Author typed metric assertions in the `## Facts` fence (`mrr=50000`, `arr=2000000`, `team_size=12`) and gbrain stores them as first-class typed columns. `gbrain eval trajectory companies/acme-example` prints the chronological history with regressions auto-flagged inline. `gbrain founder scorecard companies/acme-example` rolls up claim accuracy, consistency, growth direction, and red flags into a stable `schema_version: 1` JSON contract. New MCP op `find_trajectory` exposes the same data to agents (read scope, visibility-filtered for remote callers). The `consolidate` cycle phase now writes `valid_until` on chronologically-superseded facts AND uses semantic upsert on `(page_id, claim, since_date)` — re-running the dream cycle on stable input is now a true no-op (fixed a pre-existing duplicate-takes bug from prior versions).
> **~30 minutes to a fully working brain.** Database ready in 2 seconds (PGLite, no server). You just answer questions about API keys.
> **LLMs:** fetch [`llms.txt`](llms.txt) for the documentation map, or [`llms-full.txt`](llms-full.txt) for the same map with core docs inlined in one fetch. **Agents:** start with [`AGENTS.md`](AGENTS.md) (or [`CLAUDE.md`](CLAUDE.md) if you're Claude Code).
## 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.
GBrain runs in three shapes. Pick the one that matches how you use AI agents today.
### Have your agent install it (recommended)
### Run with your agent platform
If you don't already have an AI agent platform running, start with one of these. Both are designed to read GBrain's install protocol and execute it:
- **[OpenClaw](https://github.com/openclawagents/openclaw)** — deploy [AlphaClaw on Render](https://render.com/deploy?repo=https://github.com/chrysb/alphaclaw) (one click, 8GB+ RAM)
- **[Hermes](https://github.com/openclawagents/hermes)** — deploy on [Railway](https://github.com/praveen-ks-2001/hermes-agent-template) (one click)
Then paste this into your agent:
```
Retrieve and follow the instructions at:
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
```
The agent installs GBrain, creates the brain, asks for your API keys, loads 43 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
> **Never set up an AI agent platform before?** The [personal-brain tutorial](docs/tutorials/personal-brain.md) walks the whole path end-to-end — picking OpenClaw vs Hermes, deploying it, pointing it at INSTALL_FOR_AGENTS.md, getting the API keys, and verifying the first query. Start there if any of the above is new.
### Quick start: Claude Code or Codex
Already running Claude Code or Codex? There are two ways to wire GBrain in, depending on what you want.
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel:
Already using [OpenClaw](https://github.com/garrytan/openclaw) or [Hermes](https://github.com/garrytan/hermes)? GBrain installs as a skillpack scaffold into your agent's workspace.
```bash
gbrain init --pglite # 2-second local brain (no Docker)
claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve
gbrain init --pglite
gbrain skillpack scaffold --all # or: scaffold <name> per skill
```
**Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff:
That's it. Your agent picks up 43 skills (signal detection, brain-ops, ingest, enrich, citation-fixer, daily-task-manager, cron-scheduler, eval framework, and 35 more). Routing lives in `skills/RESOLVER.md` — the agent reads it once per request, picks the right skill, executes. Scaffolded skills are first-class members of your agent repo — you own them, edit freely; `gbrain skillpack reference <name>` diffs your copy against gbrain's bundle when you want to pull upstream improvements. (The legacy `gbrain skillpack install` managed-block model was retired in v0.36.0.0; run `gbrain skillpack migrate-fence` once if you're upgrading from an older release.)
```bash
gbrain connect https://your-host/mcp --token gbrain_xxx --install # Claude Code
gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex --install # Codex
```
### CLI standalone
**[→ Full walkthrough: give your coding agent a memory](docs/tutorials/connect-coding-agent.md)** — both paths end to end, plus the brain-first protocol you paste into `CLAUDE.md` / `AGENTS.md` and the four habits that make it actually change how you work.
### Install the full autonomous setup into your existing agent
Want the whole thing — local brain, 43 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
```
Retrieve and follow the instructions at:
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
```
This works in any agent that can read files over HTTPS and execute shell commands. Tested with Codex, Claude Code, Claude Cowork, Cursor, and AlphaClaw.
### CLI standalone (no agent)
Use gbrain from any shell, no agent platform required.
```bash
bun install -g github:garrytan/gbrain
gbrain init --pglite # 2 seconds; no server, no Docker
gbrain doctor # verify health
gbrain import ~/notes/ # index your markdown
gbrain query "what themes show up across my notes?"
gbrain init --pglite # 2 seconds; no server, no Docker
gbrain doctor # verify health
```
Postgres-at-scale, Supabase, and thin-client setup paths live in [`docs/INSTALL.md`](docs/INSTALL.md).
### Connect GBrain to your AI client (MCP)
GBrain exposes 30+ tools over MCP (stdio and HTTP). The specific snippet depends on which client you use:
- **[Claude Code](docs/mcp/CLAUDE_CODE.md)** — local: one command, `claude mcp add gbrain -- gbrain serve` (zero server, zero tunnel). Remote with just a bearer token: `gbrain connect https://your-host/mcp --token gbrain_xxx` prints a paste-ready block (or `--install` wires it up and smoke-tests the token).
- **[Codex](docs/mcp/CODEX.md)** — `gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex` (or `--install`). Codex reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands in Codex config.
- **[Cursor / Windsurf / any stdio MCP client](docs/mcp/CLAUDE_CODE.md)** — same shape, add `{"command": "gbrain", "args": ["serve"]}` to your MCP config.
- **[Claude Desktop (Cowork)](docs/mcp/CLAUDE_DESKTOP.md)** — Settings → Integrations → add the URL of your HTTP server. Remote only; the local `claude_desktop_config.json` does not work for remote servers.
- **[Claude Cowork (team plan)](docs/mcp/CLAUDE_COWORK.md)** — org Owner adds the connector under Organization Settings → Connectors.
- **[Perplexity Computer](docs/mcp/PERPLEXITY.md)** — `gbrain connect https://your-host/mcp --agent perplexity --oauth --register` mints a least-privilege OAuth client and prints the Issuer/Client ID/Secret to paste into Settings → Connectors (OAuth is the right path for a cloud connector; a bearer token also works for local use). Pro subscription required.
- **[ChatGPT](docs/mcp/CHATGPT.md)** — uses OAuth 2.1 with PKCE (the hard requirement). Register a `chatgpt` client from the admin dashboard with grant type `authorization_code`.
For the HTTP server itself:
Then point any MCP-aware client (Claude Code, Cursor, Windsurf) at it, or use it from your shell:
```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)
gbrain search "who works at acme AI?"
gbrain query "what did bob invest in this quarter?"
gbrain graph-query people/garry-tan --depth 2
```
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/).
Detailed setup paths (Postgres at scale, Supabase, thin-client mode) live in [`docs/INSTALL.md`](docs/INSTALL.md).
## 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.
### MCP server (any MCP client)
```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 serve # stdio MCP (Claude Desktop / Code / Cursor)
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard
# at /admin, SSE activity feed at /admin/events
```
**`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.
Per-client guides (Claude Desktop, Code, Cursor, ChatGPT, Perplexity, Cowork) live under [`docs/mcp/`](docs/mcp/). HTTP server supports DCR-style client registration, scope-gated access (`read`/`write`/`admin`), and built-in rate limiting.
**`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
## How to get data in (v0.38+)
One command, local or hosted, synchronous receipt:
@@ -181,7 +76,10 @@ 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.
The page lands in the DB AND on disk in one move (the v0.38 `put_page`
write-through plumbing). 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):
@@ -199,42 +97,6 @@ Third-party skillpacks can ship custom ingestion sources (Granola, Linear,
voice, OCR) against the versioned `IngestionSource` contract at
`gbrain/ingestion`. See [`docs/skillpack-anatomy.md`](docs/skillpack-anatomy.md).
## Your brain's shape (schema packs)
Most personal-knowledge tools force one fixed layout: their idea of "notes" + "people" + "tags." Drop a Notion export or your own years-old Obsidian vault on top, and the agent doesn't know what a `Projects/` folder means or whether `Reading/` is people or sources.
**gbrain doesn't have a fixed layout.** It ships with bundled schema packs and lets you author your own when none fit:
- **`gbrain-base-v2`** (default as of v0.41.22) — 15-type DRY/MECE canonical taxonomy (14 canonical + `note` catch-all): `person`, `company`, `media`, `tweet`, `social-digest`, `analysis`, `atom`, `concept`, `source`, `deal`, `email`, `slack`, `writing`, `project`, `note`. Subtypes/format/origin pushed to frontmatter. The taxonomy that responds to issue #1479.
- **`gbrain-base`** (legacy, v0.41 and earlier brains) — the original 24-type layout. Stays bundled for back-compat; brains on it can upgrade via `gbrain onboard --check --explain``gbrain jobs submit unify-types --allow-protected --params '{"target_pack":"gbrain-base-v2"}'`.
- **`gbrain-recommended`** — extends `gbrain-base` with the 13 additional directories from `docs/GBRAIN_RECOMMENDED_SCHEMA.md` (source, place, trip, conversation, personal, civic, project, etc.). Activate with `gbrain schema use gbrain-recommended`.
- **Your own pack** — `gbrain schema detect` clusters your actual filesystem into proposed types, `gbrain schema suggest` runs an LLM pass over them, and `gbrain schema review-candidates --apply` promotes the ones you like. Three commands and the brain knows your shape. Authoring a successor pack (declares `migration_from:` so existing brains can opt in): see [`docs/architecture/pack-upgrade-mechanism.md`](docs/architecture/pack-upgrade-mechanism.md).
```bash
gbrain schema active # which pack is running, which tier set it
gbrain schema list # bundled + installed packs
gbrain schema detect # propose types matching your filesystem
gbrain schema suggest # LLM-refined proposals on top of detect
gbrain schema review-candidates # human gate: promote / rename / ignore
gbrain schema use my-pack # activate
```
The active pack threads through every read + write path: `parseMarkdown` infers page type from the pack's path prefixes; `whoknows` scopes expert routing to types declared `expert_routing: true`; `extract_facts` runs only on `extractable: true` types; the search cache folds the pack name + version into its key so cross-pack contamination is structurally impossible. Switch packs and the brain re-interprets itself; switch back and nothing's lost.
Seven-tier resolution chain (per-call flag → env var → per-source DB key → brain-wide DB key → `gbrain.yml``~/.gbrain/config.json``gbrain-base` default). Full reference + authoring guide: [`docs/architecture/schema-packs.md`](docs/architecture/schema-packs.md).
## Tutorials
Step-by-step walkthroughs for getting the most out of GBrain. Each one takes you from zero to a working outcome, with concrete commands and real numbers.
- [**Set up your personal AI agent + brain from zero**](docs/tutorials/personal-brain.md) — the canonical full-stack install. Two GitHub repos, a Telegram bot, AlphaClaw on Render, OpenClaw + GBrain + Supabase. End-to-end in about 2 hours.
- [**Set up GBrain as your company brain**](docs/tutorials/company-brain.md) — federated, multi-user, OAuth-scoped institutional memory for a 10-50 person team. About 90 minutes end-to-end.
- [**Auto-improve a skill with `gbrain skillopt`**](docs/tutorials/improving-skills-with-skillopt.md) — treat a `SKILL.md` as a trainable parameter. Generate a starter benchmark straight from the skill with `--bootstrap-from-skill` (or write your own), strengthen the judges, then watch the optimizer propose edits and keep only the ones that measurably score higher. ~20 minutes, ~$1 in API calls. Flag + cost + safety reference: [`docs/guides/skillopt.md`](docs/guides/skillopt.md).
More walkthroughs in progress: connecting an existing agent (Claude Code, Cursor, OpenClaw, Hermes) to a GBrain memory layer; setting up GBrain for VC dealflow with founder scorecards and meeting prep; migrating an existing Notion or Obsidian vault; indexing a codebase as a queryable code brain. Full tutorial index: [`docs/tutorials/`](docs/tutorials/).
Want to see a tutorial that isn't here yet? [Open an issue](https://github.com/garrytan/gbrain/issues) describing the workflow you want documented.
## What it does (the loop)
```
@@ -252,20 +114,18 @@ The whole loop is described in [`docs/architecture/topologies.md`](docs/architec
## Capabilities
**Hybrid search.** Vector (HNSW on pgvector) + BM25 keyword + reciprocal-rank fusion + source-tier boost + intent-aware query rewriting. Three named search modes (`conservative`, `balanced`, `tokenmax`) bundle the cost/quality knobs into a single config key. Live cost/recall comparisons in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md). Default: `balanced` with ZeroEntropy reranker on. Per-query graph signals notice when a top result is a hub for THAT query (adjacency boost), is corroborated across team brains (cross-source boost), or is being crowded out by weak chunks from a chatty session (session demote). Run `gbrain search "<query>" --explain` to see per-stage attribution: base score, every boost that fired, what it multiplied. `gbrain doctor` ships a `graph_signals_coverage` check; `gbrain search stats` shows fire counts and failure breakdowns. Vector retrieval pools the best chunk per page, so a page surfaces on its strongest evidence instead of losing to a neighbor on one weak chunk. Queries that match a page's title phrase or a declared free-text alias (`gbrain reindex --aliases` backfills existing pages) get boosted to the page they name. Every result carries an `evidence` tag (why it matched) and a `create_safety` hint (`exists` / `probable` / `unknown`) so an agent decides whether a page already exists instead of guessing from a raw score. `gbrain search diagnose "<query>" --target <slug>` traces which retrieval layer surfaces (or misses) a page.
**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. **New in v0.40.4.0:** 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. **Obsidian-style vaults:** bare `[[note-name]]` wikilinks that point across folders — you wrote `[[struktura]]` but the page lives at `projects/struktura.md` — resolve by basename once you opt in with `gbrain config set link_resolution.global_basename true`. Off by default; `gbrain doctor` tells you how many edges you'd gain before you flip it. See [migrating an Obsidian vault](INSTALL_FOR_AGENTS.md#step-45-wire-the-knowledge-graph).
**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. `gbrain eval retrieval-quality` runs NamedThingBench, which hard-gates the named-thing retrieval families (title-substring, alias-synonym, generic-to-named, multi-chunk-dilution) so a regression in "find the page this query names" fails CI loudly. Full methodology in [`docs/eval/SEARCH_MODE_METHODOLOGY.md`](docs/eval/SEARCH_MODE_METHODOLOGY.md).
**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`.
@@ -273,7 +133,6 @@ Data flowing into the brain. Each integration is a recipe — markdown + setup h
- **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.
@@ -289,131 +148,11 @@ Data flowing into the brain. Each integration is a recipe — markdown + setup h
## Troubleshooting
**`gbrain import` fails with `expected N dimensions, not M`?** Run `gbrain doctor`. It will print the exact `gbrain config set ...` or `gbrain retrieval-upgrade` command to repair the mismatch. You should not need to delete `~/.gbrain`. Fresh `gbrain init --pglite` auto-detects your embedding provider from API keys in your environment: set `OPENAI_API_KEY` (or `ZEROENTROPY_API_KEY` / `VOYAGE_API_KEY`) before running init, or pass `--embedding-model <provider>:<model>` explicitly. With multiple keys set, init fires an interactive picker. In non-TTY contexts (CI, Docker) with no keys, init exits 1 with a paste-ready setup hint; pass `--no-embedding` to defer setup until runtime. See [`docs/integrations/embedding-providers.md`](docs/integrations/embedding-providers.md) for the full provider matrix and [`docs/operations/headless-install.md`](docs/operations/headless-install.md) for Docker/CI sequencing.
**Hourly cron sync keeps timing out on a federated brain?** v0.41.13.0 ships
two flags + a recommended pattern. Switch your cron to a per-source loop
with shell `timeout(1)` doing the OS-level kill and gbrain self-terminating
gracefully half-a-minute earlier:
```bash
gbrain sync --break-lock --all --max-age 1800
for src in $(gbrain sources list --json | jq -r '.[].id'); do
timeout 600 gbrain sync --source "$src" --timeout 540 || true
done
```
When `--timeout` fires mid-import, `gbrain sync` exits 0 with status
`partial` and `last_commit` UNCHANGED — the next run re-walks the same
diff and `content_hash` short-circuits already-imported files. The
`--max-age 1800` first command self-heals any wedged-but-alive locks
left by a hung previous run, using the v98 `last_refreshed_at` semantic
(NOT `acquired_at`) so healthy long-running holders are safe by
construction. See the v0.41.13.0 entry in [`CHANGELOG.md`](CHANGELOG.md)
for the honest scope notes (extract + embed phases run to completion;
30-min rollout window for `--max-age` post-migration v98; full-sync
triggers deferred to v0.42+).
**Dream cycle silently losing wiki links on Supabase?** v0.41.19.0 fixes
the bug class structurally. The engine now self-retries every bulk batch
write (`addLinksBatch` / `addTimelineEntriesBatch` / `upsertChunks`) on
Supavisor pooler blips, with a 12s worst-case wait that covers the full
5-10s circuit-breaker recovery window. `gbrain doctor` surfaces incidents
via the new `batch_retry_health` check (reads the last 24h of
`~/.gbrain/audit/batch-retry-YYYY-Www.jsonl`). To tune for an unusually
slow pooler:
```bash
# Defaults: 3 retries, base 1s, max 10s, decorrelated jitter.
# Override per operator without a release:
export GBRAIN_BULK_MAX_RETRIES=5 # int >= 0; 0 disables retries
export GBRAIN_BULK_RETRY_BASE_MS=2000 # int > 0
export GBRAIN_BULK_RETRY_MAX_MS=15000 # int >= base
```
Bad values surface at `gbrain doctor` startup with a paste-ready fix
(not at first-retry mid-cycle). PGLite-only installs pay zero cost — the
retry wrap is engine-level, but PGLite has no pooler so retries never
fire in practice.
**Dream cycle losing ~150 link rows per run with `'No database
connection: connect() has not been called'` errors in the log?** v0.41.27.0
makes the retry layer self-heal on a nulled-out database singleton. A
new `reconnect` callback on `withRetry` rebuilds the connection between
attempts; `PostgresEngine.batchRetry` injects `() => this.reconnect()`
so engine-level batch writes survive a mid-cycle disconnect by something
else in the same process. Same release: `gbrain capture` no longer trails
a `'No database connection'` stderr line from a background facts:absorb
worker firing after CLI exit — the op-dispatch finally block awaits
`getFactsQueue().drainPending({timeout: 1000})` before
`engine.disconnect()`. To find which code path is still calling
disconnect mid-process, run `gbrain doctor --json | jq '.checks[] |
select(.id=="batch_retry_health")'`; the extended check now surfaces
24h disconnect-call count and the most-recent caller frame from a new
`~/.gbrain/audit/db-disconnect-YYYY-Www.jsonl` audit. (Closes #1570.)
**`gbrain brainstorm` returning `judge_failed: true` with 0 scored
ideas?** v0.41.21.0 closes the two bugs that caused it. The judge
hard-coded a 4K-token output cap; for any run past ~40 ideas the call
truncated mid-JSON and the parser threw. Same release closes a slash-
form pricing miss: `gbrain brainstorm --judge-model
anthropic/claude-sonnet-4-6 --max-cost 5` failed with
`BudgetExhausted reason=no_pricing` because every pricing site only
matched the colon form. Both shapes work now. No config change, no
schema migration — `gbrain upgrade` is the whole fix.
**`gbrain reindex --markdown` wiped your auto/dream/signal-detector
tags?** v0.41.37.0 makes tag reconciliation add-only. Re-import and
`reindex --markdown` now ADD current frontmatter tags and never delete,
so enrichment tags written to the DB (auto-tag, dream synthesize,
signal-detector) survive a re-chunk. The reindex DB-only fallback also
reconstructs the full markdown (frontmatter + body + timeline) before
re-chunking, so a page with no on-disk source keeps its frontmatter,
title, and timeline instead of getting overwritten with empty
frontmatter. Trade-off: removing a tag from a page's frontmatter no
longer removes it from the DB on the next sync (frontmatter-tag removal
needs a provenance column, deferred). (Closes #1621.)
**`gbrain sync` wedges on a large brain (no progress, high CPU)?**
v0.41.37.0 ships three things. First, name the stalling file:
```bash
GBRAIN_SYNC_TRACE=1 gbrain sync --no-pull --no-embed --yes
```
The last `[sync] begin import: <path>` line with no following completion
is the file being processed when the hang hit. Second, if you suspect a
schema-pack `inference.regex` with catastrophic backtracking, complete
the sync with the pack disabled and re-run extraction later:
```bash
gbrain sync --no-schema-pack --no-pull --no-embed --yes
```
`gbrain schema lint` now warns on the classic nested-quantifier ReDoS
shapes (`(a+)+`, `(a*)*`, …) in pack regexes, and the runtime caps
inference-regex input length (override via `GBRAIN_MAX_REGEX_INPUT_CHARS`).
Third, on a PGLite brain, stop `gbrain serve` before a large sync —
PGLite is single-writer and a live MCP server contends for the write
lock. See [`docs/architecture/serve-sync-concurrency.md`](docs/architecture/serve-sync-concurrency.md)
for the full triage. (Closes #1569.)
**`gbrain init --migrate-only` / a schema migration fails on Windows
with `getaddrinfo ENOTFOUND`?** v0.41.37.0 runs the 9 schema-bring-up
phases in-process instead of spawning a child `gbrain init
--migrate-only` per phase. The spawned child died on
Windows + bun + Supabase pooler with a DNS-resolution failure even
though the parent connected fine; running in-process removes the spawn
entirely. The v0.13.1 grandfather migration that hung 70+ minutes on an
82K-page PGLite brain is also fixed — it now runs as a chunked bulk SQL
pass (keyed on the page PK, soft-delete-filtered, source-safe) that
completes in ~1-2 seconds. (Closes #1605, #1581.)
**`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`. As of v0.37, 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.
## 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)
@@ -435,8 +174,8 @@ If you find a bug or want a feature: open an issue first. Quick fixes (typo, doc
## License + credit
MIT. I built GBrain to run my OpenClaw and Hermes deployments — the production brain behind my AI agents.
MIT. Built by Garry Tan to run his OpenClaw and Hermes deployments — the production brain behind his actual AI agents.
Origin story: [`docs/ethos/ORIGIN.md`](docs/ethos/ORIGIN.md).
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that ships as the default. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
Community PR contributors are credited in `CHANGELOG.md` per release. ZeroEntropy ([@zeroentropy](https://zeroentropy.dev)) for the embedding + reranker stack that became the v0.36.2.0 default. Voyage AI for the asymmetric-encoding recipe template. Ramp Labs for the search quality improvements lineage.
+9 -78
View File
@@ -50,43 +50,6 @@ exclusively via `gbrain auth create/list/revoke`.
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
@@ -138,19 +101,6 @@ 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
@@ -174,34 +124,15 @@ 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:
Disabled by default. To honor `X-Forwarded-For` (or `X-Real-IP`) when
gbrain runs behind a trusted reverse proxy:
```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:
**Critical safety contract:** only set `GBRAIN_HTTP_TRUST_PROXY=1` 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
@@ -214,11 +145,11 @@ of these are true:
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.
If gbrain is reachable directly AND `GBRAIN_HTTP_TRUST_PROXY=1` is set,
clients can spoof their IP by sending arbitrary `X-Forwarded-For`
headers, defeating the pre-auth IP rate limit. Without the flag, gbrain
ignores all forwarded-for headers and uses the socket peer address,
which is the safe default for direct-exposure deployments.
### Body size cap
+3 -1678
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
0.42.11.0
0.40.5.0
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
<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>
<script type="module" crossorigin src="/admin/assets/index-DFgMZhBE.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-GxkWX7v3.css">
</head>
<body>
+2 -6
View File
@@ -4,14 +4,13 @@ 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';
type Page = 'login' | 'dashboard' | 'agents' | 'log' | 'calibration';
function getPage(): Page {
const hash = window.location.hash.replace('#', '') || 'dashboard';
if (['login', 'dashboard', 'agents', 'log', 'calibration', 'jobs'].includes(hash)) return hash as Page;
if (['login', 'dashboard', 'agents', 'log', 'calibration'].includes(hash)) return hash as Page;
return 'dashboard';
}
@@ -58,8 +57,6 @@ export function App() {
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
@@ -85,7 +82,6 @@ export function App() {
{page === 'agents' && <AgentsPage />}
{page === 'log' && <RequestLogPage />}
{page === 'calibration' && <CalibrationPage />}
{page === 'jobs' && <JobsWatchPage />}
</main>
</div>
);
-2
View File
@@ -50,6 +50,4 @@ export const api = {
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'),
};
-174
View File
@@ -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>
);
}
-23
View File
@@ -37,8 +37,6 @@ 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
@@ -54,15 +52,6 @@ gbrain sync --watch # live-sync a git repo (autopilot mode)
gbrain autopilot --install # background daemon for nightly enrichment
```
**Wire this same local brain into your coding agent** — zero server, zero token:
```bash
claude mcp add gbrain -- gbrain serve # Claude Code
codex mcp add gbrain -- gbrain serve # Codex
```
The agent spawns `gbrain serve` as a stdio subprocess against your local brain. Full walkthrough (both this local path and connecting to a remote brain), plus the brain-first protocol to paste into `CLAUDE.md` / `AGENTS.md`: **[Give your coding agent a memory](tutorials/connect-coding-agent.md)**.
## 3. MCP server (any MCP client)
```bash
@@ -70,21 +59,9 @@ gbrain serve # stdio MCP (Claude Desktop / Code / Cursor)
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard
```
**Wire a coding agent to a remote brain in one command** (when you have an HTTP
server + a bearer token): `gbrain connect` prints a paste-ready setup block, or
`--install` runs it and smoke-tests the token.
```bash
gbrain auth create "claude-code"
gbrain connect https://your-host/mcp --token gbrain_xxx # Claude Code (default)
gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex # Codex (env-var bearer)
gbrain connect https://your-host/mcp --agent perplexity --oauth --register # Perplexity (OAuth)
```
Per-client setup guides live in [`docs/mcp/`](mcp/):
- [`docs/mcp/CLAUDE_CODE.md`](mcp/CLAUDE_CODE.md)
- [`docs/mcp/CODEX.md`](mcp/CODEX.md)
- [`docs/mcp/CLAUDE_DESKTOP.md`](mcp/CLAUDE_DESKTOP.md)
- [`docs/mcp/CHATGPT.md`](mcp/CHATGPT.md)
- [`docs/mcp/PERPLEXITY.md`](mcp/PERPLEXITY.md)
-433
View File
@@ -1,433 +0,0 @@
# Releasing & contributing (gbrain)
The full release + contributor process. CLAUDE.md keeps the ship-critical IRON RULES
inline (the Version-locations table, branch=workspace, post-ship `/document-release`,
the Privacy + Responsible-disclosure rules, PR-title-version-first, never-hand-roll-ship)
and points here for everything else. **Before any ship, read this in full. Use `/ship`
never hand-roll a release.**
## Pre-ship requirements
Before shipping (/ship) or reviewing (/review), always run the full test suite.
Two equivalent paths:
**Path A — local CI gate (recommended, v0.23.1+):**
- `bun run ci:local` runs the entire stack inside Docker: gitleaks (host), unit
tests with `DATABASE_URL` unset, and all 29 E2E files sequentially against a
fresh pgvector container. Stronger than PR CI's 2-file Tier 1 set; closer to
what nightly Tier 1 catches. Spins up + tears down postgres automatically via
`docker-compose.ci.yml`. Override the host port with
`GBRAIN_CI_PG_PORT=5435 bun run ci:local` if 5434 collides.
- `bun run ci:local:diff` runs only the E2E files matched by the diff selector
(`scripts/select-e2e.ts`), falling back to all 29 on unmapped src/ paths or
schema/skills/package.json changes. Fast iteration during a focused branch.
**Path B — manual lifecycle (still supported):**
- `bun test` — unit tests (no database required)
- Follow the "E2E test DB lifecycle" steps above to spin up the test DB,
run `bun run test:e2e`, then tear it down.
Both must pass. Do not ship with failing E2E tests. Do not skip E2E tests.
**Always run typecheck before pushing.** `bun test` (the bun runner)
skips TypeScript type checking — it only enforces runtime behavior.
Three ways to actually gate on types:
1. `bun run test` (npm script in `package.json`) — includes `bun run typecheck`
plus the four shell pre-checks (`check-jsonb-pattern.sh`,
`check-progress-to-stdout.sh`, `check-trailing-newline.sh`,
`check-wasm-embedded.sh`) before the runner. Use this mid-branch.
2. `bun run typecheck``tsc --noEmit` standalone. Fast (~5s on this repo).
3. `bun run ci:local` — the full local CI gate from Path A.
The trap is: writing a new test, running `bun test test/foo.test.ts`,
seeing it pass, pushing — and CI's separate typecheck stage rejects an
invalid type literal that the runner accepted. Caught one of these
shipping the v0.23.2 round-trip E2E (`type: 'reflection'` is not a
member of `PageType`). Run `bun run typecheck` once before push, even
when only test files changed.
## CHANGELOG + VERSION are branch-scoped
**VERSION and CHANGELOG describe what THIS branch adds vs master, not how we got
here.** Every feature branch that ships gets its own version bump and CHANGELOG
entry. The entry is product release notes for users; it is not a log of internal
decisions, review rounds, or codex findings.
**Write the CHANGELOG entry at /ship time, not during development.** Mid-branch
iterations, review rounds (CEO/Eng/Codex/DX), and implementation detours belong
in the plan file at `~/.claude/plans/`, not in the CHANGELOG. One unified entry
per branch, covering what the branch added vs the base branch.
**Never edit a CHANGELOG entry that already landed on master.** If master has
v0.18.2 and your branch adds features, bump to the next version (v0.19.0, not
editing master's v0.18.2). When merging master into your branch, master may
bring new CHANGELOG entries above yours — push your entry above master's
latest and verify:
- Does CHANGELOG have your branch's own entry separate from master's entries?
- Is VERSION higher than master's VERSION?
- Is your entry the topmost `## [X.Y.Z]` entry?
- `grep "^## \[" CHANGELOG.md` shows a contiguous version sequence?
If any answer is no, fix it before continuing.
**CHANGELOG is for users, not contributors.** Write like product release notes:
- Lead with what the user can now **do** that they couldn't before. Sell the capability.
- Plain language, not implementation details. "You can now..." not "Refactored the..."
- **Never mention internal artifacts**: plan file IDs, decision tags (D-CX-#, F-ENG-#),
review rounds, codex findings, subcontractor credits. These are invisible to users.
- Put contributor-facing changes in a separate `### For contributors` section at the bottom.
- Every entry should make someone think "oh nice, I want to try that."
**What to omit:**
- "Codex caught X that the CEO review missed" — private process detail.
- "D-CX-3 split errors/warnings" — tag is meaningless to users; name the feature instead.
- "Fix-wave PR #N supersedes #M" — supersede chains belong in PR bodies, not release notes.
- "215 new cases, 3 decisions applied, 7 reviews cleared" — these are planning-mode metrics.
**What to keep:**
- The user-facing change: what commands exist now, what flag was added, what behavior fixed.
- Numbers that mean something to the user: TTHW, commands that timed out before, detection counts.
- Upgrade instructions: `gbrain upgrade` + any manual step if needed.
- Credit to external contributors when a community PR was incorporated.
## CHANGELOG voice + release-summary format
**IRON RULE: the CHANGELOG describes what the user gets, not how the work
happened.** Nobody reading release notes cares that codex caught a bug, that
the plan went through CEO + eng review, that the migration was originally
numbered v68 and renumbered to v79 during master merge, or that two
review rounds caught architectural mistakes. The reader cares what
`gbrain brainstorm` does and how to use it. If a fact only exists because
of the development process, it does NOT belong in the CHANGELOG.
**Specifically forbidden in CHANGELOG entries:**
- Any mention of review processes (CEO review, eng review, codex review,
plan-eng-review, outside voice, adversarial review, autoplan, /review).
- "What we caught and fixed before merging" sections. Bugs found pre-merge
are not changes — they're things that didn't ship.
- Plan file references, plan IDs, plan decision tags (D1, D14, D-CDX-3).
- Migration version drama ("originally v68", "renumbered to v77", "claimed
by parallel waves") — just say "Migration v79 adds X." If the user
cares about migration ordering, they read the diff.
- Round counts, finding counts, decision counts ("25 findings across 2
rounds", "8 architectural decisions", "5/6 expansions accepted").
- Names of internal collaborators ("codex caught", "the reviewer flagged",
"Claude noticed").
- "Plan + reviews" summary bullets. The plan lives in `~/.claude/plans/`;
if a future reader wants the backstory they can grep there.
- Any wording that frames a shipped feature as a *recovery* from a planning
mistake ("the first plan was wrong", "we corrected the approach", "the
shipped version supersedes the original design").
**Smell test:** read the entry as a stranger who has never touched gbrain.
If any sentence makes them think "why are you telling me this?", cut it.
Every sentence in the release-summary AND in the itemized changes must
answer one of three questions: *What can I now do? How do I use it? What
should I watch for after I upgrade?*
Every version entry in `CHANGELOG.md` MUST start with a release-summary section in
the GStack/Garry voice — one viewport's worth of prose + tables that lands like a
verdict, not marketing. The itemized changelog (subsections, bullets, files) goes
BELOW that summary, separated by a `### Itemized changes` header.
The release-summary section gets read by humans, by the auto-update agent, and by
anyone deciding whether to upgrade. The itemized list is for agents that need to
know exactly what changed.
### Release-summary template
**Iron rule: lead ELI10, get precise after.** The first ~150 words of every entry
must be readable by someone who does NOT know gbrain's internals. No file paths,
no function names, no internal constants, no acronyms (no "RRF", no "knobsHash",
no "MODE_BUNDLES", no "CDX-4"), no jargon that requires reading the codebase to
parse. Lead with the user-visible behavior change, in everyday English, like
you're explaining it to a smart engineer who has never opened the repo.
THEN, once the reader knows what shipped and why they'd care, drill into the
precise details: real file paths, real function names, real config keys, real
numbers. The precision part is required (the entry is also the technical record
of what changed), but it lives AFTER the plain-English lead, never before it.
The shape:
1. **One-line bold headline.** What changed for the user, in human English. No
jargon. No internal terms. Example good: "Your search stops boosting weak
pages just because they have a lot of links pointing at them." Example bad:
"PostFusionOpts gains floorRatio; KNOBS_HASH_VERSION bumped 2→3."
2. **Plain-English opener** (~3-5 sentences). Describe the problem this fixes in
everyday terms. Pretend the reader has a brain full of meeting notes and
people pages and wants to know if this release helps them. Concrete example
beats abstract description.
3. **A "How to turn it on" or "How to use it" section** with paste-ready
commands. Real flags, real config keys. This is where precision starts.
4. **A "What you'd see in a concrete example" or "The X numbers that matter"
section** with a table. Use everyday-language column headers ("Page",
"Match quality", "Has many backlinks?") even when the underlying mechanism
is technical. The table teaches what the feature does without requiring the
reader to understand how.
5. **A "What's safe to know about" or "Things to watch" section** for caveats,
side effects, cache invalidation, mid-deploy notes. Still in plain language.
6. **A "What we caught and fixed before merging" section** if the work went
through review (CEO/eng/codex/outside-voice). Translate review findings into
plain English. "We caught a stale-cache bug" beats "knobsHash() did not
include floorRatio in the v=2 hash input."
7. **`### Itemized changes`** (precision lives here). File paths, function
names, types, constants, line numbers. This section is for engineers who
need to know exactly what moved.
Voice rules (apply throughout):
- No em dashes (use commas, periods, "...").
- No AI vocabulary (delve, robust, comprehensive, nuanced, fundamental, etc.) or
banned phrases ("here's the kicker", "the bottom line", etc.).
- Real numbers, real file names, real commands AFTER the ELI10 lead. Not "fast"
but "~30s on 30K pages." In the ELI10 lead, "fast enough that you won't
notice" or "~30 seconds even on a big brain."
- Short paragraphs, mix one-sentence punches with 2-3 sentence runs.
- Connect to user outcomes: "the agent does ~3x less reading" beats "improved
precision."
- Be direct about quality. "Well-designed" or "this is a mess." No dancing.
**The smell test:** if someone who has never opened gbrain reads the first 150
words and walks away knowing what shipped and whether they care, the entry
passes. If they need to grep the codebase to follow along, rewrite the lead.
**Canonical examples in this CHANGELOG:** v0.35.6.0 (floor-ratio gate, written
ELI10-lead-first), v0.34.4.0 (embed stale fix wave). Use those shapes when in
doubt. Avoid the shape of entries that lead with internal constants or release
mechanics; those exist in older history but should not be the model for new
work.
Source material to pull from:
- CHANGELOG.md previous entry for prior context
- Latest `gbrain-evals/docs/benchmarks/[latest].md` for headline numbers (sibling repo)
- Recent commits (`git log <prev-version>..HEAD --oneline`) for what shipped
- Don't make up numbers. If a metric isn't in a benchmark or production data, don't
include it. Say "no measurement yet" if asked.
Target length: ~250-350 words for the summary. Should render as one viewport.
### "To take advantage of v[version]" block (required, v0.13+)
After the release-summary and BEFORE `### Itemized changes`, every `## [X.Y.Z]`
entry MUST include a human-readable self-repair block under the heading
`## To take advantage of v[version]`.
Why: `gbrain upgrade` runs `gbrain post-upgrade` which runs `gbrain apply-migrations`.
This chain has a known weak link — `upgrade.ts` catches post-upgrade failures as
best-effort (so the binary still works). When that chain silently fails, users end
up with half-upgraded brains. The self-repair block gives them a paste-ready
recovery path; the v0.13+ `~/.gbrain/upgrade-errors.jsonl` trail + `gbrain doctor`
integration close the loop.
Template (adapt the verify commands per release):
```markdown
## To take advantage of v[version]
`gbrain upgrade` should do this automatically. If it didn't, or if `gbrain doctor`
warns about a partial migration:
1. **Run the orchestrator manually:**
```bash
gbrain apply-migrations --yes
```
2. **Your agent reads `skills/migrations/v[version].md` the next time you interact with it.**
[One sentence on whether headless agents need manual action, or whether the
orchestrator already handled the mechanical side.]
3. **Verify the outcome:**
```bash
[release-specific verify commands, e.g. `gbrain graph ... --depth 2`]
gbrain stats
```
4. **If any step fails or the numbers look wrong,** please file an issue:
https://github.com/garrytan/gbrain/issues with:
- output of `gbrain doctor`
- contents of `~/.gbrain/upgrade-errors.jsonl` if it exists
- which step broke
This feedback loop is how the gbrain maintainers find fragile upgrade paths. Thank you.
```
**Skip this block** for patches that are pure bug fixes with zero user-facing action
(rare). If the release has a schema migration, data backfill, or new feature the
user needs to verify, the block is required.
The v0.13.0 entry in CHANGELOG.md is the canonical example.
### Itemized changes (the existing rules)
Below the release summary, write `### Itemized changes` and continue with the
detailed subsections (Knowledge Graph Layer, Schema migrations, Security hardening,
Tests, etc.). Same rules as before:
- Lead with what the user can now DO that they couldn't before
- Frame as benefits and capabilities, not files changed or code written
- Make the user think "hell yeah, I want that"
- Bad: "Added GBRAIN_VERIFY.md installation verification runbook"
- Good: "Your agent now verifies the entire GBrain installation end-to-end, catching
silent sync failures and stale embeddings before they bite you"
- Bad: "Setup skill Phase H and Phase I added"
- Good: "New installs automatically set up live sync so your brain never falls behind"
- **Always credit community contributions.** When a CHANGELOG entry includes work from
a community PR, name the contributor with `Contributed by @username`. Contributors
did real work. Thank them publicly every time, no exceptions.
### Reference: v0.12.0 entry as canonical example
The v0.12.0 entry in CHANGELOG.md is the canonical example of the format. Match its
structure for every future version: bold headline, lead paragraph, "numbers that
matter" with BrainBench-style before/after table, "what this means" closer, then
`### Itemized changes` with the detailed sections below.
## Version migrations
Create a migration file at `skills/migrations/v[version].md` when a release
includes changes that existing users need to act on. The auto-update agent
reads these files post-upgrade (Section 17, Step 4) and executes them.
**You need a migration file when:**
- New setup step that existing installs don't have (e.g., v0.5.0 added live sync,
existing users need to set it up, not just new installs)
- New SKILLPACK section with a MUST ADD setup requirement
- Schema changes that require `gbrain init` or manual SQL
- Changed defaults that affect existing behavior
- Deprecated commands or flags that need replacement
- New verification steps that should run on existing installs
- New cron jobs or background processes that should be registered
**You do NOT need a migration file when:**
- Bug fixes with no behavior changes
- Documentation-only improvements (the agent re-reads docs automatically)
- New optional features that don't affect existing setups
- Performance improvements that are transparent
**The key test:** if an existing user upgrades and does nothing else, will their
brain work worse than before? If yes, migration file. If no, skip it.
Write migration files as agent instructions, not technical notes. Tell the agent
what to do, step by step, with exact commands. See `skills/migrations/v0.5.0.md`
for the pattern.
## Migration is canonical, not advisory
GBrain's job is to deliver a canonical, working setup to every user on upgrade.
Anything that looks like a "host-repo change" — AGENTS.md, cron manifests,
launchctl units, config files outside `~/.gbrain/` — is a GBrain migration
step, not a nudge we leave for the host-repo maintainer. Migrations edit host
files (with backups) to make the canonical setup real. Exceptions: changes
that require human judgment (content edits, renames that break semantics,
host-specific handler registration where shell-exec would be an RCE surface).
Everything mechanical ships in the migration.
**Test:** if shipping a feature requires a sentence that starts with "in
your AGENTS.md, add…" or "in your cron/jobs.json, rewrite…", the migration
orchestrator should be doing that edit, not the user.
**The exception is host-specific code.** For custom Minion handlers
(host-specific integrations like inbox sweeps or third-party API scanners), shipping them as a
data file the worker would exec is an RCE surface. Those get registered in
the host's own repo via the plugin contract (`docs/guides/plugin-handlers.md`);
the migration orchestrator emits a structured TODO to
`~/.gbrain/migrations/pending-host-work.jsonl` + the host agent walks the
TODOs using `skills/migrations/v0.11.0.md` — stays host-agnostic, still
canonical.
## Schema state tracking
`~/.gbrain/update-state.json` tracks which recommended schema directories the user
adopted, declined, or added custom. The auto-update agent (SKILLPACK Section 17)
reads this during upgrades to suggest new schema additions without re-suggesting
things the user already declined. The setup skill writes the initial state during
Phase C/E. Never modify a user's custom directories or re-suggest declined ones.
## GitHub Actions SHA maintenance
All GitHub Actions in `.github/workflows/` are pinned to commit SHAs. Before shipping
(`/ship`) or reviewing (`/review`), check for stale pins and update them:
```bash
for action in actions/checkout oven-sh/setup-bun actions/upload-artifact actions/download-artifact softprops/action-gh-release gitleaks/gitleaks-action; do
tag=$(grep -r "$action@" .github/workflows/ | head -1 | grep -o '#.*' | tr -d '# ')
[ -n "$tag" ] && echo "$action@$tag: $(gh api repos/$action/git/ref/tags/$tag --jq .object.sha 2>/dev/null)"
done
```
If any SHA differs from what's in the workflow files, update the pin and version comment.
## PR descriptions cover the whole branch
Pull request titles and bodies must describe **everything in the PR diff against the
base branch**, not just the most recent commit you made. When you open or update a
PR, walk the full commit range with `git log --oneline <base>..<head>` and write the
body to cover all of it. Group by feature area (schema, code, tests, docs) — not
chronologically by commit.
This matters because reviewers read the PR body to understand what's shipping. If
the body only covers your last commit, they miss everything else and can't review
properly. A 7-commit PR with a body that describes commit 7 is worse than no body
at all — it actively misleads.
When in doubt, run `gh pr view <N> --json commits --jq '[.commits[].messageHeadline]'`
to see what's actually in the PR before writing the body.
## Community PR wave process
Never merge external PRs directly into master. Instead, use the "fix wave" workflow:
1. **Categorize** — group PRs by theme (bug fixes, features, infra, docs)
2. **Deduplicate** — if two PRs fix the same thing, pick the one that changes fewer
lines. Close the other with a note pointing to the winner.
3. **Collector branch** — create a feature branch (e.g. `garrytan/fix-wave-N`), cherry-pick
or manually re-implement the best fixes from each PR. Do NOT merge PR branches directly —
read the diff, understand the fix, and write it yourself if needed.
4. **Test the wave** — verify with `bun test && bun run test:e2e` (full E2E lifecycle).
Every fix in the wave must have test coverage.
5. **Close with context** — every closed PR gets a comment explaining why and what (if
anything) supersedes it. Contributors did real work; respect that with clear communication
and thank them.
6. **Ship as one PR** — single PR to master with all attributions preserved via
`Co-Authored-By:` trailers. Include a summary of what merged and what closed.
**Community PR guardrails:**
- Always AskUserQuestion before accepting commits that touch voice, tone, or
promotional material (README intro, CHANGELOG voice, skill templates).
- Never auto-merge PRs that remove YC references or "neutralize" the founder perspective.
- Preserve contributor attribution in commit messages.
## Checking out PRs from garrytan-agents
`garrytan-agents` is the AI-authored PR account and is NOT a collaborator on
this repo. Its PRs live in a fork, so GitHub Actions triggered by
`pull_request` events on those PRs do not receive base-repo secrets. Any CI
job that needs `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or similar will fail
with empty-env auth errors, regardless of what's set on the base repo. This
is a GitHub security default, not a config bug.
When the user says "check out <PR link>" and the PR is from `garrytan-agents`
(or any other non-collaborator fork), move the branch into the base repo
before running CI:
1. `gh pr checkout <N>` — pull down the fork's branch. Note the PR number and
head branch name (`gh pr view <N> --json headRefName --jq .headRefName`).
2. `git push origin HEAD:<branch-name>` — push the same branch to the base
repo (origin points at `garrytan/gbrain`, not the fork). This is the move
that gives CI access to secrets.
3. `gh pr close <N> --comment "moving to base-repo branch for secret access"`
— close the fork PR so the queue stays clean.
4. `gh pr create --base master --head <branch-name>` — open the replacement
PR from the base-repo branch. **Preserve the original PR's title and body
verbatim** (`gh pr view <N> --json title,body`); contributor attribution
moves to a `Co-Authored-By:` trailer if needed.
Why this over alternatives: adding `garrytan-agents` as a collaborator, or
flipping the repo-wide "send secrets to fork PRs" toggle, both broaden
secret distribution to every fork PR from that account or any fork. Moving
the branch keeps secret scope tight to just the one PR being shipped.
-289
View File
@@ -1,289 +0,0 @@
# Testing (gbrain repo)
On-demand reference (see CLAUDE.md Reference map). Current behavior + invariants
only.
### Test command tiers
Seven test command tiers, each with a clear scope:
| Command | What it runs | Wallclock | When to use |
|---|---|---|---|
| `bun run test` | Parallel unit-test fast loop. 8-shard fan-out via `scripts/run-unit-parallel.sh`, then a serial pass over `*.serial.test.ts`. Excludes `*.slow.test.ts` and `test/e2e/*`. No pre-checks, no typecheck. | ~85s on a Mac dev box (3650+ tests) | Inner edit loop. Default. |
| `bun run verify` | CI's authoritative pre-test gate set: `check:privacy && check:jsonb && check:progress && check:wasm && bun run typecheck`. The 4 checks `.github/workflows/test.yml` runs on shard 1 + typecheck. Single source of truth — CI literally calls `bun run verify`. | ~12s (wasm-compile dominates) | Before pushing; before `/ship`. |
| `bun run test:full` | `verify && bun run test && bun run test:slow && [smart e2e]`. The local equivalent of "everything CI runs." Smart e2e: runs e2e only when `DATABASE_URL` is set; else loud skip notice to stderr. | ~3-5min depending on slow + e2e | Pre-merge sanity, before opening a PR. |
| `bun run test:slow` | Just the `*.slow.test.ts` set (intentional cold-path correctness checks). | seconds-to-minutes | When touching slow-path code. |
| `bun run test:serial` | Just the `*.serial.test.ts` set (cross-file-contention quarantine; runs at `--max-concurrency=1`). | ~1s per quarantined file | Debugging a specific quarantined file. |
| `bun run test:e2e` | Real Postgres E2E. Requires Docker + `DATABASE_URL`. Sequential. | ~5-10min | Pre-ship; nightly. |
| `bun run check:all` | All 7 historical pre-checks (privacy + jsonb + progress + no-legacy-getconnection + trailing-newline + wasm + exports-count). Superset of `verify`. | ~10s | Local-only sweep. The 4 not in `verify` are nice-to-haves. |
### CI vs local: intentionally divergent file sets
- **CI matrix** (`.github/workflows/test.yml`) runs `scripts/test-shard.sh` 4-way, which uses FNV-1a hash bucketing and INCLUDES `*.slow.test.ts`. CI EXCLUDES `*.serial.test.ts` from the hash buckets and runs them on shard 1 via `bun run test:serial` at `--max-concurrency=1` — keeping serial files out of the hash buckets is what preserves the `mock.module` quarantine (top-level mocks in serial files would otherwise leak into the parallel files they share a shard process with). CI is the ground truth for "did everything pass."
- **Local fast loop** (`scripts/run-unit-shard.sh` via the parallel wrapper) uses round-robin-by-index sharding and EXCLUDES `*.slow.test.ts` AND `*.serial.test.ts`. Local trades coverage for inner-loop speed; CI catches what local skips.
This divergence is intentional. Don't try to make them equal — the two scripts deliberately solve different problems. The regression test at `test/scripts/run-unit-shard.test.ts` pins what the local fast loop should and shouldn't include.
### Failure-first logging
When `bun run test` finds any failure, the wrapper:
1. Writes failure blocks (each prefixed with `--- shard N: <test name> ---`) to `.context/test-failures.log` (workspace-local, gitignored). On systems without a writable `.context/`, falls back to `/tmp/gbrain-test-failures.log`.
2. Prints a loud stderr banner with the absolute log path, plus the last 30 lines of the failure log inlined. Banner survives `| head` / `| tail` / agent-side log truncation.
3. Writes a one-line-per-shard summary to `.context/test-summary.txt` (`shard N/M: pass=X fail=Y skip=Z rc=W`).
4. Exits non-zero. Empty failure log + non-zero exit = infrastructure problem (wedged shard, killed child); the banner says so.
If a shard wedges (per-shard `GBRAIN_TEST_SHARD_TIMEOUT` cap, default 600s), the wrapper writes `--- shard N: WEDGED after ${SHARD_TIMEOUT}s ---` to the failure log, includes the last 50 lines of the shard log, and proceeds with other shards' results.
### File taxonomy
- `*.test.ts` → fast loop (parallel 8-shard fan-out).
- `*.slow.test.ts` → run via `bun run test:slow` only (intentional cold-path tests; would dominate the fast loop's wallclock).
- `*.serial.test.ts` → run via `bun run test:serial` after the parallel pass completes; uses `--max-concurrency=1`. Quarantine for tests that share file-wide state and race when run alongside other files in the same `bun test` process. Currently: `test/brain-registry.serial.test.ts`, `test/reconcile-links.serial.test.ts`, `test/core/cycle.serial.test.ts`, `test/embed.serial.test.ts` (the latter two use `mock.module(...)` which leaks across files in the shard process). **Do not put the parallelism back on a serial file unless you've fixed the contention root cause** (it just re-introduces the flake).
- `test/e2e/*.test.ts` → real-Postgres E2E. Skipped when `DATABASE_URL` is unset.
- `tests/heavy/*.sh` → ops-shape shell scripts. Cost minutes per run; NOT in default `bun test`. Run via `bun run test:heavy` or scheduled nightly via `.github/workflows/heavy-tests.yml`. Examples: pg_upgrade matrix (boot legacy brain → walk to head), RSS budget gate (measure peak worker RSS vs committed baseline), read-latency-under-sync (p50/p95/p99 under concurrent writer load), sync lock regression (N concurrent syncs assert 1 winner + N-1 lock-busy + zero leaked `gbrain_cycle_locks` rows). See `tests/heavy/README.md` for when to add a script here vs `*.slow.test.ts`. Files prefixed with `_` (e.g. `tests/heavy/_build_legacy_fixtures.sh`) are helpers/libs invoked by sibling tests — the runner skips them.
- `test/fuzz/*.test.ts` → property-based fuzz harness. Pure-validator targets in `pure-validators.test.ts` are guarded by `scripts/check-fuzz-purity.sh` (in `bun run verify`), which `bun build --target=bun` bundles each target and greps the resulting bundle for banned transitive imports (`node:fs`, `node:child_process`, engine modules). Anything that fails the guard moves to `mixed-validators.test.ts` (still property-tested, but no purity guarantee) or `filesystem-validators.test.ts` (fs-backed, uses temp dirs). Fuzz tests run in the default `bun test` loop because they're fast (~3s for ~12 properties × 1000 runs each).
### Test-isolation lint and helpers
The cross-file flake class is enforced statically by `scripts/check-test-isolation.sh`, wired into `bun run verify` and `bun run check:all`. Rules (non-serial unit files only; `*.serial.test.ts` and `test/e2e/*` are skipped):
| Rule | What it bans | Fix |
|---|---|---|
| **R1** | `process.env.X = ...`, bracket assignment, `delete process.env.X`, `Object.assign(process.env, ...)`, `Reflect.set(process.env, ...)` | Use `withEnv()` from `test/helpers/with-env.ts`, OR rename file to `*.serial.test.ts` |
| **R2** | `mock.module(...)` anywhere in the file | Rename file to `*.serial.test.ts` (no DI on production code for testability) |
| **R3** | `new PGLiteEngine(` outside ~50 lines after a `beforeAll(` line | Use the canonical block (below) inside `beforeAll(` |
| **R4** | Files creating `new PGLiteEngine(` without `engine.disconnect(` inside an `afterAll(` block | Add `afterAll(() => engine.disconnect())` |
Files that violated these rules at the isolation-lint baseline are listed in `scripts/check-test-isolation.allowlist`. **The allow-list MUST shrink over time** — never add new entries.
#### Canonical PGLite block (R3 + R4 compliant)
Every test file that needs a PGLite engine should use this exact pattern:
```ts
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { resetPgliteState } from './helpers/reset-pglite.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
```
Why this exact shape: `beforeAll` creates a single engine per file (PGLite WASM cold-start + initSchema is ~20s); `beforeEach` truncates user data via `resetPgliteState` ("two orders of magnitude faster" than fresh-engine-per-test); `afterAll` disconnects so the engine doesn't leak across file boundaries within a shard process.
#### `withEnv` pattern (R1 fix)
```ts
import { withEnv } from './helpers/with-env.ts';
test('reads OPENAI_API_KEY', async () => {
await withEnv({ OPENAI_API_KEY: 'sk-test' }, async () => {
expect(loadConfig().openai_key).toBe('sk-test');
});
});
// Delete a var (override is undefined):
await withEnv({ GBRAIN_HOME: undefined }, fn);
// Multiple keys:
await withEnv({ A: '1', B: '2', C: undefined }, fn);
```
`withEnv` saves the prior value of every key it touches and restores via try/finally — including when the callback throws. **It is cross-test safe but NOT intra-file concurrent-safe.** `process.env` is process-global; two `test.concurrent()` calls in the same file both touching the same key will race. Files using `withEnv` stay outside the `test.concurrent()` codemod's eligibility filter.
#### When to quarantine instead of fix
Rename to `*.serial.test.ts` when:
- The file uses `mock.module(...)` (R2 — there's no clean fix without changing production code).
- The file is genuinely env-coupled (e.g. `gbrain-home-isolation.test.ts`, `claw-test-cli.test.ts`) — module-load env readers + ESM caching defeat dynamic-import-after-env tricks.
- The file's tests intentionally share state across `it()` boundaries.
Quarantine count cap: 10 (informational). Beyond that, push back on the design.
### Unit test inventory
`bun test` runs all tests without a database. E2E tests skip gracefully when `DATABASE_URL` is not set.
Unit tests and what they cover:
- `test/markdown.test.ts` — frontmatter parsing; `splitBody` sentinel precedence, horizontal-rule preservation, `inferType` wiki subtypes.
- `test/chunkers/recursive.test.ts` — chunking.
- `test/parity.test.ts` — operations contract parity.
- `test/cli.test.ts` — CLI structure.
- `test/config.test.ts` — config redaction.
- `test/files.test.ts` — MIME/hash.
- `test/import-file.test.ts` — import pipeline.
- `test/upgrade.test.ts` — schema migrations.
- `test/file-migration.test.ts` — file migration.
- `test/file-resolver.test.ts` — file resolution.
- `test/import-resume.test.ts` — import checkpoints.
- `test/migrate.test.ts` — migration: v8/v9 helper-btree-index SQL structural assertions; 1000-row wall-clock fixtures guarding the O(n²)→O(n log n) fix; v12/v13 SQL shape; `sqlFor` + `transaction:false` runner semantics; the `max_stalled DEFAULT 1` regression guard; v24 `sqlFor.pglite: ''` no-op assertion.
- `test/bootstrap.test.ts` — bootstrap contract: no-op on fresh install, idempotent across two `initSchema()` calls, no-op on modern brain that already has every probed column, full bootstrap path on a simulated legacy brain, fresh-install regression guard, legacy `links` shape coverage.
- `test/schema-bootstrap-coverage.test.ts` — CI guard. `REQUIRED_BOOTSTRAP_COVERAGE` lists every forward reference in `PGLITE_SCHEMA_SQL`; the test fails loudly if `applyForwardReferenceBootstrap` skips one (extend both arrays when adding a column-with-index to the embedded schema blob). Also parses `src/core/migrate.ts` source text for every `ALTER TABLE ... ADD COLUMN` (top-level `sql:`, `sqlFor.{postgres,pglite}` overrides, AND handler-body `engine.runMigration(N, \`ALTER TABLE ...\`)`) and asserts each (table, column) pair is covered by the bootstrap OR by the schema blob's CREATE TABLE bodies — catching the column-only forward-reference class (e.g. `sources.archived`, `oauth_clients.source_id`) that a CREATE INDEX parser alone can't see. `parseBaseTableColumns` strips SQL line + block comments before identifying column names so commented-out lines don't hide adjacent columns.
- `test/helpers/schema-diff.ts` + `test/helpers/schema-diff.test.ts` + `test/e2e/schema-drift.test.ts` — cross-engine schema parity gate. Helper exports pure `snapshotSchema(query)` / `diffSnapshots(pg, pglite, opts)` / `formatDiffForFailure(diff)` / `isCleanDiff(diff)` over a four-tuple per column (`data_type`, `udt_name`, `is_nullable`, `column_default`). E2E test spins up fresh PGLite + Postgres, runs `engine.initSchema()` on each, snapshots `information_schema.columns`, then diffs. 2-table allowlist (`files`, `file_migration_ledger`) — every other Postgres table must reach PGLite via `PGLITE_SCHEMA_SQL` or a migration's `sqlFor.pglite` branch. Sentinels for `oauth_clients`, `mcp_request_log`, `access_tokens`, `eval_candidates` give tighter blame messages. Skips without `DATABASE_URL`. Wired into `scripts/e2e-test-map.ts` so changes to `src/schema.sql`, `src/core/pglite-schema.ts`, or `src/core/migrate.ts` trigger it. The failure message names every drift with a paste-ready hint pointing at `src/core/pglite-schema.ts`.
- `test/setup-branching.test.ts` — setup flow.
- `test/slug-validation.test.ts` — slug validation.
- `test/storage.test.ts` — storage backends.
- `test/supabase-admin.test.ts` — Supabase admin.
- `test/yaml-lite.test.ts` — YAML parsing.
- `test/check-update.test.ts` — version check + update CLI.
- `test/pglite-engine.test.ts` — PGLite engine, all BrainEngine methods including `addLinksBatch` / `addTimelineEntriesBatch` (empty batch, missing optionals, within-batch dedup via ON CONFLICT, missing-slug rows dropped by JOIN, half-existing batch, batch of 100) plus `connect()` error-wrap assertion (original error nested, #223 link in message, lock released).
- `test/engine-factory.test.ts` — engine factory + dynamic imports.
- `test/integrations.test.ts` — recipe parsing, CLI routing, recipe validation.
- `test/publish.test.ts` — content stripping, encryption, password generation, HTML output.
- `test/backlinks.test.ts` — entity extraction, back-link detection, timeline entry generation.
- `test/lint.test.ts` — LLM artifact detection, code fence stripping, frontmatter validation.
- `test/report.test.ts` — report format, directory structure.
- `test/skills-conformance.test.ts` — skill frontmatter + required sections validation.
- `test/resolver.test.ts` — RESOLVER.md coverage, routing validation; round-trip that every quoted RESOLVER.md trigger matches a frontmatter `triggers:` entry in the target skill, and every `name="<word>"` reference in any SKILL.md resolves to a declared op in `src/core/operations.ts` or a Minions handler in `PROTECTED_JOB_NAMES`.
- `test/search.test.ts` — RRF normalization, compiled truth boost, cosine similarity, dedup key.
- `test/sql-ranking.test.ts` — source-boost helpers: longest-prefix-match in SQL CASE, `detail=high` temporal-bypass, three-meta-char LIKE escape (`%`, `_`, `\`), single-quote SQL-literal doubling, env override parsing for `GBRAIN_SOURCE_BOOST` + `GBRAIN_SEARCH_EXCLUDE`, `resolveBoostMap` / `resolveHardExcludes` merge semantics.
- `test/dedup.test.ts` — source-aware dedup, compiled truth guarantee, layer interactions.
- `test/intent.test.ts` — query intent classification: entity/temporal/event/general.
- `test/eval.test.ts` — retrieval metrics: `precisionAtK`, `recallAtK`, `mrr`, `ndcgAtK`, `parseQrels`.
- `test/check-resolvable.test.ts` — resolver reachability, MECE overlap, gap detection, proximity-based DRY detection, `extractDelegationTargets` coverage.
- `test/dry-fix.test.ts` — auto-fix: three shape-aware expander pure-function tests; five guards (working-tree-dirty, no-git-backup, inside-code-fence, already-delegated within 40 lines, ambiguous-multi-match, block-is-callout).
- `test/doctor-fix.test.ts``gbrain doctor --fix` CLI integration: dry-run preview, apply path, JSON output shape.
- `test/backoff.test.ts` — load-aware throttling, concurrency limits, active hours.
- `test/fail-improve.test.ts` — deterministic/LLM cascade, JSONL logging, test generation, rotation.
- `test/transcription.test.ts` — provider detection, format validation, API key errors.
- `test/enrichment-service.test.ts` — entity slugification, extraction, tier escalation.
- `test/data-research.test.ts` — recipe validation, MRR/ARR extraction, dedup, tracker parsing, HTML stripping.
- `test/minions.test.ts` — Minions job queue: CRUD, state machine, backoff, stall detection, dependencies, worker lifecycle, lock management, claim mechanics, depth/child-cap, timeouts, cascade kill, idempotency, `child_done` inbox, attachments, removeOnComplete/Fail, `max_stalled` clamp/default/plumbing coverage.
- `test/extract.test.ts` — link extraction, timeline extraction, frontmatter parsing, directory type inference.
- `test/extract-db.test.ts``gbrain extract --source db`: typed link inference, idempotency, `--type` filter, `--dry-run` JSON output.
- `test/extract-fs.test.ts``gbrain extract --source fs`: first-run inserts + second-run reports zero, dry-run dedups candidates across files, second-run perf regression guard for the N+1 dedup bug.
- `test/link-extraction.test.ts` — canonical `extractEntityRefs` both formats, `extractPageLinks` dedup, `inferLinkType` heuristics, `parseTimelineEntries` date variants, `isAutoLinkEnabled` config.
- `test/graph-query.test.ts` — direction in/out/both, type filter, indented tree output.
- `test/features.test.ts` — feature scanning, brain_score calculation, CLI routing, persistence.
- `test/file-upload-security.test.ts` — symlink traversal, cwd confinement, slug + filename allowlists, remote vs local trust.
- `test/query-sanitization.test.ts` — prompt-injection stripping, output sanitization, structural boundary.
- `test/search-limit.test.ts``clampSearchLimit` default/cap behavior across `list_pages` and `get_ingest_log`.
- `test/repair-jsonb.test.ts` — JSONB repair: TARGETS list, idempotency, engine-awareness.
- `test/migrations-v0_12_2.test.ts` — JSONB-repair orchestrator phases: schema → repair → verify → record.
- `test/orphans.test.ts` — orphans command: detection, pseudo filtering, text/json/count outputs, MCP op.
- `test/postgres-engine.test.ts``statement_timeout` scoping: `sql.begin` + `SET LOCAL` shape, source-level grep guardrail against a reintroduced bare `SET statement_timeout`.
- `test/sync.test.ts` — sync logic + regression guard asserting top-level `engine.transaction` is not called.
- `test/sync-concurrency.test.ts``autoConcurrency()` thresholds + PGLite-forces-serial + explicit-override clamping; `shouldRunParallel()` explicit-bypasses-floor contract; `parseWorkers()` validation rejecting `'0'`/`'-3'`/`'foo'`/`'1.5'`/trailing chars.
- `test/sync-parallel.test.ts` — PGLite-routed coverage of the bookmark gate under concurrency, head-drift gate, vanished-file failure capture, PGLite-stays-serial, and the `gbrain-sync` writer-lock contract.
- `test/sync-failures.test.ts``classifyErrorCode` regex coverage for all 12 codes against literal production message strings from `markdown.ts` and `import-file.ts`; `summarizeFailuresByCode` sort + pre-classified-honor; `recordSyncFailures` code-field persistence; `acknowledgeSyncFailures` `AcknowledgeResult` shape + backfill on legacy entries.
- `test/doctor.test.ts` — doctor command; assertions that `jsonb_integrity` scans the four JSONB write sites and `markdown_body_completeness` is present.
- `test/utils.test.ts` — shared SQL utilities + `tryParseEmbedding` null-return and single-warn semantics.
- `test/build-llms.test.ts``llms.txt`/`llms-full.txt` generator: path resolution, idempotence, spec shape, regen-drift guard, content contract, AGENTS.md install-path mirror, size-budget enforcement.
- `test/oauth.test.ts` — OAuth 2.1 provider: register, getClient, `client_credentials` grant exchange, `authorization_code` flow with PKCE challenge/verifier, refresh token rotation, `verifyAccessToken` with both OAuth + legacy `access_tokens` fallback, `revokeToken`, `sweepExpiredTokens`; contract test asserting `scope` + `localOnly` annotations on all operations; `coerceTimestamp` unit cases (null/undefined/string/number/throw-on-NaN); NULL-`expires_at`-as-expired contract for both refresh + access token paths; cascade-delete contract asserting `revoke-client` purges `oauth_tokens` + `oauth_codes` via FK CASCADE; cross-client isolation (wrong-client attempt MUST reject AND rightful owner MUST still succeed atomically afterward); empty-string `redirect_uri` bypass guard; PKCE DCR public-client gate (`token_endpoint_auth_method: "none"` returns no `client_secret`, default `client_secret_post` clients get the one-time-reveal secret, `getClient` NULL→undefined normalization, full PKCE `/authorize``/token` round-trip against a public client).
- `test/mcp-dispatch-summarize.test.ts``summarizeMcpParams` invariants: declared-keys allow-list intersection, attacker-key-name leak guard (unknown keys counted not named), 1KB byte bucketing for size-probe defense, missing op falls through to fully-redacted shape, declared-keys sorted for deterministic output.
- `test/trust-boundary-contract.test.ts` — fail-closed trust semantics under cast bypass: `ctx.remote === undefined` treated as remote/untrusted at every flipped call site; `as any` and `Partial<>` spreads can't downgrade trust by accident.
- `test/check-resolvable-cli.test.ts` — CLI wrapper: exit codes, JSON envelope shape, AGENTS.md fallback chain.
- `test/regression-v0_16_4.test.ts``findRepoRoot` regression guard, hermetic startDir parameterization.
- `test/repo-root.test.ts``findRepoRoot` walk semantics + default-arg parity; the 4-tier `autoDetectSkillsDir` fallback chain (`$OPENCLAW_WORKSPACE``~/.openclaw/workspace` → repo-root → `./skills`); RESOLVER.md/AGENTS.md filename precedence; explicit-env-wins-over-repo-root; tier-0 `$GBRAIN_SKILLS_DIR` valid/invalid/precedence-over-`OPENCLAW_WORKSPACE`; the install-path walk in `autoDetectSkillsDirReadOnly`; no-drift on primary success; `AUTO_DETECT_HINT` + `AUTO_DETECT_HINT_READ_ONLY` content; regression guard asserting the shared `autoDetectSkillsDir` MUST NEVER return `'install_path'` source (how the read-path/write-path split stays safe).
- `test/resolver-merge.test.ts` — multi-file resolver merge: `findAllResolverFiles` empty / RESOLVER.md-only / AGENTS.md-only / both-present (RESOLVER.md first); `checkResolvable` merge semantics across `skills/RESOLVER.md` + `../AGENTS.md` for the OpenClaw layout where the skillpack ships a thin RESOLVER.md and the real dispatcher lives at the workspace root; dedup by `skillPath` (first occurrence wins); AGENTS.md-at-workspace-root works alone.
- `test/filing-audit.test.ts` — filing audit: `writes_pages` / `writes_to` frontmatter, filing-rules JSON validation.
- `test/skill-brain-first.test.ts` — shared frontmatter parser; `analyzeSkillBrainFirst` compliance ladder across 9 fixtures under `test/fixtures/brain-first-skills/` (compliant-callout, compliant-phase, compliant-position, exempt-frontmatter, missing-brain-first, multi-pattern, negation-prose, no-external, typo-frontmatter); offset helpers; external-lookup regex shape; audit snapshot+diff transition logic; `FORMERLY_HARDCODED_EXEMPT` regression absorption.
- `test/routing-eval.test.ts` — fixture parsing, structural routing, `ambiguous_with`, Haiku tie-break layer.
- `test/skill-manifest.test.ts` — skill manifest parser: drift detection, managed-block markers.
- `test/skillify-scaffold.test.ts``gbrain skillify scaffold` stubs: SKILL.md, script, tests, routing-eval fixtures.
- `test/skillpack-install.test.ts``gbrain skillpack install` managed-block install / update / no-clobber semantics.
- `test/skillpack-sync-guard.test.ts` — sync-guard: bundled skills stay byte-identical to `skills/` source.
- `test/http-transport.test.ts` — HTTP transport: bearer auth + missing/no-Bearer/unknown/revoked + `/health` bypass; dispatch.ts round-trip; invalid_params; application/json response shape (not SSE); CORS default-deny + allowlist; body cap on Content-Length AND chunked; two-bucket rate limit (refill, exhaust+Retry-After, LRU eviction, TTL prune, pre-auth IP fires before DB); `mcp_request_log` audit on success + auth_failed.
- `test/restart-sweep.test.ts``recipes/restart-sweep.md` inlined script: sentinel-anchored fenced-block extraction with salted tmp filenames to bypass ESM cache; constructor-time env reads (proves no module-load snapshot); idempotency layer load/save/atomic-tmp-rename/corrupt-JSON-recovery/30-day-prune; `(sessionKey, lastAlertedAt)` cooldown gate with 6h threshold; AGGRESSIVE-gate two-state tests; execFile argv shape proving shell metachars in `OPENCLAW_TELEGRAM_GROUP` cannot reach `/bin/sh`; real-`\n`-not-literal alert formatting; `GBRAIN_HOME` state path override.
- `test/eval-longmemeval.test.ts` — LongMemEval harness, hermetic with no `DATABASE_URL` and no API keys: PGLite create + reset over runtime-enumerated `pg_tables`, infrastructure-table preservation across resets, JSONL question parsing, retrieval-only and answer-gen modes via stubbed `ThinkLLMClient`, `--limit` cutoff, `--keyword-only` vs hybrid, default `--expansion=off` behavior, perf gate (p50 < 30ms / p99 < 50ms warm reset+import+search on Apple Silicon), `--help` works without a configured brain, fixture round-trip via `test/fixtures/longmemeval-mini.jsonl`.
- `test/longmemeval-sanitize.test.ts` — sanitization parity pinning that `INJECTION_PATTERNS` from `src/core/think/sanitize.ts` is the single source of truth (adding a pattern there must cover both `<take>` framing and `<chat_session>` framing, no per-surface regex drift).
- `test/openai-compat-multimodal.test.ts` — gateway's openai-compatible multimodal path: happy-path single + multi-input embedding, unauthenticated proxy mode, dimension-mismatch guard (throws `AIConfigError` with model id + observed + expected pre-storage), default-dim fallback when recipe declares `default_dims`, HTTP 401 / 400 / malformed-JSON / non-array error paths, regression that the existing Voyage `/multimodalembeddings` recipe still routes through its dedicated path. Hermetic via the `__setEmbedTransportForTests` seam.
- `test/serve-stdio-lifecycle.test.ts``MCP_STDIO=1` env guard: stdin EOF does NOT trigger shutdown when the env is set, SIGTERM still does (guard scope is correct), unset env preserves the CLI lifecycle. Exercises the `ServeOptions.mcpStdio?: boolean` test seam directly so tests don't mutate `process.env`.
### E2E test inventory
E2E tests live in `test/e2e/` and run against real Postgres+pgvector (require `DATABASE_URL`), except where noted as PGLite in-memory (no `DATABASE_URL` needed).
- `bun run test:e2e` runs Tier 1 (mechanical, all operations, no API keys). Includes dedicated cases for the postgres-engine `addLinksBatch` / `addTimelineEntriesBatch` bind path — postgres-js's `unnest()` binding is structurally different from PGLite's and gets its own coverage.
- `test/e2e/search-quality.test.ts` — search quality against PGLite (no API keys, in-memory).
- `test/e2e/graph-quality.test.ts` — knowledge graph pipeline (auto-link via put_page, reconciliation, traversePaths) against PGLite in-memory.
- `test/e2e/postgres-jsonb.test.ts` — round-trips all 5 JSONB write sites (`pages.frontmatter`, `raw_data.data`, `ingest_log.pages_updated`, `files.metadata`, `page_versions.frontmatter`) against real Postgres and asserts `jsonb_typeof='object'` plus `->>'key'` returns the expected scalar. Guards against the double-encode bug.
- `test/e2e/integrity-batch.test.ts` — parity for `scanIntegrity`'s batch-load fast path vs sequential. Cases (dedup, hits, validate, topPages) seed a fixture and assert both paths return identical results. Dedup case uses raw SQL via `getConn().unsafe()` to seed a `(test-source-2, people/alice)` row alongside the default-source row, since `engine.putPage` doesn't take a `source_id`. Pins multi-source overcounting; the "multi-source duplicate slugs scan once" case expects both batch + sequential paths to report 2.
- `test/e2e/jsonb-roundtrip.test.ts` — companion regression against the 4 doctor-scanned JSONB sites. Assertion-level overlap with `postgres-jsonb.test.ts` is intentional defense-in-depth: if doctor's scan surface drifts from the actual write surface, one of these tests catches it.
- `test/e2e/sync.test.ts``--skip-failed` failure-loop test alongside happy-path tests: broken file → `performSync` returns `blocked_by_failures` with grouped breakdown → `performSync({skipFailed: true})` advances bookmark and returns `AcknowledgeResult` with code summary → second broken file → second cycle. Saves and restores the user's real `~/.gbrain/sync-failures.jsonl` so the test is hermetic. Asserts bookmark gating, JSONL state, dedup across paths, summary aggregation, and the literal doctor-rendering string format.
- `test/e2e/upgrade.test.ts` — check-update against real GitHub API (network required).
- `test/e2e/minions-shell-pglite.test.ts` — PGLite `--follow` inline shell-job path (in-memory, no `DATABASE_URL` required) — the path the minion-orchestrator skill documents for dev use.
- `test/e2e/openclaw-reference-compat.test.ts``check-resolvable` + `skillpack install` against a minimal AGENTS.md workspace fixture (`test/fixtures/openclaw-reference-minimal/`), regression guard for the OpenClaw deployment shape.
- `test/e2e/search-swamp.test.ts` — reproduces the source-swamp case. Seeds a curated `originals/talks/article-outline-fat-code` page against two `<fork>/chat/` pages stuffed with the same multi-word phrase. Asserts the article wins keyword AND vector ranking, that `detail=high` lets the chat swamp re-surface, and that `source_id` passes through the two-stage CTE intact. PGLite in-memory.
- `test/e2e/search-exclude.test.ts``test/` + `archive/` pages hidden by default, `include_slug_prefixes` opts back in, caller-supplied `exclude_slug_prefixes` adds to defaults. Both keyword and vector search paths.
- `test/e2e/engine-parity.test.ts` — Postgres ↔ PGLite top-result and result-set parity for `searchKeyword` + `searchVector` (Postgres ranks pages then picks best chunk while PGLite returns chunks directly, so the source-boost behavior needs parity coverage). Skips without `DATABASE_URL`.
- `test/e2e/postgres-bootstrap.test.ts` — exercises `PostgresEngine.initSchema()` directly against a fresh real Postgres database. Asserts the bootstrap path is no-op on fresh installs and that SCHEMA_SQL replays cleanly through the engine path (not via the standalone `db.initSchema` from `src/core/db.ts`).
- `test/e2e/http-transport.test.ts``gbrain serve --http` end-to-end against real Postgres: bearer auth round-trip, `last_used_at` SQL-level debounce, `mcp_request_log` row insertion on success and auth_failed paths, `/health` DB-down → 503 (DB-probing health check), and the dispatch round-trip with a real operation. Skips without `DATABASE_URL`.
- `test/e2e/serve-http-oauth.test.ts` — real-Postgres E2E against `gbrain serve --http` with full OAuth 2.1. Spawns a subprocess server, registers a client via the CLI, mints `client_credentials` tokens, exercises the `/mcp` JSON-RPC pipeline. Real DCR `/register` HTTP-level response-shape test (asserts `typeof body.client_id_issued_at === 'number'` over the wire, RFC 7591 §3.2.1); real CLI subprocess test for `revoke-client` (registers → mints token → revokes via `execSync` → asserts token rejected at `/mcp` → asserts re-run exits 1); server fixture flips on `--enable-dcr` so `/register` is reachable. **bun execSync env-inheritance contract:** bun's `execSync` does NOT inherit env mutations done via `process.env.X = ...`, only OS-level env from before bun started. helpers.ts loads `.env.testing` and sets `DATABASE_URL` via `process.env` mutation, which is invisible to subprocesses unless `env: { ...process.env }` is passed explicitly — every subprocess call in this file passes `env: { ...process.env }`. Reference fix for the same failure mode in sibling sync/cycle/dream/claw-test E2Es. `afterAll` cleanup is guarded on `clientId` (won't throw if `beforeAll` failed before registration); cleanup errors surface to stderr without throwing so real test failures aren't masked. Also covers the trust-boundary fix: an HTTP MCP `submit_job` for `name: "shell"` MUST reject with a permission error (request handler sets `remote: true` and `submit_job`'s protected-name guard fires), and the same guard rejects subagent submission. Skips without `DATABASE_URL`.
- `test/e2e/sync-parallel.test.ts``DATABASE_URL`-gated. 60-file Postgres sync at concurrency=4 imports all + no connection leak (probes `pg_stat_activity` before/after to confirm worker engines disconnected). 120-file serial-vs-parallel benchmark prints `SYNC_PARALLEL_BENCH N files | serial=Xms | parallel(4)=Yms | speedup=Zx`. Asserts parallel ≤ serial × 1.5 (CI-noise tolerant; not a strict speedup gate).
- `test/e2e/multi-source-bug-class.test.ts` — PGLite in-memory regression suite pinning every multi-source bug site: `listAllPageRefs` ordering by `(source_id, slug)`, `getPage` with sourceId picks the right `(source, slug)` row, `extract-takes` processes both overlapping `people/alice` rows independently, `listPages` filters correctly with `PageFilters.sourceId`, `addLinksBatch` with `from/to_source_id` targets the right rows, `validateSourceId` rejects path traversal, reverse-write disk layout uses `brainDir/.sources/<id>/<slug>.md` for non-default sources. No `DATABASE_URL` needed. Wired into `scripts/e2e-test-map.ts` so changes to extract-takes / patterns / synthesize / embed / extract / migrate-engine auto-trigger it.
- `test/e2e/source-isolation-pglite.test.ts` — PGLite in-memory regression suite pinning the source-isolation seal at two layers. Engine layer: `searchKeyword` / `searchVector` / `searchKeywordChunks` / `listPages` / `getPage` / `traverseGraph` / `traversePaths` apply `sourceId` (scalar fast path) and `sourceIds` (array path) correctly across both engines. Op-handler layer: routes through `sourceScopeOpts(ctx)` so a `read+write`-scoped OAuth client bound to `--source dept-x` cannot see rows from neighboring sources via `search`, `query`, `list_pages`, `get_page`, or `find_experts`. Covers both `ctx.sourceId` (single-source clients) and `ctx.auth.allowedSources` (federated_read clients) precedence; federated array wins over scalar wins over nothing. No `DATABASE_URL` needed.
- `test/e2e/skill-brain-first.test.ts` — doctor reports `skill_brain_first` check with structured issues; `--fix --dry-run` previews insertion without writing; `--fix` applies the canonical Convention callout idempotently; `brain_first: exempt` frontmatter resolves the warn; `brain_first_typo` surfaces a paste-ready hint; audit JSONL records `detected` / `resolved` / `fixed` transitions; stable brain emits 0 audit lines/run.
- Tier 2 (`test/e2e/skills.test.ts`) requires OpenClaw + API keys, runs nightly in CI.
- If `.env.testing` doesn't exist in this directory, check sibling worktrees: `find ../ -maxdepth 2 -name .env.testing -print -quit` and copy it here if found.
- **Run E2E tests without asking permission.** When you want to verify behavior, there's a relevant E2E test, or you're shipping anything covered by an E2E suite — spin up the test DB, run the tests, tear down. Don't ask, don't propose it, don't defer. The lifecycle is short (~2-30s startup, sub-minute tests, instant teardown) and the gate value is high. Skipping with "DATABASE_URL unset" is silent regression, not caution.
### API keys and running ALL tests
ALWAYS source the user's shell profile before running tests:
```bash
source ~/.zshrc 2>/dev/null || true
```
This loads `OPENAI_API_KEY` and `ANTHROPIC_API_KEY`. Without these, Tier 2 tests
skip silently. Do NOT skip Tier 2 tests just because they require API keys — load
the keys and run them.
When asked to "run all E2E tests" or "run tests", that means ALL tiers:
- Tier 1: `bun run test:e2e` (mechanical, sync, upgrade — no API keys needed)
- Tier 2: `test/e2e/skills.test.ts` (requires OpenAI + Anthropic + openclaw CLI)
- Always spin up the test DB, source zshrc, run everything, tear down.
### E2E test DB lifecycle (ALWAYS follow this)
You are responsible for spinning up and tearing down the test Postgres container.
Do not leave containers running after tests. Do not skip E2E tests, do not ask
permission to run them — see the "run without asking" rule above.
1. **Check for `.env.testing`** — if missing, copy from sibling worktree.
Read it to get the DATABASE_URL (it has the port number).
2. **Check if the port is free:**
`docker ps --filter "publish=PORT"` — if another container is on that port,
pick a different port (try 5435, 5436, 5437) and start on that one instead.
3. **Start the test DB:**
```bash
docker run -d --name gbrain-test-pg \
-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=gbrain_test \
-p PORT:5432 pgvector/pgvector:pg16
```
Wait for ready: `docker exec gbrain-test-pg pg_isready -U postgres`
4. **Bootstrap the schema** (required — fresh containers have no `oauth_clients`,
`mcp_request_log`, `pages` etc.; tests like `serve-http-oauth.test.ts` will fail
with `relation "oauth_clients" does not exist` if you skip this):
```bash
DATABASE_URL=postgresql://postgres:postgres@localhost:PORT/gbrain_test \
bun run src/cli.ts doctor --json > /dev/null 2>&1
```
`gbrain doctor` triggers `initSchema()` on first connect, which is the canonical
way to bring a fresh DB to head. `apply-migrations --yes` alone does NOT seed
the base schema — it runs ALTER-style migrations on top of `initSchema`. Tests
that bypass the engine (raw `execSync`-spawned `auth register-client`) hit the
schema directly and need this step to have run first.
5. **Run E2E tests:**
`DATABASE_URL=postgresql://postgres:postgres@localhost:PORT/gbrain_test bun run test:e2e`
6. **Tear down immediately after tests finish (pass or fail):**
`docker stop gbrain-test-pg && docker rm gbrain-test-pg`
Never leave `gbrain-test-pg` running. If you find a stale one from a previous run,
stop and remove it before starting a new one.
-161
View File
@@ -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.
File diff suppressed because one or more lines are too long
-32
View File
@@ -58,38 +58,6 @@ Hybrid search applies a source-factor CASE expression at the SQL layer (lives in
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.
## Named-thing retrieval (per-page pool + title + alias + evidence)
A brain organized around *chosen names* (Mingtang, Hall of Light) needs more than
embedding proximity. Four layers, added after the incident in
[`RETRIEVAL_MAXPOOL_INCIDENT.md`](./RETRIEVAL_MAXPOOL_INCIDENT.md):
- **Per-page max-pool**`searchVector` (both engines) collapses chunk-grain
candidates to the best chunk per page (`DISTINCT ON (slug)`) over the full
candidate set before the user `LIMIT`, via the shared `buildBestPerPagePoolCte`
in `sql-ranking.ts`. The vector side returns N distinct pages by best chunk,
not N chunks that collapse to fewer pages downstream.
- **Title-phrase boost** — when the normalized query is a contiguous token-run
inside `page.title` (or an exact full-title match), a floor-ratio-gated,
bounded multiplier fires (`applyTitleBoost`, `search.title_boost` knob). A
query that is a phrase from the title can't lose to a body chunk by luck.
- **Alias hop** — free-text `aliases:` frontmatter is projected into a
`page_aliases` table (separate from the `slug_aliases` wikilink redirect) and
consulted at query time: a full normalized-query match injects/boosts the
canonical page (`applyAliasHop`). The only layer that bridges true synonyms
with zero surface overlap ("Hall of Light" → the Mingtang page). Backfill
existing pages with `gbrain reindex --aliases`.
- **Evidence contract** — every result carries `evidence`
(`alias_hit | exact_title_match | high_vector_match | keyword_exact |
weak_semantic`) and `create_safety` (`exists | probable | unknown`). An agent
deciding "is this page already here, safe to NOT write a duplicate?" keys off
`create_safety`, not a raw blended score.
The `search` MCP/CLI op is **cheap-hybrid** (vector + keyword + RRF + pool +
title + alias, expansion off); `query` is the full-control variant. NamedThingBench
(`gbrain eval retrieval-quality`) gates these families on every PR. Diagnose a
specific miss with `gbrain search diagnose "<q>" --target <slug>`.
## Intent-aware query rewriting
`src/core/search/intent.ts` classifies queries into `entity`, `temporal`, `event`, or `general`. Each routes through different ranking knobs:
@@ -1,97 +0,0 @@
# Retrieval Incident: a chosen-name page was missed, and the fix
**Status:** Resolved (retrieval-cathedral wave). Supersedes the docs-only RFC in
closed PR #1616 — the diagnosis there was directionally right about the disease
but wrong on several mechanics; this is the corrected record + what shipped.
**Original author:** Garry Tan's OpenClaw. **Severity at the time:** High.
**Related:** [`RETRIEVAL.md`](./RETRIEVAL.md), [`../eval/METRIC_GLOSSARY.md`](../eval/METRIC_GLOSSARY.md).
---
## 1. What happened
The agent was asked to log that Garry "wants to build a Greek amphitheater." It
ran a retrieval for the concept, the canonical concept page (titled "...Indoor
Greek Amphitheater...") did **not** surface with enough confidence to be
recognized as the existing page, and the agent wrote a **duplicate stub** on top
of a fully-developed concept doc. Garry caught it: "It's in the brain. It's the
Hall of Light. Why did you forget?"
The page is *about* a Greek amphitheater — the phrase is in its title and first
sentence. A healthy index returns it at the top. It didn't.
## 2. The disease (the RFC got this right)
The brain is stored by **meaning and chosen name** (Mingtang, Hall of Light) but
was retrieved by **literal embedding proximity to a body chunk**, and the agent's
"is this already here?" decision keyed off a single fuzzy blended score. Three
retrieval gaps plus one contract gap produced the miss.
## 3. Verified ground truth (corrections to the RFC)
These were checked in code during the fix; several change the remedy:
1. **`gbrain search` was keyword-only**, not hybrid — so the RFC's cosine scores
(0.64/0.98) came from the hybrid `query`/MCP path the agent actually hit, not
`gbrain search`. The repro command in the RFC was mislabeled.
2. **`--mode` was never a CLI param** — mode resolves server-side from the
`search.mode` config key, which is why all three "modes" returned identical
results (the flag was silently dropped; `thorough` isn't a real mode).
3. **`hybridSearch` already max-pooled per page at the dedup layer.** So the
per-page max-pool fix's real win is *candidate-set page recall* (the vector
side returned N chunks that could collapse to fewer pages), and it is
necessary-but-not-sufficient: if a page's title chunk scores below a body
chunk on a 2-word query, or falls outside the candidate pool, pooling alone
doesn't rescue it.
4. **Frontmatter `aliases:` was dead to search** — stored in `pages.frontmatter`
JSONB, never consulted. `slug_aliases` is a *slug→slug* wikilink redirect, a
different concept.
## 4. The fix that shipped (four layers + a contract)
| Layer | Fixes | Where |
|---|---|---|
| **Per-page max-pool** (T1) | a page scored by its weakest chunk; vector page-recall | `searchVector` both engines, shared `buildBestPerPagePoolCte` |
| **Title-phrase boost** (T2) | query is a phrase in the title but matched a body chunk | `applyTitleBoost` (reads `page.title`), `title_boost` mode knob |
| **Alias hop** (T3) | true synonyms with zero surface overlap ("Hall of Light" → Mingtang) | `page_aliases` table, `applyAliasHop`, ingest projection + `reindex --aliases` backfill |
| **Evidence contract** (T4) | the agent keyed "don't duplicate" off a fuzzy score | `evidence` + `create_safety` on every result; the agent keys off `create_safety='exists'`, not a threshold |
Plus: `gbrain search "<text>"` is now cheap-hybrid (the obvious verb gives the
good path); `modes/stats/tune` stay subcommands; `--mode` works per-call for
local callers; rank-1 score drift telemetry; and **NamedThingBench**, a CI gate
that hard-gates the families that ARE this incident.
## 5. How to confirm / triage a recurrence
```
# Which layer surfaces (or misses) the target page?
gbrain search diagnose "Greek amphitheater" --target projects/new-greek-theater/concept_v0
# Backfill aliases for existing pages whose frontmatter predates the alias layer:
gbrain reindex --aliases
# Watch retrieval quality over time (a downward avg rank-1 score = regressing):
gbrain search stats --days 30
# The gate that prevents silent reintroduction:
gbrain eval retrieval-quality test/fixtures/retrieval-quality/namedthing.jsonl
```
For a page to be reliably found by its chosen name, give it `aliases:` frontmatter:
```yaml
---
title: The Mingtang — Indoor Greek Amphitheater
aliases:
- Hall of Light
- 明堂
---
```
## 6. The discipline this teaches
A benchmark that scores 97.9 R@5 while production returns a flagship page at 0.64
means the benchmark and the shipped path diverged. NamedThingBench runs the same
families through the real pipeline on every PR, and the evidence contract means
the agent's duplicate-or-not decision is grounded in *why* a page matched, not a
number that was never a calibrated probability.
-143
View File
@@ -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.
-246
View File
@@ -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,54 +0,0 @@
# `gbrain serve``gbrain sync` concurrency (PGLite)
**Short version: on a PGLite brain, stop `gbrain serve` before a large sync.**
## Why
PGLite is a single-writer embedded Postgres (WASM). A running `gbrain serve`
(stdio or HTTP MCP) holds an open PGLite connection on the brain's data
directory. `gbrain sync` needs to write to that same data directory. The two
contend for PGLite's single-writer connection / write-lock — **this is NOT the
`gbrain-sync` advisory lock** (that's a separate, DB-row coordination lock for
two concurrent *syncs*). Confusing the two sends you debugging the wrong surface.
Symptoms of serve↔sync contention on PGLite:
- `gbrain sync` blocks acquiring the PGLite write lock, or makes very slow
progress, while a `gbrain serve` process is alive on the same brain.
- Killing stale `gbrain serve` MCP processes frees the lock and sync proceeds.
## What to do
1. Stop any `gbrain serve` process for this brain before a large sync:
```bash
pkill -f 'gbrain serve' # or stop your MCP client / Claude Desktop / Cursor
gbrain sync --no-pull --no-embed --yes
```
2. Restart `gbrain serve` after the sync completes.
This contention does **not** apply to the Postgres engine — Postgres tolerates
concurrent connections, so `serve` and `sync` can run simultaneously there.
## Diagnosing a sync hang
If a sync wedges (no progress, high CPU), re-run with the per-file begin trace
so the stalling file is named:
```bash
GBRAIN_SYNC_TRACE=1 gbrain sync --no-pull --no-embed --yes
```
The last `[sync] begin import: <path>` line with no following completion is the
file being processed when the hang occurred. Under `--workers >1` / `--all`,
the stuck file is in the set of begin-lines without a matching completion.
If you suspect a schema-pack regex is the cause (a pack with a
catastrophic-backtracking `inference.regex`), complete the sync with the pack
disabled and re-run extraction afterward:
```bash
gbrain sync --no-schema-pack --no-pull --no-embed --yes
```
`gbrain schema lint` flags the classic nested-quantifier ReDoS shapes
(`(a+)+`, `(a*)*`, …) in pack regexes as warnings.
-70
View File
@@ -1,70 +0,0 @@
# Thin-client routing (remote MCP)
On-demand reference (see CLAUDE.md Reference map). Current behavior + invariants
only; release history lives in `CHANGELOG.md` + git.
`gbrain init --mcp-only` (v0.29.2) sets up a thin-client install: no local
brain content, just an OAuth client pointing at a remote `gbrain serve --http`.
v0.29.2/v0.30.0 only refused 9 obvious local-only commands; the other ~25
silently fell through to `connectEngine()` and opened the empty local PGLite,
returning "No results." against a populated remote brain. v0.31.1 fixes the
silent-empty-results bug class for every operation surface.
Key files:
- `src/cli.ts` — Routing seam INSIDE the existing op-dispatch path (CDX-1: no
parallel `src/core/thin-client/` module; routing is a ~80-line conditional
in `runThinClientRouted`). Detects `isThinClient(cfg)` BEFORE `connectEngine`
so thin-client installs never open the empty PGLite. localOnly ops on
thin-client refuse via `refuseThinClient` (with pinpoint hint table
`THIN_CLIENT_REFUSE_HINTS`). Banner via `printIdentityBannerBestEffort`
before each routed call (suppressed by `--quiet`, `GBRAIN_NO_BANNER=1`,
non-TTY default). Exhaustive TS `never` switch on `RemoteMcpError.reason`
for canned, actionable error messages. ENG-2 renderer parity: local-engine
path runs `JSON.parse(JSON.stringify(result))` so renderers see the same
shape on both paths (kills Date/bigint/Buffer drift class).
- `src/core/mcp-client.ts``callRemoteTool(config, toolName, args, opts)`.
Hardened in v0.31.1 (CDX-4): all transport errors normalized to
`RemoteMcpError` via the `toRemoteMcpError` funnel. New `CallRemoteToolOptions
{timeoutMs, signal}`; `buildAbortController` composes external signal with
timeout. New `RemoteMcpErrorReason` stable union, `RemoteMcpErrorDetail.kind`
('timeout' | 'aborted' | 'unreachable') sub-tag, `RemoteMcpErrorDetail.code`
field carrying server-supplied error codes (e.g. `missing_scope`).
`extractToolErrorCode` parses JSON envelopes first, falls back to substring
detection for legacy server messages. `unpackToolResult<T>(res)` unchanged
(parses tool-call JSON content). `_clearMcpClientTokenCache()` test escape.
- `src/core/cli-options.ts``parseGlobalFlags` adds `--timeout=Ns` (accepts
`30s`, `2m`, `500ms`, plain ms). Default `null` = per-command default (30s
for most ops, 180s for `think`). `parseTimeout(s)` exported helper.
- `src/core/doctor-remote.ts``gbrain remote doctor` adds the
`oauth_client_scopes_probe` check (CDX-5). Probes the read tier via
`get_brain_identity` and admin tier via `get_health`; reports per-tier
status with pinpoint remediation when admin is missing. `buildScopeCheck`
+ `ScopeProbeResult` exported for test access. Skippable via
`GBRAIN_DOCTOR_SKIP_SCOPE_PROBE=1` for fixtures that mock /mcp at JSON-RPC
initialize level only (MCP SDK Client hangs on shape mismatch).
- `src/core/ssrf-validate.ts` (v0.36 Commit 0) — DNS-rebinding-defended URL validation. `validateAndResolveUrl(url)` resolves the hostname via `dns.lookup({all: true, family: 0})`, checks EVERY A AND AAAA record against the internal-IP deny list, returns the resolved IP so callers fetch by IP (defeats DNS rebinding: validation IP === fetch IP). `fetchWithSSRFGuard(url, opts)` does redirect-aware fetching with per-hop re-validation, max 3 hops by default. Reusable across all URL-fetching features. Test seam `__setDnsLookupForTests` for hermetic tests.
- `src/core/search/query-intent.ts` extension (v0.36 cross-modal wave) — new `suggestedModality: 'text' | 'image' | 'both'` axis on `QuerySuggestions`. Module-scope `CROSS_MODAL_PATTERNS` regex array (compiles once at module load). `isAmbiguousModalityQuery(query)` heuristic gate fires when a visual noun + reference marker combination indicates genuinely ambiguous routing — used by the Commit 4 LLM tie-break to bound LLM calls to <1% of queries.
- `src/core/search/mode.ts` extension (v0.36 cross-modal wave) — `ModeBundle` extended with 7 cross-modal knobs: `cross_modal_both_text_weight` / `cross_modal_both_image_weight` (D6 weighted RRF for `'both'` mode, defaults 0.6/0.4), `image_query_text_refinement_weight` / `image_query_image_refinement_weight` (D13 hybrid intersect for `searchByImage` query refinement, defaults 0.4/0.6), `unified_multimodal` + `unified_multimodal_only` (Phase 3 unified column routing flags), `cross_modal_llm_intent` (Commit 4 opt-in escalation). `SEARCH_MODE_CONFIG_KEYS` extended with 7 corresponding config keys. `KNOBS_HASH_VERSION` bumped 2→3 (D2 — closes the silent cache-hit class where a cached text-mode result could leak to an image-mode caller).
- `src/core/search/hybrid.ts` extension (v0.36 cross-modal wave) — cross-modal routing branch at the embed step. Resolves `effectiveModality` from per-call `opts.crossModal` (normalized: literal `'auto'` → undefined per D22-1) → `suggestions.suggestedModality``'text'` default. Image route: `embedQueryMultimodal` + `searchVector({embeddingColumn: 'embedding_image'})`, skip expansion + keyword (D9 mode-bundle override). 'both' route: parallel text + image vector searches merged via `rrfFusionWeighted` with `effectiveRrfK(baseRrfK, weight)` from the configured cross-modal weights. Phase 3 unified routing fires when `cfg.search.unified_multimodal === true` — bypasses dual-column branching, runs `embedQueryMultimodal` + `searchVector({embeddingColumn: 'embedding_multimodal'})`, D8 fail-open on zero rows + not strict-mode falls through to dual-column. Commit 4 LLM escalation fires only when (no explicit per-call opt) AND (regex returned 'text') AND (`cfg.search.cross_modal.llm_intent` is true) AND (`isAmbiguousModalityQuery` returns true). Fail-open on every error.
- `src/core/search/image-loader.ts` (v0.36 Phase 2) — `loadImageInput(input, opts)` accepts local path, `data:` URI, or `http(s)://` URL. Magic-byte sniff for PNG/JPEG/WebP. Hard size cap (default 10 MB, configurable via `search.image_query.max_bytes`). For URLs: routes through `fetchWithSSRFGuard` so DNS rebinding + redirect chains are defeated. Pre-flight Content-Length check + post-fetch size guard for lying servers. `ImageLoadError` with discriminated `code` (INVALID_FORMAT / OVERSIZED / INVALID_URL / FETCH_FAILED / TIMEOUT / SSRF_BLOCKED / NOT_FOUND).
- `src/core/search/by-image.ts` (v0.36 Phase 2) — `searchByImage(engine, input, opts)`. Always runs image branch (`embedQueryMultimodalImage` + `searchVector(embedding_image)`). D13 hybrid intersect: when caller provides optional `query`, runs parallel text branch via `embedQueryMultimodal(query)` and merges via `rrfFusionWeighted` with weights from resolved mode. Phase 3 widens to unified column once `search.unified_multimodal=true` (transparently upgrades the retrieval quality post-reindex).
- `src/core/spend-log.ts` (v0.36 Phase 2 D23-#6) — per-OAuth-client paid-API spend tracking against the `mcp_spend_log` table (migration v74). `checkBudget(engine, clientId, capCents)` is the pre-flight gate; throws `BudgetExceededError` when today's spend has hit the cap. `recordSpend(engine, entry)` is best-effort post-call. UTC day-aligned aggregation so caps roll over deterministically regardless of server timezone. Local CLI callers (no clientId) bypass the gate. Pre-v0.36 brains without the table fail open to spend=0. `VOYAGE_MULTIMODAL_3_PER_IMAGE_CENTS` = 0.12 cents per image embed.
- `src/core/search/llm-intent.ts` (v0.36 Commit 4) — opt-in LLM tie-break. `classifyModalityWithLLM(query, fallback)` routes through `gateway.chat()` with a fixed single-word-output system prompt. 1s timeout via AbortController. `parseModality(raw, fallback)` is the pure parser — tolerates trailing punctuation + casing. Fail-open on every error (gateway unavailable, timeout, parse failure, unrecognized output) — returns fallback so a misbehaving LLM can never break search. Cost-bounded by the ambiguity heuristic in `query-intent.ts` (fires <1% of queries when on).
- `src/commands/reindex-multimodal.ts` (v0.36 Phase 3) — `gbrain reindex --multimodal [--limit N] [--dry-run] [--cost-estimate] [--no-embed] [--yes] [--json]`. Walks `content_chunks WHERE embedding_multimodal IS NULL`, batches via `embedMultimodalSafe` (Commit 0 partial-failure-aware), persists. D7 lock acquisition via `tryAcquireDbLock('gbrain-reindex-multimodal', 360min)`. Cost prompt + 10s Ctrl-C grace window in TTY. `GBRAIN_NO_REEMBED=1` bypass. Checkpoint at `~/.gbrain/reindex-multimodal-checkpoint.json` for resume. D23-#2 auto-flip prompt at coverage=100% completion (TTY: interactive; non-TTY: stderr hint with paste-ready command).
- `src/core/backfill-registry.ts` extension (v0.36) — new `modality` backfill kind. SQL filter requires `chunk_source='image_asset'` AND `embedding_image IS NOT NULL` AND `(modality IS NULL OR modality != 'image')`. D22-7 defensive guard: never flag a non-image chunk that happens to have `embedding_image` populated. Idempotent — second run finds zero rows.
- `src/core/migrate.ts` v74 (`mcp_spend_log`) + v75 (`embedding_multimodal_column`) — Phase 2 spend-log table + Phase 3 unified column ALTER. v75 is column-only (no HNSW index — deferred to post-reindex per pgvector best practice). v74 uses BTREE on `(client_id, created_at)` + `(token_name, created_at)``date_trunc('day', TIMESTAMPTZ)` is NOT IMMUTABLE so can't appear in index expressions; range scan on created_at covers the per-day rollup query.
- `src/core/operations.ts``get_brain_identity` op (read scope, no params,
banner-only): cheap counter packet `{version, engine, page_count,
chunk_count, last_sync_iso}` for the thin-client identity banner. Reuses
`engine.getStats()`; banner's 60s client-side TTL bounds frequency to
≤1/60s per CLI process (well below the Fly.io health-check cadence that
motivated the original `getStats` cost warning).
- `src/commands/{salience,anomalies,graph-query,think}.ts` — Per-command
thin-client routing branches. These commands bypass the operation-layer
dispatch in cli.ts (call `engine.foo()` directly), so each gets its own
`if (isThinClient(cfg)) { callRemoteTool(...) }` branch that maps CLI flags
to op params. `think` is a special case: the server's `think` op
intentionally disables `--save`/`--take` for remote callers
(operations.ts:1103-1135 trust-boundary gate); thin-client `think` warns
loudly when those flags are set.
-3
View File
@@ -398,6 +398,3 @@ simultaneously — that's by design.
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.).
-177
View File
@@ -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`
-28
View File
@@ -10,34 +10,6 @@ change automatically.
this mismatch and refuse to silently proceed. This doc is the recipe
they point at.
## Same-dimension model swaps (v0.41.31.0 — automatic)
If you switch to a different model at the **same** dimension count
(e.g. one 1536-dim provider to another, or a re-tuned model that keeps
its width), the column type doesn't change, so no `ALTER`/wipe recipe
is needed. As of v0.41.31.0, gbrain stamps an embedding-provenance
signature (`<provider:model>:<dims>`) onto each page when its chunks are
embedded. After you point the config at the new model, the stored
signatures differ from the current one, and `gbrain embed --stale`
re-embeds exactly those pages:
```bash
# After switching to the new same-dim model in your config:
gbrain embed --stale # re-embeds signature-drifted pages
gbrain embed --stale --dry-run # preview the count without re-embedding
```
Under federated_v2, the same drift is picked up by the per-source
`embed-backfill` jobs that `gbrain sync --all` enqueues (capped
`$X/source/24h`). **Grandfather:** pages embedded before v0.41.31.0
carry a NULL signature and are NEVER flagged stale, so upgrading to
v0.41.31.0 does NOT trigger a whole-corpus re-embed. Signatures only
get stamped going forward.
A **dimension** change still requires the wipe-and-reinit (PGLite) or
column-alter (Postgres) recipe below — the on-disk `vector(N)` width
genuinely has to change.
## Why we don't do this automatically
Switching dimensions requires:
-2
View File
@@ -25,5 +25,3 @@ None of those are novel ideas. The contribution is shipping all of them together
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.
-106
View File
@@ -8,112 +8,6 @@ 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
-52
View File
@@ -38,40 +38,6 @@ Every metric `gbrain eval *` and `gbrain search stats` reports has a plain-Engli
**Range:** 0..1, higher is better. nDCG@10 above 0.65 is the common "ship it" threshold for hybrid retrieval on technical corpora.
## Retrieval-Quality / Evidence Metrics (NamedThingBench)
### Hit rate at 1 (Hit@1)
**Key:** `hit@1`
**Plain English:** Fraction of queries where the right page is the very first result. NamedThingBench hard-gates title-substring Hit@1 >= 0.95 and alias Hit@1 >= 0.98 — a query that is a page's name or title phrase should land it at rank 1, not "somewhere in the top 10".
**Range:** 0..1, higher is better.
### Hit rate at 3 (Hit@3)
**Key:** `hit@3`
**Plain English:** Fraction of queries where the right page is in the top 3 results. NamedThingBench requires the multi-chunk-dilution family to hit 1.0 — a page with one strong chunk among many weak ones must never be buried.
**Range:** 0..1, higher is better.
### Average rank-1 match score
**Key:** `avg_rank1_score`
**Plain English:** The mean base (pre-boost) retrieval score of the TOP result across recent searches, from `gbrain search stats`. It is NOT a labeled accuracy number — it is a drift signal: if this trends DOWN over time, retrieval quality is regressing (the early warning that would have caught the duplicate-page incident before a human did).
**Range:** 0..1. Watch the trend, not the absolute value; pair with the <0.6 / 0.6-0.85 / >=0.85 bucket counts for shape.
### Create-safety hint (evidence contract)
**Key:** `create_safety`
**Plain English:** A result's answer to "is this page already in the brain — safe to NOT write a new one?" Derived from the strongest evidence, NOT a raw score: exists (alias_hit / exact_title_match / high_vector_match — do not duplicate), probable (solid keyword match — prefer updating), unknown (weak match — look closer). An agent keys its don't-duplicate decision off this, which is what prevents the incident's duplicate-stub class.
**Range:** enum: exists | probable | unknown
## Set-Similarity / Stability Metrics
### Jaccard similarity at k (set Jaccard @k)
@@ -150,24 +116,6 @@ Every metric `gbrain eval *` and `gbrain search stats` reports has a plain-Engli
**Range:** 0..unbounded. Warm-cache hits should be <50ms; tokenmax with expansion can exceed 200ms due to the Haiku call.
## Result-Sizing Metrics
### Autocut signal
**Key:** `autocut.signal`
**Plain English:** Which signal autocut used to size the result set. 'rerank' means it found a real score cliff in the cross-encoder rerank scores and cut there; 'none' means no trustworthy cliff (no reranker, <2 scored results, or the gap was too small) so it returned the full list.
**Range:** 'rerank' | 'none'. 'none' is not a failure — it means autocut declined to cut because the signal didn't justify it.
### Autocut gap ratio
**Key:** `autocut.gap_ratio`
**Plain English:** The size of the largest score drop autocut found, as a fraction of the top result's score. A gap of 0.40 means the score fell by 40% of the top score at the steepest point. Autocut cuts there only when this clears the sensitivity threshold (autocut_jump, default 0.20).
**Range:** 0..1, higher = a sharper cliff (more confident cut). Below the autocut_jump threshold → no cut.
---
## Coverage
-102
View File
@@ -1,102 +0,0 @@
# Content Guardrail Seams
GBrain exposes **vendor-neutral guardrail seams** at the boundaries where
external content enters the retrieval layer and where queries/tool-inputs enter
the LLM gateway. A guardrail is any external classifier — a content firewall, a
prompt-injection detector, a PII scrubber — that wants to *observe* content at
those boundaries.
The OSS distribution ships **inert**: zero guardrails are registered by default,
and every seam is a no-op until an operator registers a provider.
## Design contract (hard invariants)
These hold for every seam and are enforced by `test/guardrails.test.ts`:
- **Observe-only.** `runGuardrails()` returns `void`. Callers never branch on a
provider verdict. A guardrail registered through this interface *cannot*
block, rewrite, drop, retry, or reorder GBrain behavior. Enforcement, if ever
added, will get its own explicitly-named seam and its own RFC — it will not
silently reuse this one.
- **Fail open.** Missing config, provider throw/reject, timeout, and network
error are all swallowed. A broken guardrail never breaks an ingest, a query,
or a tool call.
- **Inline await.** Hooks await the provider before proceeding, so the
classifier sees content at the exact pre-persist / pre-inference moment.
- **No verdict persistence.** GBrain writes no guardrail rows. Providers own
their own audit trail.
- **Content boundaries.** Hooks pass only the ingest/user-facing payload — the
markdown/code body, the last user message, the expansion query, the tool
input. They never pass system prompts, full chat history, tool *output*, LLM
output, embeddings, or multimodal/OCR/rerank payloads.
## The five seams
All seams call `runGuardrails({ hook, content, metadata })` from
`src/core/guardrails.ts`.
| `hook` | Location | Fires |
| --- | --- | --- |
| `file_storage.markdown` | `import-file.ts``importFromContent` | After `parseMarkdown` + size guard, **before** content-sanity, hashing, chunking, embedding, DB write |
| `file_storage.code` | `import-file.ts``importCodeFile` | After code size guard, **before** hashing, code-chunking, embedding, DB write |
| `ai_gateway.chat` | `ai/gateway.ts``chat` | On the **latest user message only**, before provider inference |
| `ai_gateway.expand` | `ai/gateway.ts``expand` | On the query, before the expansion model call |
| `ai_gateway.tool_input` | `ai/gateway.ts``toolLoop` | On `{toolName, input}`, before pending-persist and before tool execution |
The two `file_storage.*` hooks cover every natural ingest caller that routes
through `importFromContent` / `importCodeFile`: `gbrain import`, sync, capture,
`put_page`, subagent `brain_put_page`, trusted-workspace writes,
`ingest_capture`, inbox daemon dispatch, reindex, code reindex, and the public
import APIs.
## Writing a guardrail provider
```ts
import { registerGuardrailProvider, type GuardrailInput } from 'gbrain/core/guardrails';
registerGuardrailProvider({
id: 'my-firewall',
async classify(input: GuardrailInput) {
// input.hook — which boundary ('file_storage.markdown', etc.)
// input.content — the raw text to classify
// input.metadata — provider-opaque context (slug, source_kind, tool_name, model, ...)
//
// Do your own timeout/retry/logging here. The return value is IGNORED by
// GBrain — return a typed verdict only if your own audit code consumes it.
await fetch(MY_API, { method: 'POST', body: JSON.stringify({ text: input.content }) });
},
});
```
Register once at process init (e.g. from a plugin entry or an operator boot
hook). Registration is idempotent by `id`, so a re-init won't double-fire.
### Provider responsibilities
GBrain deliberately keeps the seam minimal. The provider owns:
- **Timeout discipline.** GBrain does not impose a timeout in `runGuardrails`
so you can tune per-deployment latency. Use an `AbortController`.
- **Secret handling.** Read API keys from env at call time. Never log the key.
- **Redacted logging.** Don't log raw classified content (it may itself be the
payload you're trying to protect). Log a hash + verdict, not the body.
- **Async fan-out.** If you don't want to block ingest on your classifier,
enqueue inside `classify` and return immediately. The seam awaits *your*
function; what it does is up to you.
## Example: shadow-mode firewall provider
A typical "shadow mode" provider (classify, log a redacted verdict, change
nothing) is ~80 lines and lives entirely in the provider's own package. See
the reference provider doc shipped to integration partners for a complete
`classify` implementation that:
1. resolves `<base>/classify` from an env URL,
2. posts `{ text, hook, metadata }` with an `x-api-key` header,
3. parses a `{ prediction, blocked, score, threshold }` response,
4. emits one redacted stderr line (`status=… prediction=… content_sha256=…`),
5. fails open on every error path.
Because the verdict is ignored by GBrain, "shadow mode" requires *no* special
GBrain flag — it is the only mode this interface supports. Enforcement would be
a separate, future, RFC-gated seam.
-310
View File
@@ -1,310 +0,0 @@
# Scaling skills past 300 without drowning the context window
When an agent grows past 100 skills, a wall starts forming. Sessions take
longer to start. The model gets a little dumber about which skill to pick.
Tokens that should be powering reasoning are powering a skill catalog the
model reads on every turn whether it needs to or not.
This guide is the recipe for breaking through that wall without deleting
capabilities. Three tiers, one resolver, one safety net. Production-tested
on a 306-skill agent (Garry's OpenClaw, the agent behind Y Combinator's
president). The pattern works whether you run OpenClaw, Hermes, Claude Code,
Cursor, or your own MCP-aware agent.
## The problem
OpenClaw scans every skill file on disk at session start and injects them
into the system prompt as `<available_skills>` entries. The model sees a
name, description, and file path for each one. When a request matches, the
model reads the full SKILL.md and follows it.
This is great architecture at 50 skills. At 100, it's fine. At 200, it
starts to drag. At 300, the system prompt eats more than 25,000 tokens on
skill descriptions alone. Tokens that aren't going to reasoning, context,
or actual work.
The symptoms compound:
- Sessions take noticeably longer to start.
- The model has less room for conversation history.
- Skill routing gets fuzzier. With 300 descriptions competing for attention,
the model occasionally picks the wrong one.
- Cost goes up because every turn carries the full skill manifest.
The naive fix is to delete skills you don't use often. Don't do this. The
whole point of skills is that capabilities compound. A gift pipeline that
fires twice a month saves 30 minutes each time it does. A flight tracker
fires once per trip and prevents a missed Uber. Deleting low-frequency
skills optimizes for prompt size at the cost of capability. You wouldn't
delete apps from your phone because the home screen is too crowded. You'd
organize them.
## The three tiers
Not all skills need to be visible to the model at all times. Some are core.
Some are specialized. Some are dormant.
### Tier A: always loaded (~35 skills)
The skills the model needs on every single turn. Brain search, email triage,
calendar, meeting ingestion, content creation, the executive assistant.
They stay in the system prompt's `<available_skills>` manifest. The model
sees them natively and routes to them without any lookup.
### Tier B: resolver-routed (~85 skills)
Real, active skills that fire regularly but don't need to pollute every
turn. Gift pipeline, flight tracker, investor update ingestion, adversary
tracking, book mirror, civic intelligence. They live on disk. They have
full SKILL.md files. But OpenClaw doesn't inject them into the prompt.
Instead, a compact RESOLVER.md handles routing. One line per skill with
trigger phrases:
```markdown
- **gift-advisor**: gift idea | what should I bring | birthday gift | housewarming
- **flight-tracker**: track my flight | flight status | when does my flight land
- **investor-update-ingest**: investor update | portfolio update | company metrics
```
When the model sees "what should I bring to Jessica's dinner," it checks
the resolver, finds `gift-advisor`, reads the SKILL.md, and executes. Same
result. Zero wasted tokens on the other 84 turns where gifts aren't relevant.
### Tier C: dormant (~180 skills)
Built-in OpenClaw skills that aren't in active rotation (1Password, Discord,
Notion, Trello, integrations you haven't wired up yet) plus specialized
skills that almost never fire. They're explicitly disabled in the config
with `enabled: false`. They exist on disk as documentation and potential.
Flip one boolean to wake them up. Zero tokens contributed to every prompt
until then.
### The numbers
Before tiering, on Garry's 306-skill OpenClaw:
| Metric | Before |
|---|---|
| Skills in system prompt | 306 |
| Skill-description tokens per turn | ~25,000 |
| Skill routing accuracy | degrading |
| Session startup | slow |
After tiering:
| Metric | After |
|---|---|
| Skills in system prompt (Tier A) | 35 |
| Skill-description tokens per turn | ~4,000 |
| Skills still accessible (A + B + C) | 301 |
| Capability loss | zero |
| **Tokens freed per turn** | **~21,000** |
21K tokens per turn is not a small optimization. It's the difference between
the model having room to think and the model being squeezed. It's the
difference between carrying 3 pages of conversation history and carrying 15.
## What the resolver actually does
The resolver is cheaper than the manifest. That's the load-bearing insight.
OpenClaw's native skill manifest puts ~80 tokens per skill into the system
prompt (name + description + location). At 300 skills that's 24,000 tokens
spent every turn whether the model needs the catalog or not.
The resolver puts ~15 tokens per skill into a compact markdown list. At
300 skills that's 4,500 tokens. But it only fires when the model checks
it, which is only when the request doesn't match a Tier A skill. Most
turns, the resolver costs zero tokens because the Tier A match handles it.
This is the routing-table pattern but applied to the skill manifest itself.
The resolver routes to skills, but it also routes around skills, keeping
them out of the context window until they're needed.
GBrain ships with a [bundled `skills/RESOLVER.md`](../../skills/RESOLVER.md)
you can use as a reference shape. The skillpack story for distributing
your own resolvers across machines is covered in
[skillpacks as scaffolding](skillpacks-as-scaffolding.md).
## The compact list format (v0.41.7.0)
GBrain's resolver parser used to require markdown tables:
```markdown
| Trigger | Skill |
|---------|-------|
| "gift idea" | `skills/gift-advisor/SKILL.md` |
```
That's fine when you have 20 entries. It gets unwieldy at 200, and at 300
it's unreadable. OpenClaw deployments quietly evolved a compact list
format that scales better:
```markdown
- **gift-advisor**: gift idea | what should I bring | birthday gift
- **flight-tracker**: track my flight | flight status | when does my flight land
```
Before v0.41.7.0, `gbrain doctor` only spoke the table dialect. On a
306-skill compact-format resolver, the doctor reported every skill as
unreachable: **238 FAIL errors on every doctor run**. The parser was
silently treating the compact dialect as zero skills.
v0.41.7.0 ships dual-format support. The same `parseResolverEntries`
function reads both table rows and list rows in the same file, with the
v0.31.7 multi-resolver merge (skillpack `skills/RESOLVER.md` + workspace
`../AGENTS.md`) folding everything into one unified view. Run `gbrain doctor`
and the 238 FAILs collapse to 0.
### The list-format contract
A few rules to keep the parser unambiguous:
- **Skill names must be kebab-lowercase.** `gift-advisor`, `flight-tracker`,
`email-triage`. Names that start with an uppercase letter (`MyTool`,
`Note`, `Convention`) are deliberately ignored. This is what stops prose
bullets like `- **Note**: see [link]` from being mis-parsed as skill
rows in real-world AGENTS.md files.
- **The path always resolves to `skills/<name>/SKILL.md`.** An optional
`→ \`skills/path\`` (or ASCII `->`) suffix is allowed for readability,
but the parser strips it. For non-conventional paths (skills under
nested directories, references into `conventions/`, anything that
isn't `skills/<name>/SKILL.md`), use the table format.
- **Triggers separate with `|`.** Empty pieces and the literal `...`
placeholder are dropped. Each trigger becomes its own resolver entry,
all pointing at the same skill.
- **Bold or plain.** `- **name**: triggers` is preferred. `- name: triggers`
works as a fallback.
You can mix table and list rows in the same file. Useful when a brain
inherits a table-format `RESOLVER.md` from gbrain and a list-format
`../AGENTS.md` from OpenClaw.
## The doctor safety net
The danger with tiering is invisible skill loss. You disable a skill from
native scanning, forget to add it to the resolver, and now the agent can't
do something it used to do. You won't notice until the moment you need it.
`gbrain doctor` walks every skill on disk and verifies it's reachable,
either through native scanning (Tier A) or through the resolver (Tier B
and C). On Garry's setup, the first run after tiering found 63 unreachable
skills. Sixty-three capabilities that existed on disk but had no routing
path. Fixed in an hour by adding resolver entries.
Run it after every skill change:
```bash
gbrain doctor
```
For CI gates, use the JSON-emitting variant:
```bash
gbrain check-resolvable --json
gbrain check-resolvable --strict # warnings fail too
```
If a skill is unreachable, the output tells you which one and suggests
the fix. The resolver is a document. Documents are cheap to fix.
## Implementation walkthrough
Three changes. Total time about 45 minutes once you've decided which
skills go in which tier.
### 1. Audit and tier your skills
Walk through every skill. Ask: does this need to fire on every turn?
- If yes → Tier A.
- If it fires weekly or less but is real → Tier B.
- If you don't use it → Tier C.
### 2. Disable Tier B and C in your agent's config
For OpenClaw, the file is `openclaw.json`. Add an entry per disabled skill:
```json
{
"skills": {
"entries": {
"gift-advisor": { "enabled": false },
"flight-tracker": { "enabled": false },
"1password": { "enabled": false }
}
}
}
```
The exact config shape depends on which agent runtime you use. The point
is the same in all of them: tell the runtime not to inject this skill into
the system prompt. The file stays on disk; only the prompt injection stops.
### 3. Write the resolver
One line per Tier B and Tier C skill. Trigger phrases that match how you
actually ask for things:
```markdown
- **gift-advisor**: gift idea | what should I bring | birthday gift
- **flight-tracker**: track my flight | flight status | when do I land
- **investor-update-ingest**: investor update | portfolio update | company metrics
```
That's it. The model handles the rest. When a request doesn't match Tier A,
it checks the resolver, reads the matching SKILL.md, and executes.
### 4. Run `gbrain doctor` and fix any unreachable skills
The doctor sweep tells you which skills don't have a routing path. Add a
resolver entry for each one, re-run, repeat until the count is zero.
## A lesson from the first version
I initially converted my resolver from a clean list format to a table
format because the validator only spoke tables. That was wrong. When a
tool fails against valid data, the right move is to fix the tool, not
reshape the data. The list format was correct, compact, readable, easy
to maintain. The parser needed to support both shapes. v0.41.7.0 is
that fix.
The same principle applies everywhere in agent systems. Your SKILL.md is
the source of truth. Your AGENTS.md is the source of truth. Your resolver
is the source of truth. When tooling disagrees with your configuration,
the tooling is wrong. Fix the tooling.
## The scaling curve
At 50 skills, you don't need any of this. Just load everything.
At 100, you start feeling the drag but can push through.
At 200, routing accuracy drops and sessions get noticeably slower. This
is where most people stop adding skills, which means their agent stops
getting more capable. Bad trade.
At 300+, tiering is mandatory. But with tiering, there's no ceiling.
1,000 skills with 35 in the hot path and 965 in the resolver is the same
per-turn cost as 35 skills with no resolver. The cost stays flat.
Capabilities compound.
The architecture that gets you from 50 to 300 is different from the
architecture that gets you from 10 to 50. That's normal. Systems that
scale change shape. The important thing is that each tier preserves full
capability. You're organizing, not deleting.
## Related
- [Skill development cycle](skill-development.md) — the 5-step loop for
turning a repeated task into a real skill.
- [Skillpacks as scaffolding](skillpacks-as-scaffolding.md) — how to
distribute a coherent set of skills across machines and agents.
- [Sub-agent routing](sub-agent-routing.md) — when to delegate to a
sub-agent vs handle in-line, and the model routing table for each path.
GBrain: [github.com/garrytan/gbrain](https://github.com/garrytan/gbrain).
The `parseResolverEntries` parser lives at
[`src/core/check-resolvable.ts`](../../src/core/check-resolvable.ts);
the bundled resolver lives at [`skills/RESOLVER.md`](../../skills/RESOLVER.md).
-147
View File
@@ -1,147 +0,0 @@
# `gbrain skillopt` — Self-evolving skills
Treat your `SKILL.md` files as the trainable parameters of an agent that
itself never changes. Write a benchmark of realistic tasks; SkillOpt watches
the agent run them, proposes specific edits, re-tests, and only keeps changes
that measurably improve the score.
Based on [SkillOpt](https://arxiv.org/abs/2605.23904) (Microsoft Research,
May 2026).
> **New to this?** Start with the hands-on tutorial:
> [Auto-improve a skill with `gbrain skillopt`](../tutorials/improving-skills-with-skillopt.md).
> It walks you from "I have a skill" to "I accepted a measurably better version"
> in ~20 minutes, including how to write your first benchmark. This page is the
> reference — flags, exit codes, cost model, safety guards.
## The 30-second pitch
```bash
# 1. Generate a starter benchmark from the skill itself (no routing-eval needed)
gbrain skillopt my-skill --bootstrap-from-skill
# 2. Review the benchmark — STRENGTHEN the generated judges (they're weak drafts),
# then delete the trailing `# BOOTSTRAP_PENDING_REVIEW` line
# 3. Run the optimizer (--split 1:1:1 is required for a ~15-task starter)
gbrain skillopt my-skill --bootstrap-reviewed --split 1:1:1
```
That's the entire workflow. (Already have a `routing-eval.jsonl`? Swap step 1 for
`--bootstrap-from-routing` — but routing tasks test dispatch, not output quality.)
## What's in the box
```
skills/my-skill/
SKILL.md ← what gets optimized (body only; D5)
skillopt-benchmark.jsonl ← what success looks like
skillopt/
best.md ← current best version
versions/
v0001_e1_s1.md ← per-step snapshots
v0002_e1_s2.md
...
history.json ← append-only run record (D8)
rejected.json ← bounded LRU of rejected edits
```
The audit trail lives at `~/.gbrain/audit/skillopt-YYYY-Www.jsonl`
(ISO-week rotated; honors `GBRAIN_AUDIT_DIR`).
## How the loop works
For each step:
1. **Forward pass.** Run the candidate skill against a batch from `D_train`.
2. **Backward pass.** Two reflect calls (failures + successes per D7) propose
edits to address what worked / didn't work.
3. **Rank + clip.** Top-N edits within the LR budget (cosine schedule by
default; D10 has the ASCII curve in `orchestrator.ts`).
4. **Apply.** D9 tagged-result patches the body (frontmatter forbidden per
D5; ambiguous anchors rejected to the rejected-buffer).
5. **Validation gate.** D12 median-of-3 + epsilon=0.05: every sel-task runs
the judge 3 times, takes the median; only accepts if median > best by
more than 0.05.
6. **Commit.** D8 history-intent-first 5-step atomic write — crash-safe.
After each epoch with no improvement: D6 slow-update fires one meta-edit
proposal (this lives in v0.42 follow-up; v1 emits the audit event).
## Flags
| Flag | Default | Purpose |
|---|---|---|
| `--benchmark <path>` | `skills/<n>/skillopt-benchmark.jsonl` | Path to benchmark JSONL |
| `--bootstrap-from-skill` | off | Generate a starter benchmark from SKILL.md (recommended; no routing-eval needed) |
| `--bootstrap-tasks N` | 15 | How many starter tasks `--bootstrap-from-skill` generates (max 50) |
| `--bootstrap-from-routing` | off | Auto-build benchmark from routing-eval.jsonl |
| `--bootstrap-reviewed` | off | Required after human-reviewing bootstrap output |
| `--epochs N` | 4 | Outer-loop iterations |
| `--batch-size N` | 8 | Tasks per inner step |
| `--lr N` | 4 | Max edits per step |
| `--lr-schedule cosine\|linear\|constant` | cosine | Edit-budget decay |
| `--split TRAIN:SEL:TEST` | 4:1:5 | Ratio; refuses if D_sel < 5 |
| `--optimizer-model MODEL` | tier.deep | Reflects + proposes |
| `--target-model MODEL` | tier.subagent | Executes the skill |
| `--judge-model MODEL` | tier.reasoning | Scores rollouts |
| `--patch \| --rewrite` | patch | Edit ops only vs. full rewrites |
| `--dry-run` | off | Cost preview, no LLM calls |
| `--no-mutate` | off | Write proposed.md, don't replace SKILL.md (no held-out needed) |
| `--allow-mutate-bundled` | off | Required to mutate gbrain-bundled skills in place — ALSO requires `--held-out` (>=5 rows) or the run hard-refuses |
| `--held-out <path>` | — | Independent test set (same JSONL shape as the benchmark, task IDs disjoint from it). A candidate that beats the benchmark but regresses on the held-out set is refused. Required for in-place bundled mutation. |
| `--max-cost-usd N` | 5.00 | Hard cap; preflight refuses if exceeded |
| `--max-runtime-min N` | 30 | Wall-clock cap |
| `--force` | off | Bypass dirty-working-tree refusal |
| `--resume <run-id>` | off | Resume a prior interrupted run |
| `--json` | off | Machine-readable stdout |
## Exit codes
| Code | Meaning |
|---|---|
| 0 | Improved + accepted (or `--no-mutate` proposed.md written) |
| 1 | No improvement; best skill unchanged |
| 2 | Aborted by gate (dirty tree, over budget, bench validation, etc.) |
## Cost model
A typical 20-task benchmark with defaults costs ~$0.90 per run:
- 32 rollouts × Sonnet ($0.009 each) ≈ $0.29
- 8 reflect calls × Opus (cached) ≈ $0.25
- 24 sel-judges × Sonnet (cached) ≈ $0.10
- Final test eval ≈ $0.07
- **Total ≈ $0.71**
For a 100-task benchmark: ~$5.00 (right at the default cap). Preflight
refuses to start when the estimate exceeds `--max-cost-usd`.
## Safety guards (the cathedral)
| Guard | Decision | What it prevents |
|---|---|---|
| Validation gate is mandatory | D12 (paper) | Accepting LLM judge noise as improvement |
| Frontmatter mutation forbidden | D5 | Routing surface drift (`check-resolvable` regression) |
| Per-skill DB lock | D14 | Two concurrent runs corrupting history/versions |
| Bundled-skill gate | D16 | Auto-mutating skills shipped with gbrain (in-place mutation requires `--allow-mutate-bundled` + a `--held-out` set of >=5 benchmark-disjoint tasks; else hard-refuse + proposed.md) |
| Held-out gate | F11 | Accepting a candidate that overfits its own benchmark — `--held-out` refuses a candidate whose held-out score regresses below baseline |
| Bootstrap review sentinel | D15 | Self-referential benchmark gaming |
| Read-only tool sandbox in rollouts | D13 | Optimization runs writing junk pages to your brain |
| History-intent-first atomic commit | D8 | Half-written SKILL.md on crash |
| Cost preflight | D3 | Surprise mid-run budget exhaustion |
| Dirty-tree refusal | dry-fix pattern | Overwriting your uncommitted changes |
## When NOT to use SkillOpt
- **No benchmark.** Optimizing against guesses is worse than not optimizing.
- **Write-flavored skills.** Skills whose job is to `put_page` heavily can't
use the v1 read-only sandbox; mocked-write capture is a v0.42 follow-up.
- **Tiny benchmarks (<10 tasks).** D_sel < 5 refuses by default; meaningful
validation needs ≥20 tasks total per the paper.
## Related skills
- `gbrain skillify scaffold <name>` — create a new skill (use BEFORE skillopt)
- `gbrain skillpack-check <name>` — audit conformance + skillopt status
- `gbrain check-resolvable` — routing MECE validation (NOT mutated by skillopt)
+1 -2
View File
@@ -63,8 +63,7 @@ The doctor distinguishes two repair paths:
- **Cost-sensitive, English-only**: Ollama (free, local) or Voyage (paid, best quality per dollar).
- **Quality-first**: Voyage `voyage-4-large` (1024-2048 dims, ~3-4× more dense tokens than OpenAI tiktoken).
- **Code-heavy brain (gstack per-worktree, source repos)**: Voyage `voyage-code-3` (1024 default; supports 256/512/1024/2048). Tuned on programming languages. Voyage publishes head-to-head numbers showing it outperforms their general flagships on code retrieval ([voyageai.com/blog](https://voyageai.com/blog)). For gstack's per-worktree pglite-backed code brain, this is the right default — see Topology 3 in `docs/architecture/topologies.md`.
- **Reranking pair**: ZeroEntropy `zerank-2` is the hosted default in `tokenmax` mode (see [`docs/ai-providers/zeroentropy.md`](../ai-providers/zeroentropy.md)). Voyage `rerank-2.5` pairs cleanly with Voyage embeddings.
- **Local reranking (no API spend)**: `llama-server-reranker` recipe (v0.40.6.1) — point gbrain at your own `llama-server --reranking` instance running Qwen3-Reranker or self-hosted ZeroEntropy weights. Same `gateway.rerank()` seam, $0 per call. Walkthrough in [`docs/ai-providers/llama-server-reranker.md`](../ai-providers/llama-server-reranker.md).
- **Reranking pair**: Voyage (their reranker `rerank-2.5` pairs cleanly with Voyage embeddings).
- **One key for many hosted models**: OpenRouter. Set `OPENROUTER_API_KEY` and use `openrouter:<provider>/<model>` for chat against GPT-5.2, Claude 4.x, Gemini 3, DeepSeek, and dozens more without juggling per-provider keys. Embedding catalog includes OpenAI, Google, Qwen, BGE-M3.
- **Enterprise compliance**: Azure OpenAI (data residency + private endpoints) or self-hosted via llama-server / Ollama.
- **China region**: DashScope (Alibaba) or Zhipu (BigModel). DashScope's international endpoint at `dashscope-intl.aliyuncs.com`; override `provider_base_urls.dashscope` for the China endpoint.
+5 -55
View File
@@ -1,10 +1,5 @@
# Connect GBrain to Claude Code
> New to this? The [Give your coding agent a memory](../tutorials/connect-coding-agent.md)
> tutorial walks both paths (local-from-nothing and connect-to-an-existing-brain)
> end to end, plus the brain-first protocol that makes it worth it. This page is
> the connection reference.
## Option 1: Local (recommended, zero server needed)
```bash
@@ -14,44 +9,10 @@ claude mcp add gbrain -- gbrain serve
That's it. Claude Code spawns `gbrain serve` as a stdio subprocess. No server, no
tunnel, no token needed. Works with both PGLite and Supabase engines.
## Option 2: Remote, one command (fastest from a bearer token)
## Option 2: Remote (access from any machine)
If GBrain is running somewhere as an HTTP server (`gbrain serve --http`, see the
[ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md)) and you have a bearer token,
let `gbrain connect` generate the wire-up for you.
On the host (or anywhere `gbrain` is installed), mint a token and print the block:
```bash
gbrain auth create "claude-code"
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --token gbrain_xxx
```
`gbrain connect` prints a short, copy-paste block. Paste it into Claude Code — it
runs the `claude mcp add` for you and tells the agent to call `get_brain_identity`
and `list_skills` so it immediately knows what the brain can do.
Already on the machine you want to wire up? Skip the copy-paste and let `connect`
do it directly, with a built-in token smoke-test:
```bash
gbrain connect https://YOUR-DOMAIN.ngrok.app --token gbrain_xxx --install
```
(`--install` runs `claude mcp add`, then verifies the token by calling
`get_brain_identity` — so a wrong or expired token fails now, not silently on the
agent's first request. The URL is normalized: a bare host without `/mcp` gets it
appended; pass an explicit `https://` scheme.)
Pipe-friendly machine output (token redacted unless `--show-token`):
```bash
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --token gbrain_xxx --json
```
## Option 3: Remote, manual `claude mcp add`
Equivalent to what `gbrain connect` generates, if you'd rather run it yourself:
If you have GBrain running on a server with a public tunnel (see
[ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md)):
```bash
claude mcp add gbrain -t http \
@@ -59,12 +20,8 @@ claude mcp add gbrain -t http \
-H "Authorization: Bearer YOUR_TOKEN"
```
Replace `YOUR-DOMAIN` with your ngrok domain and `YOUR_TOKEN` with a token from
`gbrain auth create "claude-code"`.
> A `gbrain auth create` token is a long-lived, full-access secret. Keep it
> private (it lands in `~/.claude.json`), and prefer a scoped/short-lived token
> where your host supports one.
Replace `YOUR-DOMAIN` with your ngrok domain and `YOUR_TOKEN` with a token
from `gbrain auth create "claude-code"`.
## Verify
@@ -76,13 +33,6 @@ search for [any topic in your brain]
You should see results from your GBrain knowledge base.
> **`list_skills` returns nothing?** Skill discovery is gated by `mcp.publish_skills`
> on the host. New brains from `gbrain init` default it ON; brains upgraded from an
> older release stay OFF until you opt in. Enable it on the host with
> `gbrain config set mcp.publish_skills true`. The core tools (search, query,
> get_page, put_page, think, find_experts) work regardless. Note: `capture` is a
> CLI-only command, not an MCP tool — the agent writes over MCP with `put_page`.
## Remove
```bash
-71
View File
@@ -1,71 +0,0 @@
# Connect GBrain to Codex
> New to this? The [Give your coding agent a memory](../tutorials/connect-coding-agent.md)
> tutorial walks both paths (local-from-nothing and connect-to-an-existing-brain)
> end to end, plus the brain-first protocol that makes it worth it. This page is
> the connection reference.
Codex CLI (`@openai/codex`, v0.130+) supports remote streamable-HTTP MCP servers
with a bearer token read from an environment variable. The token lives in your
shell env, not in Codex's config file.
## Fastest path: `gbrain connect`
Run anywhere `gbrain` is installed (mint a token on the brain host first):
```bash
gbrain auth create "codex"
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --token gbrain_xxx --agent codex
```
This prints a copy-paste block. Or wire it up directly and smoke-test the token:
```bash
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --token gbrain_xxx --agent codex --install
```
`--install` runs `codex mcp add` for you, then makes one real call to the brain so
a wrong/expired token fails right away. Because Codex reads the token from the env
var at runtime, keep `GBRAIN_REMOTE_TOKEN` exported in your shell profile.
## Manual setup
```bash
export GBRAIN_REMOTE_TOKEN=gbrain_xxx
codex mcp add gbrain --url https://YOUR-DOMAIN.ngrok.app/mcp \
--bearer-token-env-var GBRAIN_REMOTE_TOKEN
```
Codex stores the env-var *name* (`GBRAIN_REMOTE_TOKEN`), not the token itself, and
reads the value when it launches the MCP server. Add the `export` line to your
`~/.zshrc` / `~/.bashrc` so it's set in every session.
## Verify
In Codex, ask it to use the brain:
```
Call get_brain_identity, then search my brain for [topic].
```
`get_brain_identity` confirms whose brain you're connected to; `list_skills` shows
everything it can do.
> **`list_skills` empty?** It's gated by `mcp.publish_skills` on the host (default
> ON for `gbrain init` brains, OFF for brains upgraded from older releases). Enable
> it on the host: `gbrain config set mcp.publish_skills true`. The core tools
> (search, query, get_page, put_page, think, find_experts) work regardless.
> `capture` is CLI-only, not an MCP tool — write over MCP with `put_page`.
## Remove
```bash
codex mcp remove gbrain
```
## Notes
- The token is a long-lived, full-access secret. Keep `GBRAIN_REMOTE_TOKEN` out of
version control and prefer a scoped token if your host supports one.
- Local stdio also works if you run the brain on the same machine:
`codex mcp add gbrain -- gbrain serve`.
+14 -83
View File
@@ -1,83 +1,20 @@
# Connect GBrain to Perplexity Computer
Perplexity Computer connects as a **remote** MCP client, so GBrain must be served
over HTTP and reachable at a public HTTPS URL. Perplexity does not run
`gbrain serve` (stdio) the way Claude Code does — it needs a reachable endpoint:
Perplexity Computer supports remote MCP servers with bearer token authentication.
```
Perplexity Computer
→ ngrok tunnel (https://YOUR-DOMAIN.ngrok.app/mcp)
→ gbrain serve --http (built-in OAuth 2.1 transport)
→ Postgres / PGLite
```
## Setup
## 1. Serve GBrain over HTTP (host side)
```bash
gbrain serve --http --port 3131 --bind 0.0.0.0 \
--public-url https://YOUR-DOMAIN.ngrok.app
```
- **`--bind 0.0.0.0` is required.** Since v0.34, `--http` defaults to
`127.0.0.1`, so without it the tunnel reaches the server but the connection is
refused (`ECONNREFUSED`).
- **`--public-url` must match the tunnel.** The OAuth issuer in the discovery
metadata has to line up with the URL Perplexity actually hits (RFC 8414 §3.3),
or OAuth client-credentials auth fails.
## 2. Expose it with a tunnel
```bash
ngrok http 3131 --url YOUR-DOMAIN.ngrok.app
```
See the [ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for a persistent
tunnel.
## 3. Create credentials
Two supported auth paths.
**OAuth 2.1 client credentials (recommended, v0.26.0+).** Perplexity is a cloud
service, so it holds whatever credential you give it. OAuth is the correct choice:
least-privilege scopes + short-lived rotating access tokens instead of a
long-lived full-access secret. Mint a client and print the connector fields in
one step (on the brain host):
```bash
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --agent perplexity --oauth --register
```
Or register separately and pass the creds (works anywhere, no DB needed):
```bash
gbrain auth register-client perplexity --grant-types client_credentials --scopes "read write"
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --agent perplexity --oauth \
--client-id gbrain_cl_xxx --client-secret gbrain_cs_xxx
```
`connect --oauth` prints the **Issuer URL + Client ID + Client Secret** to paste
in step 4.
**Legacy bearer token (simplest, best for local/personal):**
```bash
gbrain auth create "perplexity"
gbrain connect https://YOUR-DOMAIN.ngrok.app/mcp --token gbrain_xxx --agent perplexity
```
(Perplexity is a GUI connector, so there's no `--install``connect` prints the
exact values to paste in step 4.)
## 4. Add the connector in Perplexity
1. Open Perplexity (requires Pro subscription).
2. Go to **Settings → Connectors** (or **MCP Servers**).
1. Open Perplexity (requires Pro subscription)
2. Go to **Settings > Connectors** (or **MCP Servers**)
3. Add a new remote connector:
- **URL:** `https://YOUR-DOMAIN.ngrok.app/mcp`
- **Authentication:** API Key / Bearer Token, or OAuth client credentials
- Paste the token (bearer) or `client_id` + `client_secret` (OAuth).
4. Save.
- **Authentication:** API Key / Bearer Token
- **Token:** your GBrain access token
(create one with `gbrain auth create "perplexity"`)
4. Save
Replace `YOUR-DOMAIN` with your ngrok domain (see
[ngrok-tunnel recipe](../../recipes/ngrok-tunnel.md) for setup).
## Verify
@@ -87,14 +24,8 @@ In a Perplexity conversation, ask it to use your brain:
Use my GBrain to search for [topic]
```
Have it call `get_brain_identity` (whose brain this is), then `list_skills`
(everything it can do).
## Notes
- Perplexity Computer is available to Pro subscribers; both the Mac app and web
version support remote MCP connectors.
- The Mac app can also use a local MCP server (`gbrain serve` stdio) if you'd
rather not expose an HTTP endpoint.
- A `gbrain auth create` token is a long-lived, full-access secret. Keep it
private and prefer a scoped token where possible.
- Perplexity Computer is available to Pro subscribers
- Both the Perplexity Mac app and web version support MCP connectors
- The Mac app also supports local MCP servers if you prefer `gbrain serve` (stdio)
@@ -1,153 +0,0 @@
# Migrating your OpenClaw brain to gbrain v0.41.2.0 (greenfield)
The v0.41.2.0 lens packs ship a one-shot importer that re-ingests your
existing OpenClaw brain (`~/git/brain/atoms/`, `concepts/`, `ideas/`)
through the new ingestion cathedral. Pages land in gbrain with an
`imported_from: markdown-greenfield` frontmatter marker so the new
extract_atoms + synthesize_concepts cycle phases skip them (lossless
import with provenance).
## Before you migrate
1. **Upgrade gbrain to v0.41.2.0+:**
```bash
gbrain upgrade
gbrain --version # should be 0.41.2.0 or later
```
2. **Activate the creator pack** (or gbrain-everything if you also
want investor + engineer lenses on the same brain):
```bash
gbrain config set schema_pack gbrain-creator
# OR
gbrain config set schema_pack gbrain-everything
```
3. **Apply schema migration v94** (take_domain_assignments table):
```bash
gbrain apply-migrations --yes
```
## The dry-run pass
Always start with `--dry-run` to see what the importer would do
without writing anything:
```bash
gbrain capture --source markdown-greenfield \
--repo ~/git/brain \
--dry-run \
--limit 100
```
The output reports:
- `emitted` — atoms/concepts/ideas that would import cleanly
- `skipped_no_type` — files without a `type:` frontmatter (counted
as benign skips; no audit appended)
- `skipped_invalid` — files that failed validation (these append to
`~/.gbrain/audit/markdown-greenfield-failures-YYYY-Www.jsonl`)
Inspect the audit JSONL:
```bash
ls ~/.gbrain/audit/markdown-greenfield-failures-*.jsonl
cat ~/.gbrain/audit/markdown-greenfield-failures-*.jsonl | jq .
```
Common failures:
- **Empty frontmatter** — file has `---` but no fields. Not a real
brain page; safe to leave skipped.
- **Malformed YAML** — fix the file in your OpenClaw then re-run.
- **Missing required field** — usually means the original OpenClaw
skill output a partial page; check whether the file is worth
preserving.
## The actual import
When the dry-run looks clean, drop the `--dry-run` and `--limit`
flags:
```bash
gbrain capture --source markdown-greenfield --repo ~/git/brain
```
Expect ~30-60 minutes for the full 24K-page set (atoms + concepts +
ideas). The importer:
1. Walks `atoms/{YYYY-MM-DD}/*.md`, `concepts/*.md`, `ideas/*.md` in
deterministic alphabetical order so partial-run resumes pick up
where they left off.
2. Stamps `imported_from: markdown-greenfield` + `imported_at:
<ISO timestamp>` on every page's frontmatter, preserving ALL
original fields verbatim under `metadata.original_frontmatter`.
3. Emits each as an IngestionEvent with `mode: 'migration'` (T2),
which bypasses the daemon's 24h DedupWindow. The importer owns
its own permanent slug-keyed idempotency.
4. Routes through `put_page` so pages land with proper FK chains and
embedding eligibility.
## After the import
Verify counts match:
```bash
gbrain stats
# Should show ~24K new pages with type=atom/concept/idea
```
The next `gbrain dream` cycle will:
- Run `extract_atoms` on NEW transcripts (skips pages with the
`imported_from` marker — your historical atoms are frozen, not
re-extracted).
- Run `synthesize_concepts` on NEW atoms (skips imported concepts
for the same reason).
- Run `extract_facts` over the imported pages — facts fences in
imported atoms/concepts populate the facts table normally.
## Retiring your OpenClaw's parallel crons
After verifying the import, retire your OpenClaw's parallel atom
pipeline cron entries. In `~/git/your-openclaw/workspace/cron.json`:
- Remove `atom-pipeline-coordinator` (every-30-min cron)
- Remove `atom-backfill-coordinator` (every-10-min cron)
Replace with nothing — gbrain's autopilot already runs extract_atoms +
synthesize_concepts inside every dream cycle when gbrain-creator (or
gbrain-everything) is the active pack.
The OpenClaw skills themselves shrink to thin wrappers:
- `content-atom-extractor` → calls `gbrain dream --phase extract_atoms`
- `concept-synthesis` → calls `gbrain dream --phase synthesize_concepts`
- `atom-backfill-coordinator` → DELETED (backfill is now part of
extract_atoms via the Source Quote + lesson enrichment in one
Haiku call per transcript)
## Rolling back
The import is fully reversible:
```bash
# Soft-delete every page with the marker (recoverable for 72h)
gbrain query "imported_from:markdown-greenfield" --type atom --json | \
jq -r '.[].slug' | xargs -I{} gbrain pages delete {}
# OR hard-delete past the soft-delete window
gbrain pages purge-deleted --older-than 0h
```
Your OpenClaw's `~/git/brain/atoms/` + `concepts/` + `ideas/` directories
are untouched by the importer — they remain the source of truth for
rollback. The greenfield importer only READS from them.
## Re-running after partial failures
The importer is idempotent at the page-slug level: re-running on the
same `--repo` produces zero net-new pages (every page either lands
fresh or matches an existing slug). If you fix some validation
failures in your OpenClaw and want to retry just those:
```bash
gbrain capture --source markdown-greenfield --repo ~/git/brain
```
Already-imported pages stay; previously-failed pages get a fresh
attempt; the audit JSONL accumulates per-week (ISO week file rotation).
-224
View File
@@ -1,224 +0,0 @@
# Tutorial: Build your first schema pack
You'll fork the bundled `gbrain-base` pack, add a custom `researcher` page type, import a handful of placeholder researcher pages, backfill their `page.type` column with one command, then prove the wiring works by running `gbrain whoknows` and seeing your new type surface in results. End state: a forked-and-active pack on disk, ~5 pages typed as `researcher`, and a query that proves the pack-aware routing fires end-to-end.
**Want the WHY before the HOW?** Read [`what-schemas-unlock.md`](what-schemas-unlock.md) first — 7 concrete use cases (4000 invisible meetings, the founder ops brain, the research brain, the legal brain, the team brain, agent-as-co-curator) plus the structural argument for why types matter at query time. Then come back here for the 5-minute walkthrough.
The whole walkthrough takes about 5 minutes. You'll see something working by step 3.
## What you'll need
- gbrain v0.40.7.0 or later (`gbrain --version` to check)
- A brain that's been initialized (`gbrain init` already run; either PGLite or Postgres is fine)
- A terminal you can paste commands into
That's it. No API keys required for this tutorial — every step works against the bundled pack and local-only commands.
## Step 1: See what pack is active today
```bash
gbrain schema active --json
```
You'll see something like:
```json
{
"pack_name": "gbrain-base",
"version": "1.0.0",
"sha8": "...",
"page_types_count": 22,
"source_tier": "default"
}
```
`source_tier: "default"` means you haven't customized anything — you're on the bundled pack. `page_types_count: 22` is the universal starter (person, company, meeting, note, etc.).
**You can't mutate bundled packs directly.** Step 2 forks it so you have something writable.
## Step 2: Fork the bundled pack
```bash
gbrain schema fork gbrain-base mine
```
Output: `Forked 'gbrain-base' → 'mine' at ~/.gbrain/schema-packs/mine/pack.json`.
The fork is a byte-for-byte copy of `gbrain-base` living at `~/.gbrain/schema-packs/mine/pack.json`. Now you have a writable pack you can mutate.
## Step 3: Activate the fork
```bash
gbrain schema use mine
```
Output: `Pack: mine (json) ... Active.`
Run `gbrain schema active --json` again to confirm `pack_name` is now `mine` and `source_tier` is `home-config` (read from `~/.gbrain/config.json`).
**You've already accomplished something visible** — the active pack changed, and any future query will route through your fork. The next four steps add a custom type and prove it works.
## Step 4: Add a researcher type
```bash
gbrain schema add-type researcher \
--primitive entity \
--prefix people/researchers/ \
--extractable \
--expert
```
Output: `Pack: mine (json)` + `Sha8: <prev> → <new>`.
What just happened:
- The mutation went through `withMutation`'s 8-step skeleton: bundled-guard → per-pack lock → read → mutate → file-plane lint validation → atomic write → audit log → cache invalidation.
- The pack now declares `researcher` as an entity primitive bound to `people/researchers/`, marked `extractable: true` (eligible for facts extraction) and `expert_routing: true` (surfaces in `whoknows` queries).
- An audit row landed in `~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl` with your type name SHA-8-redacted and the prefix's first segment only (`people`) for privacy.
Verify the type is in the pack:
```bash
gbrain schema explain researcher
```
You'll see the resolved settings printed back.
## Step 5: Import some placeholder researcher pages
You need pages under `people/researchers/` for the next step to do anything. If your brain repo already has them, skip ahead. If not, drop 3-5 placeholder markdown files into `<your-brain-repo>/people/researchers/` and import:
```bash
mkdir -p people/researchers
cat > people/researchers/alice-example.md <<'EOF'
---
title: Alice Example
---
ML researcher at Example Lab. Works on contrastive embeddings.
EOF
cat > people/researchers/bob-example.md <<'EOF'
---
title: Bob Example
---
Vision researcher at Widget University. Recent paper on diffusion models.
EOF
cat > people/researchers/charlie-example.md <<'EOF'
---
title: Charlie Example
---
RL researcher at Acme Research. Focus on inverse reinforcement learning.
EOF
gbrain sync
```
The sync imports the new files. They'll be stored in the database but their `type` column will still be empty — the new type was added to the pack AFTER these pages already existed (the typical real-world scenario for an agent walking into an existing brain).
## Step 6: See the gap with `stats`
```bash
gbrain schema stats --json | jq '.aggregate, .dead_prefixes'
```
You'll see `untyped_pages: 3` (or however many you just imported) and `dead_prefixes: []` — your new prefix has 3 matching pages, so it's not dead.
The 3 researcher pages are "orphaned" by type even though they live in the right directory. The next step backfills them.
## Step 7: Backfill with `sync --apply`
First dry-run to see what would happen:
```bash
gbrain schema sync --json
```
You'll see something like:
```json
{
"schema_version": 1,
"apply": false,
"per_prefix": [
{
"type": "researcher",
"prefix": "people/researchers/",
"would_apply": 3,
"sample_slugs": ["people/researchers/alice-example", "people/researchers/bob-example", "people/researchers/charlie-example"],
"applied": 0
}
],
"total_would_apply": 3,
"total_applied": 0
}
```
`would_apply: 3` is what you'd touch. `sample_slugs` is the agent's drilldown signal — if those slugs look wrong, abort. They look right, so apply:
```bash
gbrain schema sync --apply
```
You'll see per-batch progress lines on stderr and a final `total_applied: 3`. The UPDATE ran in chunks of 1000 (yours fit in one chunk) and never wedged any concurrent writer.
## Step 8: Prove the wiring works
```bash
gbrain whoknows "machine learning"
```
If your researcher pages contain ML-related content, they'll surface in the ranked results — even though they're typed `researcher`, not `person` or `company`.
**This is the load-bearing demonstration of T1.5 wiring.** Pre-v0.40.7.0, `whoknows` hardcoded `['person', 'company']` as the eligible types and would have ignored your `researcher` pages entirely. The v0.40.7.0 wiring consults the active pack's `expert_routing: true` types via `expertTypesFromPack(pack.manifest)`, so your custom type now routes through expert search.
## What you built
You now have:
- A fork of `gbrain-base` named `mine` at `~/.gbrain/schema-packs/mine/pack.json`, active in your brain via `~/.gbrain/config.json`.
- A `researcher` page type registered in the pack with `entity` primitive, `people/researchers/` prefix, `extractable: true`, `expert_routing: true`.
- 3 pages typed as `researcher` (backfilled from disk via `gbrain schema sync --apply`).
- A query path that routes through the new type: `gbrain whoknows` reads the pack and includes `researcher` in its type filter.
You also exercised the full mutation skeleton: bundled-pack guard, per-pack lock, validation gate, atomic write, audit log, cache invalidation. Every step was idempotent — re-running any of them is a no-op.
## Next steps
**Add a link verb.** A `researcher` can `author` a `paper`. To model that:
```bash
gbrain schema add-type paper --primitive annotation --prefix research/papers/ --extractable
gbrain schema add-link-type authored --page-type researcher --target-type paper
gbrain schema graph
```
The graph now shows `researcher --(authored)--> paper`.
**Add aliases for query closure.** If you want `gbrain query researcher` to also surface `person` rows (because researchers ARE people):
```bash
gbrain schema add-alias researcher person
```
Read [`skills/conventions/schema-evolution.md`](../skills/conventions/schema-evolution.md) for the decision tree on when to add types vs aliases vs prefixes. The short version: <20 pages → don't pack-codify; 20-100 → alias on existing type; 100+ → first-class type.
**Lint your pack before shipping.** The 11-rule lint surface (with the optional `--with-db` flag for DB-aware checks) catches dangling references, prefix collisions, and dead-corpus warnings:
```bash
gbrain schema lint --with-db
```
**Commit your pack to source control.** If `~/.gbrain/schema-packs/mine/` is a git repo, commit `pack.json` and push. Your pack survives across machines, and the `mutation_count_anomaly` lint rule will nudge you when you hit >50 mutations in a week (the "you should be committing this" signal).
**For agents (MCP):** the same operations are reachable over HTTPS MCP via 9 new ops. Register an admin-scope OAuth client and `schema_apply_mutations` lets a remote agent compose multi-step refactors as one atomic batch. The batched MCP op + per-pack lock + audit log are the load-bearing primitives that make remote schema authoring safe. See [`skills/schema-author/SKILL.md`](../skills/schema-author/SKILL.md) for the agent dispatcher.
**Undo a mistake.** Every mutation primitive has an inverse (`remove-type`, `remove-alias`, `remove-prefix`, `remove-link-type`, `set-extractable false`, etc.). If you fork twice and want to revert, `gbrain schema downgrade` restores the previous active pack from `~/.gbrain/schema-pack-history.jsonl`.
## Related docs
- **Reference:** `gbrain schema --help` for the full 22-verb CLI surface; CLAUDE.md's "Schema Cathedral v3 (v0.40.7.0)" section for the module-by-module architecture.
- **How-to:** [`skills/schema-author/SKILL.md`](../skills/schema-author/SKILL.md) — the agent dispatcher with the 7-phase workflow (brain → assess → propose → apply → sync → verify → commit).
- **Explanation:** [`skills/conventions/schema-evolution.md`](../skills/conventions/schema-evolution.md) — when to add a type vs alias vs prefix.
- **Plan + decisions:** the original design captured 21 decisions including the bundled-pack guard rationale (D6), the empty-filter fallback contract (D4), and the MCP non-localOnly trust posture (D2). Lives in `~/.claude/plans/system-instruction-you-are-working-recursive-thacker.md` (private).
-36
View File
@@ -1,36 +0,0 @@
# Tutorials
Step-by-step walkthroughs that take you from zero to a working outcome. Concrete commands, real numbers, no abstraction-first jargon. Each tutorial assumes no prior GBrain knowledge.
## Shipped
- [**Set up your personal AI agent + brain from zero**](personal-brain.md) — the canonical solo install. Two GitHub repos, a Telegram bot, AlphaClaw on Render, OpenClaw + GBrain + Supabase. End-to-end in about 2 hours; about $100 to $150 a month sustained. The full-stack install I'd run today.
- [**Set up GBrain as your company brain**](company-brain.md) — federated, multi-user, OAuth-scoped institutional memory for a 10-50 person team. Three sources (shared / customers / internal-only), per-user scope, first synthesized query as a teammate. About 90 minutes end-to-end, about $5 in API calls for the demo, under $100 a month sustained for a 25-person company.
- [**Auto-improve a skill with `gbrain skillopt`**](improving-skills-with-skillopt.md) — treat a `SKILL.md` as the trainable parameter of a frozen agent. Write your first benchmark from scratch (the part everyone gets stuck on), preview the cost, run the optimizer, read accepted vs no_improvement vs aborted, and accept a measurably better skill. About 20 minutes, about $1 in API calls. Reference: [`../guides/skillopt.md`](../guides/skillopt.md).
- [**Give your coding agent a memory: GBrain + Claude Code / Codex**](connect-coding-agent.md) — the two-funnel walkthrough for coding-agent users. Path A: connect Claude Code / Codex to a brain you already run (OpenClaw, Hermes, any `gbrain serve --http`). Path B: start from nothing with a 2-second local PGLite brain. Both end with the brain-first protocol you paste into `CLAUDE.md` / `AGENTS.md` and the four habits (brain-first lookup, ambient capture, briefing-from-your-brain, whoknows) that make it worth it. About 10 minutes.
## In progress
These are the next tutorials on the roadmap. Open an issue if one of them is the one you need most; that's how we'll prioritize.
- **Set up GBrain for VC dealflow** — the operator's recipe. People pages for founders, companies with typed Facts fence carrying ARR / team-size / runway across dates, meetings auto-ingested, deal pages linking everything. Shows `gbrain whoknows`, `gbrain find_trajectory`, and `gbrain founder scorecard` on real workflows.
- **Migrate your existing vault into GBrain** — for Notion / Obsidian / Roam users with a vault that doesn't match GBrain's default layout. Walks through `gbrain schema detect``suggest``review-candidates` so the brain learns your shape instead of forcing you to learn its.
- **Index your codebase as a code brain** — for developers. Initialize a brain in a code repo, swap to `voyage-code-3` for embeddings, use `gbrain code-def` / `gbrain code-refs` / `gbrain code-callers` to navigate the codebase semantically from any MCP-aware editor.
- **Run GBrain fully local with Ollama or llama.cpp** — for privacy-first deployments. No cloud calls, no API keys, no telemetry. Trades some retrieval quality for full local control. Useful for regulated industries, air-gapped environments, or just paranoia.
- **Set up the dream cycle** — the overnight enrichment daemon that makes the brain self-maintaining. Fixes citations, dedupes people pages, surfaces contradictions, generates founder scorecards on the schedule you configure. The piece that turns a static knowledge base into a brain that gets smarter while you sleep.
## Want to write one?
Tutorials follow the [Diataxis](https://diataxis.fr/) tutorial pattern: learning-oriented, walks a learner from zero to a working result in one session, every step produces a visible change. If you've used GBrain for something interesting and want to write the walkthrough, the existing [`company-brain.md`](company-brain.md) is the model. Open a PR.
## Related documentation
- **Reference:** [`docs/architecture/`](../architecture/) — system design, topologies, retrieval theory
- **How-to:** [`docs/guides/`](../guides/) — task-oriented runbooks (sub-agent routing, minion deployment, skill development, brain-first lookup, idea capture, diligence ingestion). Highlight: [scaling skills past 300](../guides/scaling-skills.md) — the three-tier architecture for agents that have outgrown the always-loaded skill manifest.
- **Integrations:** [`docs/integrations/`](../integrations/) — connecting external data sources (voice, email, calendar, embedding providers)
- **MCP setup:** [`docs/mcp/`](../mcp/) — per-client setup (Claude Desktop, Code, Cursor, ChatGPT, Perplexity, Cowork)
- **Install paths:** [`docs/INSTALL.md`](../INSTALL.md) — every install path, end to end
-557
View File
@@ -1,557 +0,0 @@
# Tutorial: Extend your personal brain into a company brain
This tutorial picks up where the [personal brain tutorial](personal-brain.md) leaves off. You already have a working agent (OpenClaw on Render, talking to you on Telegram, with GBrain as memory and Supabase storing embeddings). Now you want your whole team to use it as shared institutional memory, with each person seeing only what they're allowed to see.
**Time:** about 90 more minutes on top of the personal-brain install.
**Cost:** under $100 a month sustained for a 25-person company.
If you haven't done the personal-brain install yet, [start there first](personal-brain.md). Come back when you've got the agent responding to you on Telegram. This tutorial assumes that's already working.
I'm Garry Tan. I built GBrain to run my own AI agents at Y Combinator. After a couple of months of multi-user features landing (parallel sync across team sources, per-user OAuth scoping, leak-free isolation across every read path), it's finally usable as a company brain too. This is the recipe I'd run if I were standing it up for a 10-50 person company today.
---
## Part 1: The mental model
### What changes when you go from personal to company
The personal brain you built is a single-user system: one git repo, one agent, your stuff. The company brain is the same architecture with three additions:
1. **Multiple sources** inside the same brain. Your meeting notes are one source. Each teammate's customer notebook is another. The shared company wiki is a third. They live in the same database but stay independent.
2. **Per-user logins** with scopes. Each teammate gets their own OAuth credential. The credential decides which sources they can read and write to. Alice writes to her customer source, reads hers plus the shared one. Bob writes to internal-ops, reads his plus the shared one. Neither can see the other's writes.
3. **Per-person folders, crons, and skills.** The shared brain has shared structure, but each teammate gets their own subfolder for their own work, their own scheduled tasks (weekly digest, customer follow-ups), and their own scoped skills.
### What this is NOT
It is **not** a different install. The agent runtime, Supabase backend, GBrain CLI, and AlphaClaw harness from the personal brain stay exactly as you set them up. We're adding to that stack, not replacing it.
It is also **not** a thin-client-everywhere setup. Your personal agent stays as it is (OpenClaw + Telegram). Each teammate adds their own client of choice (Claude Code, Cursor, Claude Desktop, their own OpenClaw, whatever) and points it at the brain.
### What you get that one person's brain doesn't
- **Shared memory.** The whole team queries the same brain. The contract notes that Alice wrote on Tuesday show up when Bob asks about that customer on Friday, with citations back to Alice's notes.
- **Scoped privacy.** Performance reviews don't leak into customer queries. Legal docs don't leak into sales searches. We fuzz-tested this across every read path and got zero leaks.
- **One sync pipeline.** Your brain git repo (or several if you want them isolated per team) feeds the brain. Everyone sees the latest.
- **One operating burden.** One server to monitor, not one per user.
---
## Part 2: Switch the brain backend to multi-user Postgres
The personal-brain install uses Supabase as the embeddings layer but the GBrain runtime itself might be using PGLite (single-machine) depending on which path you took. For a company brain, you want a real Postgres for the runtime too. If your personal-brain install is already on Postgres or Supabase end-to-end, skip to Part 3.
If you're on PGLite, migrate:
```bash
gbrain migrate --to supabase
```
This copies every page, chunk, embedding, link, and config over to your Supabase project. Run from the agent host machine, same one you set up in the personal-brain tutorial. Takes a few minutes per 10K pages.
Verify:
```bash
gbrain doctor
gbrain stats
```
Page count and chunk count should match what you had on PGLite.
---
## Part 3: Carve up the brain into sources
The personal brain has one source (called `default`) holding everything. For a company brain we want multiple. The right shape depends on your org. Here's a typical starting point for a 10-50 person company:
```bash
# A shared all-hands source for content everyone reads
gbrain sources add shared --path /srv/brain-repos/shared --name "Shared company wiki"
# A scoped source for sales/customer notes
gbrain sources add customers --path /srv/brain-repos/customers --name "Customer notes"
# A scoped source for internal-only docs (legal, HR, performance, board)
gbrain sources add internal --path /srv/brain-repos/internal --name "Internal-only"
```
Each `--path` is a directory on disk where you've checked out a git repo. Create them:
```bash
sudo mkdir -p /srv/brain-repos
sudo chown $USER /srv/brain-repos
cd /srv/brain-repos
git clone git@github.com:your-org/shared-wiki.git shared
git clone git@github.com:your-org/customers.git customers
git clone git@github.com:your-org/internal-docs.git internal
```
You can also keep the existing personal-brain repo as one of the sources. Just pick the role it plays (probably `shared` if it's already org-wide content).
### Two scoping models (pick the one that matches your shape)
There are two ways to scope teammates' access. They suit different deployment shapes.
**Model A: separate sources with OAuth scoping (recommended for true multi-user with different AI clients).** What this tutorial walks you through. Each teammate gets their own OAuth client, which carries `--source` + `--federated-read` flags. The brain refuses cross-source reads at the SQL layer; isolation is database-enforced. Each teammate can run their own MCP-aware client (Claude Code, Cursor, their own OpenClaw, etc.) and the scoping holds.
**Model B: one source, directory-based per-person scoping (simpler for one-agent-serves-everyone setups).** The shape I actually run in production: a single source called `default`, with a `partners/<slug>/` convention inside it (e.g. `partners/alice-example/`, `partners/bob-example/`). Each partner gets their own subdirectory holding their personal pages: `partners/alice-example/USER.md`, `partners/alice-example/concepts/`, `partners/alice-example/sources/`, etc. There's no OAuth-enforced isolation; the agent itself enforces "Alice's writes go to her partners/ subdir." This is the right model when ONE agent (yours) serves everyone over Telegram or a single shared interface. It's simpler ops, no per-user OAuth, but the scoping is convention-only.
For most company-brain installs (10+ teammates each with their own AI client), Model A is the right starting point. If you're running the fat-agent-serves-everyone pattern from the personal-brain tutorial, Model B is genuinely simpler. You can also mix: separate sources for the obviously-different ones (customer notes vs internal-only) AND a `partners/<slug>/` convention inside the shared source for per-person workspace.
### Per-person folder structure inside each source
Inside each source, give each teammate their own subfolder. This is the structure I run:
```
customers/
├── alice-example/ ← Alice's customer notebook
│ ├── customers/
│ │ ├── acme-co.md
│ │ └── widget-systems.md
│ └── meetings/
│ └── 2026-05-21-acme-renewal.md
├── bob-example/ ← Bob's customer notebook
│ └── customers/
│ └── orbit-bio.md
└── shared-customers/ ← things both can see
└── all-active-deals.md
```
Two things this structure buys you:
1. **Each teammate's writes go to their own folder** even though they're in the same source. No accidental overwrites.
2. **You can later split a person's folder into its own source** (if Alice leaves and a new person takes her accounts, you can move `alice-example/` to a new source named after the new person and adjust scoping accordingly).
Same shape for `internal/`: `internal/alice-example/` for her HR docs, `internal/bob-example/` for his, `internal/legal/` for legal docs everyone can read, etc.
Now sync everything:
```bash
gbrain sync --all
```
Each source syncs in parallel under its own lock so they don't step on each other. Output looks like:
```
[shared] 100/100 pages
[customers] 240/240 pages
[internal] 85/85 pages
✓ all sources synced
```
Check the dashboard:
```bash
gbrain sources status
```
You should see all three sources with recent sync timestamps and page counts.
---
## Part 4: Expose the brain over HTTP MCP with OAuth
The personal brain talks to you through the AlphaClaw harness over Telegram. For a company brain we need a path that each teammate's AI client can hit independently. The HTTP MCP server is that path.
```bash
gbrain serve --http --port 3131 --bind 0.0.0.0
```
The `--bind 0.0.0.0` is important. By default the server binds to localhost only, which is correct for a personal install but blocks remote teammates. Setting `0.0.0.0` accepts connections from any interface.
The server prints an admin bootstrap token to stderr on first start. Save it. You'll use it once for the admin dashboard.
For development, tunnel the local server out via ngrok:
```bash
ngrok http 3131 --domain your-brain.ngrok.app
```
For production, put your server behind a real hostname with a real TLS certificate. Let's call your final URL `https://brain.acme-co.com` for the rest of this tutorial.
Re-run the server with the public URL so the OAuth discovery metadata matches what clients hit:
```bash
gbrain serve --http --port 3131 --bind 0.0.0.0 --public-url https://brain.acme-co.com
```
You should be able to hit `https://brain.acme-co.com/health` and get `{"status":"ok"}` back.
---
## Part 5: Register one OAuth client per teammate
Each teammate (or each AI agent for a teammate) gets their own OAuth client. The client controls what they can write and what they can read.
```bash
# Alice (sales): writes customers/alice-example, reads customers + shared
gbrain auth register-client alice-example \
--grant-types client_credentials \
--scopes read,write \
--source customers \
--federated-read customers,shared
# Bob (ops): writes internal/bob-example, reads internal + shared
gbrain auth register-client bob-example \
--grant-types client_credentials \
--scopes read,write \
--source internal \
--federated-read internal,shared
# Carol (legal): writes shared/legal, reads all three
gbrain auth register-client carol-example \
--grant-types client_credentials \
--scopes read,write \
--source shared \
--federated-read shared,customers,internal
```
Each `register-client` command prints a `client_id` and a `client_secret`. Save both for each teammate. They go into the teammate's local agent config.
A note on the flags:
- `--scopes read,write` lets the client query the brain and write new pages. You can omit `write` for read-only clients (executive summaries, dashboards). The `admin` scope is needed for operational commands like `gbrain remote doctor` and is usually reserved for your own admin client.
- `--source` controls write authority. A client can only write to one source. Within that source, your folder convention from Part 3 keeps each person's writes in their own subfolder.
- `--federated-read` controls read scope. A client can read from one or more sources.
### Verify the scoping actually scopes
Before you hand the brain to teammates, verify isolation. Two terminal windows on your local machine using each client's credentials:
```bash
# Terminal 1, as Alice
export GBRAIN_REMOTE_CLIENT_ID=<Alice's client_id>
export GBRAIN_REMOTE_CLIENT_SECRET=<Alice's client_secret>
export GBRAIN_REMOTE_MCP_URL=https://brain.acme-co.com/mcp
gbrain search "performance review" --remote
```
Alice should see results only from `customers` and `shared`. The performance-review notes live in `internal`, which she's not scoped to read. She shouldn't see them.
```bash
# Terminal 2, as Bob (export his credentials similarly)
gbrain search "performance review" --remote
```
Bob should see the performance-review notes from `internal`, plus anything related from `shared`. He shouldn't see anything that lives only in `customers`.
If both queries return correctly scoped results, isolation is working.
---
## Part 6: Set up per-person crons
The personal-brain install runs the dream cycle (overnight enrichment) once per night for one user. A company brain needs per-person crons because each teammate has their own context: Alice wants a 7am customer-pipeline digest, Bob wants a 9am ops-status report, Carol wants a contract-compliance check every Monday.
Each cron is just a scheduled `gbrain agent run` call scoped to the teammate's client credentials. The schedule lives in the workspace repo (the one AlphaClaw deployed in the personal-brain tutorial), in a `crons/` directory. A typical layout:
```
your-org/myagent/
└── crons/
├── alice-example/
│ └── 07am-customer-digest.md
├── bob-example/
│ └── 09am-ops-status.md
└── carol-example/
└── monday-contract-compliance.md
```
Each cron file declares its schedule and the prompt that the agent runs:
```markdown
---
schedule: "0 7 * * *"
client: alice-example
---
# Customer pipeline digest
Pull every customer page in customers/alice-example/ that had activity in
the last 7 days. For each, summarize what changed and what the next action
is. Output as a markdown digest, post to Slack #alice-customers, save a
copy to customers/alice-example/digests/YYYY-MM-DD-pipeline.md.
```
The `client:` field tells the cron runner which OAuth client to use, which enforces the scoping. Alice's cron can only read Alice's sources and write to Alice's folder. It cannot accidentally touch Bob's customer notes.
To install the cron schedule, commit the file to the workspace repo and let AlphaClaw pick it up on next deploy. The cron-scheduler skill (one of the 60 that GBrain installed) handles the dispatch.
---
## Part 7: Add per-person skills
The 60+ skills GBrain installs are generic. Your team probably wants a few that are specific to them. Examples:
- `onboarding-new-hire`. Only Carol (HR) runs this. Walks through generating a welcome packet, scheduling intro meetings, provisioning accounts.
- `customer-success-followup`. Only Alice (sales) runs this. Pulls latest customer page, drafts a follow-up email, posts to her review queue.
- `weekly-team-digest`. Only you (admin) run this. Aggregates everyone's published pages into one weekly summary.
Skills are just markdown files in the workspace repo's `skills/` directory. The shape:
```
your-org/myagent/
└── skills/
├── onboarding-new-hire/
│ └── SKILL.md
├── customer-success-followup/
│ └── SKILL.md
└── weekly-team-digest/
└── SKILL.md
```
Each `SKILL.md` declares the trigger (verbs in plain English the agent listens for) and the procedure. Use the `gbrain skillify scaffold <name>` command to generate the boilerplate:
```bash
gbrain skillify scaffold onboarding-new-hire
```
That creates the directory + SKILL.md + routing entry. Edit the SKILL.md to describe the procedure, commit, deploy. The agent picks up the new skill on next request.
Per-person scoping for skills is handled at the routing layer: a skill can declare `allowed_clients: [carol-example]` in its frontmatter. If Alice asks her agent to run that skill, the agent refuses with "this skill is scoped to carol-example."
### Shared rule files at the skills root
Alongside individual skill directories, drop a few flat `_*-rules.md` files at the root of `skills/`. These are conventions that EVERY skill reads. The ones I run in production:
- `_brain-filing-rules.md`. the iron-rule decision tree for "where does this new page belong?" Numbered first-match-wins rules (people go in `people/`, companies in `companies/`, meetings in `meetings/`, etc.). Every ingest skill consults this before creating a page.
- `_output-rules.md`. output quality standards (deterministic links built from API data not LLM-composed strings, exact-phrasing requirements for citations, no AI-slop vocabulary).
- `_excluded-people.md`. a privacy gate. Names that must never be referenced or attributed in the brain even if they appear in source material. Re-attribute or discard. This is the file that prevents your agent from accidentally publishing things about people you've decided aren't fair game.
- `_operating-rules.md`. operational conventions (when to write to brain vs scratchpad, when to ask for confirmation, when to fire a notification).
- `_x-ingestion-rules.md`, `_x-api-rules.md`. per-source rules for specific integrations (Twitter, in this case).
These files turn into the de facto company policy for the agent. Edit one, and every skill that reads it picks up the new rule on the next request. Versioned in git, reviewable in PR.
---
## Part 8: Wire Slack carefully
Slack is the integration most teams want first, and it has enough sharp edges to deserve its own callout. The conventions I run:
**Two crons, two jobs.** One scan cron that runs every 5-15 minutes and surfaces signals (new threads in channels you care about, mentions of your teammates, decisions). One archive cron that runs nightly and stores the full conversation history. Splitting them this way means urgent signals get acted on fast while the slow archive work doesn't crowd the live channel.
**Channel-to-task-ID mapping.** Don't have your agent reference Slack channels by their actual channel IDs (`C03A8...`). Build a `topic-registry.json` (or similar) that maps each channel ID to a friendly task name (`acme-co-customer-success`, `engineering-standup`). Crons and skills reference channels by friendly name; the registry translates to IDs at runtime. This is the file you edit when a channel gets renamed or replaced.
**Deterministic links only.** When your agent writes a brain page that cites a Slack message, the link MUST be built from API data (workspace ID + channel ID + message timestamp), never composed by the LLM. LLMs hallucinate Slack URLs constantly. The convention lives in `_output-rules.md`; every skill that touches Slack inherits it.
**Dismissed-items state.** The scan cron remembers what it has already surfaced. If a channel had a thread on Tuesday that turned out to be noise, the dismissed-items file records it so the Wednesday scan doesn't surface it again. Without this, re-scans become a flood of repeat signals.
**Per-channel scoping mirrors per-person scoping.** Sensitive channels (#executive, #legal, #performance) should be scoped to teammates with the appropriate `--federated-read`. The brain stores everything, but who can query for it is gated by the same OAuth client model from Part 5.
The actual skills that implement this in production are named `slack`, `slack-scan`, `slack-archive`. Scaffold equivalents in your workspace with `gbrain skillify scaffold slack-scan`, then edit the generated SKILL.md to declare your channel mapping and triggers.
---
## Part 9: Onboard each teammate yourself (the botmaster pattern)
This is the part that decides whether your company brain actually gets adopted or sits unused.
**Do not just hand a new teammate their OAuth credential and tell them to "try it out."** They'll send one query, get a result that doesn't feel personal yet (because their slice is empty), conclude it's not useful, and never come back.
What works instead: I personally onboard each new teammate myself. The flow looks like this.
### Step 1: Pre-populate their slice
Before they ever log in, I seed their `partners/<their-slug>/` directory (or their dedicated source) with the context they need to feel like the brain already knows them:
- `partners/alice-example/USER.md`. a one-page profile: role, focus areas, current top 3 priorities, the kind of questions they tend to ask, the kind of writing they prefer (terse vs detailed, casual vs formal).
- `partners/alice-example/concepts/`. 5-10 frameworks or recurring themes that are specifically THEIRS. If Alice runs sales, that's "pipeline stage definitions," "ICP criteria," "objection-handling playbooks."
- `partners/alice-example/sources/`. links to the documents they care about (their team's shared docs, their inbox conventions, the dashboards they check).
- 2-3 example brain entries that demonstrate the shape: a customer page they'd recognize, a meeting note from a recent meeting they attended, an idea they've shared with the team.
Takes me maybe 20 minutes per teammate. The payoff: the moment they run their first query, the brain answers with their context, not a generic response. That's the difference between "this is a cool tool" and "this knows me."
### Step 2: Walk them through 2-3 wow flows
Before letting them DM the agent freely, I personally walk them through 2-3 specific flows that I know will land:
1. A query that demonstrates synthesis: "ask the brain about [a customer they know well]. Notice how it pulls together pages from three sources into one answer with citations." This shows the brain layer in action.
2. A query that demonstrates gap analysis: "ask the brain about [something it doesn't know yet]. Notice how it tells you what's missing instead of making it up." This builds trust.
3. A write-back flow: "tell the brain about [a meeting they just had]. Notice how it auto-files, links to the other people who were there, and surfaces related history." This shows the agent's value as a capture tool, not just a query tool.
These three flows take maybe 15 minutes total. By the end, the teammate has seen the brain do something they couldn't have done themselves in that time. They feel powerful.
### Step 3: Graduate to DM only after the wow moment lands
After the walkthrough, I give them their OAuth credential and the agent's DM (Telegram, Slack DM, whatever your interface is). I explicitly say "now you can ask it anything, write to it anytime, and it'll keep learning from you."
The order matters. If you give them DM access first and expect them to discover the wow moments themselves, most won't. They'll send one generic query, get a generic answer, and bounce. The botmaster pattern (pre-populate → walk through → graduate to DM) flips the conversion rate.
Repeat this flow for every new teammate. About 45 minutes per person, total. Compared to the cost of an unadopted internal tool, it's the best 45 minutes you'll spend.
---
## Part 10: Connect each teammate's AI client
Each teammate runs their AI client (Claude Code, Cursor, Claude Desktop, OpenClaw, Hermes, whatever) configured to point at your brain server through their OAuth credentials.
Recommended path for each teammate: the thin-client install. On their machine:
```bash
curl -fsSL https://bun.sh/install | bash
bun install -g github:garrytan/gbrain
gbrain init --mcp-only \
--issuer-url https://brain.acme-co.com \
--mcp-url https://brain.acme-co.com/mcp \
--oauth-client-id <their client_id> \
--oauth-client-secret <their client_secret>
```
The thin-client install creates a local config that knows how to talk to your brain but never opens its own database. Most CLI commands route through the remote server transparently.
Now they configure their AI client. For Claude Desktop, the teammate adds an MCP server entry in `~/Library/Application Support/Claude/claude_desktop_config.json`:
```jsonc
{
"mcpServers": {
"company-brain": {
"command": "gbrain",
"args": ["serve"]
}
}
}
```
When Claude Desktop launches, it talks to the local `gbrain serve` stdio bridge, which forwards every request to your remote brain over HTTPS with their OAuth token attached. From Claude Desktop's perspective it's just one MCP server.
For Claude Code, Cursor, OpenClaw, Hermes, and other clients, per-client setup steps live in [`docs/mcp/`](../mcp/). They all follow the same shape: point the agent at the local `gbrain serve` bridge, which knows about the remote.
---
## Part 11: First real query as a teammate
Have Alice run a real query from her machine. The interesting verb is `gbrain think`, which gives back a synthesized answer instead of raw pages.
```bash
gbrain think "What's the latest update from acme-co? When did we last talk to them?"
```
What Alice gets back, assuming the brain has been syncing for a week and her sources contain a customer page for acme-co and several meeting notes:
```
## Answer
The most recent customer contact with acme-co was a renewal-discussion
meeting on 2026-05-18, attended by alice-example and acme-co's CTO. Key
points discussed [customers/alice-example/meetings/2026-05-18-acme-renewal]:
- They are upgrading their plan from team to enterprise.
- Annual contract value is moving from $48K to $180K.
- Decision driver: a new compliance requirement they have to meet by Q3.
Prior contact was a quarterly check-in on 2026-04-03 [customers/alice-example/meetings/2026-04-03-acme-q2-checkin].
**Gap noted:** No customer-success notes have been filed since the
2026-05-18 renewal meeting. If a follow-up has happened, it's not in
the brain yet.
```
Three things to notice:
1. **Sourced.** Every claim cites the meeting note it came from.
2. **Synthesized.** Alice didn't read three pages and stitch them together. The brain did.
3. **Honest about gaps.** The brain knows what it doesn't know and says so, instead of inventing a follow-up that didn't happen.
That last part is the gap analysis. It's the part of the brain layer that nobody else ships.
Bob asking the same question would get nothing about acme-co. He's not scoped to read `customers`. He'd see his own internal-ops content if he asked something relevant to that. Carol asking would see both, because she's scoped to read all three sources.
---
## Part 12: Operating the company brain
Three commands do most of the operational work.
### Background daemon: `gbrain autopilot`
The personal-brain install already turned this on. For a company brain, the same autopilot covers all your sources because they live in one database. It runs every five minutes; on a healthy brain (health score 95+) it sleeps; on a brain that's drifting it submits targeted maintenance jobs.
### Self-healing: `gbrain doctor --remediate`
```bash
gbrain doctor --remediate --yes --target-score 90 --max-usd 5
```
Computes a dependency-ordered plan of maintenance jobs that would raise the brain's health score to the `--target-score`, runs the plan, refuses to spend past the `--max-usd` cap. Safe to cron.
### Monitoring: `gbrain sources status` and the admin dashboard
```bash
gbrain sources status
```
Returns a per-source dashboard: when each source last synced, how many pages, how many embedded, how many unacked sync failures. The at-a-glance health check.
The admin dashboard at `https://brain.acme-co.com/admin` shows live request volume, registered OAuth clients, recent activity, and brain stats. Use the admin bootstrap token from Part 4 to log in the first time, then register additional admin users from inside the dashboard.
---
## Part 13: Cost and speed expectations
Real numbers from the published benchmark, running the default stack (GBrain with ZeroEntropy for embedding + reranker):
- **Embedding cost:** $0.05 per million tokens. For comparison, GBrain configured with OpenAI is $0.13 (2.6× more expensive), Voyage is $0.18 (3.6× more).
- **Ingest speed:** about 22 seconds for a small test corpus of 164 pages on the host machine. For a 10K-page corpus, expect about 20 minutes the first time, then most syncs are incremental and finish in seconds.
- **Query latency:** about 122 ms median for a `gbrain search`. For comparison, the same query through GBrain with OpenAI takes about 282 ms.
- **Synthesized-answer latency:** a few seconds, dominated by the Anthropic API.
- **Retrieval quality:** on the public LongMemEval benchmark, GBrain hits 97.60% recall at the top 5 retrieved sessions, beating the previous published state of the art at 96.6%. On the in-house BrainBench corpus of relational queries, GBrain beats commodity vector retrieval by 38 percentage points, because the graph layer surfaces relationships that vector similarity alone misses.
Full methodology and per-run receipt JSONs live in [the gbrain-evals repo](https://github.com/garrytan/gbrain-evals/blob/main/docs/benchmarks/2026-05-23-v0.40.6.0-snapshot.md).
For a 25-person company at sustained use, expect about $35 a month in embeddings (ZeroEntropy at $0.05/million tokens), $50 a month in Anthropic calls for the synthesized-answer queries, plus your hosting bill. Under $100 a month for the AI side at most companies your size.
---
## Part 14: Common gotchas
### "My teammate can't see anything"
Check `gbrain auth list` on the host and confirm their client has `--source` set to a source that actually exists. Empty or null `--source` means the client falls through to the `default` source, which probably has no content if you set up three named sources.
### "Sync is slow and feels stuck"
The first sync embeds every page, which takes time. Check `gbrain sources status` for the live page count. If it's climbing you're not stuck, you're just embedding. If you've got a 10K-page corpus and ZeroEntropy is being throttled, the per-source parallel sync looks like progress on three sources at once rather than one source moving fast.
### "I see a page I shouldn't see"
This shouldn't happen, but if you suspect it, run `gbrain search <query> --remote --json` as the constrained client and inspect the `source_id` field on every returned result. Every row should be in the client's `--federated-read` set. If one isn't, file an issue with the exact slug and source IDs.
### "The synthesized answer is wrong"
The brain layer is grounded in the retrieved pages. If the retrieved pages contain bad information, the answer will too. The gap-analysis note often catches this: if the answer says "based on retrieved pages from date X" and date X is six months ago, the brain is telling you the information is stale. Run `gbrain sync --all` to refresh and try again.
### "OAuth `/token` endpoint returns 401 for my client"
Verify the client secret matches what was printed at register-client time. The server stores only a SHA-256 hash; if you lost the original, you have to revoke the client and re-register. Use `gbrain auth revoke-client <client_id>` and re-run `register-client`.
### "Postgres connection is exhausting"
Each parallel sync worker opens its own pool. With three sources and the default four workers per source, you can hit your Postgres connection limit if it's set low. Either reduce the worker count with `gbrain sync --all --parallel 2 --workers 2`, or raise your Postgres `max_connections` to at least 100. Supabase's free tier defaults to 60, which is tight.
### "I want to add a fourth teammate but they need access to all three sources"
```bash
gbrain auth register-client diana-example \
--grant-types client_credentials \
--scopes read,write \
--source shared \
--federated-read shared,customers,internal
```
That's it. Add or rotate teammates as the org grows.
---
## What you built
You now have the personal-brain agent from the previous tutorial, plus a multi-user shared layer on top: three federated sources holding shared, customer, and internal-only content; per-person folders inside each source so teammates' writes don't collide; per-person OAuth clients with scoped read and write; per-person crons that run on each teammate's own schedule with their own scoping; per-person skills the agent only runs for the right person. Each teammate queries the brain in plain English through their AI client and gets back synthesized, sourced answers that are correctly scoped.
What to do next:
- **Wire ingestion** from external systems (Granola, Linear, Slack) using the [ingestion source contract](../skillpack-anatomy.md). Most companies want their meetings auto-ingested so the brain stays current without anyone typing notes.
- **Set up team-specific dashboards** through the admin UI. Each team lead can have their own view of brain health and activity.
- **Explore the rest of the brain layer.** `gbrain whoknows` (find the expert on a topic), `gbrain find_trajectory` (how a metric changed over time), `gbrain founder scorecard` (especially useful for VC and ops teams), the contradiction-detection cycle that surfaces conflicts between different people's notes.
If you're building in this space (which YC has flagged as the [company-brain category in its Request for Startups](https://www.ycombinator.com/rfs#company-brain)), you might as well build on this. Everything described above is open source, MIT licensed, and what I run in production behind my own AI agents.
Questions, gotchas, or wins worth sharing? Open an issue at [github.com/garrytan/gbrain](https://github.com/garrytan/gbrain/issues).
-235
View File
@@ -1,235 +0,0 @@
# Give your coding agent a memory: GBrain + Claude Code / Codex
Coding agents got very good at code. They're still amnesiac about everything
else. Claude Code and Codex forget your last conversation, can't tell you what
you decided three meetings ago, and re-derive context you already have written
down somewhere. GBrain is the retrieval layer that fixes that: search, synthesis,
and a self-wiring knowledge graph, wired into your agent over MCP.
There are two ways to do this. Pick the one that matches where you are:
- **Path A — I already run a brain** (OpenClaw, Hermes, or any `gbrain serve`
host) and I want my Claude Code / Codex to reach the same brain. → [jump to Path A](#path-a-connect-an-agent-to-a-brain-you-already-have)
- **Path B — I have nothing yet.** Spin up a local brain in 2 seconds and wire it
into my coding agent. → [jump to Path B](#path-b-start-from-nothing-local-brain-local-agent)
Both end in the same place: an agent that searches your brain before it answers,
and writes new knowledge back as you work. The last section,
[Now make it actually useful](#now-make-it-actually-useful), is the same for both
and is the part that changes how you work.
Prerequisite for either path: `bun install -g github:garrytan/gbrain`.
---
## Path A: connect an agent to a brain you already have
You already have a populated brain (the OpenClaw / Hermes case: it's on your
agent host, full of meetings, people, and ideas). You want Claude Code on your
laptop, and Codex too, to query it. This is the remote path: the host serves
HTTP, your laptop agents connect with a token.
### A1. On the host: serve over HTTP
If your host isn't already serving HTTP MCP, start it:
```bash
gbrain serve --http --bind 0.0.0.0 --public-url https://your-host.example.com
```
Two flags matter and people skip them:
- **`--bind 0.0.0.0`** — the default bind is `127.0.0.1` (loopback only), which
silently refuses every remote connection. If your agent "can't reach the
brain" and you didn't pass this, that's why. `gbrain serve --http` warns you at
startup when `--public-url` is set without `--bind`.
- **`--public-url`** — the externally reachable HTTPS URL (your Render/Railway
URL, ngrok domain, Tailscale Funnel, etc.). It's the issuer the OAuth/MCP
layer advertises.
Watch the startup banner. It now prints a `Skills:` line:
```
║ Skills: published ║
```
If it says `not published`, your connected agents will be able to search and
write but won't see your skill catalog (the OpenClaw skills that make your setup
special). Turn it on:
```bash
gbrain config set mcp.publish_skills true
```
(New brains from `gbrain init` default this ON. Brains upgraded from before
v0.41.36 stay OFF until you opt in, so this is the common gotcha for existing
OpenClaw users.)
### A2. On the host: mint a token
```bash
gbrain auth create "laptop-agents"
```
Copy the `gbrain_…` token it prints. It's a long-lived, full-access secret. Treat
it like a password; prefer a scoped OAuth client for anything cloud-hosted (see
[DEPLOY.md](../mcp/DEPLOY.md)).
### A3. On the laptop: one command per agent
```bash
# Claude Code
gbrain connect https://your-host.example.com/mcp --token gbrain_xxx --install
# Codex
gbrain connect https://your-host.example.com/mcp --token gbrain_xxx --agent codex --install
```
`--install` runs the agent's `mcp add` for you AND smoke-tests the token: it
actually calls `get_brain_identity` before handing off, so a wrong or expired
token fails right now, not silently on the agent's first request. You'll see:
```
Added MCP server 'gbrain' -> https://your-host.example.com/mcp.
Verified: {"version":"0.42.x","engine":"postgres","page_count":146646,...}
```
Drop `--install` to print a paste-ready block instead (useful when the host and
the agent are different machines, or you want to read before you run). Codex
reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands
in Codex's config file. Keep that variable exported in your shell profile.
### A4. Verify
In the agent: *"Call get_brain_identity, then search my brain for [a topic you
know is in there]."* You should get your own pages back. Done.
Full per-client detail: [Claude Code](../mcp/CLAUDE_CODE.md),
[Codex](../mcp/CODEX.md), [Perplexity](../mcp/PERPLEXITY.md).
---
## Path B: start from nothing (local brain, local agent)
No OpenClaw, no server, no token. The lowest-friction path in the whole product:
a local PGLite brain in the same process your agent spawns. Zero server, zero
tunnel.
### B1. Create a local brain
```bash
gbrain init --pglite # 2 seconds; embedded Postgres via WASM, no Docker
```
### B2. Put something in it
A brain with nothing in it answers nothing, so an empty brain on day one feels
broken. Two ways to fill it:
```bash
# Bulk-import a folder of markdown you already have:
gbrain import ~/notes/
# Or capture as you go (one thought at a time):
gbrain capture "Decided to use PGLite as the default engine: zero-config beats Postgres for <1000 files."
```
You don't have to import everything up front. The capture-as-you-go habit (see
the next section) means the brain fills with the decisions and context you
generate while working, and is genuinely useful by day two.
### B3. Wire it into your coding agent
```bash
# Claude Code
claude mcp add gbrain -- gbrain serve
# Codex
codex mcp add gbrain -- gbrain serve
```
That's the whole wire-up. No token, no URL, no tunnel. The agent spawns
`gbrain serve` as a stdio subprocess and talks to your local brain directly.
### B4. Verify
In the agent: *"search my brain for PGLite"* (or whatever you just captured). You
get the page back. The same brain is now query-able from the CLI
(`gbrain query "..."`) and from your agent.
---
## Now make it actually useful
Connecting is the easy part. The value comes from teaching your agent a few
habits. These are the patterns that turn a coding agent into a knowledge-aware
one. Paste the protocol below into your agent's instructions file
(`CLAUDE.md` for Claude Code, `AGENTS.md` for Codex / Cursor / others), then lean
on the patterns.
### The brain-first protocol (paste this in)
```markdown
## Brain-first protocol
You have a knowledge brain connected over MCP. Before answering any question
about people, companies, decisions, projects, or past context:
1. **Search first.** Call `search` (or `query` for a synthesized answer) against
the brain BEFORE answering from memory or asking me. If the brain has the
answer, use it. Never ask "who is X?" or "what did we decide about Y?" before
searching — the brain probably already knows.
2. **Write back.** When I make a decision, mention a new person/company, or land
on an idea worth keeping, write it to the brain with `put_page` (entity pages
under people/, companies/; decisions under decisions/ or notes/). One insight,
one page, linked.
3. **Cite.** When you answer from the brain, name the page you used.
```
### The four patterns worth stealing
These come straight from a production OpenClaw setup. They translate directly to
any coding agent with GBrain connected:
**1. Brain-first lookup (never ask what you can retrieve).** The single highest-
value habit. Before the agent asks you "which repo?" or "who owns this?", it
searches. Try: *"What did we decide about the auth rewrite?"* and watch it pull
the decision page instead of asking you to re-explain.
**2. Ambient capture (your brain as a side effect of working).** Don't make
saving a separate chore. Tell the agent: *"As we work, capture any decision or
new idea to the brain without interrupting."* After a month of this, you have
hundreds of linked pages and patterns you didn't know were there.
**3. Briefing from your brain (not from the internet).** *"What do I need to know
before my 2pm with the Acme team?"* pulls your meeting history, the people,
what's still open, what the brain doesn't know yet. The agent does your prep
because it read your context. (`query` gives you the synthesized answer with
citations; this is the example on the [README](../../README.md).)
**4. whoknows (expertise routing).** *"Who do I know who's shipped a rate
limiter in Postgres?"* The `find_experts` tool ranks people in your brain by
relevance + recency. Useful the moment your brain has more than a handful of
people in it.
That's the spine of it. Two commands to connect, one protocol to paste, four
habits to build. Your agent stops being amnesiac.
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Agent "can't reach the brain" (Path A) | `gbrain serve --http` bound to loopback | Restart with `--bind 0.0.0.0` |
| `list_skills` returns nothing / errors | Skill publishing OFF on the host | `gbrain config set mcp.publish_skills true` |
| Token rejected on first call | Wrong/expired token | Re-mint with `gbrain auth create`; `--install` smoke-tests it for you |
| `unknown tool: capture` | `capture` is CLI-only, not an MCP tool | Use `put_page` over MCP; `capture` only on the CLI |
| Empty results (Path B) | Brain has nothing in it yet | `gbrain import ~/notes/` or `gbrain capture "..."` |
## Next steps
- Go full autonomous: the overnight enrichment daemon ([dream cycle](../../CHANGELOG.md)) fixes citations, dedupes people, builds scorecards while you sleep. See `gbrain autopilot --install`.
- Run a real agent platform on top: [personal-brain tutorial](personal-brain.md).
- Scale to a team: [company-brain tutorial](company-brain.md).
- Every MCP client's exact setup: [`docs/mcp/`](../mcp/).
@@ -1,297 +0,0 @@
# Auto-improve a skill with `gbrain skillopt`
You have a `SKILL.md`. Sometimes the agent following it does a great job, sometimes
it forgets a step or pads the output. This tutorial takes you from that skill to a
measurably better version of it, in one session, without you hand-editing the
prose. By the end you'll have written your first benchmark, watched the optimizer
propose and test edits, and accepted an improvement that actually scored higher.
Time: ~20 minutes. Cost: ~$1 in API calls for the worked example.
Based on [SkillOpt](https://arxiv.org/abs/2605.23904) (Microsoft Research, May 2026).
## The mental model (two sentences)
Your `SKILL.md` is the trainable parameter; the agent that reads it never changes.
SkillOpt runs the agent against a benchmark of realistic tasks, proposes specific
edits to the skill body, re-tests, and keeps a change **only when it measurably
beats the current version** on a held-out slice.
That's the whole idea. The benchmark is how "better" gets defined — which is why
writing it is the one part you can't skip. Everything else is mechanical.
## The easiest path: generate a starter, then strengthen it
You don't start from a blank file. One command reads the SKILL.md and writes a
full starter benchmark for you:
```bash
gbrain skillopt meeting-prep --bootstrap-from-skill
```
It infers what the skill produces, writes ~15 tasks (each with rule judges) to
`skills/meeting-prep/skillopt-benchmark.jsonl`, and appends a
`# BOOTSTRAP_PENDING_REVIEW` sentinel so nothing runs until a human has looked.
Then you **review and strengthen the judges** (the generated checks are weak
drafts), delete the sentinel line, and run:
```bash
gbrain skillopt meeting-prep --bootstrap-reviewed --split 1:1:1
```
If you run an agent over this brain (OpenClaw, Claude Code, Cursor, any MCP client
with the gbrain skills installed), it does this for you: just say "improve my
meeting-prep skill." It runs `--bootstrap-from-skill`, strengthens the judges,
dry-runs for cost, runs the optimizer, and reports the diff + score delta back.
You keep or discard.
**Read the rest of this tutorial to understand what that command produces** — the
benchmark format, how to strengthen a draft (or write one by hand), how to read
the outcome, and where the output lands.
## What you'll need
- `gbrain` installed and a brain initialized (`gbrain --version` works).
- One embedding/chat provider configured. SkillOpt makes real LLM calls.
`gbrain models doctor` should show at least one reachable chat model.
- A skill you want to improve, living at `skills/<name>/SKILL.md`. This tutorial
uses a skill called `meeting-prep` — substitute your own name everywhere.
- A clean git working tree for that skill file (SkillOpt refuses to run over
uncommitted changes so it can never clobber your edits; `--force` overrides).
If you don't have a skill yet, scaffold one first:
```bash
gbrain skillify scaffold meeting-prep
```
## Step 1: Get a benchmark — generated or hand-written
A benchmark is a `.jsonl` file — **one JSON object per line** — where each line is
a task plus a way to score the agent's answer. It's the crux: the benchmark IS
your definition of "better."
**The recommended way is to generate a starter** (the section above):
`gbrain skillopt meeting-prep --bootstrap-from-skill` writes the file for you, then
you strengthen the judges. The format below is exactly what it produces, so this
section doubles as your guide to reviewing and sharpening a generated draft.
**To follow this tutorial verbatim** (or to hand-curate from scratch), paste this
complete 15-task starter. It's deliberately generic — once you've seen the loop
work, **replace these tasks with your skill's real cases** (that's Step 6):
```bash
cat > skills/meeting-prep/skillopt-benchmark.jsonl <<'EOF'
{"task_id":"mp-001","task":"Prep me for a 1:1 with a direct report I haven't met with in 3 weeks.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"agenda"},{"op":"contains","arg":"follow-up"}]}}
{"task_id":"mp-002","task":"Prep me for a first sales call with a company I know nothing about.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"company"},{"op":"min_citations","arg":1}]}}
{"task_id":"mp-003","task":"Prep me for a board meeting where I present the quarterly numbers.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"metric"}]}}
{"task_id":"mp-004","task":"Prep me for a performance review I'm giving to an underperformer.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"example"}]}}
{"task_id":"mp-005","task":"Prep me for a candidate interview for a senior backend role.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"question"}]}}
{"task_id":"mp-006","task":"Prep me for a vendor renewal negotiation where I want a discount.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"leverage"}]}}
{"task_id":"mp-007","task":"Prep me for a kickoff with a new cross-functional project team.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"goal"},{"op":"contains","arg":"owner"}]}}
{"task_id":"mp-008","task":"Prep me for a difficult conversation about a missed deadline.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"impact"}]}}
{"task_id":"mp-009","task":"Prep me for an investor update call after a flat quarter.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"metric"},{"op":"min_citations","arg":1}]}}
{"task_id":"mp-010","task":"Prep me for a skip-level with someone two reports below me.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"question"}]}}
{"task_id":"mp-011","task":"Prep me for a customer escalation call after an outage.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"timeline"}]}}
{"task_id":"mp-012","task":"Prep me for a partnership exploration call with a competitor-adjacent company.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"company"},{"op":"min_citations","arg":1}]}}
{"task_id":"mp-013","task":"Prep me for a sprint retro where morale has been low.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"action"}]}}
{"task_id":"mp-014","task":"Prep me for a salary negotiation a report initiated.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"market"}]}}
{"task_id":"mp-015","task":"Prep me for an all-hands where I announce a reorg.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"why"}]}}
EOF
```
Each line has three fields:
- `task_id` — a unique label. Anything; you'll see it in the audit trail.
- `task` — the prompt the agent gets, exactly as a user would phrase it.
- `judge` — how the answer is scored. `kind: "rule"` is deterministic and **free**
(no LLM call): it runs a list of `checks`, and the task's score is the fraction
that pass.
The rule checks you can use:
| `op` | `arg` | Passes when the agent's answer… |
|---|---|---|
| `contains` | string | includes that substring |
| `regex` | string | matches that regex (multiline) |
| `section_present` | heading text | has a markdown heading with that text |
| `max_chars` | number | is at most that many characters (punishes padding) |
| `min_citations` | number | has at least N citations (markdown links, `wiki/…` refs, `[1]` footnotes) |
| `tool_called` | tool name | the agent called that tool during the rollout |
| `tool_not_called` | tool name | the agent did NOT call that tool |
Rule judges are the right place to start. They're free, deterministic, and they
force you to say concretely what a good answer looks like. (`judge.kind` can also
be `"llm"` with a rubric, or `"qrels"` for retrieval tasks — see the
[reference guide](../guides/skillopt.md) once you outgrow rules.)
### The one gotcha: how many tasks you need
SkillOpt splits your benchmark three ways — **train** (propose edits against),
**sel** (the held-out gate that decides accept/reject), and **test** (final
score). The sel slice must have **at least 5 tasks** or the run refuses, so noise
can't masquerade as improvement.
The default split is `4:1:5`, which means sel is 1/10th of your tasks — so the
default needs **~50 tasks** before it'll run. That's too many for a first
benchmark, which is why every command below passes `--split 1:1:1`: with the
15-task starter that's a clean **5 train / 5 sel / 5 test**, and sel hits the
floor exactly.
```bash
# 15 tasks + --split 1:1:1 → 5 train / 5 sel / 5 test
gbrain skillopt meeting-prep --split 1:1:1
```
If you ever see `D_sel has N task(s) after split (need >=5)`, you either added
fewer than 15 tasks or used a split whose middle number is too small a share.
`--split 1:1:1` on 15+ tasks is the simplest thing that works.
> When you swap in your own tasks (Step 6), keep at least 15 and cover the boring
> middle, not just the edge cases. The benchmark IS your definition of quality;
> a thin benchmark optimizes for a thin definition.
## Step 2: Preview the cost (dry run)
Before spending anything, see what the run will cost:
```bash
gbrain skillopt meeting-prep --split 1:1:1 --dry-run
```
This makes **zero LLM calls** — it just prints the plan and the cost estimate.
A ~15-task benchmark with defaults runs around $0.70$1.00. The preflight refuses
to start a real run whose estimate exceeds `--max-cost-usd` (default $5.00), so
you can't get surprise-billed mid-run.
> `--dry-run` exits with code **2** ("aborted"). That's the convention for "did
> not run the optimization," not a failure. The cost line is what you came for.
## Step 3: Run it for real
```bash
gbrain skillopt meeting-prep --split 1:1:1
```
You'll watch it work: a baseline eval to set the bar, then per-step forward passes
(run the skill), backward passes (propose edits), and a validation gate that
runs each sel task's judge 3 times and takes the median — accepting only if the
median beats the current best by more than 0.05.
When it finishes, the last lines tell you everything:
```
[skillopt] Outcome: accepted
[skillopt] Best sel-score: 0.840
[skillopt] Final cost: $0.71
[skillopt] SKILL.md rewritten with 6 optimization steps.
```
### Reading the outcome
| Outcome | Exit code | What it means | What to do |
|---|---|---|---|
| `accepted` | 0 | A candidate beat the baseline. SKILL.md was rewritten (or a proposed file written — see Step 5). | Review the diff, keep it. |
| `no_improvement` | 1 | Nothing cleared the gate. Your skill is already good, or the benchmark can't tell good from bad. | Strengthen the benchmark (Step 6) or stop. |
| `aborted` | 2 | A gate stopped it: dirty working tree, over budget, `D_sel < 5`, or `--dry-run`. | Read the message — it names the gate. |
`no_improvement` is not a failure. It's the gate doing its job: it would rather
keep your known-good skill than accept a change it can't prove is better.
## Step 4: See what changed
The optimizer leaves a full audit trail under the skill:
```bash
ls skills/meeting-prep/skillopt/
```
```
best.md ← the current winning version (== SKILL.md when accepted)
versions/
v0001_e1_s1.md ← every step's candidate, so you can diff any of them
v0002_e1_s2.md
...
history.json ← append-only record of every accept/reject + scores
rejected.json ← edits that were tried and didn't help (so it won't retry them)
```
The actual change to your skill is a normal git diff:
```bash
git diff skills/meeting-prep/SKILL.md
```
Run-level events (cost, model, scores per run) also land in the rotating audit
log at `~/.gbrain/audit/skillopt-YYYY-Www.jsonl`.
## Step 5: Accept or reject — and the bundled-skill rule
**For a skill you own** (your own `skills/` dir): an `accepted` run rewrites
`SKILL.md` in place. It's already a git diff — review it, then `git commit` to
keep it or `git checkout` to throw it away. Nothing is committed for you.
**For a skill that ships with gbrain** (anything under the gbrain repo's own
`skills/`): SkillOpt refuses to overwrite it by default and writes the winner to
`skills/<name>/skillopt/best.md` instead, so an optimization pass can never
silently mutate a skill other people depend on. Two ways to handle that:
```bash
# See the proposed improvement without touching SKILL.md (works for ANY skill):
gbrain skillopt meeting-prep --split 1:1:1 --no-mutate
# → writes skills/meeting-prep/skillopt/best.md (the proposed rewrite), prints its path. Copy what you want.
# Actually rewrite a bundled skill (explicit opt-in + an independent held-out set):
gbrain skillopt brain-ops --split 1:1:1 --allow-mutate-bundled \
--held-out skills/brain-ops/held-out.jsonl
```
Rewriting a bundled skill in place now requires BOTH `--allow-mutate-bundled` AND
`--held-out <path>` (a JSONL with the same shape as your benchmark, but at least 5
tasks whose IDs don't appear in the benchmark). The held-out set is how the run
proves the edit didn't just learn the benchmark: a candidate that climbs the
benchmark but slips on the held-out tasks is refused. Drop `--held-out` and the
run hard-refuses and points you at `proposed.md` instead.
Rule of thumb: `--no-mutate` when you want to read the diff before trusting it
(no held-out needed); `--allow-mutate-bundled --held-out` only when you intend to
commit a proven change to a shared skill.
## Step 6: Iterate
The loop that actually makes skills better:
1. Run it. If `no_improvement`, the benchmark probably can't distinguish good
from bad yet.
2. Add tasks that capture what you wish the skill did differently. Saw the agent
skip citations? Add `{"op":"min_citations","arg":2}`. Saw it ramble? Tighten
`max_chars`.
3. Re-run. A sharper benchmark gives the optimizer a real gradient to climb.
4. When a run lands `accepted`, read the diff, commit it, and bank the win.
The skill you ship gets better every time the benchmark gets sharper. That's the
whole game: you're not editing prose, you're improving the definition of done and
letting the optimizer chase it.
## What you built
You wrote a benchmark that encodes what "good" means for one skill, previewed the
cost, ran the optimizer, and either accepted a measurably better skill or learned
your benchmark needs sharpening. Same loop scales to every skill you own — and
`gbrain skillopt --all` runs it across every skill that has a benchmark, under a
brain-wide cost cap.
## Where to go next
- **Full flag + exit-code reference, cost model, safety guards:**
[`docs/guides/skillopt.md`](../guides/skillopt.md)
- **Every flag inline:** `gbrain skillopt --help`
- **Batch + fleet + background runs** (`--all`, `--target-models`, `--background`),
**LLM and qrels judges**, **held-out test sets**, and **resume after a crash**
(`--resume <run-id>`): all in the reference guide above.
- **Generate a starter benchmark from the SKILL.md** (the recommended way to start):
`gbrain skillopt <name> --bootstrap-from-skill` → review + strengthen the judges →
delete the sentinel → `--bootstrap-reviewed --split 1:1:1`. Tune the count with
`--bootstrap-tasks N` (max 50).
- **Bootstrap from existing routing fixtures** instead: `gbrain skillopt <name>
--bootstrap-from-routing` (routing tasks test dispatch, not quality — tighten them).
-257
View File
@@ -1,257 +0,0 @@
# Tutorial: Set up your personal AI agent + brain from zero
By the end of this tutorial you'll have your own AI agent running on a server you control, talking to you over Telegram, with a brain that remembers everything you tell it. About two hours end-to-end, $100 to $150 a month sustained.
This is the install I'd run if I were setting up the whole stack from scratch today. I documented it live during a setup session with a collaborator (we used Granola to capture the screen because "this is already too complicated for an archetypical person"). The tutorial is the cleaned-up version of that session.
> "This is the Apple I, we're just soldering breadboards over here."
If you only want the **brain layer** (no agent, no Telegram, just gbrain as memory for an MCP client you already use), skip to the [CLI standalone install](../INSTALL.md#2-cli-standalone) in INSTALL.md. If you want the whole agent **shared with a team**, read the [company brain tutorial](company-brain.md) instead. This tutorial is the solo, full-stack, talk-to-it-on-Telegram path.
---
## What you're building
A personal AI agent with four pieces:
- **A brain** (git repo). Your knowledge base, constantly ingesting and growing.
- **A harness** (OpenClaw via AlphaClaw). The runtime that gives the LLM tools, memory, and integrations.
- **A chat interface** (Telegram). How you talk to it.
- **Skills** (60+ installed via GBrain). Reusable capabilities the agent can invoke.
Architecture:
```
Telegram → AlphaClaw (harness) → OpenClaw (agent) → GBrain (knowledge/skills) → Supabase (embeddings/search)
```
Git repo is the system of record. The whole thing is multiplayer by default: any agent that hooks into the repo works. Conflicts resolve through git.
---
## Prerequisites
| Requirement | Why |
|---|---|
| GitHub account (org or personal) | For the two repos that store the agent + brain |
| Render account | For hosting the agent runtime |
| Telegram account | For talking to your agent |
| API keys: OpenAI, Anthropic at minimum | Embeddings + the Claude model |
| About $100 to $150 a month | Render Pro + Supabase + API usage |
---
## Step 1: Create two GitHub repos
You need two repos, not one.
1. **Workspace repo.** Agent configuration, skills, memory, crons. Example name: `your-org/myagent`. Private.
2. **Brain repo.** Knowledge base, people pages, meeting notes, all the content the agent reads and writes. Example name: `your-org/myagent-brain`. Private.
```
GitHub → New Repository → your-org/myagent (workspace)
GitHub → New Repository → your-org/myagent-brain (brain)
```
Both repos start empty. GBrain will populate the brain repo with its default structure on first install.
---
## Step 2: Generate a fine-grained Personal Access Token
GitHub → Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens.
- **Name:** `myagent-token`
- **Expiration:** 1 year (or no expiration if available)
- **Repository access:** select both repos only
- **Permissions:** Read AND Write access to both repos (Contents, Metadata, Pull requests)
GitHub's fine-grained PAT UI is painful. You may need to reload the page after creating repos before they appear in the selector. This is the worst part of the whole setup. Push through.
Save this token. You'll need it for the AlphaClaw setup.
---
## Step 3: Create a Telegram bot
1. Open Telegram, message [@BotFather](https://t.me/BotFather)
2. Send `/newbot`
3. Name your bot (whatever you want)
4. Get the bot token
5. Save it. You'll need it for the AlphaClaw setup.
---
## Step 4: Deploy via AlphaClaw on Render
AlphaClaw is the setup harness that manages OpenClaw deployment.
1. Go to [alphaclaw.com](https://alphaclaw.com)
2. Enter your **workspace repo** (not the brain repo): `your-org/myagent`
3. Select "Use existing" if the repo already exists
4. Enter your GitHub PAT from Step 2
5. Enter your Telegram bot token from Step 3
6. Deploy
Render will build a Docker container with the harness. First deploy takes about 5 minutes.
**Memory matters.** If the instance runs out of memory during install, upgrade to Render Pro. The base tier is too small for GBrain + OpenClaw together. My production instance runs 48 cores and 64GB RAM (about $1,500 a month) but that's overkill for a new setup. Pro tier ($85 a month) is the minimum viable.
---
## Step 5: Add provider API keys
In the AlphaClaw UI (Providers tab):
- **OpenAI API Key.** Required for embeddings if you use the OpenAI provider.
- **Anthropic API Key.** Required for Claude (the main model the agent talks through).
- **Perplexity API Key.** Optional, for web search.
- **Voyage API Key.** Optional, alternative to OpenAI for embeddings.
- **ZeroEntropy API Key.** Recommended. GBrain ships with ZeroEntropy as the default embedder + reranker because it's about 2× faster than OpenAI and about 2.6× cheaper.
You can use the same keys across multiple agents.
---
## Step 6: Install GBrain
Once OpenClaw is running:
```bash
gbrain install
```
This installs:
- About 60 skills
- About 9 skill packs
- Default brain structure
- MCP server configuration
- Supabase connection (for embeddings and search)
GBrain populates the brain repo with its default directory structure, skill files, and configuration. From this point, the agent has working memory and access to every skill.
---
## Step 7: Set up Supabase (embeddings and search)
GBrain uses Supabase for vector embeddings and full-text search at scale. There are three setup gotchas I hit the hard way. Walk through them in this order.
### 7a. Create the project and turn on pgvector
1. Create a Supabase project at [supabase.com](https://supabase.com). Pick a region close to where your Render host runs.
2. In the Supabase dashboard, go to **Database → Extensions**.
3. Find `vector` (the pgvector extension) and toggle it on.
Skip this and every embed write fails with "type vector does not exist" the moment GBrain tries to create its schema. pgvector is what stores the embeddings; the schema migrations refuse to run without it. Five seconds in the UI; an hour of debugging if you forget.
### 7b. Get the CONNECTION POOLER connection string, not the direct one
In **Project Settings → Database → Connection string**, Supabase shows you two options. They look almost identical. Use the right one.
- **Direct connection** (port 5432). Talks straight to the Postgres instance. IPv6-only. Will fail if your Render host doesn't have IPv6 outbound (most don't by default).
- **Connection pooler** (port 6543, hostname starts with `aws-0-...pooler.supabase.com`). Talks through Supabase's pgbouncer. Works over IPv4. Survives connection storms from parallel workers.
You want the **connection pooler** string. Format looks like:
```
postgresql://postgres.YOUR-PROJECT:YOUR-PASSWORD@aws-0-us-west-1.pooler.supabase.com:6543/postgres
```
Configure it via:
```bash
gbrain config set database_url "postgresql://postgres.YOUR-PROJECT:YOUR-PASSWORD@aws-0-us-west-1.pooler.supabase.com:6543/postgres"
```
### 7c. Buy the IPv4 add-on if your host is IPv4-only
Even with the pooler, some Supabase regions and some Render plans hit IPv6 resolution snags. If your `gbrain doctor` shows connection failures and the error mentions "network unreachable" or hangs forever on connect, you need Supabase's **IPv4 add-on**.
In the Supabase dashboard, **Project Settings → Add-ons → IPv4 address**. About $4 a month. Toggle on, wait a minute, retry the connection. This bit me on multiple installs before I learned to just buy it up front.
### 7d. Verify the connection
```bash
gbrain doctor
```
Green checks on schema, connectivity, pgvector extension, embedding provider. If any of those are yellow, the message will tell you which gotcha you hit (and which of 7a / 7b / 7c to revisit).
### Operating note
Supabase is usually the scaling bottleneck, not CPU or LLM calls. If you're doing heavy ingestion (emails, calendar, Slack streaming in), upgrade from small to large DB instance early. Don't wait for the small instance to choke; the symptoms (silent failed inserts, sync timeouts, embedding backfill stalls) all look like different bugs but are the same bug.
---
## Step 8: Verify and chat
1. Open Telegram
2. Message your bot
3. It should respond using OpenClaw + GBrain
Send a test message. If it responds with context-awareness and can search the brain, you're live.
---
## Architecture notes
### Git as system of record
The brain repo IS the brain. Any agent that can read and write to the git repo can participate. This makes the architecture inherently multiplayer: multiple agents can share a brain, work on different parts, and resolve conflicts through git.
### Thin client vs fat client
- **Fat client** (my production setup). OpenClaw + AlphaClaw + GBrain + 200 crons + email processing + Slack + calendar. About $1,500 a month. Processes everything in real time.
- **Thin client** (what this tutorial builds). OpenClaw + GBrain + Telegram. About $85 a month. Chat-driven, on-demand.
The goal for GBrain is to make the thin client as awesome as the fat client. Most users will start thin and grow.
### MCP server
GBrain exposes a Model Context Protocol server that enables inter-agent communication and integration with external systems. This is how you add read and write access to your product's API, databases, or other services.
### Brain sharing
Brains share through git. My main agent can populate another agent's brain by pushing content to its repo. The MCP layer enables cross-agent brain queries. Just push to the git repo and the other agent picks it up on next sync.
---
## What this costs
| Component | Monthly cost |
|-----------|-------------|
| Render Pro (minimum viable) | about $85 |
| Supabase (small) | free to $25 |
| OpenAI API (embeddings) | $5 to $20 (much less if you use ZeroEntropy as the default) |
| Anthropic API (Claude) | $50 to $500 (usage dependent) |
| **Total minimum** | **about $100 to $150 a month** |
My production setup is about $10,000 a month, but that's 10 instances, 200 crons, processing email and Slack and calendar in real time, running sub-agents. Not what you need on day one.
> "Next year it's not going to cost $10,000 a month. It'll cost $1,000 a month. And then the year after that, it'll be $100 a month, and then everyone will have it."
---
## Common issues
1. **Render runs out of memory during install.** Upgrade to Pro tier.
2. **GitHub PAT can't see the repos.** Reload the page after creating repos. Make sure the fine-grained token has the correct repo selection.
3. **Telegram bot doesn't respond.** Check the bot token in AlphaClaw. Make sure the Render instance is actually running.
4. **Supabase bottleneck on heavy ingestion.** Upgrade the DB instance size before the small one chokes.
5. **GBrain.io provisioning fails.** The hosted instance may need Pro tier. Check the machine allocation in the AlphaClaw UI.
---
## What you built
You now have a personal AI agent running on Render, talking to you on Telegram, with a brain that ingests and remembers everything you tell it. Every conversation gets indexed, every new entity (person, company, deal, concept) gets its own page, the overnight enrichment daemon dedupes and consolidates while you sleep. You wake up with a smarter agent than the one you went to bed with.
Where to go next:
- **Wire ingestion** from external systems. Email, calendar, voice calls, tweets, Slack. The skills are already installed; you just configure the credentials. See [`docs/integrations/`](../integrations/) for per-source recipes.
- **Connect your existing AI client** (Claude Code, Cursor, Claude Desktop) to the same brain. See [`docs/mcp/`](../mcp/) for per-client setup.
- **Set up the dream cycle** properly. The autopilot daemon runs overnight enrichment by default but you can tune what it does. See [`docs/architecture/`](../architecture/) for the full cycle reference.
- **Add a teammate to your brain**, or stand the whole thing up as a company brain. See the [company brain tutorial](company-brain.md) for the multi-user walkthrough.
Questions, gotchas, or wins worth sharing? Open an issue at [github.com/garrytan/gbrain](https://github.com/garrytan/gbrain/issues).
-177
View File
@@ -1,177 +0,0 @@
# What schemas unlock
Most note-taking apps treat every page the same. You write something, it goes in a pile, you search the pile with text matching. Tags help, but tags are flat. After a few thousand pages, the pile gets noisy and the search gets stupid.
Schemas are how gbrain stops being a pile of notes and becomes something with structure. A schema declares what KINDS of things live in your brain (`person`, `company`, `meeting`, `researcher`, `case`, `lab-result`), what they link to (`attended`, `authored`, `prescribed-by`), what facts the system should extract automatically (`mrr=50000`, `damages=5000000`), and which types route through expert search vs general search.
The default schema (`gbrain-base`) ships with 22 page types covering the universal shapes — people, companies, meetings, notes, daily, calendar events. That's enough to start. But your brain is yours, and your brain's shape is not the default shape. A research brain needs `researcher` and `paper` as first-class types. A founder brain needs `lead`, `investor`, `portco`, `deal-stage`. A lawyer brain needs `case`, `motion`, `deposition`, `precedent`. Same engine, totally different shape.
v0.40.7.0 made it possible for AGENTS to author that shape for you. Not just "the user manually edits YAML in `~/.gbrain/schema-packs/mine/pack.yaml`" but "your agent sees the corpus, proposes a type, asks for approval, applies it atomically with a full audit trail, then backfills 4000 existing pages with one chunked SQL command." That's the new thing.
This doc is the WHY. The [tutorial](schema-author-tutorial.md) is the HOW.
## Killer use cases
### 1. The 4000 invisible pages
You have 4000 markdown files under `meetings/` going back two years. The default schema doesn't have a `meeting` type, so all 4000 are typed `note` (the catchall). When you run:
```bash
gbrain whoknows "Q3 roadmap discussion"
```
You get the top 10 text matches, ranked by raw relevance. The brain has no idea these are meetings. It can't route to attendees. It can't pull dates. It can't surface "this conversation came up again with the same people three weeks later."
Add a `meeting` type:
```bash
gbrain schema add-type meeting --primitive temporal --prefix meetings/ --extractable
gbrain schema sync --apply
```
The sync backfills `page.type = 'meeting'` on all 4000 pages in 1000-row batches. Now:
- `gbrain whoknows "Q3 roadmap discussion"` routes through the meeting type, ranking by `expert_routing` signal (attendees, recency, salience) instead of raw text.
- `gbrain extract-facts` runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`.
- The downstream `think` skill can now answer "what did we decide about pricing in the last three roadmap meetings" by querying the meeting graph instead of grep'ing 4000 files.
One command. 4000 pages went from invisible to queryable. The content didn't change. The structure did.
### 2. The founder ops brain
You're a founder or investor with ~500 markdown files mixing leads, portfolio companies, deal notes, intros, and follow-ups. You've been writing freely; you have no system. Your queries are all "wait, who introduced me to that fintech founder again?" and you scroll Notion for 20 minutes.
Add the founder shape:
```bash
gbrain schema fork gbrain-base mine
gbrain schema use mine
# Types
gbrain schema add-type lead --primitive entity --prefix people/leads/ --expert
gbrain schema add-type investor --primitive entity --prefix people/investors/ --expert --extractable
gbrain schema add-type portco --primitive entity --prefix companies/portco/ --expert --extractable
gbrain schema add-type deal --primitive entity --prefix companies/deals/ --extractable
# Link verbs
gbrain schema add-link-type invested-in --page-type investor --target-type portco
gbrain schema add-link-type intro-from --page-type lead --target-type lead
gbrain schema add-link-type passed-on --page-type investor --target-type deal
gbrain schema add-link-type led-by --page-type deal --target-type investor
gbrain schema sync --apply
```
Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." `gbrain extract-facts` starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`.
The CRM you've been promising yourself you'll set up next quarter? You just shipped it in 4 commands. It's downstream of your notes, not parallel to them.
### 3. The research brain
Replace "founder" with "PhD student" and the same pattern applies with different types: `researcher`, `paper`, `lab`, `grant`, `dataset` + `authored`, `cites`, `funded-by`, `uses-dataset`.
```bash
gbrain schema add-type paper --primitive annotation --prefix research/papers/ --extractable
gbrain schema add-link-type authored --page-type researcher --target-type paper
gbrain schema add-link-type cites --page-type paper --target-type paper
gbrain schema add-link-type uses --page-type paper --target-type dataset
```
Suddenly "show me papers that cite this work AND use the same dataset" is a `gbrain graph-query` traversal, not 30 minutes in Google Scholar. The fact extraction picks up `arxiv_id=2402.04253`, `cited_by_count=140`, `published_date=2026-02-15` automatically. Your reading-list-as-markdown turns into a queryable research graph that knows who works on what and what's connected to what.
### 4. The legal brain (or any domain where claims have numbers)
Lawyers, medical providers, accountants, anyone working in a domain where the meaning of a number depends on its type. A "judgment of $5M" against a "$2M case strategy threshold" is a comparison the brain can do — but only if both numbers are typed.
```bash
gbrain schema add-type case --primitive entity --prefix legal/cases/ --extractable --expert
gbrain schema add-type motion --primitive annotation --prefix legal/motions/ --extractable
gbrain schema add-type deposition --primitive annotation --prefix legal/depositions/ --extractable
gbrain schema add-link-type filed-in --page-type motion --target-type case
gbrain schema add-link-type cites --page-type motion --target-type precedent
```
Now `## Facts` fences in your case notes can carry typed claims (`damages=5000000`, `filed_date=2026-05-23`, `judge=jane-doe`) that gbrain stores as first-class columns. `gbrain eval trajectory legal/cases/acme-v-widget` prints the case history with regressions flagged. `gbrain founder scorecard` (renamed for legal: roll up plaintiff success rate, average damages, settlement-vs-trial ratio) gives you a structured view of how your practice is performing.
This isn't possible without typed page kinds. You can write the same prose in any note-taking app. Only gbrain treats the numbers as comparable across pages of the same type.
### 5. The team brain
`gbrain mounts add` lets you stack additional brains alongside your personal one. Each mounted brain has its OWN schema pack. The eng team's brain has `incident`, `runbook`, `service`, `oncall-rotation`. The design team's brain has `component`, `experiment`, `ab-test`, `figma-link`. The legal team's brain has cases and depositions.
When you query, the schema pack governs how each source's content is routed. An eng query against the mounted eng brain knows that `incidents/2026-05-23-db-outage.md` is an `incident` page with `severity=p0`, `mttr=47min`, `on_call=alice-example` — extractable typed facts. Your personal query against the same brain still works, but the routing is sharper because the eng team has invested in their ontology.
The schema is the team's tribal knowledge made explicit. Two engineers on different teams searching the same brain get DIFFERENT routing because their personal packs declare different expert types.
### 6. The "agent co-curates your ontology" pattern (the new thing)
This is what v0.40.7.0 actually enabled, and what the closed PR #1321 was reaching for.
Your OpenClaw (or any agent connected to your brain over HTTPS MCP with admin scope) watches your ingestion stream. After a week of you dumping notes under `garrytan/companies/yc-w24/`, the agent runs `gbrain schema detect` periodically, sees that prefix accumulating, and proposes:
> You have 47 pages under `companies/yc-w24/` typed as `company` (generic). They share a structural pattern (founder names, raise amounts, batch tag). Should I add a `yc-w24-company` type with `extractable: true` and the existing aliases pointing back to `company`? I'd backfill the 47 pages and add `cohort=W24` as a typed fact extracted from each page.
You approve once. The agent calls `schema_apply_mutations` over MCP with a batch:
```json
{
"pack": "mine",
"mutations": [
{"op": "add_type", "name": "yc-w24-company", "primitive": "entity", "prefix": "companies/yc-w24/", "extractable": true, "expert_routing": true},
{"op": "add_alias", "type": "yc-w24-company", "alias": "company"}
]
}
```
All inside ONE `withPackLock` scope, atomic, audited (the agent's `client_id` captured in the audit log as `actor: mcp:<clientId8>`). Cache invalidated cross-process. Sync backfills the 47 pages. The brain learned a new category of thing without you having to think about it.
The next time you query "YC W24 companies in fintech", the brain routes through the new type. Six months later when you forget the pattern entirely, the agent reminds you it's there and offers to consolidate it with the W25 batch.
The brain learns. The agent is the curator. You approve, the agent does the work.
### 7. The before-vs-after benchmark
If you want to FEEL the difference without buying the pitch:
Pick a real corpus you have. Run `gbrain whoknows` on a topic that should match. Note the top-3 results.
Then run `gbrain schema review-orphans --limit 50 --json` and look at the untyped pages. If 10+ of them share an obvious prefix that should be a real type, add the type + sync.
Re-run the same `whoknows` query. Top-3 should shift, because the new type is now routing through expert ranking instead of being lumped into the catchall. The numerical delta IS the win. You can run a tutorial in 5 minutes; this experiment proves it matters on your actual content.
## Why this matters
Three things gbrain does that generic note systems can't:
**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. `gbrain extract-facts` only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached.
**2. Untyped content is invisible content.** If your meetings are typed as `note`, expert routing skips them, facts extraction ignores them, link inference doesn't fire. They exist on disk and they're indexed for text search, but the structural surfaces (whoknows, find_experts, recall, think) treat them as second-class. Adding a type isn't cosmetic; it's structural promotion.
**3. The schema is queryable AND mutable AND auditable.** You can ask the brain what its schema looks like (`gbrain schema graph`), evolve it through 14 atomic CLI verbs + 9 MCP ops with full lock + audit semantics, and recover from any mistake (every primitive has an inverse, plus `gbrain schema downgrade` restores the previous active pack). This isn't "vibes-based knowledge management." It's a production system with structural integrity guarantees.
## What changed in v0.40.7.0 specifically
v0.39.1.0 shipped the schema-pack engine. You could ALREADY fork the bundled pack and edit `pack.yaml` by hand. What you couldn't do was let an agent author it safely — there were no atomic file locks, no audit log, no MCP exposure, no pack-aware wiring in the query path. The cathedral was built but unreachable from the outside.
v0.40.7.0 closed those gaps:
- **`withMutation` skeleton** wraps every primitive in 8 ordered safety steps (bundled-guard → lock → read → mutate → validate → atomic write → audit → invalidate). The pack file on disk is never partial. Two concurrent agents can't race.
- **Per-pack `O_CREAT|O_EXCL` atomic lock** (not the TOCTOU `existsSync+writeFileSync` pattern from page-lock.ts — codex caught that during plan review). TTL refresh every 10s while a mutation runs; `--force` means "steal stale lock" not "skip locking."
- **Privacy-redacted audit log** at `~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl`. Type names sha8-hashed, prefixes truncated to first segment only. A leaked screenshot of the audit can't reveal sensitive taxonomy like `personal/oncology/` or `legal/depositions/`.
- **9 new MCP ops** including the batched `schema_apply_mutations` (admin scope, NOT localOnly — your OpenClaw and any remote agent author packs over normal HTTPS MCP, with `client_id` captured as `actor: mcp:<clientId8>`).
- **T1.5 wiring** finally completes for `whoknows` and `find_experts`: a custom `researcher` type marked `--expert` now actually surfaces in query results. Pre-v0.40.7 it silently never matched because the query path read hardcoded `['person', 'company']`.
- **Cross-process invalidation** via stat-mtime TTL gate inside `loadActivePack`. Operator runs `gbrain schema add-type` from a terminal; the autopilot daemon picks up the new type within 1 second without a restart.
The cumulative effect: an agent can safely co-curate your ontology with a complete forensic trail. That's the new thing.
## Where to start
- **Want to see it work in 5 minutes?** Run the [tutorial](schema-author-tutorial.md). Forks the bundled pack, adds a researcher type, proves the wiring end-to-end.
- **Want the agent recipe?** Read [`skills/schema-author/SKILL.md`](../skills/schema-author/SKILL.md). 7-phase workflow agents follow when they detect a schema-evolution opportunity.
- **Want the rules of thumb?** Read [`skills/conventions/schema-evolution.md`](../skills/conventions/schema-evolution.md). Decision tree for when to add a type vs alias vs prefix. <20 pages don't pack-codify. 100+ pages need first-class types.
- **Want the architecture?** The "Schema Cathedral v3 (v0.40.7.0)" section in `CLAUDE.md` has the 14-bullet module-by-module breakdown, each citing the design decision and codex finding that motivated it.
- **Want to set up an agent that co-curates your brain?** Run `gbrain auth register-client my-agent --scopes admin` to mint an OAuth client your remote agent can use to call `schema_apply_mutations` over MCP. The agent then runs detect → suggest → apply on its own cadence and asks you to approve substantive changes.
The killer feature isn't "schemas." Personal knowledge systems have had schemas forever. The killer feature is that your AGENT can shape them safely on your behalf, with structural integrity guarantees that match what you'd expect from a database, not a notes app.
That's what we built. Try it on a corpus you actually have and the numbers go up.
-31
View File
@@ -1,31 +0,0 @@
# SkillOpt judge LLM accuracy eval (F9)
Hand-labeled (trajectory, expected_score) pairs. Measures whether the judge
model's scores agree with human judgment within reasonable bounds.
## Fixtures
`fixtures.jsonl` — one row per (judge_kind, rubric, trajectory, gold_score)
quadruple. Gold scores are integer 1-5 (per common Likert practice);
normalized to 0..1 inside the runner.
## Runner
`runner.mjs` reads fixtures, calls `scoreTrajectory`, computes per-fixture
absolute error vs gold, aggregates to mean absolute error (MAE).
Pass criterion: MAE <= 0.15 on the 0..1 scale (judge agrees with gold
within ~one-eighth of the full range).
## Cost
~10 fixtures × ~$0.005 each = $0.05 per run. Refresh when the judge prompt
changes or when switching judge models.
## Reproduce
```bash
node evals/skillopt-judge/runner.mjs \
--judge-model anthropic:claude-sonnet-4-6 \
--output evals/skillopt-judge/receipts/$(date +%Y%m%d).json
```
-10
View File
@@ -1,10 +0,0 @@
{"id":"judge-001","rubric":"Does the output (a) name 3+ board members, (b) cite recent material, (c) flag any open risks? Score 0..1.","final_text":"Board members: alice-example, bob-example, charlie-example. Recent: 2026 funding round [wiki/companies/widget-co]. Risks: cash runway 8 months.","gold_score":1.0}
{"id":"judge-002","rubric":"Does the output (a) name 3+ board members, (b) cite recent material, (c) flag any open risks? Score 0..1.","final_text":"alice-example is the CEO.","gold_score":0.2}
{"id":"judge-003","rubric":"Does the output contain a structured summary with bullet points? Score 0..1.","final_text":"- Point 1\n- Point 2\n- Point 3","gold_score":1.0}
{"id":"judge-004","rubric":"Does the output contain a structured summary with bullet points? Score 0..1.","final_text":"It's a long story, no bullets.","gold_score":0.1}
{"id":"judge-005","rubric":"Is the output under 280 characters AND contains a verifiable claim? Score 0..1.","final_text":"Network effects compound: data → better model → more users → more data. [wiki/concepts/network-effects]","gold_score":0.9}
{"id":"judge-006","rubric":"Is the output under 280 characters AND contains a verifiable claim? Score 0..1.","final_text":"Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff. Lots of stuff.","gold_score":0.0}
{"id":"judge-007","rubric":"Does the output have a clear thesis in the first sentence? Score 0..1.","final_text":"Network effects are the most underrated business primitive. Here's why...","gold_score":0.95}
{"id":"judge-008","rubric":"Does the output have a clear thesis in the first sentence? Score 0..1.","final_text":"Various things to consider. Some are important. Others less so.","gold_score":0.15}
{"id":"judge-009","rubric":"Does the output cite at least 2 brain pages (wiki/, people/, companies/, etc)? Score 0..1.","final_text":"See wiki/people/alice-example and companies/widget-co for details.","gold_score":1.0}
{"id":"judge-010","rubric":"Does the output cite at least 2 brain pages? Score 0..1.","final_text":"No citations here.","gold_score":0.05}
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env node
// SkillOpt judge LLM accuracy eval runner (F9).
//
// Reads fixtures.jsonl, calls scoreTrajectory with llm judge mode, computes
// per-fixture absolute error vs gold, writes a JSON receipt.
//
// Pass criterion: MAE <= 0.15.
//
// Usage:
// node evals/skillopt-judge/runner.mjs \
// --judge-model anthropic:claude-sonnet-4-6 \
// --output evals/skillopt-judge/receipts/$(date +%Y%m%d).json
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
const args = process.argv.slice(2);
function flag(name, def) {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : def;
}
const judgeModel = flag('--judge-model', 'anthropic:claude-sonnet-4-6');
const fixturesPath = flag('--fixtures', join(import.meta.dirname, 'fixtures.jsonl'));
const outputPath = flag('--output');
const fixtures = readFileSync(fixturesPath, 'utf8')
.split('\n')
.filter((l) => l.trim().length > 0)
.map((l) => JSON.parse(l));
const { scoreTrajectory } = await import('../../src/core/skillopt/score.ts');
const perFixture = [];
let totalAbsError = 0;
let parseFailures = 0;
for (const fx of fixtures) {
const trajectory = {
task_id: fx.id,
task: 'judge-eval',
final_text: fx.final_text,
tool_calls: [],
usage: { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0 },
turns: 1,
stop_reason: 'end',
duration_ms: 0,
};
const result = await scoreTrajectory(trajectory, { kind: 'llm', rubric: fx.rubric }, { judgeModel });
const absErr = Math.abs(result.score - fx.gold_score);
totalAbsError += absErr;
if (result.judge_error) parseFailures += 1;
perFixture.push({
id: fx.id,
gold: fx.gold_score,
actual: result.score,
abs_error: absErr,
judge_error: result.judge_error ?? null,
rationale: result.rationale ?? null,
});
}
const mae = fixtures.length > 0 ? totalAbsError / fixtures.length : 0;
const verdict = mae <= 0.15 ? 'pass' : 'fail';
const receipt = {
schema_version: 1,
timestamp: new Date().toISOString(),
judge_model: judgeModel,
fixtures_count: fixtures.length,
parse_failures: parseFailures,
mae,
verdict,
threshold: 0.15,
per_fixture: perFixture,
};
const out = JSON.stringify(receipt, null, 2);
if (outputPath) {
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, out);
process.stderr.write(`Wrote receipt to ${outputPath}\n`);
} else {
process.stdout.write(out + '\n');
}
process.exit(verdict === 'pass' ? 0 : 1);
-35
View File
@@ -1,35 +0,0 @@
# SkillOpt reflect-prompt quality eval (F8)
Gold-labeled trajectories paired with expected-edit shapes. Measures whether
the optimizer model's reflect prompt proposes the kind of edit a human would
write given the same trajectory.
## Fixtures
`fixtures.jsonl` — one row per (skill_body, scored_rollouts, expected_edits)
triple. The `expected_edits` are loose shape constraints (the op kind + a
substring of the target/anchor), not exact-text equality, because LLMs
won't propose byte-identical text.
## Runner
`runner.mjs` reads `fixtures.jsonl`, calls `runReflect` for each fixture,
checks every proposed edit against the expected_edits set, and writes a
JSON receipt with per-fixture pass/fail + aggregate hit rate.
Pass criterion: aggregate hit rate >= 0.7 (each fixture has 1-3 expected
edits; the optimizer "wins" the fixture if at least one of its proposals
matches an expected shape).
## Cost
~5 fixtures × ~$0.10 each (Opus reflect call) = ~$0.50 per run. Refresh
the suite when the reflect prompt changes; otherwise weekly is enough.
## Reproduce
```bash
node evals/skillopt-reflect/runner.mjs \
--optimizer-model anthropic:claude-opus-4-7 \
--output evals/skillopt-reflect/receipts/$(date +%Y%m%d).json
```
-5
View File
@@ -1,5 +0,0 @@
{"id":"reflect-001","skill_body":"# Brief Generator\n\nWhen asked, produce a 3-section brief: People, Companies, Risks.\n","scored_rollouts":[{"score":0.3,"task":"Brief on widget-co-example","final_text":"Here are the people: alice-example.","tool_calls":[{"name":"search"}],"failed":[]},{"score":0.3,"task":"Brief on acme-example","final_text":"Just some people: bob-example.","tool_calls":[{"name":"search"}],"failed":[]}],"expected_edits":[{"op":"add","anchor_contains":"Brief Generator"},{"op":"replace","target_contains":"3-section"}]}
{"id":"reflect-002","skill_body":"# Citations Required\n\nAlways include 2+ citations.\n","scored_rollouts":[{"score":1.0,"task":"Cite alice-example","final_text":"alice-example [wiki/people/alice-example] worked at [wiki/companies/widget-co].","tool_calls":[{"name":"get_page"},{"name":"get_page"}],"failed":[]},{"score":1.0,"task":"Cite bob-example","final_text":"bob-example [wiki/people/bob-example] and [wiki/companies/acme-example].","tool_calls":[{"name":"get_page"},{"name":"get_page"}],"failed":[]}],"expected_edits":[{"op":"add","anchor_contains":"Citations"}]}
{"id":"reflect-003","skill_body":"# Meeting Prep\n\nProduce a brief for the upcoming meeting.\n","scored_rollouts":[{"score":0.2,"task":"Prep meeting with alice-example","final_text":"OK","tool_calls":[],"failed":[]},{"score":0.2,"task":"Prep meeting with widget-co","final_text":"Will do","tool_calls":[],"failed":[]}],"expected_edits":[{"op":"replace","target_contains":"Produce a brief"},{"op":"add","anchor_contains":"Meeting Prep"}]}
{"id":"reflect-004","skill_body":"# Tweet Composer\n\nUnder 280 chars. Include claim + evidence.\n","scored_rollouts":[{"score":0.5,"task":"Tweet about network effects","final_text":"Network effects are powerful. They compound over time.","tool_calls":[],"failed":[]}],"expected_edits":[{"op":"add","anchor_contains":"Tweet Composer"}]}
{"id":"reflect-005","skill_body":"# Fact Check\n\nVerify the claim against the brain.\n","scored_rollouts":[{"score":0.0,"task":"Check claim X","final_text":"Yes","tool_calls":[],"failed":[]},{"score":0.0,"task":"Check claim Y","final_text":"No","tool_calls":[],"failed":[]}],"expected_edits":[{"op":"replace","target_contains":"Verify the claim"}]}
-119
View File
@@ -1,119 +0,0 @@
#!/usr/bin/env node
// SkillOpt reflect-prompt quality eval runner (F8).
//
// Reads fixtures.jsonl, calls runReflect for each fixture, scores edits
// against expected_edits shape constraints, writes a JSON receipt.
//
// Usage:
// node evals/skillopt-reflect/runner.mjs \
// --optimizer-model anthropic:claude-opus-4-7 \
// --output evals/skillopt-reflect/receipts/$(date +%Y%m%d).json
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
const args = process.argv.slice(2);
function flag(name, def) {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : def;
}
const optimizerModel = flag('--optimizer-model', 'anthropic:claude-opus-4-7');
const fixturesPath = flag('--fixtures', join(import.meta.dirname, 'fixtures.jsonl'));
const outputPath = flag('--output');
const fixtures = readFileSync(fixturesPath, 'utf8')
.split('\n')
.filter((l) => l.trim().length > 0)
.map((l) => JSON.parse(l));
const { runReflect } = await import('../../src/core/skillopt/reflect.ts');
const perFixture = [];
let totalWins = 0;
let totalExpected = 0;
for (const fx of fixtures) {
const scoredRollouts = fx.scored_rollouts.map((r) => ({
trajectory: {
task_id: r.task,
task: r.task,
final_text: r.final_text,
tool_calls: (r.tool_calls ?? []).map((tc) => ({ name: tc.name, input: {}, failed: !!tc.failed })),
usage: { input_tokens: 100, output_tokens: 50, cache_read_tokens: 0, cache_creation_tokens: 0 },
turns: 1,
stop_reason: 'end',
duration_ms: 100,
},
score: r.score,
}));
const successes = scoredRollouts.filter((r) => r.score >= 0.5);
const failures = scoredRollouts.filter((r) => r.score < 0.5);
const result = await runReflect({
skillBodyText: fx.skill_body,
successes,
failures,
rejected: [],
optimizerModel,
});
const proposedEdits = [...result.failureEdits, ...result.successEdits];
// Score: for each expected edit, does ANY proposed edit match its shape?
let wins = 0;
for (const ex of fx.expected_edits) {
const matched = proposedEdits.some((pe) => editShapeMatches(pe, ex));
if (matched) wins += 1;
}
totalWins += wins;
totalExpected += fx.expected_edits.length;
perFixture.push({
id: fx.id,
expected: fx.expected_edits.length,
matched: wins,
proposed_count: proposedEdits.length,
hit_rate: fx.expected_edits.length > 0 ? wins / fx.expected_edits.length : 0,
errors: result.errors,
});
}
const aggregateHitRate = totalExpected > 0 ? totalWins / totalExpected : 0;
const verdict = aggregateHitRate >= 0.7 ? 'pass' : 'fail';
const receipt = {
schema_version: 1,
timestamp: new Date().toISOString(),
optimizer_model: optimizerModel,
fixtures_count: fixtures.length,
expected_total: totalExpected,
matched_total: totalWins,
aggregate_hit_rate: aggregateHitRate,
verdict,
threshold: 0.7,
per_fixture: perFixture,
};
const out = JSON.stringify(receipt, null, 2);
if (outputPath) {
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, out);
process.stderr.write(`Wrote receipt to ${outputPath}\n`);
} else {
process.stdout.write(out + '\n');
}
process.exit(verdict === 'pass' ? 0 : 1);
function editShapeMatches(proposed, expected) {
if (proposed.op !== expected.op) return false;
if (expected.anchor_contains && proposed.anchor) {
return proposed.anchor.toLowerCase().includes(expected.anchor_contains.toLowerCase());
}
if (expected.target_contains && proposed.target) {
return proposed.target.toLowerCase().includes(expected.target_contains.toLowerCase());
}
return true;
}
+3196 -1117
View File
File diff suppressed because one or more lines are too long
+1 -16
View File
@@ -7,9 +7,7 @@ Repo: https://github.com/garrytan/gbrain
## Core entry points
- [AGENTS.md](https://raw.githubusercontent.com/garrytan/gbrain/master/AGENTS.md): Start here if you are not Claude Code. Install order, trust boundary, skill resolver, config/debug/migration pointers.
- [CLAUDE.md](https://raw.githubusercontent.com/garrytan/gbrain/master/CLAUDE.md): Orientation + resolver. North Star, two axes, architecture + cross-cutting invariants, the reference map pointing at on-demand docs, and the inline ship IRON RULES.
- [docs/architecture/KEY_FILES.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/architecture/KEY_FILES.md): Per-file index for the gbrain repo: what each src/ file does + its load-bearing invariants. The on-demand detail CLAUDE.md's reference map routes to.
- [docs/architecture/thin-client.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/architecture/thin-client.md): The thin-client / remote-MCP / cross-modal routing seam: isThinClient detection, callRemoteTool, SSRF-hardened URL validation, per-command routing.
- [CLAUDE.md](https://raw.githubusercontent.com/garrytan/gbrain/master/CLAUDE.md): Architecture reference. Key files, trust boundaries, engine factory, test layout.
- [INSTALL_FOR_AGENTS.md](https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md): 9-step agent installation.
- [skills/RESOLVER.md](https://raw.githubusercontent.com/garrytan/gbrain/master/skills/RESOLVER.md): Skill dispatcher. Read first for any task.
- [README.md](https://raw.githubusercontent.com/garrytan/gbrain/master/README.md): Project overview, benchmarks, 30-minute setup.
@@ -18,20 +16,12 @@ Repo: https://github.com/garrytan/gbrain
- [docs/ENGINES.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ENGINES.md): PGLite vs Postgres trade-off and when to migrate.
- [docs/GBRAIN_RECOMMENDED_SCHEMA.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/GBRAIN_RECOMMENDED_SCHEMA.md): MECE directory structure (people/, companies/, concepts/).
- [docs/what-schemas-unlock.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/what-schemas-unlock.md): Why schemas matter: 7 killer use cases (4000 invisible meetings, founder ops brain, research brain, legal brain, team brain, agent-as-co-curator) + the structural argument for typed page kinds. Read this before pitching schema authoring (v0.40.7.0).
- [docs/schema-author-tutorial.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/schema-author-tutorial.md): 5-minute walkthrough: fork the bundled pack, add a custom `researcher` type, backfill existing pages via `gbrain schema sync --apply`, prove the T1.5 wiring via `gbrain whoknows` (v0.40.7.0).
- [docs/guides/live-sync.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/live-sync.md): Incremental markdown sync setup.
- [docs/guides/cron-schedule.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/cron-schedule.md): Recurring job scheduling.
- [docs/guides/minions-deployment.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/minions-deployment.md): Deploying the gbrain jobs worker: crontab + watchdog, inline --follow, systemd/Procfile/fly.toml, upgrade checklist.
- [docs/guides/quiet-hours.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/quiet-hours.md): Notification hold + timezone-aware delivery.
- [docs/guides/scaling-skills.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md): Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss.
- [docs/mcp/DEPLOY.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md): MCP server deployment.
## AI providers
- [docs/ai-providers/zeroentropy.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ai-providers/zeroentropy.md): ZeroEntropy zembed-1 embedding + zerank-2 reranker (hosted): API key, embedding switch, reranker config.
- [docs/ai-providers/llama-server-reranker.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ai-providers/llama-server-reranker.md): Local reranker via llama.cpp --reranking: Qwen3-Reranker or self-hosted ZE weights, --alias setup, gbrain config keys, cold-start timeout, budget-cap interaction.
## Debugging
- [docs/GBRAIN_VERIFY.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/GBRAIN_VERIFY.md): 7-check post-setup verification. Start here when something feels off.
@@ -44,11 +34,6 @@ Repo: https://github.com/garrytan/gbrain
- [skills/migrations/](https://raw.githubusercontent.com/garrytan/gbrain/master/skills/migrations/): Per-version (v0.5.0 - v0.14.1) agent-executable migration instructions.
- [CHANGELOG.md](https://raw.githubusercontent.com/garrytan/gbrain/master/CHANGELOG.md): Release-summary voice + itemized changes + self-repair block per version.
## Contributing
- [docs/TESTING.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/TESTING.md): Test command tiers, the test-isolation lint (R1-R4), the canonical PGLite block, withEnv, the E2E DB lifecycle, and the file taxonomy. Maintainer-facing.
- [docs/RELEASING.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/RELEASING.md): Full release + contributor process: pre-ship test requirements, the CHANGELOG voice + release-summary template, the 'To take advantage of vX' block, version migrations, GitHub Actions SHA refresh, PR conventions, community-PR-wave. (Ship IRON RULES stay inline in CLAUDE.md.)
## Philosophy
- [docs/ethos/THIN_HARNESS_FAT_SKILLS.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ethos/THIN_HARNESS_FAT_SKILLS.md): Why skills live in markdown.
+4 -16
View File
@@ -1,5 +1,6 @@
{
"name": "gbrain",
"version": "0.40.5.0",
"description": "Postgres-native personal knowledge brain with hybrid RAG search",
"type": "module",
"main": "src/core/index.ts",
@@ -38,20 +39,15 @@
"build:llms": "bun run scripts/build-llms.ts",
"build:pglite-snapshot": "bun run scripts/build-pglite-snapshot.ts",
"test": "bash scripts/run-unit-parallel.sh",
"eval:autocut": "bun test test/search/autocut-eval.test.ts",
"test:full": "bun run verify && bash scripts/run-unit-parallel.sh && bun run test:slow && ([ -n \"$DATABASE_URL\" ] && bash scripts/run-e2e.sh || echo '[test:full] skipped E2E (no DATABASE_URL); run docker-compose -f docker-compose.ci.yml up + bun run test:e2e to include' 1>&2)",
"verify": "bash scripts/run-verify-parallel.sh",
"verify": "bun run check:privacy && bun run check:proposal-pii && bun run check:test-names && bun run check:jsonb && bun run check:source-id-projection && bun run check:source-config-leak && bun run check:progress && bun run check:test-isolation && bun run check:wasm && bun run check:admin-build && bun run check:admin-scope-drift && bun run check:cli-exec && bun run check:system-of-record && bun run check:eval-glossary && bun run check:no-pii-agent-voice && bun run check:synthetic-corpus-privacy && bun run check:skill-brain-first && bun run check:fuzz-purity && bun run typecheck",
"check:source-config-leak": "scripts/check-source-config-leak.sh",
"check:no-pii-agent-voice": "scripts/check-no-pii-in-agent-voice.sh",
"check:synthetic-corpus-privacy": "scripts/check-synthetic-corpus-privacy.sh",
"check:system-of-record": "scripts/check-system-of-record.sh",
"check:admin-scope-drift": "scripts/check-admin-scope-drift.sh",
"check:cli-exec": "scripts/check-cli-executable.sh",
"check:all": "scripts/check-privacy.sh && scripts/check-proposal-pii.sh && scripts/check-test-real-names.sh && scripts/check-jsonb-pattern.sh && scripts/check-source-id-projection.sh && scripts/check-source-config-leak.sh && scripts/check-progress-to-stdout.sh && scripts/check-no-legacy-getconnection.sh && scripts/check-test-isolation.sh && scripts/check-trailing-newline.sh && scripts/check-wasm-embedded.sh && scripts/check-exports-count.sh && scripts/check-admin-build.sh && scripts/check-admin-scope-drift.sh && scripts/check-cli-executable.sh && scripts/check-skill-brain-first.sh && scripts/check-operations-filter-bypass.sh && scripts/check-gateway-routed-no-direct-anthropic.sh && scripts/check-worker-pool-atomicity.sh && scripts/check-key-files-current-state.sh && scripts/check-no-double-retry.sh && scripts/check-batch-audit-site.sh",
"check:gateway-routed": "scripts/check-gateway-routed-no-direct-anthropic.sh",
"check:worker-pool-atomicity": "scripts/check-worker-pool-atomicity.sh",
"check:doc-history": "scripts/check-key-files-current-state.sh",
"check:resolver": "bun src/cli.ts check-resolvable --strict --skills-dir skills/",
"check:all": "scripts/check-privacy.sh && scripts/check-proposal-pii.sh && scripts/check-test-real-names.sh && scripts/check-jsonb-pattern.sh && scripts/check-source-id-projection.sh && scripts/check-progress-to-stdout.sh && scripts/check-no-legacy-getconnection.sh && scripts/check-test-isolation.sh && scripts/check-trailing-newline.sh && scripts/check-wasm-embedded.sh && scripts/check-exports-count.sh && scripts/check-admin-build.sh && scripts/check-admin-scope-drift.sh && scripts/check-cli-executable.sh && scripts/check-skill-brain-first.sh",
"check:skill-brain-first": "scripts/check-skill-brain-first.sh",
"check:wasm": "scripts/check-wasm-embedded.sh",
"check:newlines": "scripts/check-trailing-newline.sh",
@@ -65,9 +61,6 @@
"ci:select-e2e": "bun run scripts/select-e2e.ts",
"typecheck": "tsc --noEmit",
"check:jsonb": "scripts/check-jsonb-pattern.sh",
"check:no-double-retry": "scripts/check-no-double-retry.sh",
"check:batch-audit-site": "scripts/check-batch-audit-site.sh",
"check:worker-lock-renewal-shape": "scripts/check-worker-lock-renewal-shape.sh",
"check:source-id-projection": "scripts/check-source-id-projection.sh",
"check:privacy": "scripts/check-privacy.sh",
"check:proposal-pii": "scripts/check-proposal-pii.sh",
@@ -79,10 +72,6 @@
"check:admin-embedded": "scripts/check-admin-embedded.sh",
"check:test-isolation": "scripts/check-test-isolation.sh",
"check:fuzz-purity": "scripts/check-fuzz-purity.sh",
"check:operations-filter-bypass": "scripts/check-operations-filter-bypass.sh",
"check:fixture-privacy": "scripts/check-fixture-privacy.sh",
"check:conversation-parser": "bun src/cli.ts eval conversation-parser test/fixtures/conversation-formats/all.jsonl --no-llm",
"check:source-scope-onboard": "scripts/check-source-scope-onboard.sh",
"postinstall": "command -v gbrain >/dev/null 2>&1 && gbrain apply-migrations --yes --non-interactive || echo '[gbrain] postinstall skipped. If installed via bun install -g github:...: run `gbrain doctor` and `gbrain apply-migrations --yes` manually. See https://github.com/garrytan/gbrain/issues/218' 1>&2",
"prepublish:clawhub": "bun run build:all",
"publish:clawhub": "clawhub package publish . --family bundle-plugin"
@@ -142,6 +131,5 @@
"engines": {
"bun": ">=1.3.10"
},
"license": "MIT",
"version": "0.42.11.0"
"license": "MIT"
}
-79
View File
@@ -1,79 +0,0 @@
#!/usr/bin/env bash
# v0.41.18.0 — CI guard against batch-audit-site typo drift (codex H-7).
#
# auditSite labels flow from call sites into the batch-retry audit JSONL
# and from there into `gbrain doctor`'s batch_retry_health check. A typo
# like `'extract.lnks_inc'` doesn't break compilation (TypeScript narrows
# string literals only via the BatchAuditSite type, but external string
# values escape this — e.g. config, environment, dynamic dispatch).
#
# This script extracts every string-literal `auditSite: '...'` value from
# src/ and validates it appears in the BATCH_AUDIT_SITES const list in
# src/core/retry.ts. Fails the build on mismatch.
#
# Usage: scripts/check-batch-audit-site.sh
# Exit: 0 when every literal matches the enum, 1 otherwise.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
RETRY_FILE="src/core/retry.ts"
if [ ! -f "$RETRY_FILE" ]; then
echo "ERROR: $RETRY_FILE missing — cannot validate audit sites."
exit 1
fi
# Extract every entry inside BATCH_AUDIT_SITES = [ ... ] as const.
# Strips quotes + trailing commas + whitespace. Strict awk window between
# the array open and closing `] as const`.
KNOWN_SITES=$(awk '
/BATCH_AUDIT_SITES = \[/ { capture = 1; next }
capture && /\] as const/ { capture = 0; exit }
capture {
# Pull out '\''xyz'\'' or "xyz" string literals on the line.
while (match($0, /['\''"]([^'\''"]+)['\''"]/)) {
print substr($0, RSTART + 1, RLENGTH - 2)
$0 = substr($0, RSTART + RLENGTH)
}
}
' "$RETRY_FILE" | sort -u)
if [ -z "$KNOWN_SITES" ]; then
echo "ERROR: Could not extract BATCH_AUDIT_SITES from $RETRY_FILE."
exit 1
fi
# Extract every `auditSite: '...'` literal from src/ (excluding retry.ts
# itself which contains the enum definition, and test files which are
# allowed to use synthetic sites for assertion scaffolding).
USED_SITES=$(
grep -rEh "auditSite:[[:space:]]*['\"][^'\"]+['\"]" src/ \
--include='*.ts' \
--exclude-dir=core/audit \
--exclude='retry.ts' \
| sed -E "s/.*auditSite:[[:space:]]*['\"]([^'\"]+)['\"].*/\1/" \
| sort -u
)
if [ -z "$USED_SITES" ]; then
echo "OK: no auditSite literals found in src/ (engines use defaults)"
exit 0
fi
UNKNOWN_SITES=$(comm -23 <(echo "$USED_SITES") <(echo "$KNOWN_SITES") || true)
if [ -n "$UNKNOWN_SITES" ]; then
echo "ERROR: Unknown auditSite literal(s) found in src/:"
echo "$UNKNOWN_SITES" | sed 's/^/ /'
echo
echo "Fix: add the value to BATCH_AUDIT_SITES in src/core/retry.ts."
echo " The enum is the closed list of known sites."
echo
echo "Known sites:"
echo "$KNOWN_SITES" | sed 's/^/ /'
exit 1
fi
echo "OK: all auditSite literals match BATCH_AUDIT_SITES enum"
-64
View File
@@ -1,64 +0,0 @@
#!/usr/bin/env bash
# v0.41.13.0 — Privacy guard for test/fixtures/conversation-formats/.
#
# Per CLAUDE.md privacy rule: "Never reference real people, companies,
# funds, or private agent names in any public-facing artifact."
# Test fixtures ship in the repo; they ARE public.
#
# This script greps for known real-name signals and fails the build if
# any leak. Add to bun run verify so the gate runs every PR.
#
# Banned tokens (case-insensitive substring match):
# - 'wintermute' / 'openclaw' (real downstream agent names)
# - 'palantir' (real company per Garry's history)
# - common real-fund names (sequoia, andreessen, founders fund, etc.)
# - 'ycombinator' / 'y combinator' (the org running gbrain)
#
# Allowed (placeholder convention):
# - alice-example / bob-example / charlie-example / diana-example
# - widget-co / acme-example
# - fund-a / fund-b / fund-c
set -euo pipefail
FIXTURE_DIR="test/fixtures/conversation-formats"
if [ ! -d "$FIXTURE_DIR" ]; then
echo "[check-fixture-privacy] $FIXTURE_DIR does not exist; nothing to check"
exit 0
fi
# Real-name signals. Add to this list when new banned tokens surface.
BANNED_TOKENS=(
"wintermute"
"openclaw"
"palantir"
"sequoia"
"andreessen"
"founders fund"
"founders\\.fund"
"ycombinator"
"y combinator"
"garry tan"
"garrytan"
)
errors=0
for token in "${BANNED_TOKENS[@]}"; do
matches=$(grep -ril "$token" "$FIXTURE_DIR" 2>/dev/null || true)
if [ -n "$matches" ]; then
echo "[check-fixture-privacy] BANNED token '$token' found in:"
echo "$matches" | sed 's/^/ - /'
errors=$((errors + 1))
fi
done
if [ "$errors" -gt 0 ]; then
echo ""
echo "[check-fixture-privacy] FAIL: $errors banned token(s) found in fixtures."
echo "[check-fixture-privacy] Fixtures must use placeholder names (alice-example, widget-co, fund-a, ...)."
echo "[check-fixture-privacy] See CLAUDE.md \"Privacy rule\" section."
exit 1
fi
echo "[check-fixture-privacy] OK: no banned tokens found in $FIXTURE_DIR"
@@ -1,133 +0,0 @@
#!/usr/bin/env bash
# CI guard: fail if gateway-routed source files reintroduce direct Anthropic
# SDK instantiation (`new Anthropic()` / `import Anthropic from '@anthropic-ai/sdk'`
# as a runtime constructor, NOT a type-only import).
#
# Why this exists: v0.35.5.0 migrated src/core/think/index.ts from `new Anthropic()`
# to a gateway.chat() adapter (closed #952). v0.41+ wave did the same for
# src/core/cycle/synthesize.ts (T5 in the community PR wave). Both files
# now route through src/core/ai/gateway.ts so any provider with a registered
# recipe (Anthropic, DeepSeek, OpenRouter, Voyage, Ollama, llama-server, ...)
# is reachable via `models.dream.synthesize_verdict` / chat model config.
#
# Without this guard, a future contributor adding `import Anthropic from
# '@anthropic-ai/sdk'` and `new Anthropic()` to either file silently re-opens
# the same provider-lock-in bug class. The symptom is "my DeepSeek config
# isn't being used by dream synthesize" — invisible until first user report.
#
# Mirrors the pattern of scripts/check-jsonb-pattern.sh.
#
# Usage: scripts/check-gateway-routed-no-direct-anthropic.sh
# Exit: 0 when clean, 1 when a guarded file imports the SDK as a runtime value.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
# Files whose contract is "ALL chat calls route through gateway.chat()".
# Extend this list when migrating another file off direct SDK construction.
GUARDED_FILES=(
"src/core/cycle/synthesize.ts"
"src/core/think/index.ts"
)
FAILED=0
for f in "${GUARDED_FILES[@]}"; do
if [ ! -f "$f" ]; then
# File was renamed or removed. Don't fail loud — flag and continue.
echo "WARN: guarded file missing: $f (rename/remove? update GUARDED_FILES in $(basename "$0"))"
continue
fi
# Match `new Anthropic(...)` — the runtime constructor call. Both `new Anthropic()`
# and `new Anthropic({apiKey: '...'})` shapes are caught.
# Exclude single-line `//` and block `*` comment lines so historical references
# in JSDoc / explanatory comments don't false-fire. Code AND code-in-template
# literals still hit (those don't start with `//` or ` *`).
if grep -En 'new\s+Anthropic\s*\(' "$f" 2>/dev/null | grep -vE '^[0-9]+:\s*(//|\*)' | grep .; then
echo
echo "ERROR: $f reintroduced direct Anthropic SDK construction (\`new Anthropic()\`)."
echo " This file's contract is to route all chat calls through gateway.chat()."
echo " Use the adapter pattern from src/core/think/index.ts:tryBuildGatewayClient"
echo " or src/core/cycle/synthesize.ts:makeJudgeClient."
FAILED=1
fi
# Match any value-shaped (NOT type-only) import of the SDK. The type-only forms
# `import type Anthropic from '@anthropic-ai/sdk'` AND
# `import { type Foo } from '@anthropic-ai/sdk'` (all-type-clauses-only) are allowed
# for typing the adapter's Anthropic.Message return shape. Covers:
# import Anthropic from '@anthropic-ai/sdk' (default)
# import { Anthropic } from '@anthropic-ai/sdk' (named)
# import Anthropic, { Other } from '@anthropic-ai/sdk' (default + named)
# import { Anthropic as A } from '@anthropic-ai/sdk' (named-renamed)
# import { type Msg, Anthropic } from '@anthropic-ai/sdk' (mixed type + value)
# import * as Anthropic from '@anthropic-ai/sdk' (namespace)
# Strategy: catch every line ending in `from '@anthropic-ai/sdk'`, strip
# comment lines, strip top-level `import type ...` (the only allowed shape),
# then check whether any remaining line contains a value identifier OUTSIDE
# type-prefixed clauses. We handle the mixed-import case by inspecting the
# specifier list: if any specifier is not `type Foo`, the import is value-shaped.
while IFS= read -r line; do
[ -z "$line" ] && continue
# Strip the leading "line-number:" prefix grep adds.
body="${line#*:}"
# Top-level `import type ...` is allowed — entire import is type-only.
if printf '%s' "$body" | grep -qE '^\s*import\s+type\s'; then continue; fi
# Comment line.
if printf '%s' "$body" | grep -qE '^\s*(//|\*)'; then continue; fi
# Extract the specifiers list (between `import` and `from`), if present.
# If the list contains any specifier NOT prefixed with `type ` (or there's
# no brace list at all — default/namespace import), it's a value import.
# POSIX character classes for cross-shell portability (macOS BSD sed
# doesn't support `\s` even in extended-regex mode).
specifiers=$(printf '%s' "$body" | sed -nE 's/^[[:space:]]*import[[:space:]]+\{([^}]*)\}[[:space:]]+from.*/\1/p')
if [ -n "$specifiers" ]; then
# Brace list present. Allow only if EVERY non-empty specifier is type-prefixed.
# Use a temp file instead of `while | exit 1` (subshell trap).
tmpflag=$(mktemp -t gateway-guard-XXXX)
echo 0 > "$tmpflag"
printf '%s' "$specifiers" | tr ',' '\n' | while IFS= read -r spec; do
spec=$(printf '%s' "$spec" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
[ -z "$spec" ] && continue
if ! printf '%s' "$spec" | grep -qE '^type\s'; then echo 1 > "$tmpflag"; fi
done
has_value=$(cat "$tmpflag")
rm -f "$tmpflag"
if [ "$has_value" = "1" ]; then
echo "$line"
echo
echo "ERROR: $f imports @anthropic-ai/sdk with a value-shaped specifier."
echo " Use \`import type ...\` for all clauses, or route runtime"
echo " chat calls through src/core/ai/gateway.ts."
FAILED=1
fi
else
# No brace list — default, namespace, or bare import — always value-shaped.
echo "$line"
echo
echo "ERROR: $f imports @anthropic-ai/sdk as a runtime value."
echo " Use \`import type Anthropic from '@anthropic-ai/sdk'\` for type-only"
echo " references to Anthropic.Message / Anthropic.MessageCreateParamsNonStreaming."
echo " Route runtime chat calls through src/core/ai/gateway.ts."
FAILED=1
fi
done < <(grep -En "from\s+['\"]@anthropic-ai/sdk['\"]" "$f" 2>/dev/null)
# Dynamic import — also a value-shaped reference.
if grep -En "import\s*\(\s*['\"]@anthropic-ai/sdk['\"]" "$f" 2>/dev/null \
| grep -vE '^[0-9]+:\s*(//|\*)' | grep .; then
echo
echo "ERROR: $f dynamically imports @anthropic-ai/sdk."
echo " Route runtime chat calls through src/core/ai/gateway.ts."
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
exit 1
fi
echo "OK: gateway-routed files have no direct Anthropic SDK construction"
echo " (guarded: ${GUARDED_FILES[*]})"
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env bash
# scripts/check-key-files-current-state.sh — the anti-disease guard.
#
# CLAUDE.md grew to ~592KB / ~147k tokens (auto-loaded every session) once its
# per-file index became append-only: one `**vX.Y.Z (#NNN):**` clause per release
# per file. This guard makes that recurrence structurally impossible. A written
# rule caused the disease; a CI guard cures it.
#
# TWO HARD GATES (fail the build):
# 1. Bolded-release-clause ban — the reference docs (docs/architecture/KEY_FILES.md,
# docs/architecture/thin-client.md, docs/TESTING.md) describe CURRENT behavior
# only. Release history lives in CHANGELOG.md + git. The bolded `**v0.<digit>`
# marker is the disease signature; it must not appear in those docs. Plain prose
# ("as of pgvector 0.7", "Postgres 11+") is fine — only the bolded release
# marker is banned, so this never false-fires on legitimate version mentions.
# 2. CLAUDE.md size cap — the structural backstop. Even if someone ignores the
# prose rule and pads CLAUDE.md, the size gate catches it.
#
# SOFT WARNS (stderr, non-fatal): prose history markers that suggest narration
# creeping back ("pre-fix", ", then v0.", "superseded by") in the reference docs.
#
# Usage:
# bash scripts/check-key-files-current-state.sh
#
# Env overrides (for the guard's own test):
# GBRAIN_DOC_GUARD_ROOT repo root to scan (default: script's ../)
# GBRAIN_CLAUDE_MD_MAX_BYTES CLAUDE.md hard cap (default: 60000; post-restructure
# CLAUDE.md is ~39KB, so this leaves headroom while
# staying far below the ~592KB disease state)
#
# Exit codes:
# 0 clean
# 1 a hard gate failed
set -uo pipefail
ROOT="${GBRAIN_DOC_GUARD_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
MAX_BYTES="${GBRAIN_CLAUDE_MD_MAX_BYTES:-60000}"
# Reference docs that MUST stay current-state (history-free).
REFERENCE_DOCS=(
"docs/architecture/KEY_FILES.md"
"docs/architecture/thin-client.md"
"docs/TESTING.md"
)
fail=0
# ── Gate 1: bolded release-clause ban ──────────────────────────────────────
for rel in "${REFERENCE_DOCS[@]}"; do
doc="$ROOT/$rel"
[ -f "$doc" ] || continue
hits=$(grep -nE '\*\*v0\.[0-9]' "$doc" || true)
if [ -n "$hits" ]; then
fail=1
echo "FAIL: $rel contains bolded release-clause markers (append-only history is the disease this guard prevents)." >&2
echo " Reference docs describe CURRENT behavior only; release history goes in CHANGELOG.md + git." >&2
echo " Collapse each version-clause chain into the single current truth. Offending lines:" >&2
printf '%s\n' "$hits" | sed 's/^/ /' | cut -c1-140 >&2
fi
done
# ── Gate 2: CLAUDE.md size cap ─────────────────────────────────────────────
claude="$ROOT/CLAUDE.md"
if [ -f "$claude" ]; then
bytes=$(wc -c < "$claude" | tr -d ' ')
if [ "$bytes" -gt "$MAX_BYTES" ]; then
fail=1
echo "FAIL: CLAUDE.md is $bytes bytes, over the $MAX_BYTES cap." >&2
echo " CLAUDE.md is orientation + resolver, not the implementation spec. Per-file/" >&2
echo " per-command/per-test detail belongs in the on-demand reference docs" >&2
echo " (docs/architecture/KEY_FILES.md, docs/TESTING.md, docs/RELEASING.md), not here." >&2
fi
fi
# ── Soft warns: prose history markers creeping into reference docs ──────────
for rel in "${REFERENCE_DOCS[@]}"; do
doc="$ROOT/$rel"
[ -f "$doc" ] || continue
warns=$(grep -cnE ', then v0\.|superseded by|pre-fix|post-fix' "$doc" || true)
if [ "${warns:-0}" -gt 0 ]; then
echo "WARN: $rel has $warns prose history marker(s) ('pre-fix' / ', then v0.' / 'superseded by'). Prefer current-state phrasing." >&2
fi
done
if [ "$fail" -ne 0 ]; then
exit 1
fi
echo "check-key-files-current-state: ok (reference docs history-free; CLAUDE.md within cap)"
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env bash
# v0.41.18.0 — CI guard against double-retry hazard.
#
# Engine batch methods (addLinksBatch / addTimelineEntriesBatch /
# upsertChunks) self-retry via withRetry(BULK_RETRY_OPTS) inside the engine
# implementation. Wrapping them ALSO at the call site produces 3×3=9 retry
# attempts under failure, amplifying load on a recovering circuit breaker
# and worsening the very incident the wave was designed to fix.
#
# This script greps src/ for the pattern and fails the build if found.
# Catches the migration-ordering hazard from the v0.41.18.0 eng review (D6)
# AND prevents future refactors from re-introducing the bug class.
#
# Usage: scripts/check-no-double-retry.sh
# Exit: 0 when no matches, 1 when matches found.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
# Match: withRetry(...) wrapping any of the 3 engine batch methods.
# The greedy `.*` between `withRetry(` and `engine.` covers both the
# arrow-fn form and any direct invocation. (gbrain-allow-direct-insert: doc comment)
# Multi-line wraps are caught by `grep -E` per file (line-wise) for the
# common single-line case; multi-line wraps still get caught by a separate
# multi-line pass below.
PATTERN='withRetry\([^)]*engine\.(addLinksBatch|addTimelineEntriesBatch|upsertChunks)'
# Single-line scan (covers ~95% of real cases).
if grep -rEn "$PATTERN" src/ --include='*.ts' 2>/dev/null; then
echo
echo "ERROR: Found withRetry(...engine.{addLinksBatch|addTimelineEntriesBatch|upsertChunks})"
echo " pattern in src/."
echo
echo " Engine batch methods self-retry via withRetry(BULK_RETRY_OPTS) in"
echo " postgres-engine.ts + pglite-engine.ts. Wrapping AGAIN at the call site"
echo " produces 3×3=9 retry attempts under failure, amplifying load on a"
echo " recovering circuit breaker."
echo
echo " Fix: delete the outer withRetry wrap. Pass auditSite as a kwarg:"
echo " await engine.addLinks(batch, { auditSite: 'extract.links_inc' }); // example"
echo
echo " Audit JSONL records the retries silently at "
echo " ~/.gbrain/audit/batch-retry-YYYY-Www.jsonl; check"
echo " \`gbrain doctor\` for the batch_retry_health surface."
exit 1
fi
# Multi-line scan: a withRetry( on one line and the engine call on the next
# few. Bounded to 3-line window so we don't flag distant unrelated calls.
# Uses pcregrep if available, else falls back to a simple awk window.
if command -v pcregrep >/dev/null 2>&1; then
if pcregrep -r -M -n --include='\.ts$' \
'withRetry\([^)]*\n\s*\(?[^)]*=>\s*engine\.(addLinksBatch|addTimelineEntriesBatch|upsertChunks)' \
src/ 2>/dev/null; then
echo
echo "ERROR: Multi-line withRetry(...engine.batch...) wrap found in src/. See above."
exit 1
fi
fi
echo "OK: no withRetry(...engine.batch...) double-retry patterns in src/"
-116
View File
@@ -1,116 +0,0 @@
#!/usr/bin/env bash
# v0.39 — CI guard against bypassing the localOnly filter on the HTTP MCP
# surface. Lives alongside check-jsonb-pattern.sh / check-progress-to-stdout.sh
# in the bun run verify chain.
#
# Background: serve-http.ts builds the HTTP MCP tools/list response from
# `operations.filter(op => !op.localOnly)`. That filter is the only thing
# keeping localOnly ops (sync_brain, file_upload, file_list, file_url —
# any admin op the user EXPLICITLY marked as CLI-only) off the wire.
#
# If a future HTTP-exposing module imports `operations` from
# core/operations.ts WITHOUT applying the filter, the localOnly contract
# silently breaks: a write-scoped OAuth client could submit `sync_brain`
# or `file_upload` over HTTP. Codex outside-voice review of the e2e-test-
# wave (CMT-3) flagged this exact bypass class.
#
# The guard works by:
# 1. Listing every file that imports `operations` from core/operations.ts.
# 2. Comparing against an explicit ALLOWLIST of known-safe importers
# (each with a rationale below).
# 3. Failing if a new file is missing from the allowlist — forces the
# author to either (a) join the allowlist with an explicit rationale,
# or (b) apply the canonical filter at import.
#
# This is a structural defense. The runtime defense lives in
# test/operations-trust-boundary.test.ts (the canonical filter contract +
# handler-invocation cases for the historically-broken classes).
#
# To allow a new importer: add the relative path to ALLOWED below with a
# one-line comment explaining why localOnly isn't a concern there.
#
# Exit: 0 when no unknown importers, 1 when at least one is missing from
# the allowlist.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
# Files allowed to import `operations` directly. Each entry must be
# accompanied by a one-line rationale (the comment on the same line).
ALLOWED=(
"src/cli.ts" # local CLI; user owns the machine, no trust boundary
"src/mcp/dispatch.ts" # shared dispatch; sets ctx.remote from caller, handlers self-gate
"src/mcp/server.ts" # stdio MCP; local-trusted (binary on user's box)
"src/mcp/http-transport.ts" # superseded by serve-http.ts; kept for back-compat tests
"src/mcp/tool-defs.ts" # pure helper; takes ops as parameter, never exposes them
"src/core/minions/tools/brain-allowlist.ts" # subagent registry; has its own opt-in allowlist (separate from localOnly)
"src/commands/capture.ts" # local CLI tool; not network-exposed
"src/commands/enrich.ts" # local CLI tool; calls put_page handler with remote=false, not network-exposed
"src/commands/book-mirror.ts" # local CLI tool; not network-exposed
"src/commands/tools-json.ts" # gbrain --tools-json introspection; full op list IS the purpose
"src/commands/serve-http.ts" # MUST APPLY .filter(op => !op.localOnly) — verified by grep below
)
# Pattern: any import that brings the `operations` VALUE in from core/operations.ts.
# Three shapes the value can enter through; each must be caught:
# - destructured: import { operations } from '...core/operations.ts'
# - aliased: import { operations as ops } from '...core/operations.ts'
# - namespace: import * as opsModule from '...core/operations.ts'
# The original narrow regex only matched the destructured form — codex caught
# the bypass class during /ship adversarial review (aliased + namespace forms
# slipped through). The broadened regex below specifically requires `operations`
# inside the destructured clause OR a namespace import (`* as X`); type-only
# imports of sibling exports like `sourceScopeOpts` / `OperationContext` are
# left alone (those don't expose the op list to a transport surface).
PATTERN='import[[:space:]]+(\*[[:space:]]+as[[:space:]]+[a-zA-Z_$][a-zA-Z0-9_$]*|\{[^}]*\boperations\b[^}]*\})[[:space:]]+from[[:space:]]*['\''"][^'\''"]*core/operations\.ts['\''"]'
# Collect files that import `operations`. Use a while-loop over grep output
# instead of `mapfile` to stay compatible with macOS's default bash 3.2.
FOUND_FILES=""
while IFS= read -r f; do
[ -n "$f" ] && FOUND_FILES="$FOUND_FILES$f"$'\n'
done < <(grep -rlE --include='*.ts' "$PATTERN" src/ 2>/dev/null | sort -u || true)
FAIL=0
# Check 1: every found file is in ALLOWED.
while IFS= read -r file; do
[ -z "$file" ] && continue
rel="${file#"$ROOT/"}"
ok=0
for allowed in "${ALLOWED[@]}"; do
if [ "$rel" = "$allowed" ]; then
ok=1
break
fi
done
if [ "$ok" -eq 0 ]; then
echo "FAIL: $rel imports operations but is not in scripts/check-operations-filter-bypass.sh ALLOWED list."
echo " Either apply .filter(op => !op.localOnly) at the import boundary,"
echo " or add this file to ALLOWED with a one-line rationale."
FAIL=1
fi
done <<< "$FOUND_FILES"
# Check 2: serve-http.ts MUST contain the canonical filter expression near
# its operations import. Without the filter, the entire HTTP MCP surface
# leaks localOnly ops.
SERVE_HTTP="src/commands/serve-http.ts"
if [ -f "$SERVE_HTTP" ]; then
if ! grep -qE 'operations\.filter\(\s*op\s*=>\s*!op\.localOnly\s*\)' "$SERVE_HTTP"; then
echo "FAIL: $SERVE_HTTP no longer contains the canonical"
echo " operations.filter(op => !op.localOnly) expression. The HTTP MCP"
echo " surface depends on this filter to enforce localOnly. Restore"
echo " the filter or refactor the trust boundary explicitly."
FAIL=1
fi
fi
if [ "$FAIL" -eq 1 ]; then
echo ""
echo "Hint: see test/operations-trust-boundary.test.ts for the runtime contract."
exit 1
fi
exit 0
-5
View File
@@ -96,11 +96,6 @@ fi
# against recipes/ all reference the banned name by necessity.
ALLOW_LIST=(
'scripts/check-privacy.sh'
# v0.41.16.0: sibling rule-enforcement script for test/fixtures/
# conversation-formats/. Same meta-exception as check-privacy.sh
# itself — the script's BANNED_TOKENS array literally names the
# tokens it forbids.
'scripts/check-fixture-privacy.sh'
'CLAUDE.md'
'llms-full.txt'
'docs/UPGRADING_DOWNSTREAM_AGENTS.md'
-70
View File
@@ -1,70 +0,0 @@
#!/bin/bash
# scripts/check-source-scope-onboard.sh
# v0.41.18.0 (A26, T17). Grep guard against SQL sites in src/core/onboard/
# and the 4 new onboard-derived doctor checks that touch source_id-bearing
# tables (pages, content_chunks, takes, links, timeline_entries) WITHOUT
# either:
# (a) including source_id / source_ids in the WHERE clause, OR
# (b) carrying the explicit opt-out marker `sourcescope:brain-wide` in
# an adjacent comment.
#
# Brain-wide metrics (embed_staleness, takes_count, total entity counts)
# are legitimate brain-wide queries — they MUST NOT auto-filter by source
# because the metric IS "across all sources". The opt-out marker is the
# explicit acknowledgement that this is intentional. Any new code touching
# per-source data WITHOUT the marker has to add source-scoping.
set -e
FILES_TO_CHECK=(
"src/core/onboard/checks.ts"
"src/core/onboard/impact-capture.ts"
"src/core/onboard/render.ts"
"src/commands/onboard.ts"
)
ERR=0
for f in "${FILES_TO_CHECK[@]}"; do
if [ ! -f "$f" ]; then
continue
fi
# Skip a file entirely when it doesn't contain SQL at all.
if ! grep -qE 'executeRaw|SELECT|INSERT|UPDATE|DELETE' "$f"; then
continue
fi
# File-level opt-out: if the file declares `sourcescope:file-brain-wide`
# in its header (first 30 lines), every SQL site inside is treated as
# intentionally brain-wide. Use sparingly — only for files whose SQL
# is structurally always-aggregate (onboard/checks.ts, impact-capture.ts).
if head -30 "$f" | grep -q 'sourcescope:file-brain-wide'; then
continue
fi
# Search for SQL-ish lines that DO NOT contain source_id and DO NOT have
# the brain-wide opt-out marker on the same line or within 3 lines above.
while IFS=: read -r line content; do
# Skip if source_id mentioned on this or nearby lines (5-line window).
start=$((line - 4))
[ "$start" -lt 1 ] && start=1
if sed -n "${start},${line}p" "$f" | grep -qE 'source_id|sourceIds|sourcescope:brain-wide'; then
continue
fi
echo "[check-source-scope-onboard] $f:$line — SQL site lacks source_id WHERE clause OR brain-wide opt-out marker"
echo " $content"
ERR=1
done < <(grep -nE 'FROM pages|FROM content_chunks|FROM takes\b|FROM links|FROM timeline_entries|UPDATE pages|UPDATE content_chunks|DELETE FROM pages|DELETE FROM content_chunks' "$f" || true)
done
if [ "$ERR" -eq 1 ]; then
echo ""
echo "[check-source-scope-onboard] One or more SQL sites in onboard surfaces lack source_id scoping."
echo "Either: (a) add source_id = \$N (or source_id = ANY(\$N::text[])) to the WHERE,"
echo " or (b) add a comment marker 'sourcescope:brain-wide' within 4 lines above"
echo " the SQL to declare intent."
exit 1
fi
exit 0
+1 -13
View File
@@ -20,19 +20,7 @@
set -euo pipefail
# Resolution order for the scan root:
# 1. $GBRAIN_SCAN_ROOT explicit override — tests pass this so they
# don't depend on `git rev-parse` walking up to an unrelated parent
# .git/ on filesystems where `git init` silently fails under
# shard-concurrency load (v0.40.10 flake-hardening fix).
# 2. `git rev-parse --show-toplevel` — production callers from inside
# the gbrain repo.
# 3. $PWD — last-resort fallback for callers without git.
if [ -n "${GBRAIN_SCAN_ROOT:-}" ]; then
ROOT="$GBRAIN_SCAN_ROOT"
else
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
fi
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
# Banned direct-call patterns. Each is a method on BrainEngine that
+2
View File
@@ -38,6 +38,7 @@ test/friction.test.ts
test/gbrain-home-isolation.test.ts
test/helpers/with-env.test.ts
test/http-transport.test.ts
test/hybrid-meta.test.ts
test/init-migrate-only.test.ts
test/integrations.test.ts
test/mcp-eval-capture.test.ts
@@ -62,6 +63,7 @@ test/resolvers.test.ts
test/scenarios.test.ts
test/schema-bootstrap-coverage.test.ts
test/search-limit.test.ts
test/seed-pglite.test.ts
test/skillpack-check.test.ts
test/source-resolver.test.ts
test/storage-sync.test.ts
+3 -17
View File
@@ -44,8 +44,7 @@ TARGET_DIR="${1:-test}"
ALLOWLIST_FILE="$ROOT/scripts/check-test-isolation.allowlist"
# Read allowlist (one filename per line, # comments allowed). Empty file
# is fine — every violation will fail. Cached into ALLOWLIST so the
# per-file check (~700 lookups per run) is one pure-bash `case` match.
# is fine — every violation will fail.
ALLOWLIST=""
if [ -f "$ALLOWLIST_FILE" ]; then
ALLOWLIST="$(grep -v '^[[:space:]]*#' "$ALLOWLIST_FILE" | grep -v '^[[:space:]]*$' || true)"
@@ -53,21 +52,8 @@ fi
is_allowlisted() {
local f="$1"
if [ -z "$ALLOWLIST" ]; then
return 1
fi
# Use a pure-bash `case` whole-line match against the newline-delimited
# allowlist instead of `echo | grep -qxF`. v0.41.8 CI flake (verify job
# 77771356276): the grep pipe form occasionally failed to match the
# first allowlist entry on Ubuntu 24.04 + bash 5 under
# `bun run` + GNU `timeout` (couldn't reproduce on macOS bash 3.2 with
# the same allowlist file content + lint script content + checkout
# state). Pure-bash case is locale-free, pipe-free, subshell-free,
# set-e-quirk-free, and ~100x faster on every call.
case $'\n'"$ALLOWLIST"$'\n' in
*$'\n'"$f"$'\n'*) return 0 ;;
esac
return 1
[ -z "$ALLOWLIST" ] && return 1
echo "$ALLOWLIST" | grep -qxF "$f"
}
# Find non-serial unit test files (excluding test/e2e). Portable across
@@ -1,93 +0,0 @@
#!/usr/bin/env bash
# v0.41.22.2 — CI guard against the v0.41.22.1 lock-renewal crash class.
#
# The bug pattern: `setInterval(async () => { await something() })` lets
# any throw inside the async callback propagate to Node's process-level
# `unhandledRejection` handler, which kills the worker with exit 1.
# Production lost ~39 worker processes/day to this exact shape when
# PgBouncer rotated connections during a renewLock call.
#
# This guard enforces two invariants on `src/core/minions/worker.ts`:
#
# 1. The BUG pattern is absent: no `setInterval(async ...)` literal.
# A future refactor that inlines `setInterval(async () => { await
# renewLock(...) })` again would re-introduce the v0.41.22.1
# crash class via the exact original surface.
#
# 2. The GOOD pattern is present: launchJob calls `runLockRenewalTick`.
# Without this call-site, the timer logic could be re-inlined via
# a different shape AND bypass the first invariant. The
# `runLockRenewalTick` extraction is also the only test seam that
# gives the state machine behavioral coverage.
#
# Intentionally bug-pattern-specific, not implementation-specific: a
# future refactor to `setTimeout`-recursion or `AbortController`-based
# scheduling passes as long as the bug pattern stays absent (codex C12
# from the v0.41.22.2 outside-voice review).
#
# Usage: scripts/check-worker-lock-renewal-shape.sh
# Exit: 0 when shape is good, 1 when violations found.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
# Allow tests to override the target file for fixture-based meta-tests.
TARGET="${GBRAIN_LOCK_RENEWAL_SHAPE_TARGET:-src/core/minions/worker.ts}"
if [ ! -f "$TARGET" ]; then
echo "ERROR: shape guard target file not found: $TARGET"
exit 1
fi
# Invariant 1: the LOCK-RENEWAL site must not use the bug shape.
#
# The bug-class regex is `setInterval(...async...)`, but it appears
# legitimately elsewhere in worker.ts (the stall-detector loop at
# line ~269 uses it with try/catch — codex C13 covers re-entrancy
# guard for that path separately). To keep this guard from fighting
# unrelated decisions, we narrow scope to the renewal timer
# specifically by requiring the assignment shape `lockTimer = setInterval(`.
#
# A future refactor that renames `lockTimer` would slip past this
# guard; that's an accepted tradeoff (the variable name has been
# stable since v0.10 and is the load-bearing test seam for
# `launchJob`'s `inFlight` accounting).
#
# Uses POSIX ERE + [[:space:]] for BSD-grep portability (macOS shipping
# grep doesn't support -P / \s).
if grep -Eq 'lockTimer[[:space:]]*=[[:space:]]*setInterval\([[:space:]]*async' "$TARGET"; then
echo "ERROR: $TARGET contains the v0.41.22.1 bug pattern (\`setInterval(async ...)\`)."
echo
echo " Async timer callbacks let unhandledRejection escape to the"
echo " process-level handler and crash the worker daemon."
echo
echo " Fix: wrap the timer callback synchronously around an IIFE that"
echo " routes through src/core/minions/lock-renewal-tick.ts:"
echo
echo " setInterval(() => {"
echo " if (tickInFlight) return;"
echo " tickInFlight = true;"
echo " void runLockRenewalTick(deps, state)"
echo " .then(handleResult)"
echo " .catch(handlePostError)"
echo " .finally(() => { tickInFlight = false; });"
echo " }, lockDurationMs / 2);"
exit 1
fi
# Invariant 2: good pattern present. launchJob must call
# `runLockRenewalTick` or the test seam is gone.
if ! grep -q 'runLockRenewalTick' "$TARGET"; then
echo "ERROR: $TARGET does not call \`runLockRenewalTick\`."
echo
echo " Lock-renewal logic must route through"
echo " src/core/minions/lock-renewal-tick.ts so the state-machine"
echo " behavior stays unit-testable (no PGLite needed, no"
echo " setInterval / process plumbing in tests). Re-introduce the"
echo " call site at launchJob's renewal timer."
exit 1
fi
echo "lock-renewal shape OK ($TARGET)"
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env bash
# CI guard: protect the worker-pool atomicity invariant (v0.41.15.0, D5).
#
# `src/core/worker-pool.ts:runSlidingPool` rests on `const idx = nextIdx++`
# being atomic across N concurrent workers. Two failure modes silently
# break the invariant; this guard rejects both.
#
# FAILURE MODE 1: `worker_threads` import in any file that imports
# `runSlidingPool` or `runWithLimit`. Pool work crossing kernel threads
# loses the JS event-loop guarantee. Two workers could claim the same
# idx; silent duplicate work, duplicate DB writes. Same failure shape as
# the per-page lock in extract-conversation-facts exists to defend
# against, but the lock is defense-in-depth — atomicity is the primary
# correctness story.
#
# FAILURE MODE 2: An `await` between the read and write of `nextIdx` in
# `worker-pool.ts` itself. Pattern like `const idx = await getNextIdx()`
# introduces a yield window between read and write; another worker can
# run during the yield and claim the same idx.
#
# Usage: scripts/check-worker-pool-atomicity.sh
# Exit: 0 when invariants hold, 1 when a violation is found.
set -euo pipefail
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
POOL_FILE="src/core/worker-pool.ts"
if [ ! -f "$POOL_FILE" ]; then
echo "OK: $POOL_FILE not present yet — guard is no-op"
exit 0
fi
# -----------------------------------------------------------------------
# FAILURE MODE 1: worker_threads alongside the helper.
# Find every src/ file that imports from the helper, then check whether
# any of them ALSO imports node:worker_threads / worker_threads.
# -----------------------------------------------------------------------
IMPORT_PATTERN="from ['\"][^'\"]*worker-pool[^'\"]*['\"]"
HELPER_CALLERS=$(grep -rlE "$IMPORT_PATTERN" src/ 2>/dev/null || true)
if [ -n "$HELPER_CALLERS" ]; then
WORKER_THREADS_VIOLATIONS=""
for caller in $HELPER_CALLERS; do
if grep -E "from ['\"](node:)?worker_threads['\"]" "$caller" >/dev/null 2>&1; then
WORKER_THREADS_VIOLATIONS="$WORKER_THREADS_VIOLATIONS$caller\n"
fi
done
if [ -n "$WORKER_THREADS_VIOLATIONS" ]; then
echo "ERROR: worker_threads imported in file(s) that also use runSlidingPool / runWithLimit:"
# shellcheck disable=SC2059
printf "$WORKER_THREADS_VIOLATIONS"
echo
echo " The sliding pool's atomicity invariant relies on the single"
echo " JS event loop. worker_threads crosses kernel threads; two"
echo " workers can claim the same idx; duplicate work + DB writes."
echo " See src/core/worker-pool.ts header for the full invariant."
exit 1
fi
fi
# -----------------------------------------------------------------------
# FAILURE MODE 2: await between nextIdx read and write inside the helper.
# The legal forms are:
# let nextIdx = 0;
# const idx = nextIdx++;
# Anything matching `await.*nextIdx` or `nextIdx.*await` in the helper
# body indicates a yield window between read and write.
# -----------------------------------------------------------------------
# Strip multi-line comments + single-line comments before checking, so
# `await` mentions in documentation don't false-fire. The pool file's
# header explicitly mentions `await getNextIdx()` as the BAD pattern;
# without comment-stripping, this guard would always fail.
STRIPPED=$(sed -E '
# Drop /** ... */ block comments (greedy single-line form only).
/^\s*\/\*/,/\*\//d
# Drop // line comments.
s|//.*$||
' "$POOL_FILE")
if echo "$STRIPPED" | grep -E '(await\s+[a-zA-Z_$]*[Nn]ext[Ii]dx|nextIdx[^+]*await)' >/dev/null 2>&1; then
echo "ERROR: found await near nextIdx in $POOL_FILE"
echo " The claim `const idx = nextIdx++` must remain a single"
echo " synchronous statement. Inserting an await between the read"
echo " and write breaks atomicity: another worker can run during"
echo " the yield window and claim the same idx."
echo " See src/core/worker-pool.ts header for the full invariant."
exit 1
fi
echo "OK: worker-pool atomicity invariant intact"
-174
View File
@@ -1,174 +0,0 @@
#!/usr/bin/env bash
# scripts/ci-cache-hash.sh — deterministic content hash of all test-
# affecting files for the CI auto-cache.
#
# Outputs a 16-character hex prefix of sha256(sorted list of
# `<git-blob-sha> <path>` lines for every tracked file EXCEPT the
# deny-list below). Same tree → same hash. Different code → different
# hash. Doc-only changes → same hash (cache hit).
#
# Used by .github/workflows/test.yml's cache-check job. Cache key is
# `ci-pass-<hash>`. When the hash matches a prior green run, the test
# matrix skips and reports green immediately.
#
# DESIGN: deny-list NOT allowlist. New files default to "include in
# hash" — worst case, cache miss (waste 8min). Allowlist would default
# to "exclude", risking false-pass (broken code shipped under green
# check) when someone adds a new file type that tests read.
#
# WHAT'S DENY-LISTED (genuinely test-irrelevant):
# - CHANGELOG.md, TODOS.md pure documentation
# - README.md, LICENSE marketing / metadata, no test reads them
# - docs/**/*.md, *.txt all docs/ subtree is doc-only
#
# WHAT'S DELIBERATELY NOT DENY-LISTED (affects test outcomes):
# - CLAUDE.md 8+ test files reference it (resolver-merge, schema-cli,
# public-exports, eval-cross-modal-batch, etc.)
# - AGENTS.md same — referenced by resolver tests; counterpart to
# CLAUDE.md for OpenClaw hosts
# - skills/** SKILL.md files are read by skill conformance tests
# - everything else under src/, test/, scripts/, .github/, package.json,
# bun.lock, tsconfig*.json, the schema files — obviously test-affecting
#
# POLICY-DOC RE-ADMIT (the docs/ exception): some docs/*.md files carry
# CI / release / test CONTRACTS that the test suite reads (e.g. the
# build-llms content-contract test, the doc-history guard). The broad
# `^docs/.*\.md$` deny above would let a policy edit to those skip CI — a
# false-pass. The ALLOW_PATTERNS list below re-admits them into the hash
# AFTER the deny. ADD a path there whenever you move a policy/contract doc
# under docs/ (current entries: docs/TESTING.md, docs/RELEASING.md).
#
# Locale-stable: LC_ALL=C on the sort step so byte-order is identical
# across runners (different default locales would re-order the line list
# and change the final hash).
#
# Usage:
# bash scripts/ci-cache-hash.sh # print hash
# bash scripts/ci-cache-hash.sh --verbose # print hash + diagnostics to stderr
#
# Exit codes:
# 0 printed a 16-char hex hash
# 1 internal error (git failure, etc.)
# 2 usage error
set -euo pipefail
VERBOSE=0
if [ "${1:-}" = "--verbose" ]; then
VERBOSE=1
shift
fi
if [ "$#" -gt 0 ]; then
echo "usage: bash scripts/ci-cache-hash.sh [--verbose]" >&2
exit 2
fi
cd "$(dirname "$0")/.."
# Deny-list as an extended regex matched against full paths emitted by
# `git ls-files`. -x makes the match anchored (full-line). Each pattern
# is a path predicate, not a glob — `\.` to literal-match dots.
#
# To add a new deny entry: add another `-e '<regex>'` line below. To
# REMOVE a deny entry (= include the path back in hash): delete its line.
# Either change invalidates the cache for everyone on next run (different
# hash output), which is the correct behavior.
DENY_PATTERNS=(
-e '^CHANGELOG\.md$'
-e '^TODOS\.md$'
-e '^README\.md$'
-e '^LICENSE$'
-e '^docs/.*\.md$'
-e '^docs/.*\.txt$'
)
# Use `git ls-files -s` for one-shot enumeration of tracked files + their
# index blob shas. Output shape: `<mode> <sha> <stage>\t<path>` per line.
# Far faster than per-file `git hash-object` (~30ms vs ~9s on 2000 files).
#
# Trade-off: this reflects the INDEX (committed/staged state), not the
# working tree. CI always works against a committed tree so this matches
# what tests actually run. Local dev with uncommitted edits sees the
# committed-side hash (close enough — the hash is for CI's cache lookup,
# not a tree-state diagnostic).
LS_FILES=$(git ls-files -s)
if [ -z "$LS_FILES" ]; then
echo "error: git ls-files -s returned empty (are we in a git repo?)" >&2
exit 1
fi
# Apply deny-list. Each line ends in `\t<path>` so the deny patterns
# anchor on a tab boundary. Compose the alternation regex from
# DENY_PATTERNS — each entry is `^<pat>$`; strip the `^` (since `\t`
# acts as our anchor in `git ls-files -s` output) and the trailing `$`
# stays as-is.
DENY_ALT=""
i=1
while [ $i -lt ${#DENY_PATTERNS[@]} ]; do
p="${DENY_PATTERNS[$i]}"
p="${p#^}"
if [ -z "$DENY_ALT" ]; then
DENY_ALT="$p"
else
DENY_ALT="$DENY_ALT|$p"
fi
i=$((i + 2))
done
DENY_RE=$(printf '\t(%s)' "$DENY_ALT")
# Note: each $p already ends in `$` to anchor end-of-line, so the full
# regex is `\t(^CHANGELOG\.md$|^TODOS\.md$|...)`. Wait — we stripped `^`
# from each but kept `$`, so the composed regex is `\t(CHANGELOG\.md$|
# TODOS\.md$|docs/.*\.md$|...)`. Each alternative anchors its own end.
INCLUDED=$(printf '%s\n' "$LS_FILES" | grep -vE "$DENY_RE" || true)
# Re-admit test-affecting policy docs that live under docs/ but carry CI /
# release / test contracts. The broad `^docs/.*\.md$` deny above removed
# them; without this re-admit a policy edit to docs/TESTING.md or
# docs/RELEASING.md would produce the SAME hash and skip the test shard
# that runs the build-llms + doc-history guards — a false-pass. Patterns
# anchor on the `\t<path>` boundary in `git ls-files -s` output, matching
# the deny-list convention above. Re-admitted lines that don't exist yet
# (pre-relocation) simply match nothing.
# Path predicates only (no leading tab here) — the `\t` boundary is added
# via printf below so it is a REAL tab byte, not the two-char string `\t`.
# GNU grep (CI/Ubuntu) does not interpret `\t` in an ERE as a tab the way
# BSD grep (macOS) does, so an inline `\t` matches nothing on CI and the
# re-admit silently no-ops. Mirror the DENY_RE construction exactly.
ALLOW_PATTERNS=(
'docs/TESTING\.md$'
'docs/RELEASING\.md$'
)
ALLOW_ALT=""
for p in "${ALLOW_PATTERNS[@]}"; do
if [ -z "$ALLOW_ALT" ]; then ALLOW_ALT="$p"; else ALLOW_ALT="$ALLOW_ALT|$p"; fi
done
ALLOW_RE=$(printf '\t(%s)' "$ALLOW_ALT")
READMIT=$(printf '%s\n' "$LS_FILES" | grep -E "$ALLOW_RE" || true)
if [ -n "$READMIT" ]; then
INCLUDED=$(printf '%s\n%s\n' "$INCLUDED" "$READMIT" | grep -v '^$' | LC_ALL=C sort -u)
fi
if [ -z "$INCLUDED" ]; then
echo "error: every tracked file is deny-listed — refusing to hash empty set" >&2
exit 1
fi
# Sort by full line (LC_ALL=C for byte-order stability across locales),
# hash the concatenation. Each line carries (mode, sha, path) so any
# change to content (sha), mode (executable bit), or path (rename) flips
# the final hash.
HASH=$(printf '%s\n' "$INCLUDED" \
| LC_ALL=C sort \
| sha256sum \
| cut -c1-16)
if [ "$VERBOSE" = "1" ]; then
included_count=$(printf '%s\n' "$INCLUDED" | wc -l | tr -d ' ')
all_count=$(printf '%s\n' "$LS_FILES" | wc -l | tr -d ' ')
denied_count=$((all_count - included_count))
{
echo "ci-cache-hash: $included_count/$all_count files in hash ($denied_count deny-listed)"
} >&2
fi
echo "$HASH"
-4
View File
@@ -76,10 +76,6 @@ export const E2E_TEST_MAP: Record<string, string[]> = {
"src/mcp/**": ["test/e2e/mcp.test.ts", "test/e2e/http-transport.test.ts"],
// Integrity batch-load fast path.
"src/commands/integrity.ts": ["test/e2e/integrity-batch.test.ts"],
// gbrain connect — raw-bearer MCP smoke probe exercised end-to-end against
// a real serve --http (PGLite), so changes to either feed it.
"src/commands/connect.ts": ["test/e2e/connect-bearer.test.ts"],
"src/core/connect-probe.ts": ["test/e2e/connect-bearer.test.ts"],
// Upgrade chains migration ledger; touches both runners.
"src/commands/upgrade.ts": [
"test/e2e/upgrade.test.ts",
-247
View File
@@ -1,247 +0,0 @@
#!/usr/bin/env bun
/**
* v0.41 E5 A/B harness (D11 + codex pass-2 #7 spec).
*
* Manually-runnable script that proves the auto-adaptive lease-cap
* controller beats fixed-cap on a real upstream. Writes a structured
* receipt to test/fixtures/e5-lease-cap-ab/{timestamp}.json that file
* is committed as the baseline. Future controller changes ship with
* their own receipt + diff against prior.
*
* **Spec (D11):**
* Workload: 500 subagent jobs, log-normal prompt distribution
* (mean 2k tokens, p99 16k tokens). Synthesized via fixture file.
* Provider: Anthropic (real API) via gateway.
* Cost cap: --budget-usd 8 per arm (D5 enforced).
* Failure injection: synthetic 429 burst at minute 15 (10s window).
* Statistical threshold: controller arm must beat fixed-cap on
* (completed_jobs / wall_clock_time) by 5% AND match
* (completed_jobs / dollars_spent) within ±2%.
*
* **Usage:**
* ANTHROPIC_API_KEY=sk-... bun scripts/e5-lease-cap-ab.ts
* ANTHROPIC_API_KEY=sk-... bun scripts/e5-lease-cap-ab.ts --dry-run
*
* **Cost:** ~$16 per full run ($8/arm × 2 arms). Approximate; depends on
* actual prompt lengths sampled from the fixture.
*
* **Not in CI:** This script requires a real API key + ~30min wall-clock
* + real Anthropic budget. Intended to be run BEFORE landing a controller
* change; receipt is the durable artifact. CI gating happens via the
* unit-test suite (`lease-cap-controller.test.ts` covers the pure
* decision function exhaustively).
*/
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import { PostgresEngine } from '../src/core/postgres-engine.ts';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { loadConfig } from '../src/core/config.ts';
import type { BrainEngine } from '../src/core/engine.ts';
interface ArmStats {
arm: 'fixed' | 'adaptive';
jobs_submitted: number;
jobs_completed: number;
jobs_dead: number;
wall_clock_ms: number;
total_cost_usd: number;
lease_cap_history: number[];
bounces: number;
upstream_429s: number;
}
interface ABReceipt {
schema_version: 1;
timestamp: string;
spec: {
job_count: number;
budget_per_arm_usd: number;
injection_at_min: number;
};
arms: ArmStats[];
verdict: {
throughput_advantage_pct: number;
cost_efficiency_delta_pct: number;
pr_gate_pass: boolean;
note: string;
};
}
function parseArgs(argv: string[]) {
const args = {
dryRun: argv.includes('--dry-run'),
jobs: 500,
budgetUsd: 8,
};
for (const arg of argv) {
if (arg.startsWith('--jobs=')) args.jobs = parseInt(arg.split('=')[1] ?? '500', 10);
if (arg.startsWith('--budget-usd=')) args.budgetUsd = parseFloat(arg.split('=')[1] ?? '8');
}
return args;
}
async function openEngine(): Promise<BrainEngine> {
const cfg = loadConfig();
if (cfg?.database_url) {
const engine = new PostgresEngine();
await engine.connect({ database_url: cfg.database_url });
return engine;
}
// Fallback: PGLite ephemeral. Real A/B runs should use Postgres so the
// lease-cap controller's elected-mutator pattern is exercised cross-process.
process.stderr.write('[e5-ab] WARN: using PGLite ephemeral (no DATABASE_URL); cross-worker tests will not run\n');
const engine = new PGLiteEngine();
await engine.connect({ database_url: '' });
await engine.initSchema();
return engine;
}
/**
* Generate a synthetic prompt with the spec'd token distribution.
* Approximate uses character counts as a stand-in for tokens (1 token
* 4 chars on English).
*/
function syntheticPrompt(index: number): string {
// Log-normal mean=2k tokens, σ such that p99 = 16k tokens.
// log(p99/p50) = z_99 * σσ ≈ ln(8) / 2.33 ≈ 0.89
const mu = Math.log(2000);
const sigma = 0.89;
// Box-Muller for deterministic-per-index pseudo-Normal sample.
const u1 = ((index * 9301 + 49297) % 233280) / 233280;
const u2 = ((index * 13849 + 65521) % 233280) / 233280;
const z = Math.sqrt(-2 * Math.log(Math.max(u1, 1e-9))) * Math.cos(2 * Math.PI * u2);
const tokens = Math.exp(mu + sigma * z);
const chars = Math.max(40, Math.min(64000, Math.floor(tokens * 4)));
return `Synthetic A/B test prompt #${index}. Body: ` + 'X'.repeat(chars - 40);
}
async function runArm(
engine: BrainEngine,
arm: 'fixed' | 'adaptive',
opts: { jobs: number; budgetUsd: number; dryRun: boolean },
): Promise<ArmStats> {
const start = Date.now();
const lease_cap_history: number[] = [];
let jobs_submitted = 0;
let jobs_completed = 0;
let jobs_dead = 0;
let total_cost_usd = 0;
let bounces = 0;
let upstream_429s = 0;
process.stderr.write(`[e5-ab] === arm=${arm} starting ===\n`);
// Configure the cap policy for this arm.
if (arm === 'fixed') {
await engine.setConfig('minions.auto_lease_cap', 'false');
await engine.setConfig('minions.lease_cap_current', '8');
} else {
await engine.setConfig('minions.auto_lease_cap', 'true');
await engine.setConfig('minions.lease_cap_current', '8');
}
if (opts.dryRun) {
process.stderr.write(`[e5-ab] --dry-run: skipping real submission. Would submit ${opts.jobs} jobs.\n`);
return {
arm,
jobs_submitted: opts.jobs,
jobs_completed: 0,
jobs_dead: 0,
wall_clock_ms: Date.now() - start,
total_cost_usd: 0,
lease_cap_history: [8],
bounces: 0,
upstream_429s: 0,
};
}
// Real-run scaffolding lives here. v0.41 ships the spec; the full
// dispatcher (queue submit + worker spin-up + 15-min 429 injector +
// tick loop) lands in the follow-up wave when the controller has been
// exercised manually first. Receipt fixture is committed as a baseline
// shape for future runs to diff against.
process.stderr.write(`[e5-ab] arm=${arm}: real-run implementation deferred to v0.41.1 follow-up.\n`);
process.stderr.write(`[e5-ab] See CHANGELOG.md "v0.41.0.0 → v0.41.1.0 follow-up" for details.\n`);
return {
arm,
jobs_submitted,
jobs_completed,
jobs_dead,
wall_clock_ms: Date.now() - start,
total_cost_usd,
lease_cap_history,
bounces,
upstream_429s,
};
}
function computeVerdict(fixed: ArmStats, adaptive: ArmStats): ABReceipt['verdict'] {
// Throughput ratio: completed_jobs / wall_clock_ms. Higher is better.
const tputFixed = fixed.wall_clock_ms > 0 ? fixed.jobs_completed / fixed.wall_clock_ms : 0;
const tputAdaptive = adaptive.wall_clock_ms > 0 ? adaptive.jobs_completed / adaptive.wall_clock_ms : 0;
const throughputAdvantage = tputFixed > 0 ? ((tputAdaptive - tputFixed) / tputFixed) * 100 : 0;
// Cost efficiency ratio: completed_jobs / dollars. Higher is better.
const effFixed = fixed.total_cost_usd > 0 ? fixed.jobs_completed / fixed.total_cost_usd : 0;
const effAdaptive = adaptive.total_cost_usd > 0 ? adaptive.jobs_completed / adaptive.total_cost_usd : 0;
const costEfficiencyDelta = effFixed > 0 ? ((effAdaptive - effFixed) / effFixed) * 100 : 0;
// PR gate: adaptive must beat fixed by ≥5% on throughput AND match
// within ±2% on cost efficiency.
const pr_gate_pass = throughputAdvantage >= 5 && Math.abs(costEfficiencyDelta) <= 2;
return {
throughput_advantage_pct: Math.round(throughputAdvantage * 100) / 100,
cost_efficiency_delta_pct: Math.round(costEfficiencyDelta * 100) / 100,
pr_gate_pass,
note: pr_gate_pass
? 'controller beats fixed-cap; safe to default ON'
: 'controller does NOT meet PR gate; defaults stay OFF',
};
}
async function main() {
const argv = process.argv.slice(2);
const opts = parseArgs(argv);
const engine = await openEngine();
try {
const fixed = await runArm(engine, 'fixed', opts);
const adaptive = await runArm(engine, 'adaptive', opts);
const verdict = computeVerdict(fixed, adaptive);
const receipt: ABReceipt = {
schema_version: 1,
timestamp: new Date().toISOString(),
spec: {
job_count: opts.jobs,
budget_per_arm_usd: opts.budgetUsd,
injection_at_min: 15,
},
arms: [fixed, adaptive],
verdict,
};
const fixtureDir = join(process.cwd(), 'test/fixtures/e5-lease-cap-ab');
if (!existsSync(fixtureDir)) mkdirSync(fixtureDir, { recursive: true });
const receiptPath = join(
fixtureDir,
`${new Date().toISOString().replace(/[:.]/g, '-')}${opts.dryRun ? '-dry-run' : ''}.json`,
);
writeFileSync(receiptPath, JSON.stringify(receipt, null, 2));
process.stderr.write(`[e5-ab] receipt written: ${receiptPath}\n`);
process.stderr.write(`[e5-ab] verdict: ${verdict.note}\n`);
process.exit(verdict.pr_gate_pass || opts.dryRun ? 0 : 1);
} finally {
await engine.disconnect().catch(() => {});
}
}
if (import.meta.main) {
main().catch(err => {
process.stderr.write(`[e5-ab] FATAL: ${err instanceof Error ? err.message : String(err)}\n`);
process.exit(2);
});
}
+3 -105
View File
@@ -48,26 +48,9 @@ export const SECTIONS: DocSection[] = [
{
title: "CLAUDE.md",
description:
"Orientation + resolver. North Star, two axes, architecture + cross-cutting invariants, the reference map pointing at on-demand docs, and the inline ship IRON RULES.",
"Architecture reference. Key files, trust boundaries, engine factory, test layout.",
path: "CLAUDE.md",
},
{
title: "docs/architecture/KEY_FILES.md",
description:
"Per-file index for the gbrain repo: what each src/ file does + its load-bearing invariants. The on-demand detail CLAUDE.md's reference map routes to.",
path: "docs/architecture/KEY_FILES.md",
// Link-only until compressed to current-state (still large pre-compression).
// Flip to inlined once the doc-history compression lands and the bundle
// budget is re-measured.
includeInFull: false,
},
{
title: "docs/architecture/thin-client.md",
description:
"The thin-client / remote-MCP / cross-modal routing seam: isThinClient detection, callRemoteTool, SSRF-hardened URL validation, per-command routing.",
path: "docs/architecture/thin-client.md",
includeInFull: false,
},
{
title: "INSTALL_FOR_AGENTS.md",
description: "9-step agent installation.",
@@ -98,25 +81,6 @@ export const SECTIONS: DocSection[] = [
description:
"MECE directory structure (people/, companies/, concepts/).",
path: "docs/GBRAIN_RECOMMENDED_SCHEMA.md",
// v0.40.6.0: 64KB reference doc. Web index entry stays; the single-fetch
// bundle gets the README + setup guides instead. Keeps llms-full.txt
// under the 600KB budget as CLAUDE.md grows with each release.
includeInFull: false,
},
{
// Re-inlined: the CLAUDE.md resolver restructure (per-file index moved to
// docs/architecture/KEY_FILES.md, link-only) freed ~530KB of bundle
// headroom, so this value-explainer rides the single-fetch bundle again.
title: "docs/what-schemas-unlock.md",
description:
"Why schemas matter: 7 killer use cases (4000 invisible meetings, founder ops brain, research brain, legal brain, team brain, agent-as-co-curator) + the structural argument for typed page kinds. Read this before pitching schema authoring (v0.40.7.0).",
path: "docs/what-schemas-unlock.md",
},
{
title: "docs/schema-author-tutorial.md",
description:
"5-minute walkthrough: fork the bundled pack, add a custom `researcher` type, backfill existing pages via `gbrain schema sync --apply`, prove the T1.5 wiring via `gbrain whoknows` (v0.40.7.0).",
path: "docs/schema-author-tutorial.md",
},
{
title: "docs/guides/live-sync.md",
@@ -133,24 +97,12 @@ export const SECTIONS: DocSection[] = [
description:
"Deploying the gbrain jobs worker: crontab + watchdog, inline --follow, systemd/Procfile/fly.toml, upgrade checklist.",
path: "docs/guides/minions-deployment.md",
// v0.41.8.0: 13KB deployment runbook. Web index entry stays;
// single-fetch bundle drops it to keep under FULL_SIZE_BUDGET
// (CLAUDE.md grew past 600KB once master's v0.41.2-v0.41.6 +
// this wave's annotations landed). Operators read this once;
// agents rarely need it in context.
includeInFull: false,
},
{
title: "docs/guides/quiet-hours.md",
description: "Notification hold + timezone-aware delivery.",
path: "docs/guides/quiet-hours.md",
},
{
title: "docs/guides/scaling-skills.md",
description:
"Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss.",
path: "docs/guides/scaling-skills.md",
},
{
title: "docs/mcp/DEPLOY.md",
description: "MCP server deployment.",
@@ -158,27 +110,6 @@ export const SECTIONS: DocSection[] = [
},
],
},
{
heading: "AI providers",
entries: [
{
title: "docs/ai-providers/zeroentropy.md",
description:
"ZeroEntropy zembed-1 embedding + zerank-2 reranker (hosted): API key, embedding switch, reranker config.",
path: "docs/ai-providers/zeroentropy.md",
// Setup walkthrough — discoverable in the index, not inlined in the
// single-fetch bundle (keeps llms-full.txt under FULL_SIZE_BUDGET).
includeInFull: false,
},
{
title: "docs/ai-providers/llama-server-reranker.md",
description:
"Local reranker via llama.cpp --reranking: Qwen3-Reranker or self-hosted ZE weights, --alias setup, gbrain config keys, cold-start timeout, budget-cap interaction.",
path: "docs/ai-providers/llama-server-reranker.md",
includeInFull: false,
},
],
},
{
heading: "Debugging",
entries: [
@@ -208,12 +139,6 @@ export const SECTIONS: DocSection[] = [
description:
"Patches for downstream agent skill forks. One section per release.",
path: "docs/UPGRADING_DOWNSTREAM_AGENTS.md",
// Excluded from inlined bundle (v0.41.7.0): 25KB of release-by-release
// migration patches that are valuable as a reference but don't need
// to ride along in every llms-full.txt fetch. Pushes the bundle back
// under FULL_SIZE_BUDGET after the v0.41.7.0 scaling-skills guide
// landed.
includeInFull: false,
},
{
title: "skills/migrations/",
@@ -230,26 +155,6 @@ export const SECTIONS: DocSection[] = [
},
],
},
{
heading: "Contributing",
optional: true,
entries: [
{
title: "docs/TESTING.md",
description:
"Test command tiers, the test-isolation lint (R1-R4), the canonical PGLite block, withEnv, the E2E DB lifecycle, and the file taxonomy. Maintainer-facing.",
path: "docs/TESTING.md",
includeInFull: false,
},
{
title: "docs/RELEASING.md",
description:
"Full release + contributor process: pre-ship test requirements, the CHANGELOG voice + release-summary template, the 'To take advantage of vX' block, version migrations, GitHub Actions SHA refresh, PR conventions, community-PR-wave. (Ship IRON RULES stay inline in CLAUDE.md.)",
path: "docs/RELEASING.md",
includeInFull: false,
},
],
},
{
heading: "Philosophy",
optional: true,
@@ -295,13 +200,6 @@ export const INLINE_TIPS = [
"`gbrain upgrade` runs post-upgrade + apply-migrations.",
];
// Target ~800KB so llms-full.txt fits in ~200k-token contexts with room to spare.
// Bumped 600KB→700KB in v0.41.9.0, then 700KB→750KB once CLAUDE.md crossed 700KB,
// then 750KB→800KB in v0.42.10.0 when the #972 global-basename Key Files annotation
// (landing alongside master's #1696/#1699 waves) crossed the 750KB line. CLAUDE.md
// is ~540KB+ (the bulk of the bundle) and grows ~5-15KB per release with each
// feature's Key Files annotation. CLAUDE.md is the whole point of the one-fetch
// bundle, so it stays inlined; the budget tracks its legitimate growth. Still fits
// comfortably in 200k+ context models.
// Target ~600KB so llms-full.txt fits in ~150k-token contexts with room to spare.
// Generator prints a WARN if exceeded; ship with includeInFull=false exclusions.
export const FULL_SIZE_BUDGET = 800_000;
export const FULL_SIZE_BUDGET = 600_000;
-247
View File
@@ -1,247 +0,0 @@
#!/usr/bin/env bun
/**
* scripts/mine-shard-weights.ts extract per-file test wallclock from a
* real CI run's logs, write scripts/test-weights.json.
*
* Why this exists: scripts/sharding.ts does LPT bin-packing over per-file
* weights, but the weights have to come from somewhere. The original
* design ran each test file in isolation (`bun test <file>` per file)
* which (a) takes ~57min to run all 676 files, and (b) measures cold-
* start dominantly because each invocation pays a fresh `bun test`
* startup. CI shards run ~150 files in ONE bun process cold-start is
* amortized away. Per-file isolated profiles are wrong-by-methodology.
*
* This script scrapes per-file wallclock from a real CI shard's log via
* GitHub's `gh run view --log` output. bun emits an `##[group]test/foo.
* test.ts:` header before each file with an ISO timestamp; the
* difference between consecutive headers = how long the previous file
* took. This is the actual CI shard runtime per file, in the right
* execution mode, for free on every green run.
*
* Usage:
* bun run scripts/mine-shard-weights.ts --run <RUN_ID> [--out PATH]
* bun run scripts/mine-shard-weights.ts --from-file <LOG_FILE> [--out PATH]
* gh run view <RUN_ID> --log | bun run scripts/mine-shard-weights.ts [--out PATH]
*
* Default output: scripts/test-weights.json (overwrites). Use --out to
* write elsewhere (useful for diffing before commit). Output is JSON
* with sorted keys for stable diffs.
*
* Regen cadence: there is none. Weights drift continuously but missing
* files fall back to the corpus median in sharding.ts, so stale weights
* degrade gracefully. Run this script when you notice a specific shard
* starts running long, or after a wave that added many heavy tests.
*
* Exit codes:
* 0 wrote weights file (count > 0)
* 1 internal error
* 2 usage error
* 3 no usable timing data found (parsed log but extracted 0 weights)
*/
import { spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..");
const DEFAULT_OUT = resolve(REPO_ROOT, "scripts/test-weights.json");
interface Args {
runId?: string;
fromFile?: string;
fromStdin: boolean;
out: string;
}
function parseArgs(argv: string[]): Args {
const out: Args = { fromStdin: false, out: DEFAULT_OUT };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--run" || a === "--run-id") {
out.runId = argv[++i];
} else if (a === "--from-file") {
out.fromFile = argv[++i];
} else if (a === "--out") {
out.out = resolve(argv[++i] ?? "");
} else if (a === "--help" || a === "-h") {
console.log(
"usage: bun run scripts/mine-shard-weights.ts (--run <ID> | --from-file <PATH> | <stdin>) [--out <PATH>]",
);
process.exit(0);
} else {
console.error(`error: unknown arg: ${a}`);
process.exit(2);
}
}
if (!out.runId && !out.fromFile) {
out.fromStdin = true;
}
return out;
}
async function readSource(args: Args): Promise<string> {
if (args.runId) {
const r = spawnSync("gh", ["run", "view", args.runId, "--log"], {
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024, // CI logs can be 50-80MB
});
if (r.status !== 0) {
throw new Error(
`gh run view ${args.runId} --log failed (exit ${r.status}): ${r.stderr}`,
);
}
return r.stdout;
}
if (args.fromFile) {
return readFileSync(args.fromFile, "utf8");
}
// Read stdin
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
/**
* Parsed timing event from a CI log line. timestamp is ms-since-epoch.
*/
interface TimingEvent {
job: string;
timestampMs: number;
file: string;
}
/**
* Parse a CI log into a list of `##[group]test/X.test.ts:` events keyed
* by job (so timing deltas don't cross shard boundaries).
*
* GH log line shape:
* <job-name>\tUNKNOWN STEP\t<ISO-timestamp> ##[group]test/foo.test.ts:
* or:
* <job-name>\t<step-name>\t<ISO-timestamp> ##[group]test/foo.test.ts:
*
* Exported for unit testing.
*/
export function parseLog(raw: string): TimingEvent[] {
const events: TimingEvent[] = [];
const lines = raw.split("\n");
// Match: <job>TAB<step>TAB<iso-ts> ##[group]<path>:
const re = /^([^\t]+)\t[^\t]*\t(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+##\[group\](test\/[^\s:]+\.test\.ts):?\s*$/;
for (const line of lines) {
const m = re.exec(line);
if (!m) continue;
const job = m[1]!.trim();
const ts = Date.parse(m[2]!);
const file = m[3]!;
if (Number.isNaN(ts)) continue;
events.push({ job, timestampMs: ts, file });
}
return events;
}
/**
* From a list of file-start events grouped by job, compute per-file
* runtime as (timestamp[i+1] - timestamp[i]) within each job. The last
* file in each job is dropped (we don't know when it ended without
* also parsing the bun summary line; the loss is acceptable since
* sharding.ts's median fallback covers missing files).
*
* When the same file appears in multiple jobs (shouldn't happen, but
* defensive against shard remix during the in-flight CI run that
* generated this log), take the max heaviest observation wins.
*
* Exported for unit testing.
*/
export function computeWeights(events: TimingEvent[]): Map<string, number> {
// Group events by job, in stream order.
const byJob = new Map<string, TimingEvent[]>();
for (const e of events) {
let bucket = byJob.get(e.job);
if (!bucket) {
bucket = [];
byJob.set(e.job, bucket);
}
bucket.push(e);
}
const weights = new Map<string, number>();
for (const [, jobEvents] of byJob) {
for (let i = 0; i + 1 < jobEvents.length; i++) {
const file = jobEvents[i]!.file;
const delta = jobEvents[i + 1]!.timestampMs - jobEvents[i]!.timestampMs;
if (delta < 0) continue; // log out-of-order; defensive
// Round to nearest ms; sub-ms doesn't matter for shard balancing.
const ms = Math.round(delta);
const prev = weights.get(file);
if (prev === undefined || ms > prev) {
weights.set(file, ms);
}
}
// Drop the last event's file (no successor → unknown duration).
}
return weights;
}
/**
* Serialize a weights map to canonical JSON (keys sorted asc) so the
* committed file produces stable diffs run-to-run.
*
* Exported for unit testing.
*/
export function serializeWeights(weights: Map<string, number>): string {
const sorted = Array.from(weights.entries()).sort((a, b) =>
a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0,
);
const obj: Record<string, number> = {};
for (const [k, v] of sorted) obj[k] = v;
return JSON.stringify(obj, null, 2) + "\n";
}
async function main(): Promise<number> {
const args = parseArgs(process.argv.slice(2));
console.error(
`[mine-shard-weights] source=${args.runId ?? args.fromFile ?? "<stdin>"}`,
);
let raw: string;
try {
raw = await readSource(args);
} catch (e) {
console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
return 1;
}
const events = parseLog(raw);
console.error(`[mine-shard-weights] parsed ${events.length} file-start events`);
if (events.length === 0) {
console.error(
"error: no ##[group]test/*.test.ts: events found in input. Was this a CI test run log?",
);
return 3;
}
const weights = computeWeights(events);
if (weights.size === 0) {
console.error("error: parsed events but extracted 0 weights (every job had ≤1 file?)");
return 3;
}
const json = serializeWeights(weights);
writeFileSync(args.out, json);
// Summary: min/median/max/total. Useful for spot-checking the file.
const values = Array.from(weights.values()).sort((a, b) => a - b);
const min = values[0]!;
const max = values[values.length - 1]!;
const median = values[Math.floor(values.length / 2)]!;
const total = values.reduce((a, b) => a + b, 0);
console.error(
`[mine-shard-weights] wrote ${weights.size} weights to ${args.out}`,
);
console.error(
`[mine-shard-weights] stats: min=${min}ms median=${median}ms max=${max}ms total=${(total / 1000).toFixed(1)}s`,
);
return 0;
}
if (import.meta.main) {
main().then((code) => process.exit(code));
}
+1 -97
View File
@@ -20,52 +20,11 @@
# which is too tight for setupDB's TRUNCATE CASCADE on ~30 tables on
# CI runners under load (one CI flake observed on PR #475 hitting
# exactly 5000.09ms in the Tags beforeAll).
#
# HOME isolation: E2E tests call paths that resolve to gbrain init / saveConfig
# (e.g. setupDB writing config for the test container) and would otherwise
# write the user's real ~/.gbrain/config.json. The wrapper redirects HOME and
# GBRAIN_HOME to a tmpdir before bun starts so config writes land in the
# tmpdir, then verifies the user's real config md5 didn't change after the run.
# Both env vars are required: loadConfig/saveConfig resolve via HOME, while
# configPath/getDbUrlSource honor GBRAIN_HOME; setting only one leaves the
# other path escaping isolation. HOME is set before bun starts because Bun's
# os.homedir() caches at first call and in-process mutation would not take.
# Trap cleans up the tmpdir even on test failure.
set -euo pipefail
cd "$(dirname "$0")/.."
# --- HOME isolation: snapshot real user config before switching ---
# Tolerate unset HOME (minimal containers, exotic CI shells) without tripping set -u.
REAL_HOME="${HOME:-/tmp}"
USER_CONFIG="$REAL_HOME/.gbrain/config.json"
USER_CONFIG_EXISTED=0
USER_CONFIG_MD5=""
# `{ ... } || true` swallows non-zero exit when the file is missing or md5 isn't
# installed, so set -e never aborts before the post-run breach detector can run.
md5_of() {
{ if command -v md5 >/dev/null 2>&1; then
md5 -q "$1" 2>/dev/null
elif command -v md5sum >/dev/null 2>&1; then
md5sum "$1" 2>/dev/null | awk '{print $1}'
fi
} || true
}
if [ -f "$USER_CONFIG" ]; then
USER_CONFIG_EXISTED=1
USER_CONFIG_MD5=$(md5_of "$USER_CONFIG")
fi
# Portable mktemp: explicit XXXXXX is required by GNU mktemp on Linux CI.
# `-t prefix` works on BSD but errors on GNU when the template lacks Xs.
E2E_TMP_HOME=$(mktemp -d "${TMPDIR:-/tmp}/gbrain-e2e.XXXXXX")
trap 'rm -rf "$E2E_TMP_HOME"' EXIT
export HOME="$E2E_TMP_HOME"
export GBRAIN_HOME="$E2E_TMP_HOME"
mkdir -p "$E2E_TMP_HOME/.gbrain"
# --dry-run-list: print the resolved file list (one per line) and exit. Used
# by scripts/ci-local.sh to smoke-test the argv branching at startup.
DRY_RUN_LIST=0
@@ -134,29 +93,7 @@ for f in "${files[@]}"; do
name=$(basename "$f")
echo ""
echo "=== $name ==="
# Cross-file isolation: terminate any stale connections from the prior
# file's pool before the next file's setupDB() runs. Without this,
# idle postgres connections from the previous bun process race with
# the next file's TRUNCATE CASCADE → cross-file fixture-state pollution
# (people/sarah-chen disappears mid-test, etc.). The terminate call is
# idempotent + fast (~50ms); on the first iteration there's nothing to
# terminate so it's effectively free.
if [ -n "${DATABASE_URL:-}" ]; then
psql "$DATABASE_URL" -At -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid != pg_backend_pid() AND datname = current_database()" >/dev/null 2>&1 || true
fi
# Hard outer timeout (180s per file). bun's --timeout is per-test; if a
# PGLite WASM call hangs in beforeAll/afterAll, --timeout never fires and
# the file wedges indefinitely. gtimeout/timeout SIGKILLs the file so the
# suite advances. gtimeout (macOS via coreutils) preferred; timeout (Linux)
# fallback; bare bun (no outer cap) if neither is installed.
if command -v gtimeout >/dev/null 2>&1; then
TIMEOUT_CMD="gtimeout 180"
elif command -v timeout >/dev/null 2>&1; then
TIMEOUT_CMD="timeout 180"
else
TIMEOUT_CMD=""
fi
if output=$($TIMEOUT_CMD bun test --timeout=60000 "$f" 2>&1); then
if output=$(bun test --timeout=60000 "$f" 2>&1); then
pass_files=$((pass_files + 1))
# Extract pass/fail counts from bun's summary (e.g., "123 pass")
p=$(echo "$output" | grep -oE '[0-9]+ pass' | tail -1 | grep -oE '[0-9]+' || echo 0)
@@ -182,39 +119,6 @@ echo "E2E SUMMARY (sequential execution)"
echo "========================================"
echo "Files: $((pass_files + fail_files)) total, $pass_files passed, $fail_files failed"
echo "Tests: $total_pass passed, $total_fail failed"
# --- HOME isolation verification: fail loud on any out-of-isolation write ---
# Runs regardless of test pass/fail; isolation breach is higher-severity than
# any individual test failure. Exit 2 distinguishes from exit 1 (test failure).
# Three breach modes covered:
# 1. Config existed before AND was modified (md5 changed)
# 2. Config existed before AND was deleted during the run
# 3. Config did NOT exist before but was created during the run
AFTER_EXISTS=0
[ -f "$USER_CONFIG" ] && AFTER_EXISTS=1
AFTER_MD5=""
if [ "$AFTER_EXISTS" = "1" ]; then
AFTER_MD5=$(md5_of "$USER_CONFIG")
fi
BREACH_REASON=""
if [ "$USER_CONFIG_EXISTED" = "1" ] && [ "$AFTER_EXISTS" = "0" ]; then
BREACH_REASON="config existed before run but was deleted"
elif [ "$USER_CONFIG_EXISTED" = "0" ] && [ "$AFTER_EXISTS" = "1" ]; then
BREACH_REASON="config did not exist before run but was created"
elif [ -n "$USER_CONFIG_MD5" ] && [ "$AFTER_MD5" != "$USER_CONFIG_MD5" ]; then
BREACH_REASON="config md5 changed during run"
fi
if [ -n "$BREACH_REASON" ]; then
echo "" >&2
echo "ERROR: HOME isolation breach detected." >&2
echo " Reason: $BREACH_REASON" >&2
echo " Path: $USER_CONFIG" >&2
echo " Before: existed=$USER_CONFIG_EXISTED md5=${USER_CONFIG_MD5:-none}" >&2
echo " After: existed=$AFTER_EXISTS md5=${AFTER_MD5:-none}" >&2
echo " A test wrote outside the tmpdir HOME despite the override." >&2
exit 2
fi
if [ ${#fail_list[@]} -gt 0 ]; then
echo ""
echo "Failing files:"
+1 -6
View File
@@ -17,9 +17,4 @@ if [ "${#slow_files[@]}" -eq 0 ]; then
fi
echo "[run-slow-tests] running ${#slow_files[@]} slow files (CI runs these as part of bun run test)"
# v0.40.10 flake-hardening: bump per-test timeout 60s → 120s. Slow tests
# legitimately approach 60s in isolation (longmemeval E2E suite is ~50s);
# when bun runs slow files in parallel, CPU contention pushes them past
# 60s and individual tests timeout even though they'd pass solo. Slow
# tests are explicit by-name — generous per-test budget is correct.
exec bun test --timeout=120000 "${slow_files[@]}"
exec bun test --timeout=60000 "${slow_files[@]}"
+11 -99
View File
@@ -58,27 +58,10 @@ N="${SHARDS_OVERRIDE:-${SHARDS:-$(detect_cpus)}}"
if ! printf '%s' "$N" | grep -qE '^[0-9]+$' || [ "$N" -lt 1 ]; then
echo "ERROR: invalid shard count: $N" >&2; exit 2
fi
# v0.40.10 flake-hardening: clamp default to 4 (was 8) to match CI's
# test-shard.sh fan-out. At 8-shard parallel on Apple Silicon we observed
# shard 5 SIGKILL during source-health.test.ts's PGLite migration replay —
# 8 parallel PGLite WASM inits contend severely on the lockfile, and the
# 92-migration replay × 8 simultaneous can wedge past even 900s. CI uses
# 4 and is stable. Trade ~2x wallclock for reliability + parity with CI's
# fan-out. Override via --shards N or SHARDS=N (still capped at 8).
[ "$N" -gt 8 ] && N=8
if [ -z "${SHARDS_OVERRIDE:-}" ] && [ -z "${SHARDS:-}" ] && [ "$N" -gt 4 ]; then
N=4
fi
INTRA_CONC="${MAX_CONCURRENCY_OVERRIDE:-${GBRAIN_TEST_MAX_CONCURRENCY:-4}}"
# v0.40.10 flake-hardening: bump per-shard cap 600 → 1500 (was 900). At
# 4-shard default each shard runs 159 files / ~2420 tests with internal
# wallclock 960-1020s. The 900s value (sized for 8-shard's ~80 files /
# 1100 tests at 620-770s) false-killed shard 1 at 900s even though it
# had completed in 968s. 1500s cap gives ~55% headroom over observed
# 4-shard wallclock; real hangs still hit it. Override via
# GBRAIN_TEST_SHARD_TIMEOUT=N.
SHARD_TIMEOUT="${GBRAIN_TEST_SHARD_TIMEOUT:-1500}"
SHARD_TIMEOUT="${GBRAIN_TEST_SHARD_TIMEOUT:-600}"
# ──────────────────────────────────────────────────────────────────────────
# Output directories. Prefer workspace-local .context/, fall back to /tmp.
@@ -181,88 +164,28 @@ bun_summary_count() {
' "$file"
}
# shard_total_files: parse the "[unit-shard N/M] running X files" line that
# run-unit-shard.sh echoes before invoking bun test. Returns the file count
# the shard was given, or 0 if the line isn't there yet (shard still
# bootstrapping). Uses sed-then-grep so it's portable to macOS awk (BSD awk
# doesn't support `match($0, /re/, arr)` with the array sink — that's gawk-only).
shard_total_files() {
local file="$1"
[ -f "$file" ] || { echo 0; return; }
local n
n=$(sed -n 's/^\[unit-shard [0-9][0-9]*\/[0-9][0-9]*\] running \([0-9][0-9]*\) files.*/\1/p' "$file" 2>/dev/null | head -1)
echo "${n:-0}"
}
# shard_pglite_init_count: count "Schema version" lines as a proxy for "test
# files initialized so far." Each PGLite-using test file's beforeAll triggers
# one initSchema() which prints this. Undercounts because not every test file
# opens a PGLite engine, but it's the only real-time progress signal bun's
# default reporter leaves in the log (bun has no per-file progress markers,
# only a final shard-end summary).
shard_pglite_init_count() {
local file="$1"
[ -f "$file" ] || { echo 0; return; }
grep -cE 'Schema version [0-9]+ → [0-9]+' "$file" 2>/dev/null || echo 0
}
# log_size_kb: total stderr+stdout written by the shard so far. Strictly
# monotonic — useful as a "definitely alive" signal when other heuristics
# read 0 (e.g. very early in shard startup before initSchema fires).
log_size_kb() {
local file="$1"
[ -f "$file" ] || { echo 0; return; }
local b
b=$(wc -c < "$file" 2>/dev/null | tr -d ' ')
echo $(( ${b:-0} / 1024 ))
}
# fmt_elapsed: pretty-print seconds → "Mm:SS" or "SSs" for short.
fmt_elapsed() {
local s=$1
if [ "$s" -ge 60 ]; then
printf '%dm%02ds' $((s / 60)) $((s % 60))
else
printf '%ds' "$s"
fi
}
heartbeat() {
local hb_start=$(date +%s)
while true; do
sleep 10
local line=""
local now; now=$(date +%s)
local hb_elapsed=$((now - hb_start))
for i in $(seq 1 "$N"); do
if [ -f "$LOG_DIR/shard-$i.exit" ]; then
local rc; rc=$(cat "$LOG_DIR/shard-$i.exit" 2>/dev/null || echo "?")
local status="✓"
[ "$rc" != "0" ] && status="✗"
local f
f=$(bun_summary_count "fail" "$LOG_DIR/shard-$i.log")
local p
p=$(bun_summary_count "pass" "$LOG_DIR/shard-$i.log")
line="$line [s$i: done $status ${p}p ${f}f]"
line="$line [s$i: done $status]"
else
local lf="$LOG_DIR/shard-$i.log"
if [ -f "$lf" ]; then
# Bun's default reporter has no per-file progress markers, only a
# final shard-end summary, so we surface three complementary signals
# mid-run: (1) PGLite initSchema() count as a "files started" proxy,
# (2) total files this shard was assigned (from the runner banner),
# (3) log size in KB as a strictly-monotonic liveness signal.
local total; total=$(shard_total_files "$lf")
local pglite; pglite=$(shard_pglite_init_count "$lf")
local kb; kb=$(log_size_kb "$lf")
local et; et=$(fmt_elapsed "$hb_elapsed")
if [ "$total" -gt 0 ]; then
line="$line [s$i: ~${pglite}/${total}f ${kb}KB ${et}]"
else
line="$line [s$i: starting ${kb}KB ${et}]"
fi
# Heartbeat: prefer Bun's per-test "✓" (passed) and "(fail)" markers
# so we see live progress; the "N pass" summary line only appears at
# the very end of the shard and would always show 0 mid-run.
local p f
p=$(grep_count '^[[:space:]]+✓' "$lf")
f=$(grep_count '^\(fail\)' "$lf")
line="$line [s$i: ${p}p ${f}f ...]"
else
line="$line [s$i: spawning]"
line="$line [s$i: starting]"
fi
fi
done
@@ -271,22 +194,11 @@ heartbeat() {
}
heartbeat &
HB_PID=$!
# v0.41.11.0 cleanup: pkill children FIRST, then kill heartbeat. If we
# kill the heartbeat shell first, its current `sleep 10` is reparented
# to init/launchd and pkill -P can no longer find it (orphan). Order:
# children first while the parent PID is still findable, then parent.
# Known bash quirk: SIGTERM to a shell sleeping inside `sleep` doesn't
# propagate to the sleep child before the wait returns. Without this,
# each invocation of this script leaks ONE orphan sleep; CI's "orphan
# process cleanup" at end-of-job reports them as (unnamed) test failures.
# Seen on the garrytan/port-pr-1406 PR, 2 CI runs in a row, 6 orphans
# matching the 6 invocations in test/scripts/run-unit-parallel.test.ts.
trap 'pkill -P "$HB_PID" 2>/dev/null; kill "$HB_PID" 2>/dev/null; wait "$HB_PID" 2>/dev/null' EXIT
trap 'kill "$HB_PID" 2>/dev/null; wait "$HB_PID" 2>/dev/null' EXIT
# Wait for every shard. Don't care about wait's exit code.
for pid in "${SHARD_PIDS[@]}"; do wait "$pid" 2>/dev/null || true; done
pkill -P "$HB_PID" 2>/dev/null
kill "$HB_PID" 2>/dev/null
wait "$HB_PID" 2>/dev/null
trap - EXIT
-202
View File
@@ -1,202 +0,0 @@
#!/usr/bin/env bash
# scripts/run-verify-parallel.sh — parallel verify dispatcher.
#
# Runs the 19+ verify checks (privacy, jsonb, source-id, … + typecheck +
# admin-build) as background jobs, waits for all, aggregates exit codes,
# surfaces failed-check name + tail of its log to stderr.
#
# Replaces the sequential `&&`-chain in package.json's `verify` script.
# Wallclock: 19 sequential checks (~15-25s on CI) → parallel (~3-5s).
#
# Usage:
# bash scripts/run-verify-parallel.sh # run every CHECK below
# bash scripts/run-verify-parallel.sh --dry-list # print check list, exit
#
# Env overrides:
# GBRAIN_VERIFY_TIMEOUT per-check wallclock cap, seconds (default 120)
# GBRAIN_VERIFY_LOG_DIR where to write per-check logs (default tempdir)
#
# Exit codes:
# 0 all checks passed
# 1 one or more checks failed (full details in stderr)
# 2 usage error / no checks defined
set -uo pipefail
cd "$(dirname "$0")/.."
# ──────────────────────────────────────────────────────────────────────────
# Checks to run. Order is irrelevant (parallel), but keep stable for log
# determinism + grep-ability. Each entry is a bun-script name (the
# `package.json` "scripts" key), invoked as `bun run <name>`.
#
# To add a check: append to this array. To skip in CI temporarily, comment
# the line — the parallel runner doesn't care about count.
# ──────────────────────────────────────────────────────────────────────────
CHECKS=(
"check:privacy"
"check:proposal-pii"
"check:test-names"
"check:jsonb"
"check:source-id-projection"
"check:source-config-leak"
"check:progress"
"check:test-isolation"
"check:wasm"
"check:admin-build"
"check:admin-scope-drift"
"check:cli-exec"
"check:system-of-record"
"check:eval-glossary"
"check:no-pii-agent-voice"
"check:synthetic-corpus-privacy"
"check:skill-brain-first"
"check:fuzz-purity"
"check:operations-filter-bypass"
"check:gateway-routed"
"check:worker-pool-atomicity"
"check:doc-history"
"check:fixture-privacy"
"check:conversation-parser"
"check:resolver"
"check:source-scope-onboard"
"check:no-double-retry"
"check:batch-audit-site"
"check:worker-lock-renewal-shape"
"typecheck"
)
if [ "${#CHECKS[@]}" -eq 0 ]; then
echo "ERROR: no checks defined in run-verify-parallel.sh" >&2
exit 2
fi
# Dry-run path: list checks, exit. Used by tests + ops debugging.
if [ "${1:-}" = "--dry-list" ]; then
printf '%s\n' "${CHECKS[@]}"
exit 0
fi
if [ "$#" -gt 0 ] && [ "${1:-}" != "" ]; then
echo "ERROR: unknown arg: $1" >&2
echo "usage: bash scripts/run-verify-parallel.sh [--dry-list]" >&2
exit 2
fi
TIMEOUT="${GBRAIN_VERIFY_TIMEOUT:-120}"
# Per-check temp dir. Each check gets its own subdir so writes can't race
# on shared scratch state (the checks themselves are read-only — they grep
# the working tree — but defense-in-depth.)
if [ -n "${GBRAIN_VERIFY_LOG_DIR:-}" ]; then
LOG_DIR="$GBRAIN_VERIFY_LOG_DIR"
mkdir -p "$LOG_DIR" || { echo "ERROR: cannot create $LOG_DIR" >&2; exit 2; }
else
LOG_DIR="$(mktemp -d /tmp/gbrain-verify-XXXXXX)"
trap 'rm -rf "$LOG_DIR"' EXIT
fi
# Resolve `timeout` for per-check wallclock cap. macOS doesn't ship one;
# brew coreutils provides `gtimeout`. If neither is available, fall back to
# bg-pid + sleep-cap (slightly less reliable but still bounded).
TIMEOUT_BIN=""
if command -v gtimeout >/dev/null 2>&1; then TIMEOUT_BIN="gtimeout"
elif command -v timeout >/dev/null 2>&1; then TIMEOUT_BIN="timeout"
fi
START_TS=$(date +%s)
echo "[verify-parallel] running ${#CHECKS[@]} checks in parallel (timeout=${TIMEOUT}s, logs=$LOG_DIR)" >&2
# ──────────────────────────────────────────────────────────────────────────
# Spawn one background process per check. Each child captures its own exit
# code into a sentinel file under $LOG_DIR/<safe-name>.exit; the parent
# never trusts `wait`'s aggregate value because that maps to last-spawned.
#
# safe_name: turn `check:privacy` into `check_privacy` so it fits a filename
# without escaping.
# ──────────────────────────────────────────────────────────────────────────
PIDS=()
SAFE_NAMES=()
for c in "${CHECKS[@]}"; do
safe="${c//:/_}"
SAFE_NAMES+=("$safe")
LOG_FILE="$LOG_DIR/$safe.log"
EXIT_FILE="$LOG_DIR/$safe.exit"
(
if [ -n "$TIMEOUT_BIN" ]; then
"$TIMEOUT_BIN" "${TIMEOUT}s" bun run "$c" > "$LOG_FILE" 2>&1
else
bun run "$c" > "$LOG_FILE" 2>&1 &
pid=$!
( sleep "$TIMEOUT" && kill -TERM "$pid" 2>/dev/null && \
sleep 5 && kill -KILL "$pid" 2>/dev/null ) &
cap_pid=$!
wait "$pid" 2>/dev/null
kill "$cap_pid" 2>/dev/null
wait "$cap_pid" 2>/dev/null
fi
rc=$?
echo "$rc" > "$EXIT_FILE"
) &
PIDS+=($!)
done
# Wait for every background job. Ignore wait's aggregate exit — exit codes
# live in the sentinel files.
for pid in "${PIDS[@]}"; do wait "$pid" 2>/dev/null || true; done
END_TS=$(date +%s)
ELAPSED=$((END_TS - START_TS))
# ──────────────────────────────────────────────────────────────────────────
# Aggregate. For each check, read its exit file; on failure, append a
# labeled block (check name + tail of log) to the failure report. Surface
# one final summary line and the report to stderr if anything failed.
# ──────────────────────────────────────────────────────────────────────────
PASS=0
FAIL=0
FAIL_NAMES=()
FAIL_REPORT=""
for i in "${!CHECKS[@]}"; do
c="${CHECKS[$i]}"
safe="${SAFE_NAMES[$i]}"
EXIT_FILE="$LOG_DIR/$safe.exit"
LOG_FILE="$LOG_DIR/$safe.log"
rc=1
[ -f "$EXIT_FILE" ] && rc=$(cat "$EXIT_FILE" 2>/dev/null || echo 1)
if [ "$rc" = "0" ]; then
PASS=$((PASS + 1))
else
FAIL=$((FAIL + 1))
FAIL_NAMES+=("$c")
if [ "$rc" = "124" ]; then
FAIL_REPORT+=$'\n--- '"$c"' (TIMED OUT after '"${TIMEOUT}"'s) ---\n'
else
FAIL_REPORT+=$'\n--- '"$c"' (rc='"$rc"') ---\n'
fi
if [ -f "$LOG_FILE" ]; then
FAIL_REPORT+="$(tail -30 "$LOG_FILE")"
FAIL_REPORT+=$'\n'
fi
fi
done
if [ "$FAIL" -gt 0 ]; then
{
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "❌ verify failed: $FAIL/${#CHECKS[@]} checks did not pass"
echo "Failed: ${FAIL_NAMES[*]}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
printf '%s' "$FAIL_REPORT"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "[verify-parallel] elapsed=${ELAPSED}s | pass=$PASS fail=$FAIL"
} >&2
exit 1
fi
echo "[verify-parallel] elapsed=${ELAPSED}s | pass=$PASS fail=0 | all checks green" >&2
exit 0
-256
View File
@@ -1,256 +0,0 @@
#!/usr/bin/env bun
/**
* scripts/sharding.ts weight-aware test file partitioning.
*
* Replaces FNV-1a path-hash sharding in scripts/test-shard.sh. Uses
* longest-processing-time-first (LPT) greedy bin-packing over measured
* per-file runtimes to balance total wallclock across N shards.
*
* LPT is a textbook approximation algorithm: sort jobs by weight desc,
* assign each to the bin (shard) with the current minimum total. Worst-
* case makespan is within 4/3 of optimal. Runs in O(n log n).
*
* Weights live in scripts/test-weights.json committed, mined from real
* CI run logs via scripts/mine-shard-weights.ts. Files absent from the
* weights map fall back to the corpus median (not zero that would
* favor unknown new files into the smallest shard, defeating balance).
*
* CLI:
* bun run scripts/sharding.ts <shard-index> <total-shards>
* Reads test file list from stdin (one path per line). Prints the
* subset assigned to <shard-index> to stdout, one per line.
*
* bun run scripts/sharding.ts <shard-index> <total-shards> --files <glob>
* Walks the filesystem for matching files instead of reading stdin.
*
* Exit codes:
* 0 success
* 1 internal error (e.g., malformed weights JSON)
* 2 usage error
*
* Used by: scripts/test-shard.sh (thin wrapper), test/scripts/sharding.test.ts.
*/
import { readFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..");
const DEFAULT_WEIGHTS_PATH = resolve(REPO_ROOT, "scripts/test-weights.json");
export type WeightMap = Map<string, number>;
export class WeightsLoadError extends Error {
constructor(public readonly path: string, public readonly cause: unknown) {
super(`failed to load weights from ${path}: ${cause}`);
this.name = "WeightsLoadError";
}
}
/**
* Read a weights JSON file. Fail-soft on missing file (returns empty map).
* Throws WeightsLoadError on malformed JSON or non-object shape the
* caller decides whether to fall through to defaults or surface.
*/
export function loadWeights(path: string = DEFAULT_WEIGHTS_PATH): WeightMap {
if (!existsSync(path)) return new Map();
let raw: string;
try {
raw = readFileSync(path, "utf8");
} catch (e) {
throw new WeightsLoadError(path, e);
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch (e) {
throw new WeightsLoadError(path, `JSON.parse: ${e}`);
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new WeightsLoadError(path, "expected top-level object {path: ms}");
}
const out: WeightMap = new Map();
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
throw new WeightsLoadError(
path,
`value for "${k}" must be a non-negative finite number, got ${JSON.stringify(v)}`,
);
}
out.set(k, v);
}
return out;
}
/**
* Median of a list of numbers. Used as the fallback weight for files
* absent from the weights map. Empty input returns 0 (no signal).
*/
export function computeMedian(values: number[]): number {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[mid - 1]! + sorted[mid]!) / 2
: sorted[mid]!;
}
export interface PartitionOpts {
/**
* Weight to assign files that are absent from the weights map. Defaults
* to the median of present weights (computed inside `partition`) so
* unknown new files cluster around the typical file's cost.
*
* Override mainly for tests; production callers should use the default.
*/
fallbackWeight?: number;
}
/**
* Partition `files` into `n` shards using LPT greedy bin-packing.
*
* Returns an array of length `n` where each entry is the (deterministic)
* file list for that shard. Files in each shard are returned in
* assignment order (heaviest first). Same input always produces same
* output no Math.random, stable sort key (weight desc, then path asc).
*
* Contracts:
* - Every file in `files` appears in exactly one returned shard.
* - If `files` is empty, returns `n` empty arrays.
* - If `n <= 0`, throws RangeError.
* - Files missing from `weights` get `opts.fallbackWeight` (or median).
*/
export function partition(
files: string[],
weights: WeightMap,
n: number,
opts: PartitionOpts = {},
): string[][] {
if (!Number.isInteger(n) || n <= 0) {
throw new RangeError(`shard count must be a positive integer, got ${n}`);
}
const shards: string[][] = Array.from({ length: n }, () => []);
if (files.length === 0) return shards;
// Compute fallback weight from the median of present weights, unless
// the caller supplied an explicit override.
let fallback: number;
if (opts.fallbackWeight !== undefined) {
if (!Number.isFinite(opts.fallbackWeight) || opts.fallbackWeight < 0) {
throw new RangeError(
`fallbackWeight must be non-negative finite, got ${opts.fallbackWeight}`,
);
}
fallback = opts.fallbackWeight;
} else {
fallback = computeMedian(Array.from(weights.values()));
}
// Cold-start guard: if the weights map is empty AND no explicit
// fallback was supplied, every effective weight would be 0 and LPT
// collapses (all ties → lowest-index wins → every file in shard 0).
// Normalize fallback to 1 so LPT degenerates to round-robin, which is
// a strictly better default than "everything in shard 1" until
// test-weights.json gets mined.
if (fallback === 0 && opts.fallbackWeight === undefined) {
fallback = 1;
}
// Build [weight, path] tuples. Sort by weight desc, then path asc for
// determinism on ties (multiple files with the same weight).
const tuples = files.map((f) => ({
path: f,
weight: weights.get(f) ?? fallback,
}));
tuples.sort((a, b) => {
if (b.weight !== a.weight) return b.weight - a.weight;
return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
});
// Running per-shard totals. argmin tiebreaker: lowest index (stable).
const totals = new Array<number>(n).fill(0);
for (const t of tuples) {
let minIdx = 0;
for (let i = 1; i < n; i++) {
if (totals[i]! < totals[minIdx]!) minIdx = i;
}
shards[minIdx]!.push(t.path);
totals[minIdx] = totals[minIdx]! + t.weight;
}
return shards;
}
/**
* Imbalance ratio: max-total / min-total over the partition. 1.0 = perfect.
* Returns Infinity when any shard is empty (degenerate). Use 1.5 as a
* loose health gate; LPT typically hits 1.1 on real corpora.
*
* Exposed for tests + the slow-test regression that pins corpus health.
*/
export function imbalanceRatio(shards: string[][], weights: WeightMap, fallback: number): number {
if (shards.length === 0) return 1;
const totals = shards.map((s) =>
s.reduce((sum, f) => sum + (weights.get(f) ?? fallback), 0),
);
const max = Math.max(...totals);
const min = Math.min(...totals);
if (min === 0) return max === 0 ? 1 : Infinity;
return max / min;
}
// ──────────────────────────────────────────────────────────────────────────
// CLI
// ──────────────────────────────────────────────────────────────────────────
async function readStdinLines(): Promise<string[]> {
// Bun gives us a readable stream on process.stdin. Read to end.
if (process.stdin.isTTY) return []; // no piped input
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) {
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
}
const text = Buffer.concat(chunks).toString("utf8");
return text
.split("\n")
.map((l) => l.trim())
.filter((l) => l.length > 0);
}
async function main(): Promise<number> {
const argv = process.argv.slice(2);
if (argv.length < 2) {
console.error("usage: bun run scripts/sharding.ts <shard-index> <total-shards>");
console.error(" (reads file list from stdin, one path per line)");
return 2;
}
const idx = Number.parseInt(argv[0]!, 10);
const total = Number.parseInt(argv[1]!, 10);
if (!Number.isInteger(idx) || !Number.isInteger(total) || idx < 1 || total < 1 || idx > total) {
console.error(
`error: shard index ${argv[0]} / total ${argv[1]} invalid (need 1 <= index <= total, both ints)`,
);
return 2;
}
const files = await readStdinLines();
if (files.length === 0) {
// Caller asked for a shard but piped no files. Exit clean — the wrapper
// will warn or no-op as it sees fit.
return 0;
}
let weights: WeightMap;
try {
weights = loadWeights();
} catch (e) {
console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
return 1;
}
const shards = partition(files, weights, total);
for (const f of shards[idx - 1]!) {
process.stdout.write(`${f}\n`);
}
return 0;
}
if (import.meta.main) {
main().then((code) => process.exit(code));
}
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env bash
# ship-remote-tests.sh — run the unit suite on GitHub's on-demand cloud
# runners instead of locally, and block until it finishes with a real
# pass/fail exit code.
#
# WHY: a local machine running many Conductor agents at once gets CPU/memory
# saturated (observed: load avg 120 on 16 cores, ~15 sibling `bun test`
# processes). The PGLite WASM test suite then OOMs (8-shard) or crawls
# (~12min for 1/3 of files vs ~85s normally). The suite already runs on
# GitHub's ephemeral runners on every PR push; this script makes a local
# caller (human or agent, e.g. /ship Step 5) AWAIT that cloud run exactly
# like a local `bun run test` — push, dispatch, `gh run watch --exit-status`.
#
# USAGE:
# scripts/ship-remote-tests.sh [--workflow test.yml] [--branch <name>]
# [--no-push] [--ref <sha>]
#
# EXIT: mirrors the GitHub run — 0 on success, non-zero on failure (so it
# drops into a test gate unchanged). 2 = usage/precondition error.
#
# REQUIRES: `gh` authenticated; the workflow must declare `workflow_dispatch:`
# (test.yml does as of v0.41.32.0).
set -euo pipefail
WORKFLOW="test.yml"
BRANCH=""
DO_PUSH=1
REF=""
while [ $# -gt 0 ]; do
case "$1" in
--workflow) WORKFLOW="$2"; shift 2 ;;
--branch) BRANCH="$2"; shift 2 ;;
--ref) REF="$2"; shift 2 ;;
--no-push) DO_PUSH=0; shift ;;
-h|--help)
sed -n '2,30p' "$0"; exit 0 ;;
*) echo "ship-remote-tests: unknown arg '$1'" >&2; exit 2 ;;
esac
done
command -v gh >/dev/null 2>&1 || { echo "ship-remote-tests: gh CLI not found" >&2; exit 2; }
gh auth status >/dev/null 2>&1 || { echo "ship-remote-tests: gh not authenticated — run 'gh auth login'" >&2; exit 2; }
[ -n "$BRANCH" ] || BRANCH="$(git branch --show-current 2>/dev/null || true)"
[ -n "$BRANCH" ] || { echo "ship-remote-tests: could not determine branch (detached HEAD?) — pass --branch" >&2; exit 2; }
if [ "$DO_PUSH" = "1" ]; then
echo "ship-remote-tests: pushing $BRANCH ..." >&2
git push -u origin "$BRANCH"
fi
# Dispatch against the branch (or an explicit ref). Requires workflow_dispatch
# on the workflow. The HEAD sha lets us disambiguate OUR run from any
# concurrent pull_request run on the same branch.
HEAD_SHA="$(git rev-parse "${REF:-HEAD}")"
echo "ship-remote-tests: dispatching $WORKFLOW on $BRANCH @ ${HEAD_SHA:0:8} ..." >&2
gh workflow run "$WORKFLOW" --ref "${REF:-$BRANCH}" >/dev/null
# Poll for the dispatched run to register (cli/cli#8194: `gh run watch` can
# skip a not-yet-registered run, so we resolve the databaseId ourselves first).
RUN_ID=""
for _ in $(seq 1 30); do
RUN_ID="$(gh run list --workflow "$WORKFLOW" --branch "$BRANCH" \
--event workflow_dispatch --limit 10 \
--json databaseId,headSha,status \
-q "[.[] | select(.headSha==\"$HEAD_SHA\")] | sort_by(.databaseId) | last | .databaseId" 2>/dev/null || true)"
[ -n "$RUN_ID" ] && [ "$RUN_ID" != "null" ] && break
sleep 3
done
if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
echo "ship-remote-tests: could not find the dispatched run after 90s." >&2
echo " Check manually: gh run list --workflow $WORKFLOW --branch $BRANCH" >&2
exit 2
fi
RUN_URL="$(gh run view "$RUN_ID" --json url -q .url 2>/dev/null || echo "")"
echo "ship-remote-tests: watching run $RUN_ID $RUN_URL" >&2
# Block until the cloud run finishes; mirror its pass/fail as our exit code.
if gh run watch "$RUN_ID" --exit-status; then
echo "ship-remote-tests: PASS $RUN_URL" >&2
exit 0
else
rc=$?
echo "ship-remote-tests: FAIL (exit $rc) $RUN_URL" >&2
echo "--- failed logs ---" >&2
gh run view "$RUN_ID" --log-failed 2>/dev/null | tail -120 >&2 || true
exit "$rc"
fi
+54 -61
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Partition unit test files into N shards and run one shard.
# Partition unit test files into N shards by stable hash and run one shard.
#
# Usage: scripts/test-shard.sh <shard-index> <total-shards>
# shard-index: 1-based (1..N)
@@ -8,27 +8,16 @@
# Excluded from sharding:
# - test/e2e/* — need DATABASE_URL; run via bun run test:e2e
# - *.serial.test.ts — concurrency-unsafe (file-wide mock.module / env
# leaks); run via bun run test:serial on its
# own runner in CI. Including these here lets
# their mock.module() calls leak into the rest
# of the shard's bun process and silently break
# unrelated tests.
# leaks); run via scripts/run-serial-tests.sh on
# shard 1 only. Including these here lets their
# mock.module() calls leak into the rest of the
# shard's bun process and silently break unrelated
# tests. See test/eval-takes-quality-runner.serial.test.ts
# mocking gateway.ts → voyage-multimodal failures.
#
# *.slow.test.ts is deliberately INCLUDED here. CI's matrix is the only
# default place these run; the local fast loop (run-unit-shard.sh)
# excludes them. See CLAUDE.md "CI vs local: intentionally divergent file
# sets" for the rationale.
#
# Partition: weight-aware LPT bin-packing via scripts/sharding.ts. Reads
# per-file runtime weights from scripts/test-weights.json (mined from real
# CI logs by scripts/mine-shard-weights.ts). Files absent from the map
# fall back to the corpus median, so adding a new test file works
# immediately without regenerating weights — worst case it lands in the
# wrong shard until next regen, never silently dropped.
#
# Stable partitioning: same `(files, weights, N)` always produces the
# same assignment, so retries are reproducible.
# Stable partitioning: a file's shard is `(hash(path) % N) + 1`. Same file
# lands in the same shard on every run, regardless of how many other files
# exist, so retries are reproducible. Hash is FNV-1a — pure shell, no jq.
set -euo pipefail
DRY_RUN_LIST=0
@@ -56,56 +45,60 @@ fi
cd "$(dirname "$0")/.."
# Collect non-E2E, non-serial unit test files. Slow files INCLUDED — see
# header comment. Local run-unit-shard.sh excludes slow files (different
# policy by design).
#
# Two test files are pulled out of the matrix and into their own dedicated
# CI jobs (see .github/workflows/test.yml):
# - eval-longmemeval-e2e.slow.test.ts (~200s after TODO #1 engine sharing)
# → job: slow-eval-longmemeval
# - entity-resolve-perf.slow.test.ts (~159s, single non-subdivisible
# perf test)
# → job: slow-entity-resolve-perf
#
# Removing both heavy atoms from matrix-eligible files keeps the per-shard
# total bounded. With 10 matrix shards the per-shard total drops to ~272s.
# Dedicated jobs run in parallel so total CI wallclock = max(matrix ~4.5min,
# slow-eval ~3.3min, slow-entity-resolve-perf ~2.6min) ≈ 4.5min.
ALL_FILES=$(find test -name '*.test.ts' \
-not -name '*.serial.test.ts' \
-not -name 'eval-longmemeval-e2e.slow.test.ts' \
-not -name 'entity-resolve-perf.slow.test.ts' \
-not -path 'test/e2e/*' | sort)
# Find all unit test files, deterministic order. Excludes test/e2e/ and
# *.serial.test.ts. Serial files share file-wide state (top-level
# mock.module, module singletons) that leaks across files in the same
# `bun test` shard process — see scripts/check-test-isolation.sh R2.
# CI runs them via `bun run test:serial` (scripts/run-serial-tests.sh) at
# --max-concurrency=1 in a separate step on shard 1. Local `bun run test`
# already excludes them from the parallel pass and runs them after the
# same way. Portable: avoid `mapfile` (bash 4+) so this runs on macOS
# bash 3.2 too.
FILES=()
while IFS= read -r line; do
FILES+=("$line")
done < <(find test -name '*.test.ts' -not -name '*.serial.test.ts' -not -path 'test/e2e/*' | sort)
if [ -z "$ALL_FILES" ]; then
if [ "${#FILES[@]}" -eq 0 ]; then
echo "no test files found under test/" >&2
exit 1
fi
# Delegate the LPT partition to scripts/sharding.ts. Stream the file list
# via stdin to keep argv small (676+ files would overflow argv in some
# shells / OSes).
SHARD_FILES=$(printf '%s\n' "$ALL_FILES" | bun run scripts/sharding.ts "$SHARD_INDEX" "$TOTAL_SHARDS")
# FNV-1a 32-bit hash of a string — implemented in pure bash so we don't depend
# on python/openssl/etc on the runner. Output is decimal.
fnv1a() {
local str="$1"
local h=2166136261 # FNV offset basis
local i ord
for (( i=0; i<${#str}; i++ )); do
ord=$(printf '%d' "'${str:$i:1}")
h=$(( (h ^ ord) & 0xFFFFFFFF ))
h=$(( (h * 16777619) & 0xFFFFFFFF ))
done
echo "$h"
}
SHARD_FILES=()
for f in "${FILES[@]}"; do
hash=$(fnv1a "$f")
bucket=$(( hash % TOTAL_SHARDS + 1 ))
if [ "$bucket" -eq "$SHARD_INDEX" ]; then
SHARD_FILES+=("$f")
fi
done
if [ "$DRY_RUN_LIST" = "1" ]; then
printf '%s' "$SHARD_FILES"
[ -n "$SHARD_FILES" ] && echo "" # trailing newline if non-empty
if [ "${#SHARD_FILES[@]}" -eq 0 ]; then
exit 0
fi
printf '%s\n' "${SHARD_FILES[@]}"
exit 0
fi
ALL_COUNT=$(printf '%s\n' "$ALL_FILES" | grep -c '^' || true)
SHARD_COUNT=$(printf '%s\n' "$SHARD_FILES" | grep -c '^' || true)
# grep -c on empty input returns 0 even with trailing newline edge cases
[ -z "$SHARD_FILES" ] && SHARD_COUNT=0
echo "shard $SHARD_INDEX/$TOTAL_SHARDS: ${SHARD_COUNT}/${ALL_COUNT} files (LPT-balanced)"
if [ "$SHARD_COUNT" -eq 0 ]; then
echo "warning: shard $SHARD_INDEX has no files (total shards may exceed file count)" >&2
echo "shard $SHARD_INDEX/$TOTAL_SHARDS: ${#SHARD_FILES[@]}/${#FILES[@]} files"
if [ "${#SHARD_FILES[@]}" -eq 0 ]; then
echo "warning: shard $SHARD_INDEX has no files (rehash or reduce shard count)" >&2
exit 0
fi
# Convert newline-separated file list to argv. xargs handles the
# whitespace correctly without word-splitting on spaces in paths.
printf '%s\n' "$SHARD_FILES" | xargs bun test --timeout=60000
exec bun test --timeout=60000 "${SHARD_FILES[@]}"
-714
View File
@@ -1,714 +0,0 @@
{
"test/active-pack-wiring.test.ts": 3,
"test/admin-agents-spend.test.ts": 4995,
"test/admin-embed-spawn.serial.test.ts": 12792,
"test/agent-cli.test.ts": 2935,
"test/agent-runner.test.ts": 2,
"test/ai/adaptive-embed-batch.test.ts": 34,
"test/ai/build-gateway-config.test.ts": 0,
"test/ai/capabilities.test.ts": 3,
"test/ai/config-no-env-mutation.test.ts": 4,
"test/ai/dims-openai.test.ts": 4,
"test/ai/dims-zeroentropy.test.ts": 4,
"test/ai/embedQuery.test.ts": 8,
"test/ai/gateway-chat.test.ts": 8,
"test/ai/gateway-tool-loop.test.ts": 9,
"test/ai/gateway.test.ts": 444,
"test/ai/header-transport.serial.test.ts": 118,
"test/ai/no-batch-cap-suppression.serial.test.ts": 98,
"test/ai/recipe-azure-openai.test.ts": 7,
"test/ai/recipe-dashscope.test.ts": 5,
"test/ai/recipe-llama-server.test.ts": 0,
"test/ai/recipe-minimax.test.ts": 1,
"test/ai/recipe-openrouter.test.ts": 1,
"test/ai/recipe-zhipu.test.ts": 3,
"test/ai/recipes-existing-regression.test.ts": 5,
"test/ai/rerank.test.ts": 16,
"test/ai/schema-templating.test.ts": 4,
"test/ai/silent-drop-regression.test.ts": 9,
"test/ai/voyage-code-3-recipe.test.ts": 3,
"test/ai/zeroentropy-compat-fetch.test.ts": 16,
"test/ai/zeroentropy-recipe.test.ts": 1,
"test/anomalies.test.ts": 1,
"test/anthropic-model-ids.test.ts": 3,
"test/apply-migrations-pglite-spawn.serial.test.ts": 18874,
"test/apply-migrations.test.ts": 5,
"test/archive-crawler-config.test.ts": 26,
"test/artifact-abstraction.test.ts": 2,
"test/asymmetric-encoding-contract.test.ts": 1,
"test/audit-slug-fallback.serial.test.ts": 98,
"test/audit-synopsis.serial.test.ts": 99,
"test/audit/audit-writer.test.ts": 9,
"test/audit/content-sanity-audit.test.ts": 6,
"test/auto-think-phase.test.ts": 3251,
"test/autopilot-cycle-failure-classification.test.ts": 4,
"test/autopilot-cycle-handler.test.ts": 3039,
"test/autopilot-fanout-wiring.test.ts": 3,
"test/autopilot-fanout.test.ts": 24,
"test/autopilot-install.test.ts": 5,
"test/autopilot-lock-path.test.ts": 13,
"test/autopilot-nightly-probe-wiring.test.ts": 2,
"test/autopilot-reconnect-classifier.test.ts": 5,
"test/autopilot-resolve-cli.test.ts": 11,
"test/autopilot-supervisor-wiring.test.ts": 10,
"test/backfill-base.test.ts": 2,
"test/backfill-concurrency-clamp.serial.test.ts": 103,
"test/backlinks.test.ts": 23,
"test/backoff.test.ts": 8,
"test/balanced-reranker-default.test.ts": 19,
"test/batch-projection.test.ts": 1,
"test/bench-publish.test.ts": 8,
"test/bench/baseline-file.test.ts": 2,
"test/bench/correctness-gate.test.ts": 5,
"test/bench/qrels-file.test.ts": 13,
"test/book-mirror.test.ts": 431,
"test/bootstrap.test.ts": 18481,
"test/brain-allowlist.test.ts": 3257,
"test/brain-registry.serial.test.ts": 104,
"test/brain-resolver.test.ts": 125,
"test/brain-score-breakdown.test.ts": 3087,
"test/brain-score-recommendations.test.ts": 23,
"test/brain-writer-partial-scan.test.ts": 4335,
"test/brain-writer-walk-prune.test.ts": 7,
"test/brain-writer.test.ts": 3949,
"test/brainstorm-timeout.test.ts": 45,
"test/brainstorm/checkpoint.serial.test.ts": 136,
"test/brainstorm/cost-guardrails.test.ts": 13,
"test/brainstorm/distance.test.ts": 26,
"test/brainstorm/eval-brainstorm.test.ts": 13,
"test/brainstorm/lsd-mode-skip.test.ts": 2,
"test/budget-meter.test.ts": 26,
"test/budget-tracker.test.ts": 3265,
"test/build-llms.test.ts": 11,
"test/calibration-cli.test.ts": 13,
"test/calibration-profile.test.ts": 7,
"test/candidate-audit.test.ts": 9,
"test/capture-build-content.test.ts": 18,
"test/capture-runcapture.test.ts": 8,
"test/cathedral-ii-brainbench.test.ts": 9162,
"test/check-resolvable-cli.test.ts": 3600,
"test/check-resolvable.test.ts": 40,
"test/check-system-of-record.test.ts": 70,
"test/check-update.test.ts": 674,
"test/child-worker-supervisor.test.ts": 144,
"test/chunk-grain-fts.test.ts": 70996,
"test/chunker-timeout.test.ts": 27,
"test/chunker-version-gate.test.ts": 6,
"test/chunkers/code.test.ts": 129,
"test/chunkers/recursive.test.ts": 57,
"test/cjk.test.ts": 4,
"test/claw-test-cli.test.ts": 9,
"test/cli-args.test.ts": 15,
"test/cli-dispatch-thin-client.test.ts": 6027,
"test/cli-help-discoverability.test.ts": 1670,
"test/cli-multimodal-integration.test.ts": 26677,
"test/cli-options.test.ts": 1658,
"test/cli-pty-runner.test.ts": 16,
"test/cli-query-image.test.ts": 56,
"test/cli.test.ts": 2633,
"test/code-callers-cli.test.ts": 2,
"test/code-def-refs.test.ts": 28662,
"test/code-edges.test.ts": 2884,
"test/code-intel/edge-densification.test.ts": 33,
"test/code-intel/eval-capture-graph.test.ts": 5,
"test/code-intel/recursive-walk.test.ts": 31107,
"test/code-intel/scope-walker-resolution.test.ts": 19,
"test/code-intel/traversal-cache.test.ts": 42036,
"test/code-retrieval-harness.test.ts": 4,
"test/commands-search.test.ts": 23985,
"test/commands/capture.test.ts": 42431,
"test/config-ensure-gitignore.test.ts": 7,
"test/config-env.test.ts": 6,
"test/config-set.test.ts": 17,
"test/config-unset.test.ts": 23831,
"test/config.test.ts": 6,
"test/connection-manager.serial.test.ts": 117,
"test/connection-resilience.test.ts": 29,
"test/console-prefix.test.ts": 5,
"test/consolidate-valid-until.test.ts": 2471,
"test/content-sanity-literals.test.ts": 2,
"test/content-sanity.test.ts": 5,
"test/context-engine.test.ts": 33,
"test/contextual-retrieval-doctor.serial.test.ts": 2755,
"test/contextual-retrieval-resolver.test.ts": 4,
"test/contextual-retrieval-service-pure.test.ts": 3,
"test/core/audit-week-file.serial.test.ts": 97,
"test/core/base-phase.test.ts": 9,
"test/core/cycle.serial.test.ts": 5563,
"test/core/diarize/payload-fitter-summarize.test.ts": 15,
"test/core/remediation-checkpoint.serial.test.ts": 129,
"test/cosine-rescore-column.test.ts": 3198,
"test/cross-brain-calibration.test.ts": 8,
"test/cross-modal-eval-aggregate.test.ts": 3,
"test/cross-modal-eval-cli.test.ts": 23,
"test/cross-modal-eval-json-repair.test.ts": 31,
"test/cross-modal-hybrid-integration.serial.test.ts": 3312,
"test/cross-modal-phase1.test.ts": 40,
"test/cross-modal-phase2.test.ts": 48,
"test/cycle-abort.test.ts": 108,
"test/cycle-consolidate.test.ts": 23952,
"test/cycle-last-full-cycle-at.test.ts": 3771,
"test/cycle-legacy-phases.test.ts": 4177,
"test/cycle-lock-per-source.test.ts": 9,
"test/cycle-pack-gating.test.ts": 5,
"test/cycle-patterns.test.ts": 2,
"test/cycle-pglite-lock-ordering.serial.test.ts": 2693,
"test/cycle-synthesize-chunker.test.ts": 21,
"test/cycle-synthesize-md-discovery.test.ts": 6,
"test/cycle-synthesize-slug-collection.test.ts": 3140,
"test/cycle-synthesize.test.ts": 17,
"test/cycle/extract-atoms-synthesize-concepts.test.ts": 60181,
"test/cycle/nightly-probe-adapters.test.ts": 6,
"test/data-research.test.ts": 17,
"test/db-lock-election.test.ts": 23346,
"test/db-lock-per-source.test.ts": 33460,
"test/db-lock-refresh.test.ts": 6,
"test/dedup.test.ts": 13,
"test/destructive-guard.test.ts": 6357,
"test/disk-walk.test.ts": 17,
"test/distribution-import-boundary.test.ts": 7,
"test/doctor-behavioral.test.ts": 5729,
"test/doctor-calibration-checks.test.ts": 5,
"test/doctor-child-orphans.test.ts": 4,
"test/doctor-cli-smoke.serial.test.ts": 4239,
"test/doctor-cycle-freshness.test.ts": 33963,
"test/doctor-cycle-phase-scope.test.ts": 7,
"test/doctor-federation-health.test.ts": 4021,
"test/doctor-fix.test.ts": 1301,
"test/doctor-frontmatter-partial.test.ts": 2,
"test/doctor-home-dir-in-worktree.test.ts": 194,
"test/doctor-minions-check.test.ts": 2697,
"test/doctor-remote.serial.test.ts": 210,
"test/doctor-report-remote.serial.test.ts": 2823,
"test/doctor-search-mode.test.ts": 3519,
"test/doctor-subagent-health.test.ts": 2972,
"test/doctor-v0_37_7_checks.test.ts": 2447,
"test/doctor-ze-checks.test.ts": 26042,
"test/doctor.test.ts": 46040,
"test/domain-aggregators.test.ts": 3512,
"test/dream-cli-flags.test.ts": 3,
"test/dream.test.ts": 26718,
"test/drift-watch.test.ts": 255,
"test/dry-fix.test.ts": 442,
"test/edge-extractor.test.ts": 252,
"test/effective-date.test.ts": 6,
"test/embed-backfill-submit.test.ts": 3099,
"test/embed-multimodal-batching.test.ts": 11,
"test/embed-skip.test.ts": 5,
"test/embed-stale.test.ts": 34959,
"test/embed.serial.test.ts": 2312,
"test/embedding-context.test.ts": 6,
"test/embedding-dim-check.test.ts": 4174,
"test/embedding-pricing.test.ts": 3,
"test/emotional-weight.test.ts": 4,
"test/engine-factory.test.ts": 13,
"test/engine-find-trajectory.test.ts": 3351,
"test/engine-parity-event-type.test.ts": 3251,
"test/engine-upsertFile.test.ts": 3801,
"test/engine-weight-rounding-integration.test.ts": 2367,
"test/enrichable-pack.test.ts": 8,
"test/enrichment-service.test.ts": 4,
"test/enrichment.test.ts": 3498,
"test/entity-resolve-perf.slow.test.ts": 158667,
"test/entity-resolve.test.ts": 24029,
"test/error-classify.test.ts": 3,
"test/errors.test.ts": 18,
"test/eval-candidates.test.ts": 18453,
"test/eval-capture-scrub.test.ts": 1,
"test/eval-capture.test.ts": 6,
"test/eval-compare.test.ts": 36,
"test/eval-contradictions-auto-supersession.test.ts": 3,
"test/eval-contradictions-cache.test.ts": 4601,
"test/eval-contradictions-calibration-join.test.ts": 2,
"test/eval-contradictions-calibration.test.ts": 12,
"test/eval-contradictions-cost-prompt.test.ts": 4223,
"test/eval-contradictions-cost.test.ts": 16,
"test/eval-contradictions-cross-source.test.ts": 5,
"test/eval-contradictions-date-filter.test.ts": 5,
"test/eval-contradictions-engine.test.ts": 5226,
"test/eval-contradictions-fixture-redact.test.ts": 5,
"test/eval-contradictions-integrations.test.ts": 4067,
"test/eval-contradictions-judge-errors.test.ts": 3,
"test/eval-contradictions-judge.test.ts": 32,
"test/eval-contradictions-runner.test.ts": 5998,
"test/eval-contradictions-severity.test.ts": 5,
"test/eval-contradictions-trends.test.ts": 36220,
"test/eval-contradictions/no-valid-until-write.test.ts": 33,
"test/eval-cross-modal-batch.test.ts": 141,
"test/eval-export.test.ts": 2399,
"test/eval-gate.test.ts": 3884,
"test/eval-longmemeval-e2e.slow.test.ts": 196000,
"test/eval-longmemeval.slow.test.ts": 42000,
"test/eval-prune.test.ts": 18396,
"test/eval-replay-gate.test.ts": 34958,
"test/eval-replay-metadata-skip.test.ts": 3430,
"test/eval-replay.test.ts": 16,
"test/eval-run-all.test.ts": 17,
"test/eval-schema-authoring.test.ts": 4,
"test/eval-shared-json-repair-shim.test.ts": 6,
"test/eval-takes-quality-aggregate.test.ts": 32,
"test/eval-takes-quality-boundaries.test.ts": 3193,
"test/eval-takes-quality-cli.test.ts": 13,
"test/eval-takes-quality-pricing.test.ts": 2,
"test/eval-takes-quality-receipt-name.test.ts": 4,
"test/eval-takes-quality-receipt-write.test.ts": 3213,
"test/eval-takes-quality-regress.test.ts": 14,
"test/eval-takes-quality-replay.test.ts": 2890,
"test/eval-takes-quality-rubric.test.ts": 3,
"test/eval-takes-quality-runner.serial.test.ts": 2642,
"test/eval-takes-quality-trend.test.ts": 23553,
"test/eval-trajectory.test.ts": 3424,
"test/eval-v041_2-scaffolds.test.ts": 23,
"test/eval-whoknows.test.ts": 8,
"test/eval.test.ts": 2,
"test/exit-classification.test.ts": 4,
"test/expert-types-pack.test.ts": 60,
"test/extract-db.test.ts": 3200,
"test/extract-facts-phase.test.ts": 24617,
"test/extract-from-fence.test.ts": 6,
"test/extract-fs.test.ts": 2618,
"test/extract-incremental.test.ts": 4523,
"test/extract-source-aware.test.ts": 23970,
"test/extract-takes-holder-producer-seam.test.ts": 2469,
"test/extract-takes.test.ts": 3153,
"test/extract.test.ts": 24,
"test/extractable-pack.test.ts": 76,
"test/facts-absorb-log.test.ts": 3161,
"test/facts-anti-loop.test.ts": 2478,
"test/facts-backstop-gating.test.ts": 3278,
"test/facts-backstop-integration.test.ts": 23696,
"test/facts-backstop.test.ts": 3118,
"test/facts-canonicality.test.ts": 3218,
"test/facts-classify.test.ts": 3,
"test/facts-context-injection.serial.test.ts": 2703,
"test/facts-decay.test.ts": 19,
"test/facts-doctor-shape.test.ts": 3109,
"test/facts-eligibility.test.ts": 5,
"test/facts-engine.test.ts": 12954,
"test/facts-extract-silent-no-op.test.ts": 4,
"test/facts-extract-smoke.test.ts": 6,
"test/facts-extract.test.ts": 1,
"test/facts-fence-typed.test.ts": 4,
"test/facts-fence.test.ts": 5,
"test/facts-mcp-allowlist.serial.test.ts": 2633,
"test/facts-meta-cache.test.ts": 3117,
"test/facts-migration-dim.test.ts": 3329,
"test/facts-multi-tenant.test.ts": 23425,
"test/facts-queue.test.ts": 604,
"test/facts-recall-render.test.ts": 2968,
"test/facts-separation-pglite.test.ts": 3260,
"test/facts-visibility.test.ts": 2917,
"test/fail-improve.test.ts": 132,
"test/feature-flags.test.ts": 3864,
"test/features.test.ts": 5,
"test/fence-extraction.test.ts": 3427,
"test/fence-write.test.ts": 2335,
"test/file-migration.test.ts": 4,
"test/file-resolver.test.ts": 24,
"test/file-upload-security.test.ts": 3,
"test/files.test.ts": 6,
"test/filing-audit.test.ts": 9,
"test/find-experts-op.test.ts": 3125,
"test/fix-wave-structural.test.ts": 6,
"test/founder-scorecard.test.ts": 9,
"test/friction-cli.test.ts": 21,
"test/friction.test.ts": 29,
"test/frontmatter-cli.test.ts": 2936,
"test/frontmatter-inference.test.ts": 10,
"test/frontmatter-install-hook.test.ts": 56,
"test/fuzz/filesystem-validators.test.ts": 87,
"test/fuzz/mixed-validators.test.ts": 279,
"test/fuzz/pure-validators.test.ts": 220,
"test/gateway-embed-model-override.test.ts": 1,
"test/gbrain-home-isolation.test.ts": 7,
"test/get-brain-identity.test.ts": 36332,
"test/git-remote.test.ts": 365,
"test/grade-takes-ensemble.test.ts": 30,
"test/grade-takes.test.ts": 6,
"test/graph-query.test.ts": 3163,
"test/gstack-learnings-coupling.test.ts": 15,
"test/handlers-embed-backfill.test.ts": 2978,
"test/handlers.test.ts": 25084,
"test/helpers/schema-diff-indexes.test.ts": 4,
"test/helpers/schema-diff.test.ts": 2,
"test/helpers/with-env.test.ts": 19,
"test/http-transport.test.ts": 100,
"test/hybrid-meta.serial.test.ts": 3314,
"test/hybrid-search-lite.serial.test.ts": 2849,
"test/import-checkpoint.test.ts": 5,
"test/import-file-content-sanity.test.ts": 3363,
"test/import-file.test.ts": 87,
"test/import-image-file.test.ts": 3539,
"test/import-resume.test.ts": 2811,
"test/import-source-id.test.ts": 3149,
"test/import-walker.test.ts": 1,
"test/incremental-chunking.test.ts": 3212,
"test/infer-type-pack.test.ts": 16,
"test/ingestion/daemon.test.ts": 39,
"test/ingestion/dedup.test.ts": 5,
"test/ingestion/gstack-learnings.test.ts": 8,
"test/ingestion/ingest-capture.test.ts": 53213,
"test/ingestion/markdown-greenfield.test.ts": 13,
"test/ingestion/migration-mode.test.ts": 8,
"test/ingestion/put-page-write-through.test.ts": 38741,
"test/ingestion/skillpack-load.test.ts": 15,
"test/ingestion/sources/file-watcher.test.ts": 1007,
"test/ingestion/sources/inbox-folder.test.ts": 518,
"test/ingestion/test-harness.test.ts": 68,
"test/ingestion/types.test.ts": 9,
"test/init-env-detection.test.ts": 11,
"test/init-mcp-only.test.ts": 4380,
"test/init-migrate-only.test.ts": 7098,
"test/init-mode-picker.test.ts": 2440,
"test/init-provider-picker.test.ts": 2,
"test/insert-facts-batch.test.ts": 2976,
"test/integrations-install.test.ts": 183,
"test/integrations.test.ts": 24,
"test/integrity.test.ts": 2678,
"test/intent-weights.test.ts": 4,
"test/jobs-watch-snapshot.test.ts": 12,
"test/language-manifest.test.ts": 17,
"test/lease-cap-controller.test.ts": 3161,
"test/lens-pack-manifests.test.ts": 13,
"test/levenshtein.test.ts": 3,
"test/link-extraction-code-refs.test.ts": 4,
"test/link-extraction.test.ts": 25,
"test/link-inference-pack.test.ts": 46,
"test/lint-content-sanity.test.ts": 36,
"test/lint-frontmatter.test.ts": 15,
"test/lint.test.ts": 1,
"test/list-all-sources.test.ts": 39599,
"test/llm-intent-escalation.test.ts": 6,
"test/llm-intent-hybrid-integration.serial.test.ts": 3246,
"test/loadConfig-merge.test.ts": 4,
"test/longmemeval-extract.test.ts": 2991,
"test/longmemeval-intent.test.ts": 3,
"test/longmemeval-sanitize.test.ts": 5,
"test/longmemeval-trajectory-routing.test.ts": 12926,
"test/markdown-serializer.test.ts": 8,
"test/markdown-validation.test.ts": 3,
"test/markdown.test.ts": 27,
"test/mcp-client-hardening.test.ts": 305,
"test/mcp-client.test.ts": 65,
"test/mcp-dispatch-summarize.test.ts": 6,
"test/mcp-eval-capture.test.ts": 25246,
"test/mcp-tool-defs.test.ts": 15,
"test/metric-glossary.test.ts": 16,
"test/migrate-extensions.test.ts": 3,
"test/migrate.test.ts": 102260,
"test/migration-orchestrator-v0_21_0.test.ts": 7,
"test/migration-orchestrator-v0_31_0.test.ts": 12960,
"test/migration-resume.test.ts": 9,
"test/migration-v0-29-1.serial.test.ts": 3273,
"test/migrations-cjk-wave.test.ts": 3280,
"test/migrations-registry.test.ts": 3,
"test/migrations-v0_11_0.test.ts": 14,
"test/migrations-v0_12_0.test.ts": 6,
"test/migrations-v0_12_2.test.ts": 3,
"test/migrations-v0_13_0.test.ts": 1,
"test/migrations-v0_13_1.test.ts": 23,
"test/migrations-v0_14_0.test.ts": 20,
"test/migrations-v0_16_0.test.ts": 1,
"test/migrations-v0_19_0.test.ts": 20826,
"test/migrations-v0_21_0.test.ts": 2,
"test/migrations-v0_22_4.test.ts": 1,
"test/migrations-v0_27_1.test.ts": 3248,
"test/migrations-v0_32_2.test.ts": 2510,
"test/migrations-v48-takes-weight-backfill.test.ts": 2501,
"test/migrations-v94.test.ts": 32374,
"test/minions-lease-full-retry.test.ts": 3031,
"test/minions-quiet-hours.test.ts": 3341,
"test/minions-shell-inherit.test.ts": 24,
"test/minions-shell-redact.test.ts": 19,
"test/minions-shell-validate.test.ts": 7,
"test/minions-shell.test.ts": 3268,
"test/minions.test.ts": 15231,
"test/minions/agent-audit.test.ts": 9,
"test/minions/budget-meter.test.ts": 44657,
"test/mode-switch-ux.test.ts": 3698,
"test/model-config.serial.test.ts": 101,
"test/mounts-cache.test.ts": 21,
"test/mounts-cli.test.ts": 20,
"test/multi-source-drift.test.ts": 2479,
"test/multi-source-integration.test.ts": 3120,
"test/nightly-quality-probe.test.ts": 50,
"test/notability-eval.test.ts": 36,
"test/nudge.test.ts": 16,
"test/oauth-confidential-client.test.ts": 3221,
"test/oauth-scope-probe.test.ts": 112,
"test/oauth.test.ts": 2151,
"test/op-checkpoint.test.ts": 5333,
"test/openai-compat-multimodal.test.ts": 6,
"test/operation-context-sourceid-required.test.ts": 10,
"test/operations-allow-list.test.ts": 4,
"test/operations-descriptions.test.ts": 11,
"test/operations-embedding-column.test.ts": 30,
"test/operations-find-trajectory.test.ts": 3419,
"test/operations-schema-pack.test.ts": 5842,
"test/operations-trust-boundary.test.ts": 43020,
"test/orphans.test.ts": 16921,
"test/page-lock.test.ts": 266,
"test/page-type-exhaustive.test.ts": 17,
"test/pages-soft-delete.test.ts": 15271,
"test/parallel.test.ts": 108,
"test/parent-scope.test.ts": 23673,
"test/parity.test.ts": 16,
"test/performfullsync-source-id.test.ts": 2869,
"test/pglite-engine.test.ts": 33476,
"test/pglite-lock.test.ts": 1007,
"test/phantom-redirect-engine-parity.test.ts": 3605,
"test/phantom-redirect-per-source-lock.test.ts": 3,
"test/phantom-redirect.test.ts": 7303,
"test/phase-scope-coverage.test.ts": 3,
"test/plugin-loader.test.ts": 12,
"test/post-install-advisory.test.ts": 15,
"test/post-write-lint.test.ts": 4180,
"test/postgres-engine.test.ts": 32,
"test/preferences.test.ts": 14,
"test/privacy-script-wired.test.ts": 3,
"test/privacy-strip-and-forget.test.ts": 2762,
"test/progress-tail.test.ts": 3,
"test/progress.test.ts": 215,
"test/propose-takes.test.ts": 15,
"test/providers.test.ts": 17,
"test/public-exports.test.ts": 35,
"test/publish.test.ts": 128,
"test/put-page-namespace.test.ts": 4,
"test/put-page-provenance.test.ts": 24839,
"test/qualified-names.test.ts": 4,
"test/query-cache-gate.test.ts": 31003,
"test/query-cache-knobs-hash.test.ts": 3122,
"test/query-cache.test.ts": 2565,
"test/query-image-flag.serial.test.ts": 2965,
"test/query-intent-legacy.test.ts": 8,
"test/query-intent.test.ts": 6,
"test/query-sanitization.test.ts": 4,
"test/queue-child-done.test.ts": 2548,
"test/rate-leases-uncapped.test.ts": 2884,
"test/rate-leases.test.ts": 2959,
"test/readme-hero-anchors.test.ts": 0,
"test/recall-extensions.test.ts": 4668,
"test/recall-footer.test.ts": 17,
"test/recall-rollup.test.ts": 84,
"test/recency-decay.test.ts": 5,
"test/recompute-emotional-weight.test.ts": 2,
"test/reconcile-links.serial.test.ts": 2702,
"test/regression-strict-source-id.test.ts": 4,
"test/regression-v0_16_4.test.ts": 28,
"test/regressions/gbrain-base-equivalence.test.ts": 12,
"test/regressions/v0.36.1.0-iron-rule.test.ts": 3,
"test/regressions/v0_36_frontier_cap.test.ts": 27151,
"test/regressions/v0_40_2_0-trajectory-backcompat.test.ts": 27576,
"test/reindex-code-max-cost.serial.test.ts": 2636,
"test/reindex-code-model-source.serial.test.ts": 2782,
"test/reindex-code-nudge.serial.test.ts": 2710,
"test/reindex-code.test.ts": 38123,
"test/reindex-frontmatter-connect.test.ts": 18383,
"test/reindex.test.ts": 19003,
"test/remediation-step.test.ts": 25,
"test/repair-jsonb.test.ts": 3,
"test/repo-root.test.ts": 14,
"test/report.test.ts": 2,
"test/repos-alias.test.ts": 3163,
"test/rerank-audit.test.ts": 24,
"test/resolve-prepare.test.ts": 11,
"test/resolver-merge.test.ts": 5,
"test/resolver.test.ts": 58,
"test/resolvers.test.ts": 4130,
"test/restart-sweep.test.ts": 42,
"test/retrieval-upgrade-planner.test.ts": 8375,
"test/retry-matcher.test.ts": 2,
"test/routing-eval-cli.test.ts": 1118,
"test/routing-eval.test.ts": 34,
"test/salience.test.ts": 2,
"test/scenarios.test.ts": 29,
"test/schema-bootstrap-coverage.test.ts": 6449,
"test/schema-cli-contract.test.ts": 4,
"test/schema-cli.test.ts": 3375,
"test/schema-pack-best-effort.test.ts": 9,
"test/schema-pack-lint-rules.test.ts": 11,
"test/schema-pack-load-active.serial.test.ts": 43,
"test/schema-pack-loader.test.ts": 10,
"test/schema-pack-manifest-v041_2.test.ts": 17,
"test/schema-pack-mutate-audit.test.ts": 8,
"test/schema-pack-mutate.test.ts": 80,
"test/schema-pack-pack-lock.test.ts": 65,
"test/schema-pack-query-cache-invalidator.test.ts": 22822,
"test/schema-pack-registry-reload.test.ts": 40,
"test/schema-pack-registry.test.ts": 52,
"test/schema-pack-stats.test.ts": 43032,
"test/schema-pack-sync.test.ts": 5580,
"test/schema-pack-trust-boundary.test.ts": 8,
"test/schema-verify.test.ts": 7,
"test/scope-agent-isolation.test.ts": 30,
"test/scope-normalize.test.ts": 24,
"test/scope.test.ts": 5,
"test/scripts/check-proposal-pii.test.ts": 511,
"test/scripts/check-test-isolation.test.ts": 223,
"test/scripts/run-unit-parallel.test.ts": 427,
"test/scripts/run-unit-shard.test.ts": 131,
"test/scripts/serial-files.test.ts": 41,
"test/scripts/test-shard.slow.test.ts": 32161,
"test/search-by-image-op.test.ts": 3869,
"test/search-image-column.test.ts": 30089,
"test/search-lang-symbol-kind.test.ts": 2556,
"test/search-limit.test.ts": 2909,
"test/search-mode.test.ts": 8,
"test/search-telemetry.test.ts": 25699,
"test/search-types-filter.test.ts": 3023,
"test/search.test.ts": 9,
"test/search/attribution-stamping.test.ts": 5,
"test/search/embedding-column.serial.test.ts": 109,
"test/search/explain-formatter.test.ts": 13,
"test/search/graph-signals-wire-integration.test.ts": 28358,
"test/search/graph-signals.test.ts": 14,
"test/search/hybrid-reranker-integration.serial.test.ts": 2905,
"test/search/knobs-hash-reranker.test.ts": 5,
"test/search/rerank.test.ts": 23,
"test/search/search-stats-graph-signals.test.ts": 23476,
"test/seed-pglite.test.ts": 12966,
"test/select-e2e.test.ts": 11,
"test/self-fix.test.ts": 28842,
"test/serve-http-bootstrap-token.test.ts": 2,
"test/serve-http-health.test.ts": 301,
"test/serve-stdio-lifecycle.test.ts": 1252,
"test/setup-branching.test.ts": 15,
"test/skill-brain-first.test.ts": 9,
"test/skill-manifest.test.ts": 25,
"test/skillify-check.test.ts": 554,
"test/skillify-scaffold.test.ts": 31,
"test/skillpack-apply-hunks.test.ts": 19,
"test/skillpack-bootstrap-display.test.ts": 4,
"test/skillpack-changed-since-version.test.ts": 361,
"test/skillpack-check.test.ts": 4691,
"test/skillpack-copy.test.ts": 11,
"test/skillpack-endorse.test.ts": 114,
"test/skillpack-frontmatter-sources.test.ts": 10,
"test/skillpack-harvest-lint.test.ts": 5,
"test/skillpack-harvest.test.ts": 35,
"test/skillpack-init-pack.test.ts": 50,
"test/skillpack-install.test.ts": 51,
"test/skillpack-manifest-v1.test.ts": 4,
"test/skillpack-migrate-fence.test.ts": 10,
"test/skillpack-reference-apply.test.ts": 13,
"test/skillpack-reference-pack-is-ten.test.ts": 14,
"test/skillpack-reference.test.ts": 29,
"test/skillpack-registry-client.test.ts": 27,
"test/skillpack-registry-schema.test.ts": 13,
"test/skillpack-remote-source.test.ts": 65,
"test/skillpack-rubric-doctor.test.ts": 45,
"test/skillpack-scaffold-third-party.test.ts": 17,
"test/skillpack-scaffold.test.ts": 44,
"test/skillpack-scrub-legacy.test.ts": 51,
"test/skillpack-state.test.ts": 30,
"test/skillpack-tarball.test.ts": 132,
"test/skillpack-trust-prompt.test.ts": 9,
"test/skills-conformance.test.ts": 25,
"test/slug-validation.test.ts": 12,
"test/sort-newest-first.test.ts": 120,
"test/source-config-redact.test.ts": 1,
"test/source-health.test.ts": 2940,
"test/source-id-routing.test.ts": 3687,
"test/source-id-tx-regression.test.ts": 3200,
"test/source-id.test.ts": 4,
"test/source-resolver-silent-fallback.test.ts": 40514,
"test/source-resolver-with-tier.test.ts": 1,
"test/source-resolver.test.ts": 27,
"test/sources-load.test.ts": 4120,
"test/sources-mcp.test.ts": 41488,
"test/sources-ops.test.ts": 4235,
"test/sources-resync-recovery.test.ts": 40765,
"test/sources-set-cr-mode.test.ts": 37772,
"test/sources-webhook.test.ts": 20,
"test/sources.test.ts": 11,
"test/spawn-helpers.test.ts": 18,
"test/sql-query.test.ts": 2327,
"test/sql-ranking.test.ts": 17,
"test/ssrf-validate.test.ts": 6,
"test/storage-backfill.test.ts": 4,
"test/storage-config.test.ts": 8,
"test/storage-export.test.ts": 3170,
"test/storage-pglite.test.ts": 23978,
"test/storage-status.test.ts": 12,
"test/storage-sync.test.ts": 27,
"test/storage.test.ts": 146,
"test/stub-guard-audit.test.ts": 27,
"test/subagent-aggregator.test.ts": 5,
"test/subagent-audit.test.ts": 29,
"test/subagent-handler.test.ts": 3178,
"test/subagent-prompt-too-long.test.ts": 17,
"test/subagent-transcript.test.ts": 3276,
"test/subagent-v1-v2-shim.test.ts": 4830,
"test/submit-agent.test.ts": 4992,
"test/supabase-admin.test.ts": 0,
"test/supervisor-audit.test.ts": 8,
"test/supervisor-tini.test.ts": 1,
"test/supervisor.test.ts": 326,
"test/svg-renderer.test.ts": 7,
"test/sync-all-parallel.test.ts": 6,
"test/sync-classifier-widening.test.ts": 4,
"test/sync-concurrency.test.ts": 6,
"test/sync-cost-preview.test.ts": 115,
"test/sync-failures.test.ts": 18,
"test/sync-parallel.test.ts": 22640,
"test/sync-strategy.test.ts": 15,
"test/sync-trigger-cli.test.ts": 3100,
"test/sync-walker-submodule.test.ts": 5,
"test/sync-walker-symlink.test.ts": 12,
"test/sync.test.ts": 5841,
"test/synth-enabled-default.test.ts": 3,
"test/system-prompt.test.ts": 8,
"test/take-forecast.test.ts": 7,
"test/takes-engine.test.ts": 3302,
"test/takes-fence-read-ops.serial.test.ts": 2720,
"test/takes-fence.test.ts": 132,
"test/takes-holder-semantics.test.ts": 5,
"test/takes-holder-validation.test.ts": 13,
"test/takes-mcp-allowlist.serial.test.ts": 2721,
"test/takes-resolution.test.ts": 5,
"test/takes-weight-rounding.test.ts": 6,
"test/thin-client-routing-audit.test.ts": 5,
"test/thin-client-upgrade-prompt.test.ts": 23,
"test/think-ab.test.ts": 18,
"test/think-entity-extract.test.ts": 4,
"test/think-gateway-adapter.test.ts": 33,
"test/think-intent.test.ts": 8,
"test/think-pipeline.serial.test.ts": 2723,
"test/think-sanitize-trajectory.test.ts": 7,
"test/think-trajectory-injection.test.ts": 3346,
"test/think-with-calibration.test.ts": 3,
"test/timing-safe.test.ts": 17,
"test/token-budget.test.ts": 3,
"test/trajectory-format.test.ts": 4,
"test/transcript-capture.test.ts": 246,
"test/transcription.test.ts": 20,
"test/transcripts.test.ts": 20,
"test/traverse-graph-dedup.test.ts": 2933,
"test/trust-boundary-contract.test.ts": 24,
"test/two-pass.test.ts": 3137,
"test/undo-wave.test.ts": 5,
"test/unified-multimodal.serial.test.ts": 3286,
"test/upgrade-checkpoint.serial.test.ts": 109,
"test/upgrade-reembed-prompt.test.ts": 3219,
"test/upgrade-reference-sweep.test.ts": 56,
"test/upgrade.serial.test.ts": 723,
"test/url-redact.test.ts": 2,
"test/utils.test.ts": 5,
"test/v0_29-tool-surfaces.test.ts": 4,
"test/v0_37_fix_wave.serial.test.ts": 211,
"test/v81-v82-smoke.test.ts": 3160,
"test/vector-index-lifecycle.test.ts": 5,
"test/voice-gate.test.ts": 9,
"test/voyage-multimodal.test.ts": 22,
"test/voyage-response-cap.test.ts": 1,
"test/wait-for-completion.test.ts": 3626,
"test/whoami.test.ts": 3,
"test/whoknows-doctor.test.ts": 70,
"test/whoknows.test.ts": 3,
"test/worker-rss.test.ts": 3,
"test/worker-shutdown-disconnect.test.ts": 3260,
"test/writer.test.ts": 37365,
"test/yaml-lite.test.ts": 9,
"test/ze-switch-cli.test.ts": 4227,
"test/zombie-reap.test.ts": 3
}
-3
View File
@@ -110,7 +110,6 @@ These apply to ALL brain-writing skills:
- `skills/conventions/quality.md` — citations, back-links, notability gate
- `skills/conventions/brain-first.md` — check brain before external APIs
- `skills/conventions/brain-routing.md` — which brain (DB) and which source (repo) to target; cross-brain federation is latent-space only
- `skills/conventions/schema-evolution.md` — when to add a type vs alias vs prefix (read before `schema-author`)
- `skills/conventions/subagent-routing.md` — when to use Minions vs inline work
- `skills/ask-user/SKILL.md` — choice-gate pattern for human input at decision points
- `skills/_brain-filing-rules.md` — where files go
@@ -129,6 +128,4 @@ These apply to ALL brain-writing skills:
| "verify this academic claim", "check this study", "academic verify", "validate citation", "is this study real" | `skills/academic-verify/SKILL.md` |
| "make pdf from brain", "brain pdf", "convert brain page to pdf", "publish this page as pdf", "export brain page" | `skills/brain-pdf/SKILL.md` |
| "voice note", "ingest this voice memo", "transcribe and file", "voice note ingest", "save this audio note" | `skills/voice-note-ingest/SKILL.md` |
| "add a page type", "add a type to my schema", "schema author", "schema mutate", "schema pack add", "my brain has untyped pages", "propose new types from my corpus", "backfill page types", "evolve my schema", "researcher type", "make X an expert type" (dispatcher for: gbrain schema active/list/show/validate/graph/lint/stats/explain/use/downgrade/reload/init/fork/edit/diff/add-type/remove-type/update-type/add-alias/remove-alias/add-prefix/remove-prefix/add-link-type/remove-link-type/set-extractable/set-expert-routing/detect/suggest/review-candidates/review-orphans/sync) | `skills/schema-author/SKILL.md` |
| "unify my types", "migrate to gbrain-base-v2", "94 types to 14", "apply canonical taxonomy", "clean up my page types", "pack upgrade", "shrink type proliferation", "consolidate page types", "retype pages to canonical" (dispatcher for: gbrain onboard --check, gbrain onboard --check --explain, gbrain jobs submit unify-types, gbrain pages restore) | `skills/schema-unify/SKILL.md` |
+1 -1
View File
@@ -3,4 +3,4 @@
{"intent": "ask the brain taxonomist before I write this page", "expected_skill": "brain-taxonomist"}
{"intent": "run a taxonomy check on yesterday's notes", "expected_skill": "brain-taxonomist"}
{"intent": "I want to refile brain page about Bob", "expected_skill": "brain-taxonomist"}
{"intent": "which directory does this page go in given the active pack?", "expected_skill": "brain-taxonomist", "ambiguous_with": ["repo-architecture"]}
{"intent": "which directory does this page go in given the active pack?", "expected_skill": "brain-taxonomist"}
+4 -17
View File
@@ -46,33 +46,20 @@ Do NOT switch brain when:
- You're unsure. Stay in host, surface what you found, let the user point
you at a specific brain.
## Source resolution chain (7-tier, v0.41.13+)
## Source resolution chain (6-tier, v0.18.0+)
`gbrain` resolves the active source via `resolveSourceId()` in
`src/core/source-resolver.ts`. Seven tiers, highest priority first:
`src/core/source-resolver.ts`. Six tiers, highest priority first:
| # | Tier | Signal |
|---|---|---|
| 1 | `flag` | Explicit `--source <id>` CLI flag (or `--source-id <id>` on `gbrain extract` / `gbrain import`) |
| 1 | `flag` | Explicit `--source <id>` CLI flag (or `--source-id <id>` on `gbrain extract`) |
| 2 | `env` | `GBRAIN_SOURCE` environment variable |
| 3 | `dotfile` | `.gbrain-source` file in CWD or any ancestor directory |
| 4 | `local_path` | A registered source whose `local_path` contains CWD (longest prefix wins) |
| 5 | `brain_default` | Brain-level `sources.default` config key (explicit user intent) |
| 5.5 | `sole_non_default` | When tiers 15 missed AND exactly one registered source has a `local_path` AND isn't `'default'`, auto-route to it. Fires a one-time stderr nudge per CLI invocation. Suppress with `GBRAIN_NO_SOLE_NON_DEFAULT_NUDGE=1`. |
| 5 | `brain_default` | Brain-level `sources.default` config key |
| 6 | `seed_default` | Literal `'default'` (always exists post-migration v16) |
**v0.41.13 tier 5.5 (`sole_non_default`):** added for single-source brains
(typical for users with one Obsidian vault, one notes folder, one project).
Pre-fix, `gbrain sync` from `/tmp` against a brain registering only
`studiovault` silently routed to `'default'` and every edit failed at
`createVersion` because the slug didn't exist there. The tier auto-routes
to the obvious single answer. Multi-source brains (2+ non-default registered)
still fall through to `seed_default` and require explicit `--source`.
Placement AFTER `brain_default` is deliberate: a user who explicitly set
`sources.default` via `gbrain sources default <id>` has stated intent that
wins over the auto-route. Archived sources are excluded from the count.
**v0.37.7.0 tooling:**
- `gbrain sources current [--json]` echoes the resolved source AND
-146
View File
@@ -1,146 +0,0 @@
# Convention: schema evolution — when to add a type vs alias vs prefix
Cross-cutting convention for any skill that proposes a change to the
active schema pack. Read first before invoking `schema-author`. The
goal: keep the pack small enough that an agent can hold the whole type
graph in its head, but expressive enough that custom domains
(research, legal, founder ops) get first-class types.
## Decision tree
```
You see a cluster of pages that share a domain meaning.
How many pages in the cluster?
┌─────┴───────┬──────────────┐
▼ ▼ ▼
<20 20-100 100+
│ │ │
▼ ▼ ▼
One-off. Big enough. First-class.
Don't pack- Add an alias Add a new
codify. to an existing page_type with
type OR a its own prefix,
Use the narrow prefix primitive, and
nearest branch. flags.
existing
type +
frontmatter
tag.
```
### Concrete examples
**One-off (don't add to pack):**
> "I have 3 pages under `2026-projects/skunkworks-spec/`. Should I add
> a `skunkworks` type?"
No. Three pages doesn't justify a permanent pack entry. Type these as
the nearest existing match (`concept` or `note`) and use a frontmatter
`project:` tag. If the cluster grows to 20+, revisit.
**20-100 pages — alias OR narrow prefix:**
> "I have 50 pages under `people/researchers/` that overlap with my
> `person` type. Should I add a `researcher` type?"
Two valid options:
1. **Alias on `person`**`add-alias person researcher`. Closure
queries for `researcher` will surface `person` rows too.
2. **New type sharing the `entity` primitive** — `add-type researcher
--primitive entity --prefix people/researchers/`. Distinct type, can
be marked `--extractable` or `--expert` independently.
Pick alias when researchers are people first, researchers second
(they share enrichment rules, expert-routing semantics, link verbs).
Pick new type when researcher-specific behavior diverges (different
extractable rules, different link verbs, different rubric).
**100+ pages — first-class type:**
> "I have 4000 pages under `meetings/`. I want them typed as `meeting`,
> not the legacy default `note`."
Add the type:
```
gbrain schema add-type meeting \
--primitive temporal \
--prefix meetings/ \
--extractable
gbrain schema sync --apply
```
The `sync --apply` backfills all 4000 pages. From here forward,
imports under `meetings/` infer `meeting` type via the pack.
## Don'ts
- **Don't add a type for a directory you imported once for triage.**
Pack types are permanent decisions; one-time imports are not.
- **Don't add a type just to silence `dead_prefixes` in `schema stats`.**
A dead prefix is a *signal* that the prefix is mis-declared or the
corpus moved. Remove the prefix or migrate the content, don't add an
empty type.
- **Don't promote a candidate from `schema suggest` without verifying
the path prefix matches real content.** The suggester is heuristic;
it can propose types that overlap existing ones. Run `lint --with-db`
before `add-type` to catch prefix collisions pre-write.
- **Don't add `--expert` to a type that has no `path_prefixes`.** The
`expert_routing_without_prefix` lint rule warns about this exact
shape: an expert-routed type with no prefix never matches a put_page
inference, so `whoknows` silently never surfaces it.
- **Don't mutate `gbrain-base` or `gbrain-recommended`.** Fork first.
## When to remove a type
Removing a type is RARE. Only do it when:
1. The type was added in error (typo, premature abstraction).
2. The corpus the type was meant for has been migrated to a different
type.
3. The type is dangling (no `path_prefixes` actually match pages, no
queries reference it, no other type's aliases/link_types reference it).
`remove-type` is guarded by the `STILL_REFERENCED` check (codex C14): if
ANY other type's aliases / enrichable_types / link_types / frontmatter_links
references the target, the remove fails loud with the reference list.
Break those references first.
## When to commit the pack
If your pack lives in source control (`~/.gbrain/schema-packs/<name>/`
is a git repo), commit after every batch of mutations. The
`mutation_count_anomaly` lint rule warns at >50 mutations in 7 days —
that's the hint to start committing rather than relying on disk-only
state.
## When to upgrade your pack (v0.42+)
A pack can declare `migration_from: {pack: <name>, version: <semver-range>}`
to register itself as the successor to another pack. When a brain's
active pack matches the declared `from`, the `pack_upgrade_available`
onboard check surfaces the successor + a `manual_only` RemediationStep
pointing at the `unify-types` PROTECTED Minion handler.
v0.41.22 ships **gbrain-base-v2** as the declared successor to
gbrain-base@1.x — collapses 94 noisy types to 15 canonical via
declarative mapping_rules. Run via `gbrain onboard --check --explain`
(preview) → `gbrain jobs submit unify-types --allow-protected --params
'{"target_pack":"gbrain-base-v2"}'` (apply). See
`skills/schema-unify/SKILL.md` for the full playbook.
Authoring a successor pack: declare
`migration_from: {pack: <parent>, version: "1.x"}` in the manifest
plus `mapping_rules:` (discriminated union over retype / page_to_link /
page_to_alias kinds). Catch-all sentinel `from_type: '*unknown*'` MUST
appear last. Subtype_field is restricted to ALLOWED_SUBTYPE_FIELDS
(`subtype, legacy_type, origin, format, kind, period, domain`) per
codex D9 — third-party packs cannot inject `title` / `slug` / `type`.
When NOT to upgrade:
- Custom types not covered by the successor's mapping_rules → fork the
successor first (`gbrain schema fork gbrain-base-v2 my-pack`), edit
rules, then target your fork.
- Mid-ingest or autopilot maintenance → wait. Unify holds the
`gbrain-unify` db-lock for ~10 min on big brains.
- Federated brain with sources you don't want to touch → scope per
source via `--params sourceId`.
-31
View File
@@ -53,34 +53,6 @@ Every cron job MUST be idempotent:
Job configuration saved. Report: "Job '{name}' scheduled at {cron expression}. Next run: {time}."
## Multi-source brains: use `sync --all`, not per-source entries
When the brain has 2+ active sources (anything `gbrain sources list` shows
with a non-null `local_path` that isn't archived), use one consolidated
cron line instead of N per-source entries.
**Preferred (multi-source)**:
```cron
*/5 * * * * gbrain sync --all --parallel 4 --workers 4 --skip-failed
```
This replaces N per-source lines AND auto-picks-up future sources without
a crontab edit. Concurrency budget: `parallel × workers × 2 ≈ 32`
connections during the wave (each per-file worker opens its own
2-connection pool). Stay under your Postgres `max_connections` setting.
**Avoid (legacy)**: separate `gbrain sync --source default` and
`gbrain sync --source zion-brain` entries staggered by 5 minutes. They
require manual deconfliction every time a new source is added, and a
slow source can race a fast source on the legacy global `gbrain-sync`
lock (v0.40.3.0+ uses per-source `gbrain-sync:<sourceId>` locks but the
per-source cron pattern doesn't benefit from the parallelism that
`--all --parallel` actually delivers).
`gbrain doctor` surfaces the recommended line as a `sync_consolidation`
check whenever it detects 2+ active sources. Paste-ready from there.
## Anti-Patterns
- Scheduling jobs at the same minute (:00 for everything)
@@ -88,6 +60,3 @@ check whenever it detects 2+ active sources. Paste-ready from there.
- Running cron jobs without testing on 3-5 items first
- Jobs that produce different output on re-run (not idempotent)
- Sending notifications during quiet hours (save to held queue instead)
- Separate per-source `gbrain sync --source <id>` cron entries when
`gbrain sync --all --parallel N --workers N` would replace them with
one line that auto-picks-up future sources.
-15
View File
@@ -144,11 +144,6 @@
"path": "minion-orchestrator/SKILL.md",
"description": "Unified Minions skill for deterministic shell jobs and LLM subagent orchestration. Submit, monitor, steer, pause/resume, replay. Replaces the older gbrain-jobs routing intent and sessions_spawn for durable observable background work."
},
{
"name": "schema-author",
"path": "schema-author/SKILL.md",
"description": "Evolve the active schema pack. Add page types, propose new types from a corpus scan, backfill page.type via sync. Wraps the 14 gbrain schema CLI verbs + 9 MCP ops shipped in v0.40.7.0."
},
{
"name": "skillify",
"path": "skillify/SKILL.md",
@@ -238,16 +233,6 @@
"name": "eiirp",
"path": "eiirp/SKILL.md",
"description": "Everything In Its Right Place — post-work organizer. 7-phase audit: inventory, taxonomy, schema check (via cathedral CLI), file, skill graph audit, verify, report."
},
{
"name": "schema-unify",
"path": "schema-unify/SKILL.md",
"description": "Migrate a brain off a noisy 24+-type pack onto gbrain-base-v2 (15 canonical types). 7-phase workflow: brain → assess → propose → apply → sync → verify → commit. Wraps the v0.41.22 unify-types PROTECTED Minion handler."
},
{
"name": "skill-optimizer",
"path": "skill-optimizer/SKILL.md",
"description": "Self-evolving skill optimization via gbrain skillopt — SkillOpt-paper-grounded text-space optimizer with validation gating (median-of-3 + epsilon=0.05), bundled-skill safety, bootstrap review sentinel, per-skill DB lock, and atomic versioned writes."
}
],
"dependencies": {
-61
View File
@@ -1,61 +0,0 @@
# v0.41.11.0 migration — conversation retrieval upgrade
The v0.41.11.0 upgrade ships:
1. Schema-pack additions: `conversation` (temporal, extractable) and `atom` (annotation, NOT extractable) promoted into `gbrain-base`.
2. New CLI command `gbrain extract-conversation-facts` for bulk fact extraction over long-form conversation pages.
3. Opt-in cycle phase `conversation_facts_backfill` (default OFF) for autopilot-driven backlog drain.
4. New doctor check `conversation_facts_backlog` (3-state; SKIPPED when feature disabled so no opt-out noise).
5. Schema migration v94: partial index on `facts(source_id, source_session) WHERE source LIKE 'cli:extract-conversation-facts%'` so the doctor query stays fast on big brains.
`gbrain upgrade` + `gbrain apply-migrations --yes` handles the binary + schema. The cycle phase and doctor check require no agent action.
## Agent action — when to run the backfill
When the user mentions:
- "search isn't finding things in my chat history"
- "my iMessage / Slack import isn't searchable"
- "the page exists but search misses the answer"
- "long conversation pages aren't returning what I want"
…then this command is the right tool. Don't run it speculatively — it costs real money against the user's Anthropic budget.
## Recommended flow
1. **Estimate cost first.** For each source the user wants to backfill:
```bash
gbrain sources audit <source-id> --json | jq '.facts_backfill_estimate'
```
Returns `{pages, est_segments, est_cost_usd, types}`. Show the user the cost estimate before recommending the run.
2. **Run a dry-run** to preview segmentation:
```bash
gbrain extract-conversation-facts --dry-run --slug <slug-of-one-conversation-page>
```
Confirms the segmenter parses the page correctly without spending any tokens.
3. **Run with explicit cost cap.** Default cap is $5. Recommend matching the audit estimate (rounded up):
```bash
gbrain extract-conversation-facts --background --max-cost-usd <cap>
gbrain jobs follow <printed-job-id>
```
Use `--background` so the user can keep working. The Minion job is resumable — if it hits the budget cap mid-run, re-running with a higher cap continues from where it left off.
4. **Verify with doctor:**
```bash
gbrain doctor --json | jq '.checks[] | select(.name == "conversation_facts_backlog")'
```
Should show OK with backlog: 0 after a successful complete run.
5. **(Optional) Enable autopilot drain** if the user has steady conversation ingest:
```bash
gbrain config set cycle.conversation_facts_backfill.enabled true
```
The cycle phase will then drain new conversation pages each tick under bounded per-source ($1/cycle) and brain-wide ($5/cycle) budgets.
## Caveats
- Pages over 25MB body are skipped (memory cap). Surface in doctor `details`; streaming for huge pages is a v0.42+ TODO.
- If `facts.extraction_enabled` is false, the command refuses. Pass `--override-disabled` only when the user explicitly opted out and now wants this one-time run.
- Extracted facts use `source = 'cli:extract-conversation-facts'`. To remove them in bulk (rare), the only path today is raw SQL: `DELETE FROM facts WHERE source LIKE 'cli:extract-conversation-facts%'`. A `gbrain forget --where` bulk flag is a v0.42+ TODO.
- The recall-quality eval (under `test/eval/conversation-extraction-quality.eval.ts` — added in this wave) is env-gated on `ANTHROPIC_API_KEY`. Run nightly or on-demand for quality verification; the hermetic wiring tests run in CI by default.
-305
View File
@@ -1,305 +0,0 @@
---
name: schema-author
description: Evolve your brain's schema pack. Add page types, propose new ones from corpus scans, backfill page.type on existing pages, audit pack health. Triggers when an agent notices untyped pages, custom domains needing typed entities (researcher, contract, deposition), or wants to see what types the pack declares.
tools:
- gbrain schema active
- gbrain schema list
- gbrain schema stats
- gbrain schema review-orphans
- gbrain schema detect
- gbrain schema suggest
- gbrain schema lint
- gbrain schema graph
- gbrain schema explain
- gbrain schema fork
- gbrain schema use
- gbrain schema add-type
- gbrain schema remove-type
- gbrain schema update-type
- gbrain schema add-alias
- gbrain schema remove-alias
- gbrain schema add-prefix
- gbrain schema remove-prefix
- gbrain schema add-link-type
- gbrain schema remove-link-type
- gbrain schema set-extractable
- gbrain schema set-expert-routing
- gbrain schema sync
- gbrain schema reload
- mcp:get_active_schema_pack
- mcp:list_schema_packs
- mcp:schema_stats
- mcp:schema_lint
- mcp:schema_graph
- mcp:schema_explain_type
- mcp:schema_review_orphans
- mcp:schema_apply_mutations
- mcp:reload_schema_pack
triggers:
- "add a page type"
- "add a type to my schema"
- "my brain has untyped pages"
- "schema isn't matching my notes"
- "propose new types from my corpus"
- "backfill page types"
- "evolve my schema"
- "extend the schema pack"
- "create a custom type for"
- "researcher type"
- "make X an expert type"
- "schema pack add"
- "schema mutate"
- "schema sync"
- "schema author"
brain_first: exempt
writes_pages: []
---
# schema-author — evolve your schema pack
## Non-goals (use these other skills instead)
This skill AUTHORS the schema pack (adds page types, link verbs, prefixes,
flags). For these adjacent jobs, route elsewhere:
- **Filing one specific page**`skills/brain-taxonomist/SKILL.md`. Brain-
taxonomist routes at WRITE TIME ("where does this note go?"). schema-author
changes the rules at AUTHORING TIME ("what types and prefixes exist?").
- **Schema-check as part of EIIRP iteration**`skills/eiirp/SKILL.md`
already has a schema-check phase. Don't duplicate.
- **Just looking up a type's settings**`gbrain schema explain <type>`
directly. This skill is for CHANGING the pack, not READING from it.
- **Querying who knows about X**`skills/expert-routing/SKILL.md` (or
`gbrain whoknows` directly). schema-author makes a type expert-routable;
it does not run the query.
## Convention
> **Convention:** see [conventions/brain-first.md](../conventions/brain-first.md) for the lookup chain (search → query → get_page → external).
> **Convention:** see [conventions/schema-evolution.md](../conventions/schema-evolution.md) for "when to add a type vs alias vs prefix" — the heuristic.
## When to invoke
Invoke when the user (or a sibling skill) says any of:
- "Add a `researcher` type to my schema"
- "I have 4000 untyped pages under `meetings/`"
- "My brain doesn't know that `journal-article` is a type"
- "Set `paper` to be extractable"
- "Propose types from what I've ingested"
- "Sync the new types to backfill existing pages"
DON'T invoke for "where does THIS note go" (use brain-taxonomist) or
"who knows about X" (use expert-routing / `gbrain whoknows`).
## Tutorial + vision
- **Why this matters:** [`docs/what-schemas-unlock.md`](../../docs/what-schemas-unlock.md) — 7 killer use cases (4000 invisible meetings made queryable, founder ops brain, research brain, legal brain, team brain, agent-as-co-curator) plus the structural argument for why types matter at query time. Read this before pitching schema authoring to a user — it's the doc that explains the difference between a pile of notes and a brain with structure.
- **5-minute walkthrough:** [`docs/schema-author-tutorial.md`](../../docs/schema-author-tutorial.md) — fork the bundled pack, add a researcher type, sync, prove the T1.5 wiring via `gbrain whoknows`. Use placeholder pages so it runs against any brain without affecting real content.
## Workflow
### Phase 1 — Brain (know which pack is active)
```
gbrain schema active --json
```
Output gives you `pack_name`, `version`, `sha8`, `page_types_count`, `source_tier`.
If `source_tier === "default"`, the user is on bundled `gbrain-base` and any
mutation will need a fork first (Phase 4).
### Phase 2 — Assess (what does the current pack cover?)
```
gbrain schema stats --json
```
Returns per-type page counts, untyped count, and `dead_prefixes` (pack-
declared prefixes with zero matching pages — probable mis-declarations).
If coverage < 90%, there's untyped content worth typing.
```
gbrain schema review-orphans --limit 50 --json
```
Untyped pages drilldown. Look for shared path prefixes (e.g. "12 of these
are under `research/papers/`") — those are candidates for a new type.
### Phase 3 — Propose (what types should the pack add?)
```
gbrain schema detect --json
```
Clusters pages by `source_path` and proposes candidate types. Heuristic
only (no LLM call).
```
gbrain schema suggest --json
```
LLM-refined candidates with confidence scores. Use the top-3 hit rate as
the signal for which to promote.
### Phase 4 — Apply (mutate the pack)
If the active pack is bundled (`gbrain-base` or `gbrain-recommended`),
fork it first:
```
gbrain schema fork gbrain-base mine
gbrain schema use mine
```
Then add the types one at a time:
```
gbrain schema add-type researcher \
--primitive entity \
--prefix people/researchers/ \
--extractable \
--expert
```
For complex multi-mutation refactors (e.g. add a type AND the link verb
that points to it), agents reaching this surface over MCP can use the
batched `schema_apply_mutations` op:
```jsonl
{"op": "add_type", "name": "researcher", "primitive": "entity", "prefix": "people/researchers/", "extractable": true, "expert_routing": true}
{"op": "add_type", "name": "paper", "primitive": "annotation", "prefix": "research/papers/", "extractable": true}
{"op": "add_link_type", "name": "authored", "inference": {"page_type": "researcher", "target_type": "paper"}}
```
Validate before sync:
```
gbrain schema lint --with-db
```
The `--with-db` flag opts into the 2 DB-aware rules
(`extractable_empty_corpus`, `mutation_count_anomaly`) that detect
mis-declared types you'd otherwise discover only at runtime.
### Phase 5 — Sync (backfill existing pages with the new types)
Dry-run first:
```
gbrain schema sync --json
```
Returns per-prefix `would_apply` counts + sample slugs. If the numbers
look right:
```
gbrain schema sync --apply
```
Chunked UPDATE in 1000-row batches; never wedges concurrent writers.
Idempotent on re-run (second `--apply` finds nothing to backfill).
### Phase 6 — Verify
```
gbrain schema stats --json
```
Coverage should be ≥95% now. Spot-check the new type:
```
gbrain whoknows "machine learning"
```
If `researcher` was declared `--expert`, results should include
researcher-typed pages. (The pack-aware wiring at the query path was
added in v0.40.6.0 — pre-v0.40.6 brains silently ignored custom
expert-routed types.)
### Phase 7 — Commit (preserve the change)
If the pack is in source control, commit:
```
cd ~/.gbrain/schema-packs/mine
git add pack.json
git commit -m "schema: add researcher + paper types + authored link"
git push
```
If the brain daemon is running (`gbrain serve --http`), other processes
pick up the change within 1 second (stat-mtime TTL gate in
loadActivePack — v0.40.6.0 closed the cross-process invalidation gap).
## Outputs
- Mutated pack file at `~/.gbrain/schema-packs/<name>/pack.{json,yaml}`.
- Audit row in `~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl` per mutation.
- `pages.type` backfilled on matching rows after `sync --apply`.
- Query paths (`whoknows`, `find_experts`) now route through the new
expert types.
## Contract
- **Inputs:** a natural-language request that names a type / prefix / link verb / flag change, OR the result of `gbrain schema review-orphans` showing untyped pages that need a new type.
- **Outputs:** mutated pack file at `~/.gbrain/schema-packs/<name>/pack.{json,yaml}` + an audit row in `~/.gbrain/audit/schema-mutations-YYYY-Www.jsonl` + (if `sync --apply` ran) backfilled `pages.type` on matching rows.
- **Side effects:** invalidates the in-process pack cache + the query cache for the source. Other processes pick up the change within 1 second (stat-mtime TTL).
- **Idempotency:** every primitive is idempotent. `add-alias`/`add-prefix` no-op on duplicate; `sync --apply` finds nothing to update on second run.
- **Trust:** CLI = local trust (no scope check). MCP = OAuth `admin` scope (write ops). Audit log captures `actor: mcp:<clientId8>` per mutation.
- **Atomicity:** every mutation is wrapped in `withMutation`'s atomic write (`.tmp + fsync + rename`) + per-pack `O_CREAT|O_EXCL` lock. Crash mid-write leaves the original file untouched.
## Anti-Patterns
- **Don't mutate `gbrain-base` or `gbrain-recommended`.** Fork first (`gbrain schema fork gbrain-base mine`). These are bundled packs; edits would be lost on upgrade. The mutation primitives refuse with `PACK_READONLY`.
- **Don't add a type for a directory you imported once for triage.** Pack types are permanent decisions; one-time imports are not. See `skills/conventions/schema-evolution.md` for the <20-pages-don't-pack-codify heuristic.
- **Don't add `--expert` to a type with no `path_prefixes`.** The `expert_routing_without_prefix` lint warns about this — expert-routed types with no prefix never match a put_page inference, so `whoknows` silently never surfaces them.
- **Don't promote a `schema suggest` candidate without verifying the prefix matches real content.** Run `lint --with-db` before `add-type` to catch prefix collisions pre-write.
- **Don't conflate "filing one page" with "evolving the schema."** Filing routes via `brain-taxonomist`; schema-author is for authoring the type taxonomy itself. The Non-goals section above names the boundary.
- **Don't skip the dry-run before `sync --apply`.** Always run `sync` first to see `would_apply` counts + sample slugs. A pack prefix that matches 50,000 pages is recoverable but slow; verifying first is cheap.
- **Don't remove a type without checking references.** `remove-type` refuses with `STILL_REFERENCED` if another type's `aliases` / `enrichable_types` / `link_types` / `frontmatter_links` references it. Break the references first; don't add `--force`.
## Output Format
When invoked, this skill produces structured output suitable for both human + JSON consumption:
**Per-mutation result (JSON):**
```json
{"schema_version": 1, "pack": "mine", "path": "/Users/.../pack.json", "format": "json", "prev_sha8": "a1b2c3d4", "new_sha8": "e5f6g7h8"}
```
**Per-batch result (from `schema_apply_mutations` MCP op):**
```json
{"schema_version": 1, "pack": "mine", "batch_id": "batch-1716491400-abc123", "mutations_applied": 3, "results": [{...}, {...}, {...}]}
```
**Stats JSON (per-source + aggregate + dead-prefix hints):**
```json
{"schema_version": 1, "pack_identity": "mine@1.0.0+abc12345", "aggregate": {"total_pages": 4823, "typed_pages": 4710, "untyped_pages": 113, "coverage": 0.9766, "by_type": [{"type": "person", "count": 2104}, ...]}, "per_source": [...], "dead_prefixes": [{"type": "researcher", "prefix": "people/researchers/"}]}
```
**Sync dry-run JSON:**
```json
{"schema_version": 1, "apply": false, "pack_identity": "mine@1.0.0+abc12345", "per_prefix": [{"type": "meeting", "prefix": "meetings/", "would_apply": 4000, "sample_slugs": ["meetings/2026-01-01-foo", ...], "dead_prefix": false, "applied": 0}], "total_would_apply": 4000, "total_applied": 0}
```
**Human output (the agent's final summary):**
- One line per mutation: `Pack: <name> (<format>)` and `Sha8: <prev> → <new>`
- Stats: total pages, typed %, untyped count, per-type breakdown, dead-prefix list
- Sync: per-prefix `would_apply`/`applied` count + sample slugs in dry-run mode
On failure, the error envelope follows the standard `StructuredAgentError` shape from `src/core/errors.ts`: `{error, code, message, details?}`. Codes from the mutation primitives: `PACK_NOT_FOUND`, `PACK_READONLY`, `PACK_CORRUPT`, `TYPE_EXISTS`, `TYPE_NOT_FOUND`, `INVALID_PRIMITIVE`, `INVALID_RESULT`, `IO_ERROR`, `STILL_REFERENCED`, `LOCK_BUSY`.
## Failure modes
- `PACK_READONLY` → you tried to mutate `gbrain-base` or `gbrain-recommended`. Fork first.
- `INVALID_RESULT` → the mutation would create a dangling reference or
prefix collision. The pre-write lint gate caught it. Read the error
message; the lint rule name names the problem.
- `STILL_REFERENCED` → you tried to remove a type that another type's
`aliases` / `enrichable_types` / `link_types` / `frontmatter_links`
references. The error names every reference. Remove those first.
- `LOCK_BUSY` → another process is mid-mutation. Wait 30s and retry, or
pass `--force` if you know the holder is wedged.
- `permission_denied` (MCP only) → your OAuth client doesn't have `admin`
scope. Re-register with `gbrain auth register-client --scopes admin`.
-251
View File
@@ -1,251 +0,0 @@
---
name: schema-unify
description: Migrate a brain from gbrain-base (or any pack) to gbrain-base-v2's 14-canonical-type taxonomy via gbrain onboard --check + the unify-types Minion handler. Collapses 94 noisy types to 15 canonical with subtypes, alias rows, and link rows. Triggers when an agent notices pack_upgrade_available, type_proliferation, or asks "what is the canonical taxonomy / how do I clean up my page types".
brain_first: exempt
tools:
- gbrain onboard --check
- gbrain onboard --check --explain
- gbrain onboard --check --json
- gbrain jobs submit unify-types
- gbrain jobs follow
- gbrain schema active
- gbrain schema use
- gbrain schema stats
- gbrain pages restore
- mcp:run_onboard
triggers:
- "unify my types"
- "migrate to gbrain-base-v2"
- "94 types to 14"
- "apply canonical taxonomy"
- "clean up my page types"
- "pack upgrade"
- "shrink type proliferation"
- "what does the canonical taxonomy look like"
- "consolidate page types"
- "retype pages to canonical"
---
# Schema Unification (gbrain-base → gbrain-base-v2)
v0.41.22 ships **gbrain-base-v2** — a 15-type DRY/MECE taxonomy (14 canonical + `note` catch-all) — as the install default for new brains. Existing brains on `gbrain-base` can opt in via the `pack_upgrade_available` onboard finding + the `unify-types` PROTECTED Minion handler.
This skill is the playbook for that migration.
## brain_first: exempt
This skill is ABOUT the brain's shape — it can't depend on the brain it's reshaping. No `gbrain search` lookup first; jump straight to onboard.
## When this skill fires
- Agent runs `gbrain onboard --check` and sees `pack_upgrade_available` or `type_proliferation` warnings
- User asks "what is the canonical taxonomy / how do I clean up my page types / migrate to v2"
- A `dangling_aliases` finding surfaces (post-unify GC)
- An agent ingesting from a custom pack wants to consult the v2 taxonomy as a reference
## Mental model (one paragraph)
A production gbrain brain accreted **94 distinct `pages.type` values** over years of ingestion: tweet / tweet-thread / tweet-bundle / tweet-single / media/x-tweet/bundle / tweet-stub all coexisting; 5.5K concept-redirect pages; atom-partner-link pages that should be links; civic / framework / insight / memo / anecdote one-offs. The cure: collapse to **15 canonical types** (person, company, media, tweet, social-digest, analysis, atom, concept, source, deal, email, slack, writing, project, note) with subtypes/format/origin pushed to frontmatter, alias-rows for redirects, real link-rows for edge-shaped pages, and a catch-all that bins long-tail unknowns to `note` with `frontmatter.legacy_type = <original>` for rollback.
## Workflow
### Phase 1: Discovery
Confirm the brain is actually on `gbrain-base` (not already on v2).
```bash
gbrain schema active --json | jq -r '.identity'
```
Expected: `gbrain-base@1.0.0+<sha>`. If you see `gbrain-base-v2@...`, the brain is already on v2 — skip the migration.
Then run onboard to see what would change:
```bash
gbrain onboard --check
```
Look for the `pack_upgrade_available` finding. If it's `ok`, there's no successor declared for the active pack — done.
### Phase 2: Preview
Run the per-cluster narrative:
```bash
gbrain onboard --check --explain
```
This invokes the `unify-types` handler in dry-run mode and prints:
- How many pages would retype per cluster (tweets, articles, companies, etc.)
- How many concept-redirect pages would become alias rows
- How many edge-shaped pages would convert to real links
- The synthesized catch-all rules for unknown types
Review the output. If the proposed changes look wrong, **don't** proceed — file an issue or write a custom pack with adjusted mapping_rules.
### Phase 3: Apply
The handler is PROTECTED (manual_only per D17) — autopilot will never auto-fire it. Submit explicitly:
```bash
gbrain jobs submit unify-types \
--allow-protected \
--params '{"target_pack":"gbrain-base-v2"}'
```
Watch progress per phase:
```bash
gbrain jobs follow <job_id>
```
On a 186K-page brain expect ~10 minutes. The handler runs:
1. Preflight (validate target pack has `mapping_rules:`)
2. Stats snapshot (pre-state for celebration summary)
3. Acquire `gbrain-unify` db-lock (60min TTL)
4. Apply phases:
- Explicit retype rules (tweets, articles, companies, etc.)
- Catch-all retype (unknown types → note with legacy_type)
- Page-to-link rules (atom-partner-link, symlink)
- Page-to-alias rules (concept-redirect)
5. Final sync (untyped rows by path-prefix)
6. **Flip active pack** to gbrain-base-v2 (D13)
7. Verify + celebration summary
### Phase 4: Verify
```bash
gbrain onboard --check
gbrain schema stats
```
Expected:
- `pack_upgrade_available``ok` (active pack is now v2)
- `type_proliferation``ok` (≤16 distinct typed values)
- `dangling_aliases``ok` (slug_aliases all point at active canonicals)
- `gbrain schema stats` shows ≤16 distinct types
### Phase 5: Post-migration
Anything that used `--type article` keeps working post-unify if your CLI calls go through the `expandTypeFilter` helper (it expands `article` to `media+subtype=article` automatically). Direct SQL against `pages.type` needs updating to the canonical types.
Search queries get a small ranking signal: pages reached via `slug_aliases` (canonicals of one or more aliases) get a 1.05x boost. Visible via `gbrain search --explain`.
## Rollback
Every retyped page preserves `frontmatter.legacy_type = <original>` per D8. Restore types via:
```sql
UPDATE pages SET type = frontmatter->>'legacy_type'
WHERE source_id = 'default' AND frontmatter->>'legacy_type' IS NOT NULL;
```
Page-to-alias and page-to-link source pages soft-delete with 72h TTL. Restore within that window:
```bash
gbrain pages restore <slug>
```
Revert the active pack flip:
```bash
gbrain schema use gbrain-base
```
## Anti-patterns
- **Don't run unify-types under autopilot.** It's manual_only by design. Autopilot remediation should never silently change your taxonomy.
- **Don't expect mapping_rules to cover every legacy type explicitly.** Use the catch-all (`*unknown*`) for the long tail. Pages get retyped to `note` with `legacy_type` preserved.
- **Don't rewrite body-text wikilinks.** D15: the slug_aliases table IS the resolver. `[[old-redirect-slug]]` keeps working via `engine.resolveSlugWithAlias` short-circuit.
- **Don't bypass the dry-run.** Always run `--explain` before applying. The trust delta is real.
- **Don't run two unify jobs concurrently.** The `gbrain-unify` db-lock serializes them; the second submission rejects with "already in progress."
## Decision tree
```
Active pack already gbrain-base-v2?
→ Skip migration.
Custom pack with own mapping_rules?
→ Run --check --explain to see if your pack declares migration_from
for the active pack. If yes, target_pack = your pack name.
Brain has many custom types not covered by gbrain-base-v2 mapping_rules?
→ The catch-all retype binds them to `note` with legacy_type preserved.
Review by inspecting frontmatter.legacy_type after the migration.
Federated brain (multiple sources)?
→ Add --params source_id to scope the migration per-source. Each
source can be migrated independently.
Worried about a specific cluster's mapping?
→ Fork gbrain-base-v2 (`gbrain schema fork gbrain-base-v2 my-pack`),
edit mapping_rules in your fork, then target the fork.
```
## Contract
Inputs:
- A brain on `gbrain-base` (or any pack with `migration_from: gbrain-base-v2`).
- Write access to submit a PROTECTED Minion handler (`--allow-protected`).
- ~10 min wallclock on a 186K-page brain.
Outputs:
- Pages retyped to canonical types with `frontmatter.legacy_type` preserved (per-page rollback signal).
- `slug_aliases` rows for concept-redirect pages (alias table IS the resolver — no link rewrite).
- Real `links` rows for edge-shaped pages (`atom-partner-link`, `symlink`, etc.).
- Active pack flipped to `gbrain-base-v2` atomically at end of successful run.
Side effects:
- Source pages soft-deleted with 72h restore TTL (`gbrain pages restore <slug>`).
- One-time cache invalidation on KNOBS_HASH_VERSION bump (5→6); self-healing in `cache.ttl_seconds`.
- Query-time `--type X` alias-expands via `expandTypeFilter` (D14 back-compat).
Failure modes:
- Concurrent submission rejected by the `gbrain-unify` db-lock; second call exits gracefully.
- Catch-all retype excludes `page_to_link` + `page_to_alias` source types (caught in E2E pre-merge).
- Phase failures abort the run before `active_pack_flipped`; partial state restorable via op_checkpoint resume.
## Anti-Patterns
DON'T:
- Submit `unify-types` directly via the MCP `submit_job` op without `--allow-protected`. PROTECTED handlers require trusted local callers; remote MCP rejection is the intentional trust boundary.
- Edit `mapping_rules` in `gbrain-base-v2.yaml` to skip clusters you don't trust. Fork the pack instead (`gbrain schema fork`) so the source-of-truth migration stays consistent across brains.
- Run `unify-types` from inside an autopilot tick. The check is `manual_only` per D17 — autopilot deliberately never auto-fires it because pack upgrades are one-time consenting taxonomy decisions.
- Hard-delete soft-deleted source pages before the 72h restore window. Use `gbrain pages restore <slug>` first if rollback is needed.
- Assume `frontmatter.legacy_type` survives every roundtrip. The marker is canonical for the immediate post-migration window; downstream re-imports may overwrite it.
## Output Format
Per phase, the handler emits to stderr:
```
[unify-types] phase=retype-explicit applied=N skipped=M cost=USD ttl=Ns
[unify-types] phase=retype-catch-all applied=N
[unify-types] phase=page-to-link converted=N pages soft-deleted
[unify-types] phase=page-to-alias aliased=N pages soft-deleted
[unify-types] phase=sync residual=N
[unify-types] active_pack flipped from gbrain-base to gbrain-base-v2
```
Final celebration summary to stderr:
```
═══════════════════════════════════════════════════════════
gbrain-base-v2 migration complete
═══════════════════════════════════════════════════════════
Before: 94 distinct page types
After: 15 canonical types
Retyped: 25,632 pages
Aliased: 5,521 redirects → slug_aliases table
Linkified: 65 ghost pages → real link rows
Soft-deleted: 5,586 pages (restorable for 72h)
═══════════════════════════════════════════════════════════
```
JSON output (`gbrain jobs follow <id> --json`) returns the structured `UnifyTypesResult` shape with `per_phase`, `pack_identity_after`, `active_pack_flipped`.
## Reference
- Plan + decisions: `~/.claude/plans/system-instruction-you-are-working-transient-elephant.md`
- Architecture: `docs/architecture/type-taxonomy.md`
- Pack-upgrade mechanism: `docs/architecture/pack-upgrade-mechanism.md`
- Issue: https://github.com/garrytan/gbrain/issues/1479
-188
View File
@@ -1,188 +0,0 @@
---
name: skill-optimizer
version: 0.1.0
description: Self-evolving skill optimization via SkillOpt-paper-grounded text-space optimizer.
triggers:
- "optimize this skill"
- "tune the skill against the benchmark"
- "make the skill better"
- "run skillopt"
- "skillopt for"
mutating: true
brain_first: exempt
---
# Skill Optimizer
Self-evolving skill optimization. Treats SKILL.md as the trainable parameters
of a frozen agent. Validation-gated, budget-capped, atomic-versioned.
Based on SkillOpt (arXiv 2605.23904, Microsoft Research, May 2026).
## When to invoke this skill
The user wants to:
- Improve an existing skill's execution quality against a benchmark
- Bootstrap a benchmark file for a new skill
- Re-tune a skill after switching target models
## Iron Law
- **Validation gating is MANDATORY.** Every candidate must clear median-of-3
+ epsilon=0.05 margin against the sel-set before SKILL.md gets rewritten.
- **Frontmatter mutation is FORBIDDEN.** The optimizer only edits the body.
Routing surface (`triggers:`, `brain_first:`) stays invariant.
- **Bundled skills require explicit opt-in AND an independent held-out set.**
Skills shipping with gbrain cannot be auto-mutated. To rewrite one in place
the user passes BOTH `--allow-mutate-bundled` AND `--held-out <path>` with
at least 5 benchmark-disjoint tasks; without the held-out set the run
hard-refuses (exit 2). Drop `--allow-mutate-bundled` (or pass `--no-mutate`,
the default for the dream-cycle phase) to write proposed.md for review
instead — no held-out needed for review-only output.
- **Bootstrap output requires human review.** Both `--bootstrap-from-skill`
and `--bootstrap-from-routing` write a sentinel; you must review + STRENGTHEN
the generated judges, delete the sentinel, and re-run with
`--bootstrap-reviewed` before optimization can use the file.
## The pipeline
```
gbrain skillopt <skill-name> [flags]
├── Pre-flight gates
│ ├── working tree clean (or --force)
│ ├── benchmark valid + D_sel >= 5 (D17)
│ ├── cost preflight (D3) — refuses over --max-cost-usd
│ └── per-skill DB lock (D14)
├── Baseline eval on D_sel (sets best_sel_score)
├── for epoch in 1..N:
│ for step in 1..steps_per_epoch:
│ ├── forward pass: rollouts on D_train batch
│ ├── backward pass: reflect × 2 (failures + successes per D7)
│ ├── rank + clip via LR cosine schedule
│ ├── apply edits (body-only per D5, tagged result per D9)
│ ├── validation gate: median-of-3 + epsilon=0.05 (D12)
│ └── if accept: commit via D8 history-intent-first
│ │
│ └── slow update (D6) if no improvement this epoch
└── Final test eval on D_test → run receipt
```
## Starting a benchmark from the skill itself (the common case)
**The user will NOT hand-write a benchmark, and you shouldn't start from a blank
file either.** When the user says "make skill X better" and
`skills/X/skillopt-benchmark.jsonl` doesn't exist, generate a starter from the
SKILL.md directly:
1. **Generate the starter.** Run:
```
gbrain skillopt X --bootstrap-from-skill
```
One LLM call reads `skills/X/SKILL.md`, infers what the skill produces and what
"good" looks like, and writes ~15 tasks (each with rule judges) to
`skills/X/skillopt-benchmark.jsonl` plus a `# BOOTSTRAP_PENDING_REVIEW`
sentinel. No `routing-eval.jsonl` is needed. Tune the count with
`--bootstrap-tasks N` (max 50).
2. **Review AND STRENGTHEN the judges.** This is YOUR job and it is load-bearing.
The generated rule checks are weak drafts — the model tends to emit generic
`contains`, loose `max_chars`, or invented headings. Read each task, fix soft
checks, add the must-haves the skill actually requires (real section names,
real length ceilings, `min_citations` where sources are expected,
`tool_called`/`tool_not_called` for tools the skill genuinely uses). A thin
benchmark optimizes for a thin definition of quality — do not rubber-stamp.
3. **Delete the sentinel line** (`# BOOTSTRAP_PENDING_REVIEW`, the last line).
4. **Run the optimizer with `--split 1:1:1`:**
```
gbrain skillopt X --bootstrap-reviewed --split 1:1:1
```
The 1:1:1 split is REQUIRED for a 15-task starter — the default `4:1:5` makes
the validation set `floor(15/10)=1`, below the `D_sel >= 5` floor, and the
optimizer refuses with `d_sel_too_small`. (4:1:5 needs ~50 tasks.) Add
`--dry-run` first to preview cost.
Benchmark line shape (what the generator writes, one per line):
```
{"task_id":"x-001","task":"<user prompt>","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1800},{"op":"contains","arg":"agenda"}]}}
```
Rule-check vocabulary you'll strengthen with: `contains`, `regex`,
`section_present`, `max_chars`, `min_citations`, `tool_called`, `tool_not_called`.
Rule judges are deterministic and free, but shallow for skills whose quality is
sequencing, privacy, refusal boundaries, or file placement — for those, hand-add
richer checks (or an `llm` judge) during review.
**Fallback — author freehand.** If the generated starter is poor (rare, but
possible for very behavior-shaped skills), discard it and write the JSONL
yourself: read the SKILL.md, write ~15 realistic tasks covering the boring middle,
attach >=2 rule checks each, save to `skills/X/skillopt-benchmark.jsonl`, run with
`--split 1:1:1`. The human walkthrough lives at
`docs/tutorials/improving-skills-with-skillopt.md`.
## Decision tree
| Situation | Action |
|---|---|
| Skill has no benchmark | `gbrain skillopt foo --bootstrap-from-skill` → review + strengthen the judges → delete sentinel → `gbrain skillopt foo --bootstrap-reviewed --split 1:1:1` (see section above) |
| Skill has a `routing-eval.jsonl` and you want a head start | `gbrain skillopt foo --bootstrap-from-routing` → review the generated tasks → `--bootstrap-reviewed` (routing tasks test dispatch; tighten them into quality tasks before trusting) |
| Iterating on an existing skill | `gbrain skillopt foo --benchmark skills/foo/skillopt-benchmark.jsonl` |
| Costly run, want preview | Add `--dry-run` |
| Bundled skill (skills/ in gbrain repo) | Default writes proposed.md; to commit in place add `--allow-mutate-bundled` AND `--held-out <path>` (>=5 benchmark-disjoint tasks) — else it hard-refuses |
| Want to review changes before applying | Add `--no-mutate` (writes proposed.md, no held-out needed) |
| Guard against benchmark overfitting | Add `--held-out <path>` — a candidate that beats the benchmark but regresses on the held-out set is refused |
| Mid-run crash | `gbrain skillopt foo --resume <run-id>` |
## Output Format
When invoked, this skill produces:
- Updated `skills/<name>/SKILL.md` (when mutation is allowed)
- `skills/<name>/skillopt/best.md` — pointer copy of current best
- `skills/<name>/skillopt/versions/vNNNN_eN_sN.md` — per-step snapshots
- `skills/<name>/skillopt/history.json` — append-only run record
- `skills/<name>/skillopt/rejected.json` — bounded LRU of rejected edits
- `~/.gbrain/audit/skillopt-YYYY-Www.jsonl` — ISO-week-rotated audit trail
## Anti-Patterns
- **Don't bypass the validation gate.** The median-of-3 + epsilon=0.05 is
load-bearing; without it, the optimizer accepts noise as improvement.
- **Don't optimize bundled skills without `--allow-mutate-bundled` AND
`--held-out`.** They ship with gbrain and are load-bearing for downstream
agents. In-place mutation requires both flags (held-out >=5 benchmark-disjoint
tasks); without the held-out set the run hard-refuses and points you at
proposed.md.
- **Don't use bootstrap output without strengthening it.** Both
`--bootstrap-from-skill` and `--bootstrap-from-routing` have the optimizer
model invent success criteria — generic and weak by default. Review and
tighten the judges before SkillOpt optimizes against them, or it trains the
skill toward benchmark artifacts instead of real quality.
- **Don't skip `--split 1:1:1` on a ~15-task starter.** The default `4:1:5`
split drops the validation set below the `D_sel >= 5` floor and the run
aborts with `d_sel_too_small`.
## Contract
`runSkillOpt(opts)` returns:
```
{
outcome: 'accepted' | 'no_improvement' | 'aborted' | 'errored',
receipt: {
run_id, skill_sha8, benchmark_sha8, models, cost,
baseline_sel_score, best_sel_score, // real measured baseline (no longer hardcoded 0)
baseline_test_score, test_score, // final held-out test-split eval
},
finalText: string,
mutatedSkillFile: boolean,
proposedPath?: string
}
```
## Related skills
- `skillify` — scaffolds a new skill (use BEFORE skillopt)
- `skillpack-check` — audits skill conformance (item 13 surfaces skillopt status)
- `conventions/quality.md` — output quality standards skillopt enforces via judges
@@ -1,6 +0,0 @@
{"intent":"Can you optimize this skill against my benchmark?","expected_skill":"skill-optimizer"}
{"intent":"Tune the skill against the benchmark fixtures","expected_skill":"skill-optimizer"}
{"intent":"Run skillopt for the brain-ops skill","expected_skill":"skill-optimizer"}
{"intent":"Make the skill better via the optimizer","expected_skill":"skill-optimizer"}
{"intent":"Run skillopt for my-skill to improve it","expected_skill":"skill-optimizer"}
{"intent":"How do I create a new skill from scratch?","expected_skill":"skill-creator","ambiguous_with":["skill-optimizer"]}
@@ -1,7 +0,0 @@
{"task_id":"meta-001","task":"Explain in 3 sentences when to use the skill-optimizer skill vs the skillify skill.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"skillify"},{"op":"contains","arg":"optimiz"}]}}
{"task_id":"meta-002","task":"What does --bootstrap-reviewed do and why is it required?","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"sentinel"},{"op":"contains","arg":"review"}]}}
{"task_id":"meta-003","task":"List the three model roles in a skillopt run and their default tiers.","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"optimizer"},{"op":"contains","arg":"target"},{"op":"contains","arg":"judge"}]}}
{"task_id":"meta-004","task":"Why is the validation gate (median-of-3 + epsilon=0.05) load-bearing?","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"noise"}]}}
{"task_id":"meta-005","task":"What happens to bundled skills (those shipped under skills/) by default?","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"proposed"},{"op":"contains","arg":"--allow-mutate-bundled"}]}}
{"task_id":"meta-006","task":"How does the rejected-edit buffer prevent the optimizer from repeating itself?","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"hash"},{"op":"min_citations","arg":1}]}}
{"task_id":"meta-007","task":"Why is the LR cosine schedule the default?","judge":{"kind":"rule","checks":[{"op":"max_chars","arg":1500},{"op":"contains","arg":"cosine"}]}}
+4 -9
View File
@@ -8,16 +8,11 @@ description: |
judgment-heavy genericization (scrub real names, generalize triggers,
lift fork-specific conventions to references).
triggers:
- "harvest this skill"
- "harvest my skill"
- "harvest this skill into gbrain"
- "publish this skill to gbrain"
- "lift this skill"
- "share this skill"
- "promote this skill"
- "promote my skill"
- "skill upstream"
- "into the gbrain core"
- "gbrain bundle"
- "lift this skill upstream"
- "share this skill with other gbrain clients"
- "promote my skill to gbrain"
mutating: true
writes_pages: false
writes_to:
@@ -1,7 +1,3 @@
// Routing eval fixtures for skills/skillpack-harvest.
// Positive cases: every intent contains a trigger substring (the
// trigger set was broadened from 5 to 10 in v0.41.11 per
// kylma-code's PR #1331 to cover realistic user phrasings).
{"intent": "lift this skill back into gbrain so others can use it", "expected_skill": "skillpack-harvest"}
{"intent": "publish my fork-only skill upstream", "expected_skill": "skillpack-harvest"}
{"intent": "share this skill with the gbrain bundle", "expected_skill": "skillpack-harvest"}
@@ -9,16 +5,3 @@
{"intent": "promote this skill to gbrain so neuromancer can scaffold it", "expected_skill": "skillpack-harvest"}
{"intent": "I want this skill in the gbrain bundle", "expected_skill": "skillpack-harvest"}
{"intent": "move my custom skill into the gbrain core", "expected_skill": "skillpack-harvest"}
// Negative cases (v0.41.11): the broader trigger set contains generic
// substrings ("publish this skill to gbrain", "skill upstream",
// "gbrain bundle", "into the gbrain core") that could match unrelated
// user intents under substring routing. expected_skill=null asserts
// NO specific skill (skillpack-harvest OR any other) matches these.
// "save this report as PDF" and "share this article with the channel"
// were considered but excluded: they trip idea-ingest's existing "save
// this" / "share" triggers (real overlap, but a v0.42+ idea-ingest
// concern out of scope for #1451's structural fix).
{"intent": "publish this report to the team", "expected_skill": null}
{"intent": "promote my role on LinkedIn", "expected_skill": null}
{"intent": "bundle these screenshots into a deck", "expected_skill": null}
{"intent": "lift weights at the gym", "expected_skill": null}
+1 -1
View File
@@ -1,7 +1,7 @@
// Routing eval fixtures for skills/strategic-reading. Each intent
// includes at least one trigger string as substring.
{"intent":"Do a strategic reading of 'The Power Broker' against my current situation","expected_skill":"strategic-reading"}
{"intent":"Read this through the lens of the board meeting next week and give me tactics","expected_skill":"strategic-reading","ambiguous_with":["idea-ingest"]}
{"intent":"Read this through the lens of the board meeting next week and give me tactics","expected_skill":"strategic-reading"}
{"intent":"Apply this to my problem with the launch — what to do, what to avoid, what to watch for","expected_skill":"strategic-reading"}
{"intent":"What can I learn from this about handling a hostile gatekeeper","expected_skill":"strategic-reading"}
{"intent":"Extract a playbook from this case study for my product launch","expected_skill":"strategic-reading"}
+1 -1
View File
@@ -3,6 +3,6 @@
// matcher requirement) while still paraphrasing real user phrasing.
{"intent":"Please ingest this voice memo I just sent and file it into my brain","expected_skill":"voice-note-ingest"}
{"intent":"Transcribe and file this audio message into the right directory","expected_skill":"voice-note-ingest"}
{"intent":"Save this audio note as a brain page with the original audio attached","expected_skill":"voice-note-ingest","ambiguous_with":["idea-ingest"]}
{"intent":"Save this audio note as a brain page with the original audio attached","expected_skill":"voice-note-ingest"}
{"intent":"Run voice note ingest on what I just sent — preserve my words verbatim","expected_skill":"voice-note-ingest"}
{"intent":"This voice note has a thought I want preserved word-for-word","expected_skill":"voice-note-ingest"}
+3 -3
View File
@@ -1,13 +1,13 @@
// AUTO-GENERATED — do not edit by hand.
// Run `bun run scripts/build-admin-embedded.ts` to regenerate.
// Source: admin/dist/ at 2026-05-24.
// Source: admin/dist/ at 2026-05-22.
//
// Bun resolves the file: imports to a path that works at runtime even
// inside a compiled binary (`bun build --compile`). The manifest maps
// the request path the express handler sees to (resolved-path, mime).
// @ts-ignore — type: 'file' is Bun ESM, not in lib.d.ts
import A_0_assets_index_DqP_zmqH_js from '../admin/dist/assets/index-DqP-zmqH.js' with { type: 'file' };
import A_0_assets_index_DFgMZhBE_js from '../admin/dist/assets/index-DFgMZhBE.js' with { type: 'file' };
// @ts-ignore — type: 'file' is Bun ESM, not in lib.d.ts
import A_1_assets_index_GxkWX7v3_css from '../admin/dist/assets/index-GxkWX7v3.css' with { type: 'file' };
// @ts-ignore — type: 'file' is Bun ESM, not in lib.d.ts
@@ -19,7 +19,7 @@ export interface AdminAsset {
}
export const ADMIN_ASSETS: Record<string, AdminAsset> = {
"/admin/assets/index-DqP-zmqH.js": { path: A_0_assets_index_DqP_zmqH_js as unknown as string, mime: "application/javascript; charset=utf-8" },
"/admin/assets/index-DFgMZhBE.js": { path: A_0_assets_index_DFgMZhBE_js as unknown as string, mime: "application/javascript; charset=utf-8" },
"/admin/assets/index-GxkWX7v3.css": { path: A_1_assets_index_GxkWX7v3_css as unknown as string, mime: "text/css; charset=utf-8" },
"/admin/index.html": { path: A_2_index_html as unknown as string, mime: "text/html; charset=utf-8" },
};

Some files were not shown because too many files have changed in this diff Show More