Commit Graph
5 Commits
Author SHA1 Message Date
af5ee1eb5a v0.40.8.1 docs: README rewrite + personal-brain + company-brain tutorials (#1345)
* docs: rewrite README lead around search-vs-think differentiator

The current README opened with a generic "smart but forgetful" tagline that
buried the actual differentiator. Garry's 2026-05-23 X thread crystallized the
positioning: "Search gives you raw pages. Think gives you the answer." That
plus graph traversal plus gap analysis is what nobody else ships in one box.

Changes:
- README lead now leads with the search-vs-think frame, the "nobody else does
  this" claim, and the "strategic moat / so you don't lose context" framing.
- Collapsed five stacked "New in vX.Y.Z" paragraphs in the lead into one
  Recent Releases section after the install path, freeing the first viewport
  to be about what gbrain IS, not what shipped last week.
- New ## Search vs think section with side-by-side CLI example, gap-analysis
  explanation, and the find_trajectory + think compounding story.
- ORIGIN.md closing paragraph names think as the reason the brain is worth
  building.
- No em dashes used as connectors (per humanizer rules).
- All factual claims (page counts, benchmark numbers, version references)
  preserved verbatim.

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

* docs: add v0.40.6.0 to README Recent Releases

Picked up sync --all + per-source locks + sources status dashboard from
the v0.40.6.0 merge.

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

* docs(README): add visceral "what this looks like" before/after table

Pulled verbatim from BrainBench Cat 29 — same question, same brain, Haiku judge. Shows what a typical personal-knowledge brain (top-K vector retrieval, what MemPalace / Mem0 / Hindsight ship) returns vs what gbrain think returns. Search hallucinates three people who actually work at OTHER companies; think correctly identifies what's known + flags the gap. Score: search 1/10 vs think 9/10.

The before/after lands above Install so the reader sees concrete differentiation before they decide to install. Backs the abstract "search vs think" claim from the lead with a real receipt.

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

* docs(README): swap before/after example to verbatim Cat 29 receipt (Q2 ARR)

Per @garrytan — the prior example used the synthetic Q1 (employees of Horizon TECH 6) with paraphrased answer text. Replaced with the verbatim Cat 29 Q2 receipt: actual question (with the in-question typo that exists in the eval), actual truncated search-answer text from the JSON receipt, actual think-answer text with all three ARR readings + citations, and the actual Haiku judge verdicts pasted verbatim.

Also strips Mem0 + Hindsight references from the comparator phrasing — Mem0 is a YC company we don't want to single out, and Hindsight was a hackathon-stage project that never launched. The comparison phrasing now reads "MemPalace and most peer AI-memory stacks" — accurate without naming systems we shouldn't be benchmarking against.

The three things gbrain think did that a typical top-K retrieval cannot are listed below the table for readers who want the takeaway in plain English:
1. Caught the name typo (question said "Acme AI 0", brain has "Acme CO 0")
2. Walked the typed-claim Facts fence to build a chronological trajectory
3. Cited every claim

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

* docs(README): add "Your brain's shape (schema packs)" section

Per @garrytan — the README had nothing on schema packs (v0.38/v0.39
dynamic-schema cathedral). New section lands between "How to get data in"
and "Recent releases" so the narrative flow reads:

  what gbrain is → concrete example → install → query (search vs think)
  → get data in → schema packs (how the brain understands your shape)
  → recent releases → loop → capabilities → ...

The section opens by naming the differentiator out loud: "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 and the
agent doesn't know what your folders mean."

Three options surfaced:
  - gbrain-base (default, zero-config Garry layout)
  - gbrain-recommended (extends base with 13 more dirs)
  - your own pack via the schema detect → suggest → review-candidates
    three-command magical moment

Six representative CLI verbs shown verbatim. Closes with one paragraph
explaining the threading through every read/write path (parseMarkdown,
whoknows, extract_facts, search cache) and a one-line summary of the
7-tier resolution chain pointing at docs/architecture/schema-packs.md
for the full reference.

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

* docs(README): de-brand "Think" in the lead; reframe as GBrain's brain layer

Per @garrytan — "we don't need to brand it Think, we want to say the think
command later but we don't lead on it!"

Changes:

- Lead sentence: "Search gives you raw pages. Think gives you the answer"
  → "Search gives you raw pages. GBrain gives you the answer, through a
  brain layer." The product is GBrain; think is the CLI verb that runs
  the brain layer, introduced later.

- Two-bullet differentiator list: the "gbrain think" bullet now leads with
  the capability ("A synthesis layer that gives you the actual answer.")
  rather than the CLI command. The bullet body still names what the layer
  does (synthesized prose, citations, gap analysis).

- Strategic-moat paragraph: "`gbrain think` is what makes the moat
  usable" → "The brain layer is what makes the moat usable."

- "What this looks like" table header: "GBrain `think`" →
  "GBrain's brain layer (one synthesized answer, run via `gbrain think`)".
  CLI command stays in the cell so the example is reproducible; the
  framing leads with what it IS, not the verb.

- Three-bullet takeaway under the table: "Three things `gbrain think`
  did" → "Three things the brain layer did". Aggregate sentence: "gbrain
  think averages 5.60/10" → "GBrain's synthesis layer averages 5.60/10".

- Section heading: "## Search vs think" → "## Two ways to query your
  brain". The section still introduces `gbrain search` and `gbrain think`
  as the two CLI verbs side by side; the heading no longer brands "think"
  as the thing.

The "think command" comes through naturally where it appears as a CLI
example. The PRODUCT is GBrain.

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

* docs(README): switch to first-person voice (Garry speaking directly)

Per @garrytan — drop "Built by..." third-person framing and write in
first person.

Edits:
- Lead credibility paragraph: "Built by the President and CEO of Y
  Combinator to run his actual AI agents" → "I'm Garry Tan, President
  and CEO of Y Combinator. I built GBrain to run my own AI agents."
  Subsequent sentences switch "his deployments" → "my deployments",
  "the agent ingests... you wake up smarter" → "my agent ingests...
  I wake up smarter — and so will you" (the closing "and so will
  you" connects Garry's experience to the reader's).
- Compounding paragraph: "As Garry's personal agent gets smarter, so
  does yours" → "As my personal agent gets smarter, so does yours."
- Schema packs section: "the layout used by Garry's production brain"
  → "the layout my production brain uses".
- License + credit: "Built by Garry Tan to run his OpenClaw and
  Hermes deployments — the production brain behind his actual AI
  agents" → "I built GBrain to run my OpenClaw and Hermes deployments
  — the production brain behind my AI agents."

The whole top of the README now reads as Garry talking directly to
the reader about what he built and why.

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

* docs(README): DRY the lead — drop redundant "through a brain layer" + repeat GBrain

"GBrain gives you the answer, through a brain layer. GBrain is the brain
layer your AI agent has been missing..." — two GBrains, two brain layers.
Tightened to: "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."

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

* docs(README): frame GBrain as a company brain too, link to YC RFS

Per @garrytan — GBrain is now usable as a company brain (federated sync,
OAuth scoping, Cat 22 source isolation), and YC just put company-brain
on its Request for Startups.

Added a paragraph after the personal-brain lead that names the three
v0.34+ features that make multi-user safe (federated sync, per-source
OAuth scoping, the Cat 22 leak-free source isolation), then links to
https://www.ycombinator.com/rfs#company-brain with a one-line pitch:
"if you're building in that space, you might as well build on this."

The framing carries forward the personal-brain story while opening the
aperture: GBrain works for one person (Garry's production deployment)
AND for a team (per the features Cat 22 just verified).

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

* docs(README): rewrite company-brain paragraph in plain English

@garrytan caught me writing internal eval-suite jargon ("Cat 22 proves
source isolation is leak-free across hybrid search, listPages, getPage,
and federated reads") in a paragraph aimed at someone deciding whether
to use GBrain. Rewritten in plain English:

  "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."

Same factual content, zero internal vocabulary. The "Cat 22" name
belongs in the benchmark page, not the front-door README.

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

* docs(tutorials): add company-brain tutorial (Diataxis tutorial quadrant)

End-to-end walkthrough for setting up GBrain as a multi-user company
brain. Audience: founder / CTO / head of ops at a 10-50 person company
who has heard about GBrain (from the YC RFS company-brain page or my
tweets) and wants to set it up as their team's shared institutional
memory. ~3700 words, written for a learner with zero prior gbrain
knowledge.

Twelve parts walk the reader from "I've never run gbrain" to "three
teammates each query the brain through their own AI agent and see the
correctly scoped answer":

  1. The mental model (personal brain vs company brain, federated
     sources, OAuth scoping, what you get)
  2. Prerequisites table (Postgres, embedding key, Anthropic key, git
     repo, Bun, host machine + cost projection)
  3. Install + Postgres + API keys + doctor verify
  4. Create three sources (shared / customers / internal) + sync
  5. Spin up HTTP MCP server with --bind 0.0.0.0 + --public-url
  6. Register one OAuth client per teammate with --source +
     --federated-read
  7. Verify scoping works (alice can't see internal, bob can't see
     customers)
  8. Connect each teammate's AI agent via thin-client install
  9. First real `gbrain think` query showing sourced + synthesized +
     gap-analysis answer
  10. Operating the brain (autopilot, doctor --remediate, sources
      status, admin dashboard)
  11. Cost + speed expectations from the v0.40.6.0 benchmark
  12. Common gotchas + troubleshooting

Voice:
- First-person Garry in intro / motivation paragraphs
- Zero internal jargon. No "Cat 22", "P@5", "knobsHash", "RRF", "MRR"
- Plain English throughout
- No em dashes used as connectors (zero in final draft)
- "Brain layer" / "synthesized answer" framing, not "Think" branding
- No Hindsight, no Mem0 (per repeated session feedback)
- Every example uses placeholder names (alice-example, bob-example,
  acme-co)

Cross-linked from:
- README.md company-brain paragraph
- docs/INSTALL.md (under the migrate --to supabase block)
- docs/architecture/topologies.md (See also section)

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

* docs(README): drop version chatter, add Tutorials section, expand tutorial roadmap

Per @garrytan: the README should read as the current docs written for
people who have never known GBrain before. Versions are what the
CHANGELOG is for.

Version-chatter sweep across the README:

- Killed the entire ## Recent releases section. That's a changelog
  summary, not docs. CHANGELOG.md owns it.
- Stripped "(v0.38+)" from the ## How to get data in heading.
- Rewrote "(the v0.38 put_page write-through plumbing)" as
  "(the database and on disk in one move)" — describes WHAT happens,
  not WHEN it shipped.
- Stripped "(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.)" from the skillpack
  paragraph. Upgrade history goes in the CHANGELOG.
- Stripped "New in v0.40.4.0:" from the hybrid-search graph-signals
  description. Just describes what the feature does.
- Stripped "As of v0.37," from the embedding-provider auto-detect
  paragraph in the Troubleshooting section.
- Stripped "the embedding + reranker stack that became the v0.36.2.0
  default" → "ships as the default" in the License + credit section.
- Stripped "in Cat 29" from the synthesis-benchmark sentence (same
  internal-jargon class as Cat 22 which @garrytan called out earlier).

Replaced ## Recent releases with ## Tutorials:

- Links to the one shipped tutorial (company-brain.md) with a
  one-line description.
- Names the next several planned tutorials in prose (not as broken
  links): personal brain quickstart, connect your agent, VC dealflow,
  vault migration, code brain. No fake links.
- Points at the tutorial index page for the full roadmap.
- Closes with "Open an issue describing the workflow you want
  documented" to invite prioritization input from real users.

New tutorial index at docs/tutorials/README.md:

- Shipped section: company-brain.md
- In-progress roadmap with 7 candidate tutorials (personal brain
  quickstart, connect your agent, VC dealflow, vault migration, code
  brain, fully local, dream cycle setup)
- Each roadmap entry names the persona, the core commands the
  tutorial would demonstrate, and the differentiator
- "Want to write one?" section invites community PRs and points at
  company-brain.md as the model

Reverted the obscure topologies.md "See also" link from pointing at
company-brain.md specifically to pointing at the tutorials index
overall — the tutorial belongs in the README's Tutorials section
where users actually look, not buried in an architecture doc.

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

* docs(tutorials): land personal-brain tutorial (full-stack install)

The canonical solo install walkthrough, adapted from Garry's live setup
session notes (the "Apple I, soldering breadboards" session). Builds the
full stack: 2 GitHub repos, Telegram bot, AlphaClaw on Render, OpenClaw
+ GBrain + Supabase. About 2 hours end-to-end, $100-150/month sustained.

Replaces the "Set up your personal brain in 30 minutes" stub on the
tutorials roadmap with something genuinely complete.

Edits to the source draft:
- Stripped brain-page YAML frontmatter (type/access/links/etc — not
  needed for a public docs file)
- Privacy sweep per CLAUDE.md: removed real-name references to the
  collaborator and the agents involved in the session, replaced with
  "a collaborator" / "my main agent" / generic placeholder names
- First-person voice consistency: the source draft slipped between
  first person and third person ("Garry walked through"); rewrote
  everything in first person
- Zero em-dashes used as connectors (verified by grep)
- Added ZeroEntropy to the providers list (it's the default; not
  mentioning it would leave readers paying 2.6× more on embeddings)
- Opened with a router paragraph that points brain-layer-only readers
  at INSTALL.md and team readers at company-brain.md, so each
  audience finds the right walkthrough fast

Tutorials index updated:
- personal-brain.md promoted from In Progress to Shipped (it IS the
  "set up your personal brain in 30 minutes" entry that was on the
  roadmap, just better-named and more complete)

README.md Tutorials section now lists both shipped tutorials side by
side. Reads naturally: solo install first (broader audience), team
install second.

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

* docs(tutorials): rewrite company-brain as a true superset of personal-brain

Per @garrytan: the company brain tutorial should pick up from where the
personal brain tutorial leaves off, not duplicate the install. Pedagogical
flow now reads: "you already did personal-brain; here is what to add
to make it multi-user."

Restructure:

- Opens with explicit "this tutorial picks up where the personal brain
  tutorial leaves off" + a router for readers who haven't done that one
  yet. No duplicated install steps.
- Part 1 (mental model) reframed as "what changes when you go from
  personal to company" + "what this is NOT" (not a different install,
  not a thin-client-everywhere replacement).
- Part 2 is now "switch the brain backend to multi-user Postgres" —
  surfacing the migrate --to supabase path for readers who started on
  PGLite, skip-to-Part-3 for readers already on Postgres.
- Part 3 adds the new content @garrytan called for: per-person folder
  structure inside each source. customers/alice-example/, internal/
  alice-example/, internal/bob-example/, internal/legal/ etc. So
  teammates' writes don't collide and the per-person/per-role
  abstraction is real on disk, not just in OAuth scope.
- NEW Part 6: per-person crons. Each teammate gets their own
  scheduled tasks (7am customer digest for alice, 9am ops status for
  bob, weekly contract compliance for carol) scoped to their OAuth
  client so the cron can only touch their slice.
- NEW Part 7: per-person skills. The 60+ shipped skills are generic;
  teams want a few specific ones (onboarding-new-hire,
  customer-success-followup, weekly-team-digest). Scaffolded via
  gbrain skillify scaffold, scoped via allowed_clients in
  frontmatter.
- Existing strong parts retained: OAuth scoping + verify, per-
  teammate AI client connect, first synthesized query, operating
  notes, gotchas. Reworded where needed to refer back to personal-
  brain steps instead of re-explaining them.

Voice + style sweeps:
- Zero em-dashes used as connectors (verified by grep)
- Zero internal jargon (no "Cat 22", "P@5", "RRF", "MRR", etc.)
- Zero version chatter in body text (only the one acceptable
  reference to the dated benchmark filename in a docs link)
- "Brain layer" / "synthesized answer" framing, not "Think" branding
- First-person Garry voice in intro/motivation paragraphs
- All examples use placeholder names (alice-example, bob-example,
  carol-example, acme-co, diana-example)
- No mentions of Mem0 / Hindsight (per session-long convention)

Word count: 3608 (vs 3717 before — about the same length, but more
of those words now describe the NEW content rather than re-explaining
prereq install).

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

* docs(tutorials): enrich company-brain with real-world patterns from production deployment

Mined production patterns from my own running company-brain deployment
to ground the tutorial in shapes that actually work. Added four
substantive sections.

Part 3 (sources) gains "Two scoping models" sidebar:

- Model A: separate sources with OAuth scoping (SQL-enforced isolation,
  right for multi-user with different AI clients per person — what the
  tutorial walks you through).
- Model B: one source with partners/<slug>/ directory convention
  (simpler ops, scoping is convention-only, right when one agent serves
  everyone over Telegram — what I actually run in production).
- Mix-and-match guidance: separate sources for the obviously-different
  ones AND partners/<slug>/ inside the shared source for per-person
  workspace.

Part 7 (skills) gains "Shared rule files at the skills root" subsection:

- _brain-filing-rules.md: iron-rule decision tree for where new pages
  belong. Every ingest skill consults it before creating a page.
- _output-rules.md: output quality standards (deterministic links built
  from API data not LLM-composed strings, citation format, no AI-slop).
- _excluded-people.md: privacy gate naming people the brain must never
  reference even when they appear in source material. Re-attribute or
  discard. The file that prevents accidental publication of things
  about people who aren't fair game.
- _operating-rules.md, _x-ingestion-rules.md, _x-api-rules.md.
- These turn into the de facto company policy for the agent. Edit one,
  every skill picks it up next request.

NEW Part 8 "Wire Slack carefully":

- Two crons, two jobs (scan every 5-15min for live signals + archive
  nightly for full history).
- Channel-to-task-ID mapping via topic-registry.json (don't reference
  raw Slack channel IDs in skills; friendly names that resolve at
  runtime).
- Deterministic links rule (LLM-composed Slack URLs hallucinate
  constantly; build from API data only).
- Dismissed-items state so re-scans don't surface noise that was
  already triaged.
- Per-channel scoping mirrors per-person scoping. Sensitive channels
  scope by OAuth client.
- Names the actual production skills (slack, slack-scan, slack-archive)
  for scaffold reference.

NEW Part 9 "Onboard each teammate yourself (the botmaster pattern)":

- The load-bearing UX gate for adoption. Don't hand a teammate an
  OAuth credential and tell them to "try it out." That's how internal
  tools die.
- Step 1: pre-populate their slice (partners/<their-slug>/USER.md
  with role/focus/priorities/preferences, 5-10 concepts that are
  theirs, 2-3 example brain entries that demonstrate the shape).
  About 20 minutes per teammate.
- Step 2: walk them through 2-3 wow flows personally. A synthesis
  query (show the brain layer). A gap-analysis query (build trust).
  A write-back flow (show capture value). About 15 minutes.
- Step 3: graduate to DM only after the wow moment lands. The order
  flips the conversion rate.
- About 45 minutes per person total. Cheaper than an unadopted tool.

Parts 8-12 renumbered to 10-14 to make room. Cross-references in body
text checked (Part 3 ref in Part 5, Part 4 ref in operating notes,
Part 5 ref in Slack section all still correct).

Word count: 4938 (vs 3608 before). Still readable in one sitting per
the original target; added content is all load-bearing patterns from
production.

Voice gates:
- Zero em-dashes used as connectors (sed-replaced all 8 introduced
  by the additions with periods)
- Zero internal jargon (no Cat N, no P@5, no RRF, no MRR)
- Zero banned names (no Brad, Gessler, Wintermute, Zion, Straylight,
  Seibel, Caldwell, Mem0, Hindsight)
- "Brain layer" / "synthesized answer" framing preserved
- First-person Garry voice throughout
- All examples use placeholder names (alice-example, bob-example,
  carol-example, diana-example, acme-co)

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

* docs(tutorials): expand personal-brain Step 7 with the three real Supabase gotchas

@garrytan: the personal-brain tutorial needs to surface the specific
Supabase setup gotchas I hit the hard way. Rewrote Step 7 with the
operational detail.

Three new subsections:

- 7a: Turn on pgvector. The vector extension has to be toggled in
  Database → Extensions before GBrain's schema migrations will run.
  Five seconds in the dashboard, an hour of debugging if you forget.

- 7b: Use the CONNECTION POOLER string, not the direct connection.
  Direct is port 5432, IPv6-only. Pooler is port 6543 via pgbouncer,
  IPv4-compatible, survives connection storms from parallel workers.
  Shows the exact pooler hostname format and the gbrain config set
  command.

- 7c: Buy the IPv4 add-on. About $4/month. Even with the pooler, some
  Supabase regions / Render plans hit IPv6 resolution snags. Symptom:
  network-unreachable errors or connect hangs in gbrain doctor.
  Toggle on in Project Settings → Add-ons. Saves debugging time on
  multiple installs.

- 7d: Verify with gbrain doctor — names which of 7a / 7b / 7c to
  revisit if a check fails.

The old "Operating note" about Supabase being the scaling bottleneck
preserved at the end of the section since it's a different concern
(scale, not setup).

Voice gates: 0 em-dashes (verified), first-person Garry voice ("I hit
the hard way"), no internal jargon, no banned names.

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

* docs(README): rewrite "What this looks like" for someone arriving cold

@garrytan: the prior version assumed too much. "Eval receipt", "top-K
vector retrieval", "MemPalace", "Haiku judge", "Facts fence", "synthesis
layer" — all jargon that requires reading the rest of the project to
parse. A new reader bounces.

New version requires zero prior context:

- Sets up a universal scenario anyone gets: "you have a meeting with
  alice tomorrow, what do you need to know?"
- Shows what a typical tool returns (a list of 5 pages with snippets)
  so the reader sees the gap themselves
- Shows what gbrain returns (a real briefing with the open items
  surfaced, plus a "heads up" about what's missing from the brain)
- Lets the two outputs speak for themselves, no judge scores or
  benchmark numbers in the body
- Closes with one plain-English sentence on the difference:
  "Search finds the pages. The brain reads them for you and writes
  the answer."

What got cut:
- The eval-receipt path reference (means nothing to a new reader)
- The Haiku judge scores (0/10 vs 9/10) — not useful out of context
- The verbatim judge verdict quotes (long, internal vocabulary)
- The "Facts fence", "typed-claim", "synthesis layer" feature names
- The MemPalace name-drop
- The link to the comprehensive benchmark page (it's still
  reachable from the Tutorials and benchmark sections; doesn't
  need to be in the intro)
- The numbered "three things gbrain think did" breakdown

The example content is illustrative (same scenario shape as a real
production query) but stripped of the internal benchmark wrapper.
Reads naturally as "imagine you're about to ask gbrain something."

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

* docs: capitalize proper-noun names in prose across README + tutorials

@garrytan caught lowercase "alice" in prose — proper nouns should be
capitalized. The lowercase was leaking from the slug convention
(people/alice as the actual storage slug) into descriptive text.

Rule applied: capitalize Alice / Bob / Carol / Diana / Acme when used
as a person or company name in prose. Keep lowercase in:
- Slugs and file paths (people/alice, customers/acme-co)
- Code identifiers in fenced blocks where the slug IS the value
- URLs and hostnames (brain.acme-co.com)
- Channel-name-style references (#alice-customers)

Files swept: README.md, docs/tutorials/company-brain.md,
docs/tutorials/personal-brain.md, docs/tutorials/README.md.

The README sweep was the primary target (Garry's actual call-out);
the tutorial sweep keeps voice consistent across all the front-door
docs. Hand-fixed four occurrences inside code-fenced blocks that
were human comments rather than code (terminal session comments
"# Terminal 1, as Alice", "# Terminal 2, as Bob", directory-tree
inline comments "← Alice's customer notebook").

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

* test(readme-hero-anchors): rotate ZeroEntropy anchor → search-vs-answer headline

CI caught one expected regression after v0.40.8.1's README rewrite. The
D9 hero-anchors guard required "ZeroEntropy" in the first 50 lines of
README.md (the v0.36.0.0 default story). The post-rewrite hero
intentionally rotated that out per Garry's "no version chatter in
README" directive — ZeroEntropy still appears further down (line 211,
231, 279) but no longer in the hero. The guard's docstring explicitly
handles this case: "did we deliberately rotate the headline? If yes:
update the anchors here."

Rotation:

- Dropped: regex /ZeroEntropy|\bZE\b/ in the first 50 lines
- Added: regex matching the new headline "Search gives you raw pages.
  GBrain gives you the answer." which is the load-bearing differentiator
  of the post-rewrite hero. If a future cleanup PR accidentally rewords
  the search-vs-answer framing, the new anchor catches it the same way
  the ZeroEntropy anchor caught accidental drops before.

Other 4 anchors unchanged (OpenClaw + Hermes + production-number +
P@5/R@5 — all still load-bearing).

Updated docstring records the v0.40.8.1 rotation as the audit trail
for the next time this happens.

Verified: `bun test test/readme-hero-anchors.test.ts` → 5 pass / 0 fail.

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

* docs(README): restore agent-led install path + per-client MCP guides

The README install section had collapsed to three generic shapes
("agent platform", "CLI", "MCP server") that buried the load-bearing
flow: paste a URL pointing at INSTALL_FOR_AGENTS.md into your agent and
let it do the work. That's the path most users actually take.

Restored the original three-tier structure with an explicit second tier
for "install it into your existing agent" (Codex, Claude Code, Cursor),
plus surfaced the per-client MCP guides individually so users see the
command shape they actually need instead of one generic docs/mcp/ link.

Six per-client MCP links now in the README itself: Claude Code,
Cursor/Windsurf (stdio), Claude Desktop, Claude Cowork, Perplexity
Computer, ChatGPT. Each carries the one-line shape that matters
(claude mcp add, Settings > Integrations, OAuth 2.1, etc.).

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

* docs(README): link personal-brain tutorial from the agent-install path

People landing on the README without an existing OpenClaw or Hermes
deployment need a starting point that walks the whole flow, not just
"paste this URL into your agent." The personal-brain tutorial already
covers picking a platform, deploying it, pointing it at
INSTALL_FOR_AGENTS.md, and verifying the first query.

Surfaced as a callout right under the agent-install snippet so the path
is visible to first-time users without burying the experienced-user
flow.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:00:39 -07:00
d0d0e2a64a v0.37.11.0: fresh-install PGLite embedding setup fix wave (#1286)
* chore(test): preload gateway to OpenAI/1536 so 1536-dim test fixtures keep working

The v0.37 fix wave changes the canonical gateway defaults to
zeroentropyai:zembed-1 / 1280 (matching what v0.36 already chose as the
system default). 20+ test files have hardcoded new Float32Array(1536)
fixtures that match the OLD schema default. Without this preload, those
tests fail with a vector-dim-mismatch on insert.

The preload is gateway-only — it doesn't change which model gbrain ships
to production users. Tests that want the new ZE/1280 defaults call
configureGateway() explicitly in their own beforeAll.

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

* feat(ai): canonical embedding defaults + sweep across schema/engines/registry

Closes the v0.36 defaults drift bug class. The gateway shipped
zeroentropyai:zembed-1 / 1280 as the system default in v0.36 but eight
other places kept hardcoding 1536 / text-embedding-3-large. Fresh
gbrain init --pglite sized the column to 1536, the embed pipeline used
ZE/1280, and every page failed with dim mismatch.

- New src/core/ai/defaults.ts leaf module is the canonical source for
  DEFAULT_EMBEDDING_MODEL / DEFAULT_EMBEDDING_DIMENSIONS. Schema and
  registry helpers import from this lean module instead of pulling the
  full gateway (which loads every provider SDK).
- src/core/ai/gateway.ts re-exports the constants for back-compat.
- src/core/pglite-schema.ts getPGLiteSchema() defaults track gateway.
- src/core/postgres-engine.ts getPostgresSchema() default args track
  gateway (same drift on the Postgres path — codex round 1 CDX-1).
- Both engine.initSchema() fallbacks track gateway constants (no more
  stale OpenAI/1536 catch-block defaults).
- Schema seed stops stripping the provider prefix; full provider:model
  is stored in the DB config table (codex round 1 CDX-4).
- Chunk-row INSERT defaults track gateway (codex round 2 CDX2-4 —
  pglite-engine:1611 + postgres-engine:1647 were production write
  sites previously hardcoded to text-embedding-3-large).
- src/core/search/embedding-column.ts loadRegistry + isCacheSafe gain
  the cfg > gateway > DEFAULT resolution chain (codex round 2 CDX2-3).
  The gateway tier matters because callers that configure the gateway
  (init paths, tests, programmatic SDK) expect the registry to mirror
  that state when cfg doesn't have an explicit embedding_model.

Tests:
- schema-templating: default expectation flips to ZE/1280 (v0.37 truth).
- embedding-dim-check: 3 new engine-kind branching cases + updated
  fresh-brain expectation (under legacy preload).
- embedding-column: registry + isCacheSafe expectations match new chain.
- v0_28_5-fix-wave E2E: engineKind required arg propagated.

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

* feat(init+config+cli): always-configure gateway, file-only loader, honest config-set, sync/reinit help

Closes the "fresh init doesn't work + config-set silently lies" bug
class end-to-end. Six related changes that ship together because the
file-plane/DB-plane contract only holds when init paths, config-set,
the gateway env mapping, and the recipe text all agree.

Lane B (init paths):
- initPGLite, initPostgres, initMigrateOnly always configureGateway()
  before engine.initSchema(). Pre-fix the call was gated on flags, so
  bare `gbrain init --pglite` left the gateway unconfigured and the
  engine fell through to stale OpenAI/1536 defaults instead of the
  ZE/1280 the gateway would have resolved.
- New configureGatewayWithMergedPrecedence() helper applies the locked
  precedence chain `CLI > env > existing file > gateway internal`.
- printResolvedAIChoice() shows the resolved model/dim at init time +
  surfaces a ZE setup hint inline when the API key is missing.
- B.4: saveConfig merge uses loadConfigFileOnly() so transient env
  state (DATABASE_URL, etc.) never poisons ~/.gbrain/config.json
  (codex round 2 CDX-5).
- B.5: extend the v0.28.5 dim-mismatch detector so it fires when the
  gateway-resolved dim differs from the existing column, not only
  when --embedding-dimensions is explicit (codex round 2 CDX-6).

Lane C (config plane):
- New `loadConfigFileOnly()` reads ~/.gbrain/config.json only — no env
  merge, no DATABASE_URL inference. Safe write-back source for init.
- GBrainConfig gains `zeroentropy_api_key?: string`. loadConfig merges
  process.env.ZEROENTROPY_API_KEY. buildGatewayConfig at cli.ts:1401
  maps it into env.ZEROENTROPY_API_KEY so ZE recipes finally see it
  (codex round 2 CDX2-5+6 — the v1 fix landed in the wrong file).
- `gbrain config set embedding_model` and `... embedding_dimensions`
  refuse unconditionally and print a paste-ready wipe-and-reinit
  recipe. No --force escape (codex round 2 CDX2-13).
- migrate-engine.ts adds a contract comment at the DB-plane write
  site documenting "DB stores schema-applied metadata; file plane is
  canonical for runtime gateway config" + preserves the existing
  file-plane config across engine migration.

Lane D.1 (recipe text):
- embeddingMismatchMessage() takes an `engineKind` arg. PGLite branch
  emits a wipe-and-reinit recipe using gbrainPath('brain.pglite') or
  the caller's databasePath override. Postgres branch keeps the SQL
  ALTER recipe.
- The PGLite recipe recommends `gbrain reinit-pglite` (new sugar
  command below) as the one-line path before falling back to the
  by-hand mv + init + sync sequence.

Lane D.4 (sync help dispatch):
- `sync` and `reinit-pglite` added to CLI_ONLY_SELF_HELP so their own
  --help branches reach the user (pre-fix the generic short-circuit
  fired first and the dedicated usage was unreachable; codex round 2
  CDX2-12).
- `gbrain sync --help` short-circuits BEFORE engine bind so users on
  a fresh tmpdir (no config) can read the help without hitting
  no-such-config errors.

Sugar:
- New `gbrain reinit-pglite --embedding-model X --embedding-dimensions N`
  wraps the wipe + init + sync dance into one command. Backs up the
  brain to <path>.bak. TTY confirmation unless --yes. --no-sync to
  defer the resync. --json for scripts.

Tests:
- test/cli.test.ts sync-help test rewritten for the new
  per-command-usage output (lists --no-embed which is the v0.37
  user-visible flag the wave wanted to surface).

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

* feat(embed+sync): pre-flight dim-mismatch guard + sync hint at both catch sites

embedding-pipeline error UX. Pre-fix, a fresh-install dim mismatch
produced raw Postgres "expected N dimensions, not M" errors page after
page, surfacing only after the worker pool drained the entire corpus.
Sync swallowed embed errors at TWO catch sites and never surfaced
the recovery recipe.

embed.ts:
- New `EmbeddingDimMismatchError` tagged class with the paste-ready
  recipe baked in.
- `runEmbedCore` pre-flights via `readContentChunksEmbeddingDim` +
  gateway.getEmbeddingDimensions() before the worker pool spins up.
  On mismatch, throws the typed error which the CLI wrapper catches
  and prints. Dry-run skips the check (no embed risk).
- Catches the headline fresh-install bug class at first call instead
  of letting it hammer N parallel API calls into dim-rejected inserts.

sync.ts:
- Both embed catches at sync.ts:990 (incremental) and sync.ts:1129
  (first-sync) detect EmbeddingDimMismatchError and surface the recipe
  + a `--no-embed` tip on stderr (codex round 2 CDX2-8: incremental
  path was previously silent; only the first-sync path was flagged).
- Non-mismatch embed failures still stay best-effort (rate limits,
  transient network) — those shouldn't break sync.
- Sync calls runEmbedCore directly instead of runEmbed (which calls
  process.exit on error and bypasses sync's catch).
- Sync gets a proper --help block listing every meaningful flag:
  --no-embed, --workers, --source, --skip-failed, --retry-failed,
  --watch, --interval, --no-pull, --all, --json, --yes, --dry-run.

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

* feat(doctor): read gateway for schema-sizing checks + provider-aware key lookup

Doctor's embedding checks were reading the DB config table for
embedding_model / embedding_dimensions / zeroentropy_api_key. Post
v0.37 the file plane is canonical (the DB plane is schema-applied
metadata, not runtime gateway config) so those reads produced stale
verdicts on fresh installs whose DB row hadn't been written.

- checkEmbeddingWidthConsistency reads gateway.getEmbeddingDimensions()
  and gateway.getEmbeddingModel() instead of engine.getConfig(...).
  Reuses readContentChunksEmbeddingDim from the same shared helper
  init + embed use. On mismatch, the fix hint threads engineKind +
  databasePath into the new branched recipe (codex round 1 CDX-8 +
  Lane E.1/E.2).
- checkZeEmbeddingHealth reads gateway for the model + loadConfigFileOnly
  for the key. Fires when (a) resolved model starts with zeroentropyai:
  AND (b) ZEROENTROPY_API_KEY is unset in env AND (c) file plane has
  no zeroentropy_api_key (codex round 2 CDX2-10).
- loadRecommendationContext reads gateway for both fields and
  recognizes the ZE key alongside OpenAI/Anthropic in the
  hasEmbeddingApiKey check, so brains on ZE no longer look "healthy"
  just because OPENAI_API_KEY happens to be set (codex round 2 CDX2-11).

Tests rewritten for the gateway-source-of-truth contract via
configureGateway() in beforeAll. Added a "gateway unconfigured: skips
with ok" case so doctor doesn't false-warn on cold-boot brains.

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

* test+docs(v0.37): fix-wave unit coverage + PGLite-first migration recipe + TODOS

Lands the v0.37 PGLite fresh-install fix wave's structural tests and
the user-facing migration recipe overhaul.

test/v0_37_fix_wave.test.ts (new): 22 unit cases pinning the lanes:
- Lane A: defaults module exports, getPGLiteSchema/getPostgresSchema
  default-args, registry + isCacheSafe under the `cfg > gateway >
  DEFAULT` chain (both gateway-set and gateway-reset branches).
- Lane B: loadConfigFileOnly env isolation + DATABASE_URL inference
  refusal + null-on-missing.
- Lane C.3: buildGatewayConfig maps zeroentropy_api_key + process.env
  wins over config (operator escape hatch contract).
- Lane D.2: EmbeddingDimMismatchError shape + tag.
- Lane D.4: structural assertion that `sync` is in CLI_ONLY_SELF_HELP.
- Deferred-TODO ship: reinit-pglite is registered correctly +
  embeddingMismatchMessage PGLite branch recommends it.

docs/embedding-migrations.md: PGLite section moved to top (the default
install). The recommended path is `gbrain reinit-pglite` one-liner;
the by-hand mv + init + sync sequence stays as the fallback recipe.
Postgres SQL ALTER recipe preserved. New section on `gbrain config
set` refusal explains the file-plane vs DB-plane contract so users
don't follow stale documentation.

TODOS.md: 4 deferred follow-ups filed with concrete file pointers:
- gbrain embed --try-fallback (provider auto-switch with consent gate)
- Full plane unification for non-schema-sizing fields
- Worker-pool shared AbortController for mid-run dim drift
- Cleanup of back-compat constants in src/core/embedding.ts

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

* test(v0.37): fill behavior gaps + headline fresh-install E2E

The structural fix-wave tests in test/v0_37_fix_wave.test.ts pin lane-level
invariants (exports, registry chain, signature shapes). The audit found 10+
END-TO-END behaviors that the structural tests didn't actually reach.
This file fills the highest-leverage gaps.

Unit coverage (test/v0_37_gap_fill.test.ts, 12 cases):
- Lane A.7: chunk-row INSERT default tracks DEFAULT_EMBEDDING_MODEL
  constant (pre-fix this was the literal 'text-embedding-3-large' at
  pglite-engine.ts:1611 + postgres-engine.ts:1647 — production write
  sites that were never directly tested; codex round 2 CDX2-4).
- Lane A.8: schema seed stores full provider:model in DB config
  (pre-fix the .split(':') strip dropped the prefix; codex round 1
  CDX-4). Asserts a fresh ZE init stores `zeroentropyai:zembed-1`
  in the config table, not bare `zembed-1`.
- Lane B precedence: explicit CLI > env > existing file > default
  test (codex round 2 CDX2-7 contradiction guard).
- Lane C.3 env merge: process.env.ZEROENTROPY_API_KEY threads through
  loadConfig → cfg.zeroentropy_api_key; loadConfigFileOnly does NOT.
- Lane D.2 end-to-end: schema=1536 + gateway=1280 →
  EmbeddingDimMismatchError fires AND the embed transport is never
  called (the whole point of pre-flight). Plus dry-run skips the
  check.
- Lane D.3 source-text grep: both sync.ts catch sites detect the
  typed error + the `--no-embed` tip is present (CDX2-8).
- Lane E.4 source-text grep: loadRecommendationContext is
  provider-aware (reads gateway + branches on ZE/OpenAI key).
- reinit-pglite contract: refuses on non-PGLite engines + refuses
  when required flags are missing.

E2E (test/e2e/fresh-install-pglite.test.ts, 2 cases):
- Bare `gbrain init --pglite` produces a `vector(1280)` schema, prints
  the resolved choice, persists defaults to config.json — the headline
  scenario that v0.37 ships to fix.
- init → seed page → embed end-to-end: chunks have non-null
  embeddings; no dim mismatch despite the wave's defaults change.

Both E2E cases are IN-PROCESS (per CDX2-12: CLI-subprocess E2E can't
inherit `__setEmbedTransportForTests`). They run with stubbed transport
returning synthetic 1280-dim vectors so we never hit real provider APIs.

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

* test(v0.37): defensive gateway restore in reinit-pglite describe block

Adds an afterAll that restores the gateway to OpenAI/1536 (matching the
bunfig preload) at the end of the reinit-pglite describe. Belt-and-
suspenders: earlier describe blocks in this file already restore, but
if the reinit-pglite tests ever start mutating the gateway in the
future, this protects downstream test files in the same bun-test shard
from inheriting a non-default state.

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

* chore: bump version and changelog (v0.37.10.0)

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

* docs: scrub stale config-set recipes for embedding model (v0.37.10.0)

README + topologies + embedding-providers were still pointing users at
`gbrain config set embedding_model X` / `embedding_dimensions N`. As of
v0.37.10.0 those writes are refused — the schema column has to resize
alongside the config. Point at `gbrain reinit-pglite` (PGLite) and the
SQL recipe in `docs/embedding-migrations.md` (Postgres) instead.

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

* chore: bump version to v0.37.11.0

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

* test: quarantine v0.37 fix-wave tests to .serial.test.ts

CI's `check:test-isolation` lint flagged R1 violations (direct
`process.env.GBRAIN_HOME` mutation) in both new fix-wave test files.
Per the documented quarantine pattern in CLAUDE.md, rename to
`*.serial.test.ts` instead of refactoring through `withEnv()` — both
files use beforeEach/afterEach env wiring that's already serial-safe.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:11:00 -07:00
54a0629745 v0.37.8.0 feat: voyage-code-3 discoverability + reindex-code cost-preview fix (#1267)
* v0.37.6.0 feat: voyage-code-3 discoverability + reindex-code cost-preview fix

For agents indexing source code with gbrain, the right embedding model
is now obvious — and the brain tells you so out loud. Decision tree +
Topology 3 doc + Topology 3 setup-skill pointer + runtime stderr nudge
from `gbrain reindex --code` against non-code-tuned models. Same diff
fixes the stale hardcoded `text-embedding-3-large` in the cost preview
that would have made the nudge land badly.

Tests: 3 new files (recipe regression, nudge logic + CLI integration,
cost-preview IRON-RULE regression).

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

* todos: file pre-existing doctor-report-remote score regression (#1215 follow-up)

Noticed during /ship of v0.37.6.0. The skill_brain_first check added in
v0.37.3.0 (#1215) appears to tank the doctor health score on fresh PGLite
test brains, causing test/doctor-report-remote.test.ts:65 to fail with
health_score: 50 (expects >=70). Pre-existing on master; not in this
branch's touch surface.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 08:59:29 -07:00
488e4824e8 v0.34.1.0 fix(mcp): MCP fix wave — source-isolation P0 + PKCE DCR + federated_read + 3 more (#996)
* fix(mcp): skip stdin EOF handlers when MCP_STDIO=1

OpenClaw's bundle-mcp gateway and similar wrappers pipe the JSON-RPC
handshake on stdin then close their stdin half. Pre-fix, both stdin
'end' and 'close' listeners (server.ts:65-66 and serve.ts:204-206)
treated this as a permanent disconnect and shut the server down before
the first tool call arrived.

Guard both sites with `process.env.MCP_STDIO !== '1'`. Signal handlers
(SIGTERM/SIGINT/SIGHUP), transport.onclose, and the parent-process
watchdog still cover legitimate shutdown paths. The serve.ts site
threads the env read through an injectable `mcpStdio?: boolean` on
ServeOptions so tests stay isolated (no process.env mutation per
scripts/check-test-isolation.sh R1).

Tests: 3 new cases in test/serve-stdio-lifecycle.test.ts pin the
guard's invariants — mcpStdio=true must NOT trigger shutdown on stdin
EOF, signals must still drive shutdown with mcpStdio=true, and
mcpStdio=false (default) preserves existing CLI behavior. 25/25 pass.

Origin: PR #870.

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

* fix(oauth): honor token_endpoint_auth_method=none for PKCE public clients

RFC 7591 §3.2.1: when a DCR client declares
token_endpoint_auth_method="none" (PKCE-only public clients like Claude
Code, Cursor), the authorization server MUST NOT issue a client_secret.
Pre-fix, registerClient unconditionally minted a secret, and the MCP
SDK's clientAuth middleware then rejected valid public-client flows on
/token because it expected client.client_secret to match.

Three changes to src/core/oauth-provider.ts:registerClient:

  - Gate clientSecret generation on isPublicClient = (auth_method === 'none').
    Public clients store client_secret_hash = NULL.
  - Omit client_secret from the response payload for public clients.
    Confidential clients (default client_secret_post and explicit
    client_secret_basic) keep their existing one-time-reveal shape.
  - Normalize NULL secret_hash to JS undefined in getClient so SDK
    middleware (which checks client.client_secret === undefined, not
    === null) correctly identifies public clients and skips the
    secret-comparison branch on /token.

Schema is already permissive (client_secret_hash TEXT, no NOT NULL on
both src/schema.sql and src/core/pglite-schema.ts) — no migration
needed.

Tests: 5 new cases in test/oauth.test.ts pin:
  - public client → no client_secret in response (#11 from plan)
  - default auth_method → secret unchanged (regression guard)
  - explicit client_secret_post → secret unchanged
  - getClient NULL→undefined normalization
  - PKCE full /authorize → /token end-to-end with no secret (#15 from plan)

69/69 oauth.test.ts cases pass. typecheck clean.

Origin: PR #909.

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

* feat(serve-http): --bind HOST, default to loopback (127.0.0.1)

Adds `gbrain serve --http --bind <interface>` to control which network
interface the HTTP MCP server listens on. Default flipped from
`0.0.0.0` (pre-v0.34) to `127.0.0.1` (v0.34.0+).

Why the flip: gbrain's primary use case is a personal-knowledge brain on
a laptop. The previous default exposed brains on every interface — one
accidental `--http` invocation away from publishing the brain to a LAN.
Server operators who need remote access pass `--bind 0.0.0.0` (or a
specific interface). Codex's outside-voice on the original PR #864
correctly flagged that the additive flag wasn't actually the fix; the
default needed to change for the safety claim to hold.

If `--public-url` is set but `--bind` is unset, runServeHttp prints a
loud stderr WARN at startup recommending `--bind 0.0.0.0`. Declaring a
public URL while quietly binding loopback is almost always a
misconfiguration; we want the operator to see it on first start, not
silently fail remote requests.

Startup banner now includes a `Bind:` row so the listening interface is
visible alongside Port / Engine / Issuer.

Origin: PR #864, extended with D11 (default flip) per /plan-eng-review
codex outside-voice review.

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

* fix(mcp): seal source-isolation leak on read path (P0)

Pre-fix, an authenticated OAuth MCP client scoped to source-A could
enumerate source-B pages via six read-side ops: search, query (text
AND image paths), list_pages, traverse_graph, and find_experts. The
v0.31.8 source-scoping pattern shipped through dispatch.ts but the op
handlers never threaded ctx.sourceId into their engine calls, and
hybridSearch.ts:223's explicit SearchOpts rebuild dropped sourceId
even when callers passed it.

Sealing the leak:

  - src/core/operations.ts adds sourceScopeOpts(ctx), the canonical
    precedence ladder: ctx.auth.allowedSources (federated) wins over
    ctx.sourceId (scalar) wins over nothing. Threaded into all 5
    read-side op handlers + the query-image-path searchVector call
    (the 6th leak surface codex caught in plan review).

  - src/core/search/hybrid.ts:223 now threads sourceId + sourceIds
    fields through the inner SearchOpts rebuild. The explicit pick
    shape is preserved (HNSW inner-CTE ordering depends on it) but
    extended.

  - src/core/types.ts adds sourceIds?: string[] to SearchOpts +
    PageFilters (D9: federated read needs array-shaped engine filter
    or fan-out; array wins for hot retrieval).

  - src/core/operations.ts AuthInfo gains sourceId + allowedSources
    (D2: identity surface symmetric with the federated_read column
    #876 will add).

  - Both engines now apply WHERE source_id = $N (scalar) or = ANY($N::text[])
    (array) at the SQL layer for searchKeyword, searchKeywordChunks,
    searchVector, listPages, traverseGraph, traversePaths. Array form
    wins when both are set. The searchVector filter pushes into the
    inner HNSW CTE (codex flagged this placement during plan review).

  - traverseGraph + traversePaths signatures gain opts.sourceId +
    opts.sourceIds; engine.ts interface updated.

  - findExperts (the whoknows op, D3 5th leak surface) accepts
    sourceId + sourceIds and threads them into its internal
    hybridSearch call. PR #861 was authored before v0.33 shipped so
    this op wasn't covered in the original PR.

Auth wiring:

  - GBrainOAuthProvider.verifyAccessToken populates AuthInfo.sourceId
    from oauth_clients.source_id. JOIN guarded by isUndefinedColumnError
    so pre-v55 brains degrade to legacy projection rather than refusing
    every token verification.

  - GBrainOAuthProvider.registerClientManual gains a sourceId
    parameter (defaults to 'default'). DCR registerClient also sets
    source_id='default' on the inserted row.

  - serve-http.ts:929 cleanup: AuthInfo.sourceId is now a real typed
    field. The cast + GBRAIN_SOURCE env fallback chain is gone (D13).
    Legacy bearer tokens default to 'default' source in
    verifyAccessToken.

  - http-transport.ts (legacy access_tokens path) threads
    sourceId='default' through DispatchOpts so v0.22.7 callers stay
    source-scoped.

  - auth.ts CLI adds --source flag to gbrain auth register-client.

Migration v55 (D10 + D13):

  - ALTER TABLE oauth_clients ADD COLUMN source_id TEXT (nullable).
  - Backfill UPDATE source_id = 'default' WHERE source_id IS NULL —
    preserves v0.33 effective behavior verbatim for legacy clients.
  - ADD CONSTRAINT FK ... REFERENCES sources(id) ON DELETE SET NULL,
    wrapped in DO block so re-runs against fresh-install brains (where
    the FK already lives inline in SCHEMA_SQL) no-op cleanly.
  - CREATE INDEX idx_oauth_clients_source_id WHERE source_id IS NOT NULL
    for the verifyAccessToken JOIN.
  - GBRAIN_ACCEPT_SILENT_WIDEN env-flag wired through the runner via
    SET LOCAL gbrain.accept_silent_widen — reserved for future migrations
    that hit the silent-widen footgun codex flagged. This migration
    doesn't need it (column is brand new; no pre-existing stale values
    possible by definition).
  - src/core/pglite-schema.ts + src/schema.sql include the column +
    FK + index inline for fresh installs.

Tests: new test/e2e/source-isolation-pglite.test.ts with 13 regression
cases — one per leak surface (search/list_pages/traverse/etc.) plus
explicit AuthInfo.sourceId and AuthInfo.allowedSources op-handler
threading checks. Full unit suite: 6034 pass / 0 fail. PGLite
initSchema time dropped from 2.4s to 850ms after consolidating v55's
DO blocks (multiple DO blocks were slow on PGLite; one DO block for
the FK install only is fine).

Origin: PR #861 + plan-eng-review decisions D2/D3/D4/D9/D10/D13 + F2.

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

* feat(gateway): multimodal embedding for openai-compatible providers

Pre-fix, embedMultimodal hardcoded a recipe.id === 'voyage' branch and
threw AIConfigError for every other recipe. Multimodal-capable providers
fronted by LiteLLM (or any openai-compatible proxy) were unreachable
even when the operator had wired up the model.

The fix:

  - src/core/ai/gateway.ts adds embedMultimodalOpenAICompat() that
    POSTs to the standard /embeddings endpoint with content arrays
    carrying image_url entries. Routing comes from the existing
    recipe.implementation switch — Voyage stays on its own
    /multimodalembeddings path; every other openai-compatible recipe
    flows through the new helper.

  - src/core/ai/recipes/litellm-proxy.ts declares
    supports_multimodal: true so embedMultimodal accepts the recipe.
    No multimodal_models allow-list: LiteLLM is a passthrough proxy
    and the user owns model-id selection; provider rejection (400 from
    upstream) is the right enforcement layer there. Voyage's static
    allow-list shape stays unchanged (its 12 models share
    supports_multimodal but only one is multimodal-capable).

  - D12 runtime dimension validation: the new helper checks the
    returned vector length against the recipe's declared default_dims
    (preferred) or the brain's embedding_dimensions config. Mismatch
    throws AIConfigError with model id + observed + expected so the
    operator can swap models or rebuild the column. Pre-fix, a
    wrong-dim response would surface as a cryptic pgvector
    "vector dimension mismatch" at INSERT time.

  - Auth resolution routes through the existing defaultResolveAuth
    helper so optional-auth recipes (LiteLLM proxy with no
    LITELLM_API_KEY) and required-auth recipes both share one code
    path. Optional-auth sends "Authorization: Bearer unauthenticated"
    which servers like Ollama / llama-server ignore but the SDK
    contract requires.

Tests: 11 new cases in test/openai-compat-multimodal.test.ts cover
happy-path, multi-input batching, unauthenticated proxy, D12 dim
mismatch + default-dim fallback, 401 / 400 / malformed-JSON / non-array
error paths, and an explicit Voyage-regression test pinning that the
new openai-compat route doesn't accidentally hijack the Voyage path.
All 41 multimodal-related tests pass (existing voyage suite + new).
typecheck clean.

Origin: PR #875 + plan-eng-review D12 (runtime dim validation).

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

* feat(oauth): federated_read read scope (#876)

Pre-fix, OAuth clients had a single source-scope axis (source_id, added
in v55). A client could either write+read one source OR be a super-reader
across all sources (via NULL source_id). There was no middle ground —
WeCare-style L3 dept clients that need to write to dept-x but read
dept-x + parent canon + shared canon had no expression.

#876 adds federated_read TEXT[] as an orthogonal read-scope axis. source_id
is the WRITE authority; federated_read is the READ authority. They default
to matching values (read scope == write scope, the pre-v0.34 default)
when a client is registered without an explicit federated read list.

Migrations v56-v60 (six new migrations on top of v55):

  - v56: ALTER TABLE ... ADD COLUMN federated_read TEXT[] NOT NULL DEFAULT '{}'.
  - v57 (F5): explicit CASE backfill so source_id IS NULL → '{}' (not an
    array containing NULL — codex caught this ambiguity during plan review).
  - v58: post-backfill validation. Fails loud if any row's source_id isn't
    in its federated_read array, pointing at a logic bug in v57 if fired.
  - v59: flip the source_id FK from ON DELETE SET NULL to ON DELETE
    RESTRICT now that federated_read provides the alternative scope-loss
    path. Pre-flip, deleting a source could silently widen any oauth_client
    to super-reader; post-flip, source delete is refused if any client
    references it (operator must revoke/re-scope first).
  - v60: GIN index on federated_read for array-containment queries.

Auth wiring:

  - GBrainOAuthProvider.verifyAccessToken JOINs c.federated_read and
    populates AuthInfo.allowedSources. Pre-v56 / pre-v55 brains degrade
    via the existing isUndefinedColumnError fallback chain.
  - registerClientManual gains a federatedRead?: string[] parameter
    (defaults to [sourceId]).
  - DCR registerClient sets source_id='default' + federated_read=['default']
    on the inserted row.
  - auth.ts CLI adds --federated-read SRC1,SRC2,... flag. The
    register-client output now prints "Federated reads:" so operators
    confirm the scope they set.

Engines consume the federated array through the SearchOpts.sourceIds /
PageFilters.sourceIds field that #861 added (no engine changes here — the
plumbing was D9). sourceScopeOpts in operations.ts already prefers the
auth.allowedSources array over scalar ctx.sourceId when set.

Test seam:
  - test/book-mirror.test.ts now spawns the CLI with GBRAIN_HOME pointed
    at a tempdir so the test isn't sensitive to the developer's local
    ~/.gbrain/config.json. Pre-fix the test could silently inherit a real
    Postgres connection and hang past the default 5s test timeout. Fresh
    GBRAIN_HOME → "No brain configured" → exit 1 in <1s.
  - test/e2e/source-isolation-pglite.test.ts gains one more regression
    case: AuthInfo.allowedSources = [] (explicit empty) MUST NOT widen
    scope to "all sources" — the silent-widen footgun precedence ladder.
  - test/openai-compat-multimodal.test.ts is part of the wave's commits
    via the migrate.ts changes that bump the schema chain. typecheck-only
    fix on a captured-auth type was already in #875's tree.

6045 unit tests pass / 0 fail. typecheck clean. PGLite initSchema runs
v55-v60 in ~786ms total (within the test-harness budget for tests using
the canonical beforeAll engine pattern).

Origin: PR #876 + plan-eng-review F5 (CASE backfill).

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

* v0.34.0.0: MCP fix wave (#870 #909 #864 #861 #875 #876)

VERSION + package.json + CHANGELOG bump for the six-PR MCP fix wave.
Schema chain extends from v54 → v60; oauth_clients gains source_id +
federated_read columns; auth'd MCP clients now stay inside their scope
across all read-side ops; PKCE-only DCR works; --bind defaults to
loopback; LiteLLM multimodal embedding ships.

Contributed by @Hansen1018 (#870), @ding-modding (#909), @DukeDawg
(#864), @toilalesondev (#861 + #876), @yoelgal (#875).

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

* docs: update project documentation for v0.34.0.0

Sync README, CLAUDE.md, SECURITY.md, docs/architecture/topologies.md,
and docs/mcp/DEPLOY.md to reflect the v0.34.0.0 MCP fix wave:

- README: document --bind HOST default (loopback), --source +
  --federated-read register-client flags, PKCE public-client gate
- SECURITY.md: note loopback-by-default for serve --http, update the
  trust-proxy contract to point at the new default
- CLAUDE.md: annotate operations.ts (sourceScopeOpts helper),
  oauth-provider.ts (verifyAccessToken JOIN + PKCE public clients),
  serve-http.ts (--bind flag), gateway.ts (openai-compat multimodal +
  dim validation), mcp/server.ts (MCP_STDIO guard), auth.ts (--source
  + --federated-read), migrate.ts (v58-v63 chain), engine.ts
  (sourceIds field). Add 4 new test-file entries for
  source-isolation-pglite, openai-compat-multimodal,
  serve-stdio-lifecycle, oauth.test.ts PKCE cases
- docs/architecture/topologies.md: source-scoped register-client
  example, --bind 0.0.0.0 for thin-client host setup
- docs/mcp/DEPLOY.md: --bind explanation in the ngrok section,
  source-scoped client recipe
- llms-full.txt: regenerated per the CLAUDE.md-edit chaser rule

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

* chore: bump v0.34.0.0 → v0.34.1.0

Renumbering the MCP fix wave from v0.34.0.0 to v0.34.1.0 so the
release slot lands between master's v0.33.2.1 and the next minor.

Touches every release-artifact mention:
- VERSION: 0.34.0.0 → 0.34.1.0
- package.json: same
- CHANGELOG.md header + "To take advantage" block
- CLAUDE.md key-files annotations (8 entries that document this wave)
- llms-full.txt (regen from CLAUDE.md)
- README.md / SECURITY.md / docs/architecture/topologies.md / docs/mcp/DEPLOY.md
- Wave code-comment markers ("// v0.34.0 (#NNN):" → "// v0.34.1 (#NNN):")

Test files renamed alongside since they were committed with the wave.

Commit subjects on the original 6 PR commits + the v0.34.0.0 bump
commit (4f533c726b47db7e) intentionally NOT rewritten — those are
history. `git log` finds the implementation by message subject, not by
version tag.

6275 unit tests pass, typecheck clean, migration chain v58-v63 unchanged.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:15:29 -07:00
8392d434a9 v0.29.2 feat: thin-client mode (gbrain init --mcp-only + gbrain remote ping/doctor + topologies) (#732)
* feat(config): add remote_mcp field + isThinClient() helper

Adds a top-level optional remote_mcp config block to GBrainConfig
(issuer_url, mcp_url, oauth_client_id, oauth_client_secret) for
thin-client installs that consume a remote `gbrain serve --http` over
MCP instead of running a local engine.

isThinClient(config) returns true when remote_mcp is set; used by the
CLI dispatch guard, doctor branch, and init re-run guard. The engine
field stays as today (postgres|pglite); thin-client mode is a separate
config field, NOT an engine kind extension (codex outside-voice review
flagged the engine='remote' extension as overreach).

GBRAIN_REMOTE_CLIENT_SECRET env var overrides the config-file value at
load time so the secret can stay out of disk for headless agents.

Foundation commit for multi-topology v1; no behavior change yet.

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

* feat(probe): outbound OAuth + MCP smoke probes

Adds three pure async functions over the standard fetch API:
  - discoverOAuth(issuerUrl): GET /.well-known/oauth-authorization-server
  - mintClientCredentialsToken(tokenEndpoint, id, secret): POST /token
  - smokeTestMcp(mcpUrl, accessToken): POST /mcp initialize

Discriminated 'ok=true' / 'ok=false + reason' return shapes so callers
render error messages consistently. No SDK dependency to keep init's
setup-flow scope tight; Lane B's mcp-client.ts will pull in the
official @modelcontextprotocol/sdk Client for full session semantics.

Used by both 'gbrain init --mcp-only' (Lane A's setup smoke) and
runRemoteDoctor (Lane A's thin-client doctor checks).

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

* feat(init): --mcp-only branch + re-run guard

Adds 'gbrain init --mcp-only' for thin-client setup. Required flags
(or env vars):
  --issuer-url     OAuth root (e.g. https://host:3001)
  --mcp-url        MCP tool dispatch path (e.g. https://host:3001/mcp)
  --oauth-client-id, --oauth-client-secret

Pre-flight runs three smoke probes (discovery, token round-trip, MCP
initialize) BEFORE writing the config — fail-fast on bad URL beats
fail-late on bad credentials. On success, writes ~/.gbrain/config.json
with remote_mcp set and NO local DB created.

Re-run guard (A8): when ~/.gbrain/config.json already has remote_mcp,
'gbrain init' (any flag set) refuses without --force. Catches the
scripted-setup-loop friction from the user-reported scenario where
re-running setup-gbrain on a thin-client machine kept trying to
re-create a local DB.

Two URLs in config (issuer + mcp) instead of one because OAuth
discovery + /token live at the issuer root while tool dispatch is at
/mcp — they compose from a common base in practice but reverse-proxy
setups need them explicit (codex review #2).

Tests: 15 cases covering happy path, env-var-supplied secret stays
out of disk, all four required-flag missing-error paths, three
smoke-failure paths, network-unreachable path, and the four re-run
guard variants (default/--pglite/--mcp-only without --force / with
--force). Uses async Bun.spawn (NOT execFileSync) — sync exec
deadlocks against in-process HTTP fixtures because the parent's
event loop can't accept connections while sync-blocked on a child.

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

* feat(doctor): runRemoteDoctor for thin-client mode

Replaces every DB-bound check from runDoctor() with a tighter set
scoped to 'is the remote MCP we configured actually reachable?'.
Five checks:
  - config_integrity (URL fields well-formed)
  - oauth_credentials (secret resolvable from env or config file)
  - oauth_discovery (GET /.well-known/oauth-authorization-server)
  - oauth_token (POST /token client_credentials)
  - mcp_smoke (POST /mcp initialize)

Output shape matches the local doctor's Check surface so JSON
consumers can union the two without conditional logic. schema_version
is 2 (matches local doctor).

collectRemoteDoctorReport() is the pure data collector;
runRemoteDoctor() is the print/exit wrapper. Tests pin the data
collector so we don't have to intercept stdout / process.exit.

Tests: 12 cases over a tiny in-process HTTP fixture covering happy
path, every probe failure mode (404/parse/auth/network/server-error),
malformed-URL config integrity, missing-secret short-circuit, and
the env-var-overrides-config-file secret resolution. withEnv() helper
used for env mutations to satisfy the test-isolation lint.

Module is added but not yet wired into the CLI doctor branch; the
wiring lands in the next commit (cli dispatch guard + doctor routing).

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

* feat(cli): thin-client dispatch guard + doctor routing

Adds a single canonical refusal at the top of handleCliOnly() for the
9 DB-bound commands when ~/.gbrain/config.json has remote_mcp set:
  sync, embed, extract, migrate, apply-migrations, repair-jsonb,
  orphans, integrity, serve

Single dispatch check (not 9 sprinkled assertLocalEngine calls per
codex review #1) — avoids the blast radius of letting commands enter
connectEngine before the check fires. Refused commands exit 1 with a
canonical error naming the remote mcp_url.

doctor branch routes to runRemoteDoctor when isThinClient(config)
returns true; falls through to the existing local-doctor flow
otherwise. Wires the module added in the previous commit into the
user-facing CLI surface.

Safe commands (init, auth, --version, --help, etc.) still work in
thin-client mode and are NOT in the refused set.

Tests: 14 cases — 9 refused commands × 1 each, 2 safe commands, 1
doctor-routing assertion (fingerprints the thin-client output by
'mode:"thin-client"' in JSON), 2 regression tests asserting local
config still passes through normally.

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

* docs(topologies): multi-topology architecture guide + setup skill Phase A.5

New docs/architecture/topologies.md covering three deployment shapes:
  1. Single brain (today's default)
  2. Cross-machine thin client (consume a remote brain over MCP)
  3. Split-engine per-worktree (Conductor users with per-worktree
     code engines + shared remote artifacts brain)

Each topology gets an ASCII diagram, when-it-fits guidance, and
concrete setup recipes. Topology 3's alias-level routing footgun
(wrong alias = silent wrong-brain writes) is called out explicitly
per codex review #6.

Topology 3 needs zero gbrain code changes — GBRAIN_HOME already
overrides ~/.gbrain and 'gbrain serve --http --port N' already runs
on any port. gstack composes these primitives on its side.

skills/setup/SKILL.md gets Phase A.5 BEFORE the local-engine phases.
Asks the user which topology fits, walks thin-client setup through
'gbrain init --mcp-only', skips Phases B/C/C.5/H entirely for thin
clients (host's autopilot handles sync/extract/embed).

README.md gets a one-line link to the topology doc from the
Architecture section.

llms-full.txt regenerated to include the new doc.

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

* test(e2e): thin-client end-to-end skeleton

Spins up 'gbrain serve --http' against real Postgres, registers a
client with read,write,admin scope, runs 'gbrain init --mcp-only'
from a separate tempdir GBRAIN_HOME, exercises the canonical
thin-client flows:

  - init --mcp-only succeeds against the live host
  - doctor reports mode: thin-client + all checks green
  - sync is refused with the canonical thin-client error
  - re-running init refuses without --force

Tier B flows (gbrain remote ping / doctor) will be added alongside
their Lane B implementation. Skips when DATABASE_URL unset (matches
the e2e gate convention used across the suite).

Async Bun.spawn (NOT execFileSync) so the test event loop stays
responsive — execFileSync deadlocks against in-process HTTP fixtures
because the parent's event loop can't accept connections while
sync-blocked on a child process.

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

* feat(doctor): doctorReportRemote core for thin-client + run_doctor op

Adds three new exports to src/commands/doctor.ts that the run_doctor MCP
op + gbrain remote doctor CLI both consume:

  - DoctorReport interface       schema_version=2 stable shape
  - computeDoctorReport(checks)  status + health_score math
  - doctorReportRemote(engine)   focused 5-check thin-client surface

doctorReportRemote runs:
  1. connection      (engine reachable + page count via getStats)
  2. schema_version  (engine.getConfig('version') vs LATEST_VERSION)
  3. brain_score     (the 5-component composite)
  4. sync_failures   (file-plane JSONL count from gbrainPath('sync-failures.jsonl'))
  5. queue_health    (Postgres-only: stalled active jobs > 1h)

Engine-agnostic: works on both Postgres and PGLite via engine.executeRaw +
engine.getConfig + engine.getHealth — no reliance on db.getConnection()
which is Postgres-only.

Deliberately a focused subset of the local doctor surface, NOT a full
mirror. Generalizing to lint/integrity/orphans is filed as follow-up
pending demand. Local doctor (runDoctor) is unchanged; operators on the
host machine still get the full check set.

schema_version=2 matches the local doctor's --json output schema, so JSON
consumers can union the two without conditional logic.

Tests: 11 unit cases against PGLite covering the 5-check happy path,
schema version reporting (latest), PGLite-specific queue_health
informational message, and the score+status math via computeDoctorReport.

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

* feat(mcp-client): outbound HTTP MCP client over @modelcontextprotocol/sdk

New src/core/mcp-client.ts wraps the official SDK's Client +
StreamableHTTPClientTransport with OAuth client_credentials minting,
in-process token caching with expires_at, and refresh-on-401 retry.

Public surface:
  - callRemoteTool(config, toolName, args)   tool call w/ auto-refresh
  - unpackToolResult(res)                    parse content[0].text JSON
  - RemoteMcpError                           discriminated by `reason`

Token cache: module-level Map keyed by mcp_url. CLI processes are
short-lived; the cache amortizes when one invocation makes multiple
calls (gbrain remote ping submits then polls). Persisting to disk would
be a credential-on-disk surface for marginal benefit since /token
round-trip is sub-100ms.

401 retry: ONLY for mid-session token rotation (initial good token →
stale → 401). If the FIRST mint fails auth, surface immediately as
RemoteMcpError(auth) — retry won't help when credentials are wrong from
the start. If a fresh-mint-after-401 still 401s, surface as
RemoteMcpError(auth_after_refresh) which the CLI renders with a hint
pointing the operator at gbrain auth register-client.

Used by gbrain remote ping (submit_job + get_job poll) and gbrain
remote doctor (run_doctor). Test-only _clearMcpClientTokenCache export
for fixture isolation.

Tests: 13 unit cases over an in-process HTTP fixture mimicking gbrain
serve --http (OAuth discovery + /token + /mcp JSON-RPC handshake).
Covers happy path, token cache reuse + force-refresh, args passthrough,
config-error paths (no remote_mcp / no secret), token mint 401, network
unreachable, tool isError envelope, and unpackToolResult parse failures.

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

* feat(operations): add run_doctor MCP op (admin scope, HTTP-reachable)

New op in src/core/operations.ts wraps doctorReportRemote() and returns
the structured DoctorReport JSON over MCP.

  scope:     'admin'       (system-state read; not for routine consumers)
  localOnly: false         (reachable over HTTP)
  mutating:  false         (safe to call repeatedly)
  params:    {}            (no caller arguments needed)

First read-only diagnostic op exposed over HTTP MCP. Used by gbrain
remote doctor — the matching client-side renderer lives in
src/commands/remote.ts.

Precedent: doctor only. Generalizing run_lint / run_integrity /
run_orphans to MCP is filed as follow-up work pending demand. Local
doctor stays unchanged; this op is the operator-friendly subset for
remote callers.

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

* feat(remote): gbrain remote ping + gbrain remote doctor

Two thin-client convenience commands that round-trip through the host's
HTTP MCP endpoint:

  - gbrain remote ping     submit_job(autopilot-cycle) → poll get_job →
                           exit when terminal. The "I just wrote markdown,
                           tell the host to re-index" affordance.
  - gbrain remote doctor   run_doctor MCP op → render the host's
                           DoctorReport → exit 0/1 based on status.

Both require a thin-client install (~/.gbrain/config.json with
remote_mcp). Local installs get a clear error pointing at the local
equivalents.

Polling backoff (ping): 1s × 30s, then 5s × 5min, then 10s. Default cap
15min, configurable via `--timeout`. Without backoff, a 5-min cycle
would burn 300 round-trips against the host's rate limiter.

Payload uses `data: {phases: [...]}`, NOT `params:` — the submit_job op
shape takes `data`. Codex review #8 catch.

NO `repo` arg passed to autopilot-cycle — uses the server's configured
brain repo. This sidesteps TODO #1144 (sync_brain repo-path validation
for caller-controlled paths) entirely.

src/cli.ts wires the `remote` subcommand into CLI_ONLY + the dispatch.
Help (`gbrain remote --help`) and unknown-subcommand handling included.

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

* test(e2e): thin-client Tier B + scope-mismatch regression

Extends the existing test/e2e/thin-client.test.ts with three new cases:

  1. gbrain remote doctor returns the host's DoctorReport — pins the
     run_doctor MCP op round-trip. Asserts schema_version=2, all 5
     check names present, connection + schema_version ok against a
     fresh host.
  2. gbrain remote ping triggers autopilot-cycle and returns terminal
     state — pins the submit_job → poll → terminal wire path. Accepts
     any terminal state (success / failed / dead / cancelled / timeout)
     because autopilot on an empty no-repo brain may fail-fast in the
     sync phase. What this test pins is the JSON shape (job_id present,
     state populated), NOT cycle success on a no-repo fixture.
  3. read+write client cannot call run_doctor — codex review #7
     regression guard. Registers a separate client with
     `--scopes "read write"` (no admin), runs `gbrain remote doctor`
     against it, asserts exit 1 with auth/auth_after_refresh/tool_error
     reason. Keeps the verification flow honest: the canonical setup
     MUST require admin scope.

`gbrain auth register-client` doesn't have --json, so the test parses
the human output for "Client ID:" and "Client Secret:" lines via a
helper.

Test-level timeout bumped 60s → 120s for the ping wait + auth/init
overhead.

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

* chore: bump version and changelog (v0.29.2)

v0.29.2 ships thin-client mode: gbrain init --mcp-only, gbrain remote
ping/doctor, run_doctor MCP op, and the docs/architecture/topologies.md
deployment guide.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:56:22 -07:00