Files
gbrain/CONTRIBUTING.md
T
32f8be96c2 v0.41.14.0 fix(#1451): close RESOLVER.md drift bug class structurally (#1458)
* feat(core): loadSkillTriggerIndex shared primitive (closes #1451 drift class)

Single loader that unions per-skill SKILL.md frontmatter triggers: with
curated RESOLVER.md / AGENTS.md rows. UNION semantics — explicit
RESOLVER.md rows ADD to frontmatter triggers for the same skill (don't
replace). Dedup keyed on (skillPath, normalized trigger string) so case
or whitespace drift between the two surfaces collapses to one entry.

This is the structural foundation for #1451: pre-fix, gbrain skills
declared triggers in two places (per-skill frontmatter and a curated
RESOLVER.md table) that could silently drift. Three consumers
(checkResolvable, routing-eval CLI, mounts-cache.composeResolvers) each
built their own resolver index from RESOLVER.md only, so fixing
frontmatter would have closed doctor's warning without closing the
other two surfaces. This primitive becomes the single join point for
all three; consumers are wired in the next commit.

Tests: 18 hermetic cases pinning frontmatter auto-registration,
RESOLVER.md/AGENTS.md merge, case-insensitive dedupe, OpenClaw
workspace-root layout (../AGENTS.md), graceful skip of conventions /
deprecated skills / non-directory entries / missing skillsDir, plus
synthesis round-trip and findPrimaryResolverPath.

Plan: ~/.claude/plans/system-instruction-you-are-working-tidy-storm.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: wire 3 consumers through loadSkillTriggerIndex (#1451)

Replace the three independent resolver-content loaders with calls to
the v0.41.11 shared primitive so frontmatter triggers propagate to
every dispatch surface, not just doctor.

Before: checkResolvable, runRoutingEvalCli, and mounts-cache each
walked RESOLVER.md / AGENTS.md files separately. Adding frontmatter
triggers to one consumer (e.g. checkResolvable) wouldn't have reached
the routing-eval CLI or cross-brain composed dispatchers — the same
drift bug class as #1451 in cross-consumer form. Codex caught this in
plan-eng-review.

After: all three consumers fold through loadSkillTriggerIndex. UNION
semantics across both surfaces means new skills with frontmatter
triggers are reachable everywhere without editing RESOLVER.md.

Also updates:
- check-resolvable action text on routing_miss to point at the
  canonical surface (SKILL.md frontmatter triggers) first, with
  RESOLVER.md row as secondary.
- test/resolver-merge.test.ts to test BOTH the legacy
  RESOLVER.md-only authority path (skills with no frontmatter
  triggers) AND the new auto-registration path (skills reachable via
  frontmatter alone, no RESOLVER.md needed).
- 3 routing-eval.jsonl fixtures (voice-note-ingest, brain-taxonomist,
  strategic-reading) gain `ambiguous_with` declarations for skill
  overlaps that auto-registration newly exposes. These overlaps are
  legitimate (voice-note vs idea-ingest on audio notes,
  brain-taxonomist vs repo-architecture on filing, strategic-reading
  vs idea-ingest on reading-through-a-lens) — the agent picks based
  on context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(#1451): broaden skillpack-harvest triggers + negative fixtures + tighten gate

Closes the 7 residual routing_miss warnings on skillpack-harvest that
gbrain doctor reported on every fresh install (resolver_health: WARN,
~5 health-score points).

Three changes:

1. Broaden skills/skillpack-harvest/SKILL.md frontmatter triggers from
   5 narrow to 10 realistic phrasings. Each new trigger is a
   contiguous substring of one of the 7 shipped routing-eval.jsonl
   intents (per kylma-code's design in PR #1331; moved from
   RESOLVER.md to frontmatter under the v0.41.11 frontmatter-
   authoritative contract). Existing RESOLVER.md row stays for
   human-readability of the dispatcher map.

2. Add 4 negative-fixture cases to skills/skillpack-harvest/
   routing-eval.jsonl with expected_skill=null to defend against
   false positives the broader triggers might introduce
   ("publish this report to the team", "promote my role on
   LinkedIn", "bundle these screenshots into a deck", "lift weights
   at the gym"). Two candidate negatives ("save this report as PDF",
   "share this article with the channel") were excluded — they trip
   idea-ingest's existing "save this"/"share" triggers, a real
   overlap but a separate v0.42+ concern.

3. Tighten test/check-resolvable.test.ts's "repo skills/ pass cleanly"
   assertion: the v0.25.1 carve-out that allowed routing_miss as
   informational is removed. The contract is back to zero errors AND
   zero warnings — the CI gate (next commit) enforces this for PRs
   so future drift fails the build instead of degrading user-install
   resolver_health silently.

Co-Authored-By: kylma-code <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): register reindex in CLI_ONLY so --help works (closes part of #1354)

Pre-fix: src/cli.ts had a `case 'reindex':` handler at line 1334 that
dispatched to reindex-multimodal or reindex.ts based on flags, but
'reindex' was missing from the CLI_ONLY Set at line 38. The dispatcher
rejected the command with "Unknown command: reindex" before the handler
ever ran.

Post-fix: 'reindex' is in CLI_ONLY (recognized as a registered command).
NOT added to CLI_ONLY_SELF_HELP — the handler doesn't have its own
--help branch, so the dispatcher's generic printCliOnlyHelp() shows
"gbrain reindex - run gbrain --help for the full command list."
Polishing this to per-flag help text (--multimodal, --markdown, --code)
is a follow-up TODO.

Regression test in test/cli.test.ts asserts `'reindex'` is in the
CLI_ONLY Set source string. Mirrors the existing pattern for
'reinit-pglite' in test/v0_37_fix_wave.serial.test.ts:284 and
'book-mirror' in test/book-mirror.test.ts:73.

Cherry-picked from lost9999's PR #1354 (which bundled this fix with
their fixture-rewrite approach to #1451 — the routing-eval half of
that PR was superseded by kylma-code's trigger-broadening direction
in #1331, which we took structurally in the previous commits).

Co-Authored-By: lost9999 <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ci): wire check:resolver into bun run verify

Adds `bun run check:resolver` (= `bun src/cli.ts check-resolvable
--strict --skills-dir skills/`) to package.json scripts and registers
it in scripts/run-verify-parallel.sh's CHECKS array.

This gates PR CI on resolver health: any future drift between a
skill's frontmatter triggers and its routing-eval.jsonl fixtures
fails the build, instead of silently degrading the resolver_health
score on user installs after merge. The --strict flag exits non-zero
on warnings (not just errors), so routing_miss / routing_ambiguous /
routing_false_positive all block.

Closes the CI half of #1451's structural fix: doctor catches drift
at runtime, this gate catches drift at PR time.

Local pre-flight: `bun run check:resolver`.

Codex finding #9 from plan review: scripts/run-verify-parallel.sh
invokes entries as `bun run <script-name>`, not raw shell. The
package.json script name + CHECKS-array entry is the correct shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(test): update CLI unreachable test for v0.41.11 contract change

The prior fixture used `triggers: ['alpha']` + `inResolver: false` to
simulate an unreachable skill. Under v0.41.11's structural fix,
frontmatter triggers auto-register the skill independently of
RESOLVER.md, so this skill is reachable now — the assertion
`errors.length > 0` failed.

Drop the `triggers:` array from the fixture so the skill is genuinely
unreachable (neither frontmatter nor RESOLVER.md row), preserving the
test's regression-guard intent: doctor/check-resolvable still exits 1
when a manifest skill is truly unreachable.

Caught by the full unit test suite after the merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: CLAUDE.md Key Files note for skill-trigger-index + regen llms.txt

Document the v0.41.11 shared primitive (loadSkillTriggerIndex) in
the Key Files section so future contributors find it before they
reach for parseResolverEntries directly. Notes the 3 consumers
(checkResolvable, runRoutingEvalCli, mounts-cache.composeResolvers),
the UNION semantics, skip rules, parseSkillFrontmatter dependency,
test coverage, and the CI gate wiring.

Regenerated llms.txt + llms-full.txt per the CLAUDE.md edit rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* v0.41.14.0 fix(#1451): close RESOLVER.md drift bug class structurally

Frontmatter triggers + RESOLVER.md / AGENTS.md rows now union into one
unified index via the new loadSkillTriggerIndex primitive, consumed by
all three dispatch surfaces (checkResolvable, routing-eval CLI,
mounts-cache.composeResolvers). Closes the 7 residual routing_miss
warnings #1451 reported on every fresh install, and the drift bug class
that produced them.

Highlights:
- New shared primitive src/core/skill-trigger-index.ts (252 lines + 361
  lines of tests across 18 cases). UNION semantics, case-insensitive
  dedupe keyed on (skillPath, normalized trigger).
- Three consumers wired through the primitive — fixing frontmatter
  triggers for doctor now also fixes routing-eval CLI and
  cross-brain mounted dispatch (codex outside-voice catch).
- skillpack-harvest frontmatter broadened from 5 to 10 triggers per
  kylma-code's design in #1331, plus 4 negative-fixture cases for
  false-positive defense.
- reindex CLI added to CLI_ONLY set so `gbrain reindex --help` works
  instead of "Unknown command: reindex" (lost9999's #1354 hunk).
- check:resolver wired into bun run verify CI gate so future drift
  fails PR CI instead of silently degrading user-install
  resolver_health.
- check-resolvable's repo skills/ test tightened from "warn-tolerant"
  to "zero errors AND zero warnings" — the carve-out was a stop-gap
  pre-structural-fix.

Plan + 5 decisions + codex outside-voice recalibration captured at
~/.claude/plans/system-instruction-you-are-working-tidy-storm.md.

Co-Authored-By: kylma-code <noreply@github.com>
Co-Authored-By: lost9999 <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.41.14.0

- CLAUDE.md: tag skill-trigger-index entry with correct shipped version
  (v0.41.14.0, closes #1451) instead of the stale v0.41.11 draft tag.
- CONTRIBUTING.md: list the new `check:resolver` gate in the `bun run
  verify` chain so contributors know to expect resolver-drift failures
  in PR CI.
- llms-full.txt: regenerated from updated CLAUDE.md (mandatory per
  CLAUDE.md's auto-derived files rule; CI shard 1 fails the build
  otherwise).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: kylma-code <noreply@github.com>
2026-05-25 20:00:03 -07:00

12 KiB

Contributing to GBrain

Setup

git clone https://github.com/garrytan/gbrain.git
cd gbrain
bun install
bun test

Requires Bun 1.0+.

Project structure

src/
  cli.ts                  CLI entry point
  commands/               CLI-only commands (init, upgrade, import, export, etc.)
  core/
    operations.ts         Contract-first operation definitions (the foundation)
    engine.ts             BrainEngine interface
    postgres-engine.ts    Postgres implementation
    db.ts                 Connection management + schema loader
    import-file.ts        Import pipeline (chunk + embed + tags)
    types.ts              TypeScript types
    markdown.ts           Frontmatter parsing
    config.ts             Config file management
    storage.ts            Pluggable storage interface
    storage/              Storage backends (S3, Supabase, local)
    supabase-admin.ts     Supabase admin API
    file-resolver.ts      MIME detection + content hashing
    migrate.ts            Migration helpers
    yaml-lite.ts          Lightweight YAML parser
    chunkers/             3-tier chunking (recursive, semantic, llm)
    search/               Hybrid search (vector, keyword, hybrid, expansion, dedup)
    embedding.ts          OpenAI embedding service
  mcp/
    server.ts             MCP stdio server (generated from operations)
  schema.sql              Postgres DDL
skills/                   Fat markdown skills for AI agents
test/                     Unit tests (bun test, no DB required)
test/e2e/                 E2E tests (requires DATABASE_URL, real Postgres+pgvector)
  fixtures/               Miniature realistic brain corpus (16 files)
  helpers.ts              DB lifecycle, fixture import, timing
  mechanical.test.ts      All operations against real DB
  mcp.test.ts             MCP tool generation verification
  skills.test.ts          Tier 2 skill tests (requires OpenClaw + API keys)
docs/                     Architecture docs

Running tests

# Inner edit loop (~85s on a Mac dev box, 3700+ unit tests)
bun run test                      # parallel 8-shard fan-out + serial post-pass
bun test test/markdown.test.ts    # specific unit test

# Pre-push gate (matches what CI runs on shard 1 + typecheck)
bun run verify                    # privacy + jsonb + progress + test-isolation + wasm + admin-build + resolver + typecheck

# Pre-merge sanity (everything CI runs)
bun run test:full                 # verify + parallel unit + slow + smart e2e

# Slow / serial / e2e in isolation
bun run test:slow                 # *.slow.test.ts only (cold-path correctness)
bun run test:serial               # *.serial.test.ts only (--max-concurrency=1)
bun run test:e2e                  # real-Postgres E2E (requires DATABASE_URL)

# E2E setup (Postgres with pgvector)
docker compose -f docker-compose.test.yml up -d
DATABASE_URL=postgresql://postgres:postgres@localhost:5434/gbrain_test bun run test:e2e

# Or use your own Postgres / Supabase
DATABASE_URL=postgresql://... bun run test:e2e

Use bun run verify before pushing. The guard chain catches: banned fork-name leaks (scripts/check-privacy.sh), JSON.stringify(x)::jsonb interpolation patterns (scripts/check-jsonb-pattern.sh), \r progress bleed to stdout (scripts/check-progress-to-stdout.sh), test-isolation rule violations (scripts/check-test-isolation.sh — see "Writing tests that survive the parallel loop" below), silent fallback to recursive chunking in the compiled binary (scripts/check-wasm-embedded.sh), stale admin-dashboard build artifacts (scripts/check-admin-build.sh), and resolver drift on bundled skills (bun run check:resolver — strict-mode check-resolvable that exit-1s on any warning, added in v0.41.14.0 to catch SKILL.md frontmatter ↔ RESOLVER.md drift before merge). bun run check:all runs the full historical sweep including the trailing-newline and exports-count checks.

Writing tests that survive the parallel loop

bun run test shards 92+ unit-test files across 8 worker processes. Files in the same shard share a process, so process-global state leaks between them. Four lint rules (scripts/check-test-isolation.sh, R1-R4) enforce isolation:

Rule What it bans Fix
R1 Direct process.env.X = ... mutation Use withEnv() from test/helpers/with-env.ts, or rename to *.serial.test.ts
R2 mock.module(...) anywhere in the file Rename to *.serial.test.ts
R3 new PGLiteEngine( outside ~50 lines after beforeAll( Use the canonical PGLite block (see below)
R4 new PGLiteEngine( without paired afterAll(disconnect) Add the afterAll(() => engine.disconnect())

Canonical PGLite block (R3 + R4 compliant — paste this verbatim):

import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { resetPgliteState } from './helpers/reset-pglite.ts';

let engine: PGLiteEngine;

beforeAll(async () => {
  engine = new PGLiteEngine();
  await engine.connect({});
  await engine.initSchema();
});
afterAll(async () => { await engine.disconnect(); });
beforeEach(async () => { await resetPgliteState(engine); });

Env-touching tests:

import { withEnv } from './helpers/with-env.ts';

test('reads OPENAI_API_KEY', async () => {
  await withEnv({ OPENAI_API_KEY: 'sk-test' }, async () => {
    expect(loadConfig().openai_key).toBe('sk-test');
  });
});

withEnv saves and restores keys via try/finally including when the callback throws. Cross-test safe; NOT intra-file concurrent-safe (process.env is process-global). Files using withEnv stay outside the future test.concurrent() codemod's eligibility filter.

When to quarantine instead of fix: rename to *.serial.test.ts if the file uses mock.module(...), is genuinely env-coupled (module-load env readers + ESM caching defeat dynamic-import-after-env tricks), or intentionally shares state across it() boundaries. Quarantine count cap: 10 (informational).

Files that violated these rules at the v0.26.7 baseline are listed in scripts/check-test-isolation.allowlist. The allow-list MUST shrink over time ... never add new entries. v0.26.8 (env sweep) and v0.26.9 (PGLite sweep

  • codemod) remove entries as files get fixed.
bun run ci:local         # full gate: gitleaks + unit + ALL 29 E2E files (sequential)
bun run ci:local:diff    # gate with diff-aware E2E selector
bun run ci:select-e2e    # print which E2E files the selector would run

ci:local spins up pgvector/pgvector:pg16 + oven/bun:1 via docker-compose.ci.yml, runs everything PR CI runs plus the full E2E suite, then tears down. Named volumes keep the install warm across runs (~16-20 min sequential E2E after the first cold pull). Requires Docker (Docker Desktop, OrbStack, or Colima) and gitleaks on host (brew install gitleaks). Override the postgres host port with GBRAIN_CI_PG_PORT=5435 bun run ci:local if 5434 collides.

Fail-closed selector: an unmapped src/ change runs all 29 E2E files. Hand-tune narrower mappings via scripts/e2e-test-map.ts.

Building

bun build --compile --outfile bin/gbrain src/cli.ts

Adding a new operation

GBrain uses a contract-first architecture. Add your operation to one file and it automatically appears in the CLI, MCP server, and tools-json:

  1. Add your operation to src/core/operations.ts (define params, handler, cliHints)
  2. Add tests
  3. That's it. The CLI, MCP server, and tools-json are generated from operations.

For CLI-only commands (init, upgrade, import, export, files, embed, doctor, sync):

  1. Create src/commands/mycommand.ts
  2. Add the case to src/cli.ts

Parity tests (test/parity.test.ts) verify CLI/MCP/tools-json stay in sync.

Adding a new engine

See docs/ENGINES.md for the full guide. In short:

  1. Create src/core/myengine-engine.ts implementing BrainEngine
  2. Add to engine factory in src/core/engine.ts
  3. Run the test suite against your engine
  4. Document in docs/

The original SQLite engine plan was superseded by PGLite (embedded Postgres 17 via WASM), which uses the same SQL dialect as Postgres and eliminates the need for a separate FTS5/sqlite-vss translation layer. See docs/ENGINES.md for the engine architecture and the rationale.

CONTRIBUTOR_MODE — turn on the dev loop

gbrain captures retrieval traffic so you can replay real queries against your code changes before merging. This is off by default (production users get a quiet brain, no surprise data accumulation). Contributors turn it on with one shell rc line:

# In ~/.zshrc or ~/.bashrc:
export GBRAIN_CONTRIBUTOR_MODE=1

That's it. Every query / search you (or agents pointed at your dev brain) run from that shell now writes a row to eval_candidates, and the replay tool has data to work against.

What CONTRIBUTOR_MODE actually does:

  • Turns on query/search capture into the local eval_candidates table. Without it the gate is closed and capture is a no-op.
  • That's all. PII scrubbing, retention, and replay are independent.

Resolution order (most explicit wins):

  1. eval.capture: true in ~/.gbrain/config.json → on
  2. eval.capture: false in ~/.gbrain/config.json → off
  3. GBRAIN_CONTRIBUTOR_MODE=1 → on
  4. otherwise → off

Quick check that capture is actually running:

gbrain query "anything" >/dev/null
psql $DATABASE_URL -c 'SELECT count(*) FROM eval_candidates'
# (or `gbrain doctor` — surfaces silent capture failures cross-process)

To disable capture even with the env var set, write {"eval": {"capture": false}} to ~/.gbrain/config.json — explicit config beats the env var both directions.

Running real-world eval benchmarks (touching retrieval code)

If your PR touches retrieval — search ranking, RRF fusion, embeddings, intent classification, query expansion, source boost, or the query / search op handlers — run gbrain eval replay against a snapshot of real traffic before merging. Requires CONTRIBUTOR_MODE (above) so you have captured rows to replay against.

Quick loop:

gbrain eval export --since 7d > baseline.ndjson    # snapshot before your change
# ... make your change ...
gbrain eval replay --against baseline.ndjson       # diff retrieval, get Jaccard@k

Three numbers come back: mean Jaccard@k between captured and current slug sets, top-1 stability, and mean latency Δ. The replay tool flags the worst regressions so you can eyeball whether the change is hurting real queries.

Trigger paths (rerun if your diff touches any of these):

  • src/core/search/hybrid.ts
  • src/core/search/source-boost.ts, sql-ranking.ts
  • src/core/search/intent.ts, expansion.ts, dedup.ts
  • src/core/embedding.ts
  • src/core/operations.ts (query / search handlers)
  • src/core/postgres-engine.ts / pglite-engine.ts (searchKeyword / searchVector SQL)

See docs/eval-bench.md for the full guide including CI integration, hand-crafted NDJSON corpora (so a fresh checkout without captured data can still replay), and cost considerations. The NDJSON wire format is documented in docs/eval-capture.md.

For public benchmark coverage on top of replay, gbrain eval longmemeval <dataset.jsonl> (v0.28.1) runs LongMemEval against gbrain's hybrid retrieval. One in-memory PGLite per question, runtime-enumerated TRUNCATE between questions, ground-truth scoring via LongMemEval's published evaluate_qa.py. Use it alongside replay when changes affect retrieval quality on long-context conversational data — replay catches regressions on YOUR queries, LongMemEval catches them on a public set the benchmark community already cites. See the "Public benchmarks: LongMemEval" section in docs/eval-bench.md.

Welcome PRs

  • SQLite engine implementation
  • Docker Compose for self-hosted Postgres
  • Additional migration sources
  • New enrichment API integrations
  • Performance optimizations