Compare commits

...
Author SHA1 Message Date
Garry TanandClaude Fable 5 00279392b6 fix(cli): regenerate the flag registry for the cathedral-1 command set
Master's #2185 strict flag validation ships a committed, generated
CLI_FLAG_REGISTRY with drift + freshness guards keyed off CLI_ONLY. The
merge added this branch's `protocol` command to CLI_ONLY, but the
committed registry was generated on master without it, failing CI shard
7's drift guard. Regenerated via `bun run build:flag-registry` on the
merged tree (95 commands; picks up `protocol` and the `--surface`
help-text mentions from the verbs work).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:22:45 -07:00
Garry Tan d68afbb534 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1 2026-08-09 09:52:28 -07:00
Garry Tan 99101aea34 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1 2026-08-08 17:22:37 -07:00
Garry TanandClaude Fable 5 19d0da22cb docs: apply cross-model doc-review fixes for v0.43.0.0
Verified findings from the /document-release Codex pass:

- MEMORY_VERBS_v1.md: disclose that conformance WRITES (marker-suffixed
  seed page + remember/forget facts, write-scope creds, artifacts not
  auto-deleted); quickstart header says 4 commands because it has 4.
- CHANGELOG v0.43.0.0: cost claim scoped per verb (budget/latency/cost
  block); remember "updates in place" -> supersedes-with-history (what
  write-single.ts actually does).
- DEPLOY.md: legacy bearer fallback works on BOTH engines now (both
  schemas carry access_tokens per http-transport.ts) — was Postgres-only.
- CLAUDE_CODE.md: list_skills/core-tools verify note marked full-surface
  (on --surface verbs those tools aren't on the surface).
- skills/query/SKILL.md: --surface full INCLUDES the verbs; fallback is
  for pre-0.43 servers only.
- skills/brain-ops/SKILL.md: search/query tool labels updated to the
  #2416 reality (cheap hybrid vs hybrid + LLM expansion).
- search-modes.md: stale "keyword search returns chunks" label.
- RETRIEVAL.md: note the query op defaults expand:true per call.
- Counts: 43 skills -> 52 (README x2, INSTALL), 30 skills -> 52
  (CLAUDE.md), "30+ tools over MCP" -> 110 (README).
- TODOS.md: filed 2 code-vs-spec follow-ups (recall filter composition +
  limit cap; synthesize unavailable-mapping breadth) instead of silently
  editing the frozen contract.
- llms-full.txt + skills.lock.json regenerated; freshness guards green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 11:58:12 -07:00
Garry TanandClaude Fable 5 61da5e7732 docs: update project documentation for v0.43.0.0
Post-ship /document-release drift pass over the MEMORY_VERBS v1 +
#2416 query-steering waves:

- docs/tutorials/connect-coding-agent.md: the pasteable brain-first
  protocol still taught the pre-#2416 "Search first" rule and named
  only full-surface tools; step 1 now routes by question shape
  (exact tokens -> search, concept/landscape -> query first) and both
  steps map the five-verb surface names (recall/synthesize/remember).
  find_experts and query callouts marked with their surface.
- README.md + tutorial: "90-tool wall" count refreshed to 110 (the
  actual operation catalog).
- CLAUDE.md: contract-first op count ~90 -> ~110 with the v0.43.0.0
  MEMORY_VERBS clause; reference-map row for the memory-verbs
  protocol / --surface / conformance docs.
- TODOS.md: blank line before the v0.42.74.0 section header.
- llms-full.txt regenerated (build:llms; freshness test green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 11:48:20 -07:00
Garry TanandClaude Fable 5 a794c3b0d8 docs(protocol): reword so the #3502 docs-lint doesn't read a phantom command
Master's docs-lint (landed after this branch forked) scans for
`gbrain <verb>` references; the MEMORY_VERBS diagram's "gbrain reference
implementation" phrasing parsed as a nonexistent `gbrain reference`
command. Post-merge-only failure; both sides were green alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 10:23:31 -07:00
Garry TanandClaude Fable 5 6d8d218a80 docs(changelog): cover the #2416 query-steering wave under v0.43.0.0
The 0.43.0.0 entry described only MEMORY_VERBS; the #2416 retrieval-routing
wave rides the same release. Adds its user-facing summary and refreshes the
release date to the actual ship date.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 09:42:06 -07:00
Garry TanandClaude Fable 5 be48f9c599 fix(test): pin think's stub-path usage at null per the E2 accounting contract
The MEMORY_VERBS [E2] usage-accounting change deliberately moved "no LLM
ran" from `undefined` to `null` (the frozen cost-block spelling), but the
older #1698 stub-path test still pinned `undefined` and failed on every
run of this branch. The protected semantic is unchanged: no fake usage or
cost on the stub path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 09:42:06 -07:00
Garry Tan 1552d3ec80 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1 2026-08-08 09:33:20 -07:00
Garry TanandClaude Fable 5 f3b858cd01 feat(search): steer concept/landscape questions to query — descriptions, docs, lookup chain, CLI nudge (#2416)
The search/query routing guidance shipped a stale and backwards model:
SEARCH_DESCRIPTION still led with "Keyword search using full-text search"
(search has been cheap-hybrid — vector + keyword + RRF, expansion off —
since D4/D15), and the mandatory lookup chain in brain-first.md ordered
search-first with query only "if search is thin". For concept/landscape
questions ("all the X that do Y"), keyword+vector recall always returns
something, so the fallback never fires and expansion-recoverable
synonym-phrased matches drop silently.

- operations-descriptions.ts: fix the stale SEARCH lead (cheap-hybrid, no
  LLM expansion); both constants now route concept/synonym/landscape
  questions to query, cap the claim honestly (both are top-K — exhaustive
  enumeration belongs to list_pages), and keep every pinned redirect.
- brain-first.md: lookup chain is intent-driven (exact token → search;
  concept/landscape → query FIRST), cost claims corrected, and the
  anti-pattern is explicit: a nonzero search count is not a completeness
  signal. Same sweep in brain-ops/SKILL.md and query/SKILL.md, which
  duplicated the backwards order.
- search-modes.md: decision tree + quick-ref updated to the cheap-hybrid
  reality, new Tricky Spot #6 (count ≠ completeness), example names moved
  to placeholder people per the privacy convention. RETRIEVAL.md gets the
  matching one-liner.
- query-intent.ts: looksConceptShaped + conceptNudge — deterministic
  concept-shape classifier in the existing classifier home, tuned to favor
  false-negatives; cues owned by other routers (find_experts, salience
  ops) deliberately excluded. cli.ts prints the hint-only stderr nudge on
  BOTH result paths (local engine + thin-client routed), --quiet-gated;
  search stays the cheap hot path (never auto-reroutes).
- salience-llm-routing e2e: new concept-phrasings block asserts the
  descriptions actually route concept questions to query (verified live:
  20/20 — all 12 personal-query regressions still route to salience ops,
  all 8 concept phrasings route to query).
- KEY_FILES.md: query-intent entry updated to current state (the old
  entry pointed at a renamed file); llms bundle regenerated (no diff —
  entry is link-only).
- TODOS.md: MCP-envelope hint follow-up filed (remote agents are the
  higher-value half; needs a return-contract change kept out of this
  commit).

Verification: typecheck clean; 70 targeted unit tests + 20 live routing
evals green; all 24 check:all guards green; failing full-suite shards
triaged — think-pipeline.serial.test.ts:434 fails identically on the
clean tree (pre-existing on this branch, filed separately), the rest are
PGLite parallel-load contention that pass in isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 14:19:46 -07:00
Garry TanandClaude Opus 4.8 da80f91f0b Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
Catch up to master: 526 commits (up to v0.42.73.2 — CI PR-usefulness gate,
OAuth slug-prefix write fence, GitHub-release publishing, community fix waves,
dep CVE-pin overrides, Windows path fixes, silent-failure doctor batch, and more).

Conflicts resolved (9):
- VERSION / package.json: kept 0.43.0.0 (Cathedral 1 — higher semver); merged in
  master's new CVE-pin `overrides` block.
- CHANGELOG.md / TODOS.md: kept both sides (ours on top, master's below).
- src/cli.ts: CLI_ONLY = master's superset (now `export`) + our `protocol`;
  CLI_ONLY_SELF_HELP keeps both (`protocol` + master's `init`/`migrate`/`retrieval-upgrade`).
- src/commands/serve-http.ts: ServeHttpOptions keeps both new fields
  (`surface` ours, `printAdminToken` master).
- src/commands/serve.ts: runServeHttp call keeps both options + master's new
  finishHttpServe teardown; stdio path keeps master's boot-readiness deadline
  (#3273) while preserving our `start(engine, { surface })`.
- src/core/config.ts: config allowlist keeps both (`mcp_surface`/`protocol_installed_at`
  + `provider_chat_options`).
- src/core/think/index.ts: both sides added the same token-`usage` feature; unified
  to one nullable `usage` (interface field, local var, return) and kept master's
  additive `cost_usd?`. (Duplicate-identifier build break caught by typecheck and fixed.)

Verified: 3-line version audit agrees on 0.43.0.0; bun install reconciled bun.lock
against master's overrides; bun run typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-07 12:30:40 -07:00
Garry TanandClaude Opus 4.8 0176f11cd4 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
Brings in v0.42.58.0 (provider-agnostic AI gateway — env clobber, base-URL
/v1 normalization, embedding-dim validation; #1249 #1250 #1292 #2271 #2209).

Conflicts resolved (version trio + TODOS only; AI-gateway code auto-merged):
- VERSION / package.json: kept 0.43.0.0 (Cathedral 1 — higher semver wins).
- CHANGELOG.md / TODOS.md: kept both sides — ours (0.43.0.0) on top, master's
  0.42.58.0 entry / provider-agnostic follow-ups below.

Verified: 3-line version audit agrees on 0.43.0.0; bun.lock unchanged;
bun run typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 18:55:50 -07:00
Garry TanandClaude Opus 4.8 e9c92173f9 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
Brings in v0.42.55.0–v0.42.57.0 (security-hardening pass, Life Chronicle
temporal spine, PGLite live-lock-steal incident fix).

Conflicts resolved:
- VERSION / package.json: kept 0.43.0.0 (Cathedral 1 — higher semver wins);
  auto-merged master's new check:search-path npm script.
- CHANGELOG.md / TODOS.md: kept both sides (ours on top, master's below).
- src/commands/serve.ts: runServeHttp call keeps BOTH new options —
  enableDcrInsecure (master, #1353) and surface (ours, MEMORY_VERBS v1).

Verified: 3-line version audit agrees on 0.43.0.0; bun.lock unchanged
(no dep changes); bun run typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 02:47:58 -07:00
Garry Tan 962bb0d35f Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	TODOS.md
#	VERSION
#	package.json
2026-06-25 09:47:42 -07:00
Garry Tan ae27978053 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
2026-06-18 00:27:19 -07:00
Garry Tan ad36729a80 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
2026-06-17 08:35:05 -07:00
Garry Tan 0ed0adeba9 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	TODOS.md
#	VERSION
#	package.json
2026-06-17 07:40:48 -07:00
Garry Tan 6b7c6e49e1 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	TODOS.md
#	VERSION
#	package.json
#	src/cli.ts
2026-06-16 22:49:06 -07:00
Garry Tan 66ad939109 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	TODOS.md
#	VERSION
#	package.json
2026-06-16 15:14:41 -07:00
Garry TanandClaude Fable 5 6820dbf0ff fix(verbs): green the 3 failing CI shards — 3-seg migration version, hermetic synthesize test, fresh llms
Three real CI-shard failures from the propagation commit + master merges:

- migrations-registry (shard 10): v0_43_0 used a 4-segment version 0.43.0.0,
  but the registry enforces 3-segment semver (/^\d+\.\d+\.\d+$/). Changed
  the migration version to 0.43.0 (the RELEASE stays 0.43.0.0; migrations have
  always used 3-seg, e.g. 0.32.2). apply-migrations future-list fixtures
  updated to match.
- synthesize conformance test (shard 5): asserted a successful answer via the
  chat seam, but runThink builds its client through a real-key check (not the
  seam), so CI (credential-free) took the NO_ANTHROPIC_API_KEY path → the
  verb's unavailable conversion → isError. Now wraps the call in withEnv with a
  fake key AND the chat seam, so it's deterministic regardless of ambient
  credentials.
- build-llms (shard 8): the v0.42.44.0 master merge changed an inlined doc;
  regenerated llms.txt/llms-full.txt.

Cathedral 1 CI fixes (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 10:37:51 -07:00
Garry Tan 49ee3a7e7c Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
2026-06-15 15:53:25 -07:00
Garry TanandClaude Fable 5 d01185baf7 feat(verbs): propagate MEMORY_VERBS v1 to existing harnesses on upgrade
The verbs shipped reachable + self-describing, but an EXISTING install that
upgrades to 0.43 and keeps its skills had no active push toward them: no
post-upgrade notice (default surface stays 'full'), and no skill/guidance
routing — agents kept calling search/query/get_page/extract_facts.

Two additive propagation fixes:

- Post-upgrade feature pitch: src/commands/migrations/v0_43_0.ts (pitch-only,
  no-op idempotent orchestrator — the verbs ride existing tables, no schema/
  data migration). Registered in the migrations index so
  and the self-upgrade NOTIFY channel announce the five verbs +  +  on any upgrade past 0.43.
  Verified live: the pitch fires for an install upgrading from 0.42.43.0.
- Skill routing: skills/query (memory reads) and skills/brain-ops (read/write
  cycle) now teach the verbs — recall/entity/synthesize vs search/get_page/
  query, remember vs extract_facts, forget — with a fall-back-to-classic-ops
  note for older brains / full surface. The in-MCP tool descriptions already
  steered tool-list readers; this adds the narrative layer the audit found
  missing across all 29 skills.

apply-migrations future-list fixtures updated for the new 0.43.0.0 registry
entry. No DB migration; no contract change; verbs unaffected.

Cathedral 1 propagation (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 09:46:03 -07:00
Garry Tan 3c974d9597 Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1
# Conflicts:
#	CHANGELOG.md
#	TODOS.md
#	VERSION
#	package.json
#	src/cli.ts
2026-06-14 09:38:51 -07:00
Garry TanandClaude Fable 5 0b63783d95 fix(doctor): assign memory_verbs_usage check to the SKILL category set
The new memory_verbs_usage doctor check (Cathedral 1, E4) was added without a
category, tripping the doctor-categories drift guard (every check name must
belong to exactly one category set). It's an agent-capability adoption signal,
a sibling of retrieval_reflex_health — assigned to SKILL_CHECK_NAMES.

Cathedral 1 ship-review fix (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 11:06:09 -07:00
Garry TanandClaude Opus 4.7 1b24b0085d docs: surface --surface verbs in local-stdio quickstart paths for v0.43.0.0
The MEMORY_VERBS v1 quickstart flag (gbrain serve --surface verbs) was added to
the README quickstart, CLAUDE_CODE.md, and CODEX.md during the build but three
other docs that show the local-stdio wire-up still presented bare `gbrain serve`:
the connect-coding-agent Path B walkthrough (the doc the README links to),
INSTALL.md, and DEPLOY.md's Local stdio path. Brought them in line so the
five-verb surface is consistent across every local-quickstart entry point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 10:59:01 -07:00
Garry TanandClaude Fable 5 9c9de76d15 v0.43.0.0 feat(mcp): MEMORY_VERBS v1 — frozen five-verb memory protocol + conformance kit (cathedral 1)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:52:25 -07:00
Garry TanandClaude Fable 5 3f5ee1842a fix(verbs): close 3 ship-review P1s — forget cross-source, entity backlink leak, conformance exit code
Pre-landing review (codex, on the committed diff) caught three P1s the
plan-stage reviews flagged only in the abstract:

P1.1 trust boundary — forget expired by GLOBAL fact id with no source/visibility
scope: a remote caller in source A could expire facts in source B (incl.
private) by guessing ids. forgetFactInFence gains sourceId + worldOnly opts
(scope-check before any state inspection; out-of-scope → not_found, no
existence leak); the forget verb passes ctx.sourceId + worldOnly=remote.

P1.2 source isolation — entity card incoming edges + backlink_count leaked
across sources: getBacklinks(slug,{sourceId}) scopes only the TARGET page and
getBacklinkCounts has no source param, so a foreign-source page's slug surfaced
in a same-named entity's card. Replaced both with a both-sides-scoped query
(f.source_id = t.source_id = entity source, mentions excluded) inside the card
module — no engine-interface change.

P1.3 CLI exit code — protocol conformance/unknown-subcommand set
process.exitCode directly, which the PGLite/WASM force-exit seam ignores
(it reads the gbrain-owned setCliExitVerdict). A NOT CONFORMANT target printed
the banner but exited 0 — CI would green-light a non-conformant server. Now
routes through setCliExitVerdict; proven: unknown subcommand exits 1.

Regression tests: cross-source forget → not_found + foreign fact stays active;
remote can't forget a private fact; entity card omits foreign-source backlinks
from both edges and the count.

Cathedral 1 ship-review fixes (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 10:52:19 -07:00
Garry Tan 0bc02ea36b Merge remote-tracking branch 'origin/master' into garrytan/cathedral-1 2026-06-13 10:42:06 -07:00
Garry TanandClaude Fable 5 c3c046bac5 fix(verbs): verify-gate compliance — test seam, allow-comment placement, import hygiene
usage-log gains __setUsageLogPathForTests so the verb tests redirect the
sidecar without mutating process.env.GBRAIN_HOME (test-isolation lint);
the gbrain-allow-direct-insert rationale moves onto the insertFact call line
(system-of-record guard greps per-line); protocol.ts drops its unused
`operations` import (operations-filter-bypass guard — operationsByName only).
The synthesize unit test now drives the chat-transport seam (deterministic
success envelope + priced cost block, zero real API spend); the keyless
`unavailable` conversion stays covered end-to-end by the CI conformance step,
which runs credential-free.

Cathedral 1 verify pass (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 11:01:21 -07:00
Garry TanandClaude Fable 5 f60ba9c83d test+docs(protocol): conformance suite, p99 gate, MEMORY_VERBS_v1 doc, funnel
test/memory-verbs-conformance.test.ts: 26 tests through dispatchToolCall —
G1B superset regression (legacy fields byte-equal, additions allowed), budget
math incl. budget<first-item, keyword degradation (never an error), remember
provenance/ttl-trap/null-echo, remote round-trip via the world default +
private facts hidden, entity card schema-validated with the chat gateway
rigged to throw (zero-LLM guard) + all arms + fence test, synthesize
[EXPENSIVE + annotations + clean unavailable, forget idempotency/not_found,
writeSingleFact X1 supersession via the embed-transport seam + degraded
dedup, the F3 negative self-test (the runner FAILS lying servers: dropped
fields, bad enums, re-typed ids), fixture-mirror drift guard.
test/mcp-surface.test.ts: verbs=exactly 5, full=identity, dispatch-layer
fail-closed, strict flag parsing. test/entity-card-perf.slow.test.ts: 20K
pages/100K links/30K aliases/40K facts — measured p99 22.9ms vs the 100ms
contract + a recalibrated 50x ratio guard; wired into the CI perf job along
with stdio self-certification (23/23 CONFORMANT live). tool-defs byte-
stability test now pins per-op: ops WITHOUT annotations keep the exact
pre-v1 shape.

docs/protocol/MEMORY_VERBS_v1.md: the frozen contract — envelopes, error
codes, additive-forever policy, ttl forms (P30D rejected), status-not-
status_text, surface modes + why default full, per-harness installs, consent
line, conformance pass criteria + copyable --target examples, local-only
stats privacy, 200K manual bench recipe. README quickstart + docs/mcp pages
moved to --surface verbs with the claude-not-found recovery; llms-config
registered + bundles rebuilt; KEY_FILES entries for the 7 new modules.

Cathedral 1 stage 4/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:24:47 -07:00
Garry TanandClaude Fable 5 926579c051 feat(protocol): gbrain protocol — schemas, conformance kit, stats + TTHW
gbrain protocol [--json] emits the live verb input schemas + the hand-authored
RESPONSE_SCHEMAS + error contract + versioning policy. gbrain protocol
conformance certifies ANY MCP endpoint (default: self-spawned stdio; --target
http://...[--token] or a stdio command) against the embedded fixture set —
shape/enum/behavior/round-trip only, deterministic; entity-page cases seed via
put_page when available and skip honestly on verbs-only targets; synthesize is
cost-gated behind --synthesize (asserts the clean unavailable error on key-less
CI). Fixtures double as BrainBench seeds (test/fixtures/memory-verbs/cases.json
mirror). Self-certification passes 23/23 live.

gbrain protocol stats [--days N]: per-verb calls/error-rate/latency, budget
drops, entity hit rate, and measured TTHW (init stamps protocol_installed_at;
delta to the sidecar's first verb call) — local JSONL only, never uploaded.
init epilogues print the 3-command quickstart (mcp add --surface verbs +
key-free resurrection demo) with the visibility consent line. entity-card fix:
raw namespaced-slug input now matches exactly (slugify flattens slashes).

Cathedral 1 stage 3/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:11:52 -07:00
Garry TanandClaude Fable 5 cab7d81f54 feat(mcp): MEMORY_VERBS v1 surface mode + fail-closed dispatch + usage sidecar
gbrain serve --surface verbs|full (config mcp_surface registered in the merge
allowlist; default full — existing installs unchanged). Enforcement is
two-layer and fail-closed: filtered ListTools AND dispatchToolCall's new
allowedOps set, applied on stdio + BOTH HTTP paths (serve-http.ts OAuth and
the second http-transport.ts). ToolAnnotations passthrough in buildToolDefs +
the HTTP inline mapper, emitted only when an op defines them (byte-stability
for existing tools). Verb validation/internal errors speak the protocol
envelope (suggestion + protocol_version) at the dispatch layer.

E4 observability: per-verb JSONL sidecar (usage-log.ts — local only, never
uploaded; O_APPEND line-atomic, lock-free 10MB rotation) written from dispatch
so param-validation failures count; brain_id via gbrainPath; doctor gains the
memory_verbs_usage check. CLI: human-readable rendering for remember/entity/
synthesize with trailing --json escape; gbrain recall gains --query +
--budget-tokens routed through the recall OP (same arm MCP exercises) with the
budget footer; the forget verb carries no cliHint (CLI_ONLY forget dispatches
first and would shadow it).

Cathedral 1 stage 2/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:04:12 -07:00
Garry TanandClaude Fable 5 395a2d080f feat(verbs): MEMORY_VERBS v1 core — five frozen verbs over existing machinery
recall extended in place (G1B additive superset: protocol_version + string
fact_id/provenance on every response; query arm via cheap-hybrid with
keyword-only degradation when no embedding provider; op-layer budget packing
facts-first via the new generic packToBudget). remember/entity/synthesize/
forget as first-class Operations in src/core/verbs.ts (runtime leaf — TDZ-safe
both import directions) with MEMORY VERB descriptions, ToolAnnotations,
populated suggestions on every error (verbError + protocol ErrorCodes +
additive detail/protocol_version on OperationError.toJSON).

writeSingleFact (facts/write-single.ts): zero-LLM single-fact seam reusing the
pipeline's dedup; X1 deterministic supersession (same entity+kind, similar,
text differs -> supersedeId / fence expire+link); validUntil threaded through
FenceInputFact (was silently dropped). buildEntityCard (verbs/entity-card.ts):
reflex resolution arms + parallel depth-1 reads, per-arm degradation on
pre-page_aliases brains, near-miss suggestions with create_safety. runThink
gains additive usage aggregation; synthesize converts the no-LLM graceful
result to the unavailable protocol error and prices the cost block via
canonical model-pricing. RESPONSE_SCHEMAS registry + ERROR_SCHEMA for the
conformance kit.

Cathedral 1 stage 1/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:53:20 -07:00
65 changed files with 5218 additions and 213 deletions
+13
View File
@@ -187,6 +187,19 @@ jobs:
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
- run: bun install
- run: bun test test/entity-resolve-perf.slow.test.ts --timeout=300000
# MEMORY_VERBS v1 (Cathedral 1): the entity() p99 < 100ms contract gate
# (20K-page corpus + ratio guard) shares this runner — same perf-job
# shape, runs in parallel with the matrix.
- run: bun test test/entity-card-perf.slow.test.ts --timeout=300000
# Protocol self-certification: init a scratch brain and run the
# conformance kit against gbrain's own stdio server. --synthesize is
# safe here: no LLM key in CI, so it asserts the clean `unavailable`
# protocol error instead of spending tokens.
- name: MEMORY_VERBS conformance (self-certify, stdio)
run: |
export GBRAIN_HOME="$RUNNER_TEMP/gbrain-conformance"
bun run src/cli.ts init --pglite --no-embedding --non-interactive
bun run src/cli.ts protocol conformance --synthesize
test:
# Pure matrix shard — no verify, no serial. Each shard runs its slice
+76
View File
@@ -2,6 +2,82 @@
All notable changes to GBrain will be documented in this file.
## [0.43.0.0] - 2026-08-08
**Your agent now has five memory verbs it can actually reach.** Cathedral 1 freezes
a stable, versioned memory protocol — `recall`, `remember`, `entity`, `synthesize`,
`forget` — over the brain's operation catalog, the way Postgres speaks one wire
protocol to every client. Point any MCP harness at it (`claude mcp add gbrain --
gbrain serve --surface verbs`, or the Codex/OpenClaw equivalents) and the agent sees
exactly five self-describing tools instead of a wall of internal ops. Every response
carries what it is, why it matched, where it came from, and what it spent (the token
budget on `recall`, latency on `entity`, the full cost block on `synthesize`) — and the
contract never breaks: v1 field names and meanings are frozen, changes are
additive-forever.
What you can do now that you couldn't before:
- **Remember a fact once, recall it in a fresh session — in any harness.** `remember`
takes mandatory provenance (where the fact came from) and an optional expiry, dedupes
against what's already known, and supersedes the old fact when it changes
("X joined acme-example" → "X left acme-example" — the outdated fact expires, the
history stays). `recall` retrieves saved facts and,
with a query, budget-packed page snippets — the server enforces the token budget and
tells you what it dropped instead of trusting the client to trim.
- **Look up one person/company/project as a compact card in well under 100ms, zero LLM
calls.** `entity` resolves a name to a privacy-safe card (who/what, aliases,
last-touched, open threads, top typed edges) — or, on a miss, near-miss suggestions
instead of a dead end.
- **Reason across pages when you actually need it.** `synthesize` is the explicitly
expensive verb (it says so in its own description), with a best-effort cost block so
agents choose it deliberately. No API key configured? It says `unavailable` with a
fix, never a fake answer.
- **Certify any memory server against the contract.** `gbrain protocol --json` publishes
the machine-readable spec; `gbrain protocol conformance [--target <endpoint>]` runs the
frozen-contract test suite against gbrain's own server or any MCP endpoint — and
provably fails servers that don't comply. `gbrain protocol stats` shows per-verb
adoption and your real time-to-first-use, all from a local file that never leaves
your machine.
`gbrain serve` keeps every operation by default (`--surface full`) — existing installs
are unchanged. The new `--surface verbs` is the quickstart surface for agents. No schema
migration; the verbs ride the existing facts, pages, and typed-graph tables. The full
contract, per-harness install blocks, and the additive-forever versioning policy live in
[docs/protocol/MEMORY_VERBS_v1.md](docs/protocol/MEMORY_VERBS_v1.md).
**This release also fixes which search verb your agent reaches for (#2416).** The
`search` and `query` tool descriptions, the mandatory lookup chain, and the search
guides had drifted from reality: they described `search` as keyword-only (it has been
cheap hybrid — vector + keyword, no LLM expansion — for many releases) and told agents
to try `search` first for everything, falling back to `query` only when results looked
"thin". For concept questions ("all the X that do Y", "the ecosystem around Z") that
fallback never fired, and synonym-phrased matches dropped silently.
- **Concept questions now route to `query` by default.** The tool descriptions and the
lookup-chain convention are intent-driven: exact known tokens → `search` (cheaper, no
expansion call); concept / synonym / exhaustive-set questions → `query` first. Verified
with a live LLM routing eval: all concept phrasings route to `query`, and personal
questions still route to the salience ops.
- **"Got results" is no longer treated as "got everything."** The descriptions, docs, and
conventions now say it plainly: a populated `search` result set is not proof of
coverage, and `query` is still top-K — literal exhaustive enumeration belongs to
`list_pages` pagination.
- **The CLI nudges you when it can help.** A concept-shaped `gbrain search "..."` prints a
one-line hint on stderr suggesting the equivalent `gbrain query` call (results stay
clean on stdout; `--quiet` silences it; never auto-reroutes).
- **`think` cost accounting reads consistently.** When no LLM call ran (stubbed or no
API key), `usage` is now uniformly `null` in `--json` output rather than sometimes
missing.
To take advantage of v0.43.0.0: re-run `gbrain serve` with `--surface verbs` to give
your coding agent the five-verb memory protocol (or keep `--surface full` for the
complete operation catalog — both speak the verbs). Run `gbrain protocol conformance`
to self-certify, and `gbrain protocol stats` to watch adoption. Memories your agent
saves are readable by every agent connected to the brain by default; pass
`visibility: "private"` for local-only facts. If your agent instructions or skill
files copy the old "search first, query if thin" rule, refresh them from
`skills/conventions/brain-first.md` — the shipped skillpack carries the corrected
routing.
## [0.42.76.0] - 2026-08-08
**Mistyped or unsupported flags now fail loudly instead of being silently ignored — including the ones that were supposed to make a command safe.**
+3 -2
View File
@@ -38,7 +38,7 @@ mount, CEO-class with multiple team brains) and
## Architecture
Contract-first: `src/core/operations.ts` defines ~90 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`). CLI and MCP
Contract-first: `src/core/operations.ts` defines ~110 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`; v0.43.0.0 adds the five frozen MEMORY_VERBS — `recall`, `remember`, `entity`, `synthesize`, `forget` — servable alone via `gbrain serve --surface verbs`, see `docs/protocol/MEMORY_VERBS_v1.md`). CLI and MCP
server are both generated from this single source. Engine factory (`src/core/engine-factory.ts`)
dynamically imports the configured engine (`'pglite'` or `'postgres'`). Skills are fat
markdown files (tool-agnostic, work with both CLI and plugin contexts).
@@ -119,6 +119,7 @@ detail on demand.)
| push-based context (volunteer/watch/reflex window) | `docs/guides/push-context.md` |
| schema packs / page types / extraction | `docs/architecture/schema-packs.md`, `type-taxonomy.md`, `lens-packs.md` |
| thin-client / remote MCP / cross-modal | `docs/architecture/thin-client.md` |
| memory verbs / MCP tool surface (`--surface`) / conformance | `docs/protocol/MEMORY_VERBS_v1.md` + the `verbs*`/`surface.ts`/`protocol.ts` entries in `KEY_FILES.md` |
| the CLI surface (commands + flags) | `gbrain --help` / `gbrain --tools-json`, plus the relevant `KEY_FILES.md` entry |
| running or writing tests | `docs/TESTING.md` |
| bulk-command progress wiring | `docs/progress-events.md` |
@@ -272,7 +273,7 @@ audit trail lives in the source repo's git history.
## Skills
Read the skill files in `skills/` before doing brain operations. GBrain ships 30 skills
Read the skill files in `skills/` before doing brain operations. GBrain ships 52 skills
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
+8 -6
View File
@@ -91,7 +91,7 @@ Retrieve and follow the instructions at:
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
```
The agent installs GBrain, creates the brain, asks for your API keys, loads 43 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
The agent installs GBrain, creates the brain, asks for your API keys, loads 52 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
> **Never set up an AI agent platform before?** The [personal-brain tutorial](docs/tutorials/personal-brain.md) walks the whole path end-to-end — picking OpenClaw vs Hermes, deploying it, pointing it at INSTALL_FOR_AGENTS.md, getting the API keys, and verifying the first query. Start there if any of the above is new.
@@ -99,13 +99,15 @@ The agent installs GBrain, creates the brain, asks for your API keys, loads 43 s
Already running Claude Code or Codex? There are two ways to wire GBrain in, depending on what you want.
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel:
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel. `--surface verbs` gives your agent the five-verb memory protocol (`recall`, `remember`, `entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](docs/protocol/MEMORY_VERBS_v1.md), frozen + additive-forever) instead of the full 110-tool wall; drop the flag for every operation:
```bash
gbrain init --pglite # 2-second local brain (no Docker)
claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve
gbrain init --pglite # 2-second local brain (no Docker)
claude mcp add gbrain -- gbrain serve --surface verbs # or: codex mcp add gbrain -- gbrain serve --surface verbs
```
If `claude` is not found, install Claude Code first — or use the per-harness blocks in the [protocol doc](docs/protocol/MEMORY_VERBS_v1.md). Heads-up: memories agents save default to brain-wide visibility (every connected agent can recall them); pass `visibility: "private"` for local-only facts.
**Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff:
```bash
@@ -117,7 +119,7 @@ gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex --install
### Install the full autonomous setup into your existing agent
Want the whole thing — local brain, 43 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
Want the whole thing — local brain, 52 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
```
Retrieve and follow the instructions at:
@@ -140,7 +142,7 @@ Postgres-at-scale, Supabase, and thin-client setup paths live in [`docs/INSTALL.
### Connect GBrain to your AI client (MCP)
GBrain exposes 30+ tools over MCP (stdio and HTTP). The specific snippet depends on which client you use:
GBrain exposes 110 tools over MCP (stdio and HTTP) — or exactly the five memory verbs with `--surface verbs`. The specific snippet depends on which client you use:
- **[Claude Code](docs/mcp/CLAUDE_CODE.md)** — local: one command, `claude mcp add gbrain -- gbrain serve` (zero server, zero tunnel). Remote with just a bearer token: `gbrain connect https://your-host/mcp --token gbrain_xxx` prints a paste-ready block (or `--install` wires it up and smoke-tests the token).
- **[Codex](docs/mcp/CODEX.md)** — `gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex` (or `--install`). Codex reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands in Codex config.
+55
View File
@@ -1,5 +1,59 @@
# TODOS
## #2416 follow-ups (query-steering wave)
- [ ] **P2 — MCP-envelope `hint` field for concept-shaped `search` calls.**
**What:** surface the concept→query nudge to remote/MCP agent callers, not
just the CLI. **Why:** MCP agents are the primary misrouting class the
#2416 issue describes; the shipped CLI stderr nudge covers the caller class
*least* at risk. **Context:** the `search` op returns a bare
`SearchResult[]` (`src/core/operations.ts` — both return sites), so a hint
needs an envelope change that ripples into `formatResult`, MCP
serialization, and array-shape tests — deliberately kept out of the atomic
#2416 commit. The pure classifier already exists
(`looksConceptShaped`/`conceptNudge` in `src/core/search/query-intent.ts`);
only the transport is missing. Consider a sibling metadata channel (like
`_meta.metric_glossary`) rather than changing the array shape.
**Depends on:** agreeing an envelope pattern that doesn't break existing
MCP consumers.
## MEMORY_VERBS v1 follow-ups (filed v0.43.0.0 — Cathedral 1)
Deferred from the Cathedral 1 ship (CEO review, EXPANSION mode). Both are
additive to the frozen v1 contract — neither breaks it. See plan + GSTACK
REVIEW REPORT at `~/.claude/plans/system-instruction-you-are-working-agile-iverson.md`
and the scope record at `~/.gstack/projects/garrytan-gbrain/ceo-plans/2026-06-12-memory-verbs-protocol.md`.
- [ ] **P3 — external-implementation certification PROGRAM.** The conformance
TOOLING shipped (`gbrain protocol conformance --target <endpoint>`); the
PROGRAM around it (badges, a registry of conformant implementations, listed
third-party servers) waits for a second implementation to exist. **Why:** the
protocol-not-product thesis only pays off once someone else implements
MEMORY_VERBS; until then a certification program certifies an empty set.
**Where:** new — would build on `src/commands/protocol.ts` conformance output.
- [ ] **P3 — persistent open-threads model for the entity card.** v1 derives
`entity.open_threads` from active commitment-kind facts + recent timeline
entries (best-effort, possibly empty). A richer model (a real threads table:
conversation id, opened/closed state, last activity) would make open-threads
authoritative. **Why:** the card's open-threads field is the weakest signal
in v1; a first-class threads store would make it load-bearing. **Where:**
`src/core/verbs/entity-card.ts` open-threads assembly + a new schema table
(additive — the card field already exists, so this is a quality upgrade, not
a contract change).
- [ ] **P2 — `recall` filter composition vs the spec (found by the v0.43.0.0
cross-model doc review).** The handler dispatch is first-match
(`supersessions` > `entity` > `session_id` > `since`), so `since` is
silently ignored when `entity`/`session_id` is supplied, and `limit` has no
server-side cap. Either compose the filters (additive — the spec's "filters
the FACTS arm" wording already reads that way) or spell the precedence out
in `docs/protocol/MEMORY_VERBS_v1.md`. **Where:** the `recall` handler in
`src/core/operations.ts`.
- [ ] **P3 — widen `synthesize`'s `unavailable` mapping.** Only the
missing-key gateway warning maps to the `unavailable` error today; other
no-usable-model failures can surface as `internal` (contract-legal but less
actionable) or, worst case, a stubbed success. Audit the gateway failure
modes and map every model-unusable path to `unavailable` with a fix
suggestion. **Where:** the `synthesize` handler in `src/core/verbs.ts`.
## Fix-wave 1 follow-ups (upgrade-wedge + trust-seam wave, 2026-08)
Deferred from the un-wedge-v121 hotfix wave (eng review + codex outside voice
@@ -135,6 +189,7 @@ voice CLEARED). None block the wave.
clause (`src/mcp/http-transport.ts` validateToken). Apply the same pattern —
one fewer write per request on the `serve --http` hot path.
Where: `src/core/oauth-provider.ts`.
## v0.42.67.0 follow-ups (Windows build tooling)
Filed as follow-ups from v0.42.67.0 (`.gitattributes` LF pin for `*.sh` +
+1 -1
View File
@@ -1 +1 @@
0.42.76.0
0.43.0.0
+5 -4
View File
@@ -9,7 +9,7 @@ Already running [OpenClaw](https://github.com/garrytan/openclaw) or [Hermes](htt
```bash
bun install -g github:garrytan/gbrain
gbrain init --pglite # 2 seconds; no server
gbrain skillpack scaffold --all # 43 skills scaffolded into your agent workspace
gbrain skillpack scaffold --all # 52 skills scaffolded into your agent workspace
gbrain doctor # green checks all the way down
```
@@ -58,16 +58,17 @@ gbrain autopilot --install # background daemon for nightly enrichment
**Wire this same local brain into your coding agent** — zero server, zero token:
```bash
claude mcp add gbrain -- gbrain serve # Claude Code
codex mcp add gbrain -- gbrain serve # Codex
claude mcp add gbrain -- gbrain serve --surface verbs # Claude Code
codex mcp add gbrain -- gbrain serve --surface verbs # Codex
```
The agent spawns `gbrain serve` as a stdio subprocess against your local brain. Full walkthrough (both this local path and connecting to a remote brain), plus the brain-first protocol to paste into `CLAUDE.md` / `AGENTS.md`: **[Give your coding agent a memory](tutorials/connect-coding-agent.md)**.
The agent spawns `gbrain serve` as a stdio subprocess against your local brain. `--surface verbs` gives the agent the five-verb memory protocol (`recall`, `remember`, `entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](protocol/MEMORY_VERBS_v1.md)) instead of the full tool catalog; drop the flag (default `full`) for every operation. Full walkthrough (both this local path and connecting to a remote brain), plus the brain-first protocol to paste into `CLAUDE.md` / `AGENTS.md`: **[Give your coding agent a memory](tutorials/connect-coding-agent.md)**.
## 3. MCP server (any MCP client)
```bash
gbrain serve # stdio MCP (Claude Desktop / Code / Cursor)
gbrain serve --surface verbs # stdio MCP, just the 5 memory verbs (quickstart)
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard
```
+8 -1
View File
@@ -73,7 +73,7 @@ per-release `**vX.Y.Z:**` narration — CI enforces this
- `src/commands/code-def.ts` + `src/commands/code-refs.ts` — symbol definition + references lookup. Query `content_chunks.symbol_name` or chunk_text ILIKE with `page_kind='code'` filter. Auto-JSON when stdout is not a TTY (gh-CLI convention). Bypass the standard `searchKeyword` `DISTINCT ON (slug)` collapse so multiple call-sites from the same file surface. The JSON envelope (CLI + the `code_def`/`code_refs` MCP ops) carries `status` + `ready` from `src/core/code-graph-readiness.ts` so a `count:0` result is distinguishable as `not_built` (no code indexed) vs `ready` (genuinely no match); human output prints a one-line hint when not ready.
- `src/core/code-graph-readiness.ts` — typed readiness signal shared by the four code-* surfaces (`code-def`/`code-refs`/`code-callers`/`code-callees`). `resolveCodeReadiness(engine, {kind:'symbol'|'edge', count, sourceId?, allSources?})` returns `{status:'not_built'|'indexing'|'ready'|'unknown', ready, has_code, pending_edges}`. `count>0` short-circuits to `ready` with no query; on empty it runs `EXISTS` probes against `content_chunks` JOIN `pages` (`page_kind='code'`) — no `page_kind` index needed, and the pending probe rides the partial `idx_content_chunks_edges_backfill`. `kind:'symbol'` (code-def/refs) is 2-state + brain-wide because symbol metadata is set at chunk time; `kind:'edge'` (code-callers/callees) is 3-state + source-scoped, with the pending predicate mirroring the resolver (`edges_backfilled_at IS NULL OR < EDGE_EXTRACTOR_VERSION_TS` from `src/core/chunkers/symbol-resolver.ts`) so a resolver-version bump never falsely reports `ready`. Probe scope matches each command's result-query `deleted_at` posture (def/refs don't filter `deleted_at`, so neither do the probes). Any DB error returns `status:'unknown'` (fail-open; never breaks the command). `readinessHint(r)` renders the human one-liner. Wired into `code-def.ts`/`code-refs.ts` (brain-wide), `code-callers.ts`/`code-callees.ts` (resolved `sourceId`/`allSources`), and all four `code_*` MCP op handlers in `src/core/operations.ts`. Pinned by `test/code-graph-readiness.test.ts` + readiness-envelope cases in `test/e2e/code-intel-mcp-ops-pglite.test.ts`.
- `src/core/search/` — Hybrid search: vector + keyword + RRF + multi-query expansion + dedup. `searchKeyword`/`searchKeywordChunks`/`searchVector` apply source-aware ranking at the SQL layer (curated content like `originals/`, `concepts/`, `writing/` outranks bulk content like `<fork>/chat/`, `daily/`, `media/x/`). `searchVector` uses a two-stage CTE so source-boost re-ranking doesn't kill the HNSW index. Hard-exclude prefixes (`test/`, `archive/`, `attachments/`, `.raw/` by default) filter at retrieval, not post-rank. Both gates honor `detail !== 'high'` so temporal queries surface chat pages normally.
- `src/core/search/intent.ts`Query intent classifier (entity/temporal/event/general → auto-selects detail level).
- `src/core/search/query-intent.ts`Deterministic query classifiers (pure, no LLM): `classifyQuery`/`classifyQueryIntent` (entity/temporal/event/general → auto-selects detail level, salience/recency/modality axes), `isAmbiguousModalityQuery` (LLM-escalation gate), and the #2416 concept-shape pair — `looksConceptShaped` (fuzzy-quantifier/landscape cues minus exact-identifier anti-signals, tuned to favor false-negatives; cues owned by other routers like "who are the"/find_experts and bare "anything"/salience are deliberately excluded) + `conceptNudge` (full one-line CLI hint string steering a concept-shaped `search` toward `query`; consumed by `maybePrintConceptNudge` in `src/cli.ts` on BOTH the local-engine and thin-client result paths, stderr-only, `--quiet`-gated). Pinned by `test/query-intent-concept.test.ts` + `test/cli-concept-nudge.test.ts`.
- `src/core/search/eval.ts` — Retrieval eval harness: P@k, R@k, MRR, nDCG@k metrics + runEval() orchestrator.
- `src/core/search/source-boost.ts` — Source-type boost map keyed by slug prefix. `DEFAULT_SOURCE_BOOSTS` (originals/ 1.5, concepts/ 1.3, writing/ 1.4, people/companies/deals/ 1.2, daily/ 0.8, media/x/ 0.7, <fork>/chat/ 0.5, archive/ 0.5, extracts/ 0.3) and `DEFAULT_HARD_EXCLUDES` (test/, attachments/, .raw/). `archive/` is DEMOTED (findable, ranked below curated), not hard-excluded — archive holds high-signal history users expect to retrieve; the demote is a prior at the SQL/fusion layer and the cross-encoder reranker can still promote a strongly-matching archive page. `parseSourceBoostEnv`/`parseHardExcludesEnv` parse comma-separated `prefix:factor` pairs from `GBRAIN_SOURCE_BOOST`/`GBRAIN_SEARCH_EXCLUDE`. `resolveBoostMap` and `resolveHardExcludes` merge defaults + env + caller `SearchOpts.exclude_slug_prefixes`/`include_slug_prefixes`. The surviving exclude policy is auditable via the `hidden_by_search_policy` doctor check (`src/commands/doctor.ts`, local + remote paths) which counts chunked pages withheld per active exclude prefix, reusing `resolveHardExcludes` + `buildVisibilityClause` + the exported `escapeLikePattern`.
- `src/core/search/sql-ranking.ts` — Pure SQL string builders. `buildSourceFactorCase(slugColumn, boostMap, detail)` emits a CASE with longest-prefix-match wins (returns literal `'1.0'` when `detail === 'high'` for temporal-bypass parity with COMPILED_TRUTH_BOOST). `buildHardExcludeClause(slugColumn, prefixes)` emits `NOT (col LIKE 'p1%' OR col LIKE 'p2%')` — OR-chain wrapped in NOT, NOT `NOT LIKE ALL/ANY` (those quantifiers don't express set-exclusion). LIKE meta-character escape covers all three of `%`, `_`, AND `\` (backslash is Postgres LIKE's default escape char). Single-quote doubling on SQL string literals so injection-style inputs are inert text. `buildBestPerPagePoolCte(...)` is the shared per-page max-pool CTE both engines' `searchVector` inject — instead of returning the single best chunk per page from an inner `ORDER BY embedding <=> vec LIMIT N` (which let a page lose to a neighbor on ONE weak chunk while its strong chunk sat just below the inner cut), the CTE pools the BEST chunk score per `(source_id, slug)` composite key so a page surfaces on its strongest evidence; composite key (not bare slug) keeps multi-source brains correct; single source of truth so the two engines can't drift.
@@ -276,6 +276,13 @@ per-release `**vX.Y.Z:**` narration — CI enforces this
- `src/core/minions/plugin-loader.ts``GBRAIN_PLUGIN_PATH` discovery. Absolute paths only, left-wins collision, `gbrain.plugin.json` with `plugin_version: "gbrain-plugin-v1"`, plugins ship DEFS only (no new tools), `allowed_tools:` validated at load time against the derived registry.
- `src/core/minions/tools/brain-allowlist.ts` — derives the subagent tool registry from `src/core/operations.ts` (15-name allow-list, size pinned by `test/brain-allowlist.serial.test.ts`). Includes `add_timeline_entry` (the canonical timeline write), fenced server-side by the same `enforceSubagentSlugFence` policy as `put_page`. By default `put_page` schema is namespace-wrapped per subagent (`^wiki/agents/<subagentId>/.+`). When `BuildBrainToolsOpts.allowedSlugPrefixes` is set, the put_page schema describes the prefix list to the model and the OperationContext is threaded with `allowedSlugPrefixes` — trust comes from `PROTECTED_JOB_NAMES` gating subagent submission (MCP cannot reach this field); only cycle.ts (synthesize/patterns) and direct CLI submitters set it. Allow-list includes `get_recent_salience` + `find_anomalies` but deliberately NOT `get_recent_transcripts` (all subagent calls run `ctx.remote === true` and the trust gate rejects remote callers, so it would always reject; the cycle synthesize phase calls `discoverTranscripts` directly instead). `paramsToInputSchema()` consumes `paramDefToSchema` from `src/mcp/tool-defs.ts`; required-aggregation at the tool-def level stays here (the shared helper is per-param).
- `src/mcp/tool-defs.ts``buildToolDefs(ops)` helper; MCP server + subagent tool registry both call it, byte-for-byte equivalence pinned by `test/mcp-tool-defs.test.ts`. Exports the recursive `paramDefToSchema(p: ParamDef)` — single source of truth for ParamDef→JSON Schema mapping shared by three consumers: `buildToolDefs` (stdio MCP), `src/commands/serve-http.ts:837` (HTTP MCP `tools/list`), and `src/core/minions/tools/brain-allowlist.ts:84` (subagent registry). Recursive on `items` so nested array-of-arrays preserves inner shape on the wire. Key ordering (type, description, enum, default, items) is intentional so `JSON.stringify` output stays byte-stable. `test/mcp-tool-defs.test.ts` has a `findArrayWithoutItems` walker that fails on any `type: 'array'` lacking `items.type`.
- `src/core/verbs.ts` — MEMORY_VERBS v1 (Cathedral 1): the four new frozen protocol verbs (`remember`, `entity`, `synthesize`, `forget`) as first-class Operations, plus `MEMORY_VERBS_VERSION` (single source of truth, =1), `VERB_NAMES`, the hand-authored `RESPONSE_SCHEMAS` registry (Operation carries input params only; response shapes live here and conformance validates LIVE responses against them), and `ERROR_SCHEMA`. The fifth verb is the extended `recall` op in operations.ts. RUNTIME LEAF invariant: operations.ts spreads `verbOperations` into its array at module-eval time, so this file must never statically import operations.ts VALUES (type-only imports fine; handlers use dynamic import) — violating it reintroduces a TDZ crash on whichever module evaluates second. Every verb error carries a populated `suggestion` + `protocol_version` (via `verbError` in operations.ts). The `forget` verb deliberately has NO cliHints (CLI_ONLY `forget` dispatches first and would shadow it). Frozen contract: docs/protocol/MEMORY_VERBS_v1.md; pinned by test/memory-verbs-conformance.test.ts.
- `src/core/verbs/entity-card.ts``buildEntityCard(engine, sourceId, name, {remote})`: the zero-LLM sub-100ms card behind the `entity` verb. Resolution reuses the Retrieval Reflex arms (alias > exact title/slug > slug-suffix; exact-slug candidates include the RAW input because slugify flattens slashes in namespaced slugs); ties break on GREATEST(updated_at, last_retrieved_at). Per-arm degradation: a pre-page_aliases brain still resolves via arm 2 (aka returns empty, never throws). Card assembly is a parallel Promise.all of depth-1 indexed reads (page row, alias reverse lookup, getLinks+getBacklinks mentions-excluded cap 10, getBacklinkCounts, getTimeline(5), listFactsByEntity world-only-when-remote); deliberately NOT the recursive-CTE traversePaths — the card is a latency contract (CI gate: test/entity-card-perf.slow.test.ts, p99 < 100ms × GBRAIN_PERF_BUDGET_MULTIPLIER + 50× getPage-p50 ratio guard on a 20K corpus). `summary` runs through the exported `safeSynopsis` (the get_page fence boundary). Miss → keyword near-miss suggestions with create_safety hints.
- `src/core/verbs/usage-log.ts` — E4 observability sidecar: one JSONL line per verb call at `~/.gbrain/integrations/memory-verbs/usage.jsonl` (gbrainPath — GBRAIN_HOME honored; `brainId()` = the resolved gbrain home). LOCAL ONLY, never uploaded, stats-only (lock-free 10MB rotation may drop lines; O_APPEND line-atomic, best-effort on Windows). `logVerbUsage` is fire-and-forget (never awaited, never throws); written from the DISPATCH layer so param-validation failures count. `readVerbUsage`/`earliestVerbUsageTs` feed `gbrain protocol stats` (incl. measured TTHW vs the init-stamped `protocol_installed_at`) and the doctor `memory_verbs_usage` check.
- `src/core/verbs/conformance.ts` + `src/core/verbs/conformance-fixtures.ts` — the conformance runner core (transport-agnostic: minimal `ConformanceClient` = list_tools + call_tool) and the embedded fixture set. Deterministic by construction: shape/enum/behavior/round-trip checks only, never ranking quality. Validation is NON-STRICT on extra fields (additive-forever means unknown fields are always legal). Entity-card cases seed via put_page when the target exposes it and skip honestly on verbs-only targets; synthesize is cost-gated behind --synthesize. `validateAgainstSchema` is a minimal JSON-Schema-subset validator (type unions, required, properties, enum, const, items). Fixtures mirror to `test/fixtures/memory-verbs/cases.json` (BrainBench seeds; drift-guarded by the conformance test). The negative self-test (test/memory-verbs-conformance.test.ts) proves the runner FAILS a lying server.
- `src/core/facts/write-single.ts``writeSingleFact(fact, ctx)`: the zero-LLM single-fact seam behind `remember` [E1]. `runFactsPipeline` is extraction-first (LLM-gated) and cannot back a pre-formed fact; this reuses the pipeline's post-extraction stages directly: resolve → embedding-cosine dedup (same 0.95 threshold) → fence-first write with the same legacy DB-only fallbacks (thin-client, unparented, stub-guard). Supersession [X1]: deterministic rule — same entity_slug + same kind + similarity ≥ threshold + DIFFERENT text ⇒ the new fact supersedes (fence path: append new + forgetFactInFence(old) + superseded_by link; DB path: engine insertFact supersedeId). Provenance lands on NewFact.source verbatim (no FactsBackstopCtx). No embedding provider ⇒ `degraded_dedup: true` (near-duplicates may insert — documented).
- `src/mcp/surface.ts` — MCP tool-surface modes: `'verbs'` (exactly the ops marked `verb: true`) | `'full'` (default — identity; existing installs unchanged). `parseSurfaceFlag` (strict, loud reject), `resolveSurface` (flag > config `mcp_surface` > full), `filterOpsForSurface`, `allowedOpNames`. Enforcement is TWO-layer and fail-closed: the advertised list AND `dispatchToolCall`'s `allowedOps` set (a hidden op returns `unknown_tool` even when called by name) — applied on stdio (server.ts) and BOTH HTTP paths (serve-http.ts after `!localOnly`, and the second transport http-transport.ts). Pinned by test/mcp-surface.test.ts.
- `src/commands/protocol.ts``gbrain protocol [--json] | conformance [--target <http-url|stdio-cmd>] [--token] [--synthesize] | stats [--days N]`. `--json` emits input schemas from the LIVE Operation defs + RESPONSE_SCHEMAS (doc/code can't drift). Conformance default target self-spawns gbrain's own stdio server (dev `.ts` entry vs compiled binary both handled); CI certifies stdio with --synthesize (no key ⇒ asserts the clean `unavailable` error). Stats aggregates the usage sidecar + the measured TTHW; output states "local JSONL only — never uploaded". CLI_ONLY + SELF_HELP wired in cli.ts; no pre-bound engine.
- `src/core/minions/attachments.ts` — Attachment validation (path traversal, null byte, oversize, base64, duplicate detection).
- `src/commands/agent.ts``gbrain agent run <prompt> [flags]` CLI. Submits `subagent` (or N children + 1 aggregator) under `{allowProtectedSubmit: true}`. Single-entry `--fanout-manifest` short-circuits. Children get `on_child_fail: 'continue'` + `max_stalled: 3`. `--follow` is the default on TTY; streams logs + polls `waitForCompletion` in parallel. Ctrl-C detaches, does not cancel.
- `src/commands/agent-logs.ts``gbrain agent logs <job> [--follow] [--since]`. Merges JSONL heartbeat audit + `subagent_messages` into a chronological timeline. `parseSince` accepts ISO-8601 or relative (`5m`, `1h`, `2d`). Transcript tail renders only for terminal jobs.
+6 -2
View File
@@ -97,7 +97,11 @@ Promote or reject them via `gbrain extraction-pending` / `gbrain
extraction-review`.
The `search` MCP/CLI op is **cheap-hybrid** (vector + keyword + RRF + pool +
title + alias, expansion off); `query` is the full-control variant. NamedThingBench
title + alias, expansion off); `query` is the full-control variant. Route
concept / landscape / "all-of-X" questions to `query` — expansion recovers
synonym-phrased matches `search` can miss, and a populated `search` result set
is not proof of coverage (both are top-K; exhaustive enumeration belongs to
`list_pages`). NamedThingBench
(`gbrain eval retrieval-quality`) gates these families on every PR. Diagnose a
specific miss with `gbrain search diagnose "<q>" --target <slug>`.
@@ -116,7 +120,7 @@ The classifier is deterministic (no LLM call). Wrong classification degrades gra
For `detail: 'high'` searches, `src/core/search/expansion.ts` runs a Haiku-class LLM call to produce 2-3 query variants. Each variant runs through the full hybrid stack; results merge via RRF. Catches synonym misses without recall loss.
Expansion is opt-in per mode bundle (`tokenmax` on by default; `balanced` + `conservative` off). Default off in the cheap tiers because the LLM call adds ~$0.001/query and ~200ms — real money at scale.
Expansion is opt-in per mode bundle (`tokenmax` on by default; `balanced` + `conservative` off). Default off in the cheap tiers because the LLM call adds ~$0.001/query and ~200ms — real money at scale. The `query` op is the exception: it defaults `expand: true` per call (pass `expand: false` to opt out) — expansion-by-default is what makes it the concept/landscape verb.
## Putting it together
+28 -19
View File
@@ -15,40 +15,48 @@ on user_asks_about(topic):
if know_exact_slug(topic):
# MODE 3: Direct get -- instant, no search overhead
result = gbrain get <slug>
# e.g., "Tell me about Pedro" -> gbrain get pedro-franceschi
# e.g., "Tell me about Alice" -> gbrain get alice-example
# Returns the FULL page -- compiled truth + timeline
elif topic.is_exact_name or topic.is_keyword:
# MODE 1: Keyword search -- fast, no embeddings needed, day-one ready
# MODE 1: Cheap-hybrid search -- vector + keyword + RRF, NO LLM
# expansion. Embeds the query when embeddings are configured; the
# keyword arm still works day-one without them (keyword-only is
# also available via the search.mcp_keyword_only opt-out).
results = gbrain search "{name_or_keyword}"
# e.g., "Find anything about Series A" -> gbrain search "Series A"
# Returns CHUNKS, not full pages
# IMPORTANT: keyword search returns chunks
# IMPORTANT: search returns chunks
# If the chunk confirms relevance, THEN load the full page:
if chunk.confirms_relevance:
full_page = gbrain get <slug_from_chunk>
elif topic.is_semantic_question:
# MODE 2: Hybrid search -- semantic + keyword, needs embeddings
elif topic.is_semantic_question or topic.is_concept_or_landscape:
# MODE 2: Full hybrid -- adds multi-query LLM expansion on top of
# vector + keyword + RRF. Owns concept / landscape / "all-of-X"
# questions: expansion recovers synonym- and outcome-phrased
# matches a single embedding misses. Costs one LLM expansion call
# per query -- worth it for these question shapes.
results = gbrain query "{natural language question}"
# e.g., "Who do I know at fintech companies?" -> gbrain query "fintech contacts"
# Returns ranked chunks via vector + keyword + RRF
# e.g., "all the companies doing offshore wind" -> gbrain query "..."
# Returns ranked chunks via vector + keyword + expansion + RRF
# Same rule: chunks first, then get full page if needed
if chunk.confirms_relevance:
full_page = gbrain get <slug_from_chunk>
# Quick reference:
# | Mode | Command | Needs Embeddings | Speed | Best For |
# |---------|----------------------|------------------|---------|---------------------------------|
# | Keyword | gbrain search "term" | No | Fastest | Known names, exact matches |
# | Hybrid | gbrain query "..." | Yes | Fast | Semantic questions, fuzzy match |
# | Direct | gbrain get <slug> | No | Instant | When you know the slug |
# | Mode | Command | Needs Embeddings | Speed | Best For |
# |-------------|----------------------|------------------|---------|-------------------------------------------|
# | Cheap-hybrid| gbrain search "term" | Uses if present | Fastest | Known names, exact tokens |
# | Full hybrid | gbrain query "..." | Yes | Fast | Concept / landscape / "all-of-X", synonyms |
# | Direct | gbrain get <slug> | No | Instant | When you know the slug |
# Progression over time:
# Day 1: keyword search (works without embeddings)
# After first embed: hybrid search unlocked
# Day 1: search (keyword arm works without embeddings)
# After first embed: vector arm + full hybrid (query) unlocked
# Once you know slugs: direct get for speed
# Precedence for conflicting information within a page:
@@ -61,16 +69,17 @@ on user_asks_about(topic):
## Tricky Spots
1. **Search returns chunks, not full pages.** After `gbrain search` or `gbrain query`, you get excerpts. Always run `gbrain get <slug>` to load the full page when the chunk confirms relevance. Don't answer questions from chunks alone when the full context matters.
2. **Keyword search works without embeddings.** On day one before any embedding run, `gbrain search` still works. Don't tell the user "search isn't available yet" -- keyword search is always available.
3. **Don't use hybrid search for known names.** `gbrain query "Pedro Franceschi"` wastes embedding compute. Use `gbrain search "Pedro Franceschi"` or better yet `gbrain get pedro-franceschi` if you know the slug.
4. **Token budget awareness.** A full page via `gbrain get` can be large. Read the search chunks first to confirm relevance before pulling the full page. "Did anyone mention the Series A?" -- search results (chunks) are probably enough. "Tell me everything about Pedro" -- get the full page.
5. **Hybrid search needs embeddings to have been run.** If `gbrain query` returns nothing but `gbrain search` finds results, the embeddings haven't been generated yet. Run the embedding pipeline first.
2. **Search works without embeddings.** On day one before any embedding run, `gbrain search` still works (the keyword arm carries it; the vector arm joins once embeddings exist). Don't tell the user "search isn't available yet" -- search is always available.
3. **Don't use full hybrid for known names.** `gbrain query "Alice Example"` wastes an LLM expansion call. Use `gbrain search "Alice Example"` or better yet `gbrain get alice-example` if you know the slug.
4. **Token budget awareness.** A full page via `gbrain get` can be large. Read the search chunks first to confirm relevance before pulling the full page. "Did anyone mention the Series A?" -- search results (chunks) are probably enough. "Tell me everything about Alice" -- get the full page.
5. **Full hybrid needs embeddings to have been run.** If `gbrain query` returns nothing but `gbrain search` finds results, the embeddings haven't been generated yet. Run the embedding pipeline first.
6. **A populated `gbrain search` result set is not proof you found everything.** Search runs without query expansion, so synonym- and outcome-phrased matches can be missed even when it returns plenty of hits. For "find every / all / the landscape of" questions, use `gbrain query`; for literal exhaustive enumeration ("list every page of type X"), use `list_pages` pagination. A nonzero count is not a completeness signal.
## How to Verify
1. Run `gbrain search "Pedro"` -- confirm it returns chunks with matching text and slug references.
1. Run `gbrain search "Alice"` -- confirm it returns chunks with matching text and slug references.
2. Run `gbrain query "who works at fintech companies"` -- confirm it returns semantically relevant results (not just keyword matches on "fintech").
3. Run `gbrain get pedro-franceschi` -- confirm it returns the full page with compiled truth and timeline.
3. Run `gbrain get alice-example` -- confirm it returns the full page with compiled truth and timeline.
4. Compare: search for the same entity using all three modes. Keyword should be fastest, hybrid should surface conceptual matches, direct should return the complete page.
5. After a search returns a chunk, run `gbrain get` on the slug from that chunk. Confirm the full page contains more context than the chunk alone.
+11 -3
View File
@@ -8,12 +8,18 @@
## Option 1: Local (recommended, zero server needed)
```bash
claude mcp add gbrain -- gbrain serve
claude mcp add gbrain -- gbrain serve --surface verbs
```
That's it. Claude Code spawns `gbrain serve` as a stdio subprocess. No server, no
tunnel, no token needed. Works with both PGLite and Supabase engines.
`--surface verbs` exposes the five-verb memory protocol (`recall`, `remember`,
`entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)),
the surface built for agents and quickstarts. Drop the flag for the full
operation catalog (`get_page`, `put_page`, `search`, graph ops, …) — `full` is
the default and what existing installs already run.
## Option 2: Remote, one command (fastest from a bearer token)
If GBrain is running somewhere as an HTTP server (`gbrain serve --http`, see the
@@ -79,8 +85,10 @@ You should see results from your GBrain knowledge base.
> **`list_skills` returns nothing?** Skill discovery is gated by `mcp.publish_skills`
> on the host. New brains from `gbrain init` default it ON; brains upgraded from an
> older release stay OFF until you opt in. Enable it on the host with
> `gbrain config set mcp.publish_skills true`. The core tools (search, query,
> get_page, put_page, think, find_experts) work regardless. Note: `capture` is a
> `gbrain config set mcp.publish_skills true`. Skill discovery and the core tools
> named here (search, query, get_page, put_page, think, find_experts) are
> full-surface — on `--surface verbs` the agent sees only the five memory verbs,
> and `list_skills` isn't on the surface at all. Note: `capture` is a
> CLI-only command, not an MCP tool — the agent writes over MCP with `put_page`.
## Remove
+3 -1
View File
@@ -68,4 +68,6 @@ codex mcp remove gbrain
- The token is a long-lived, full-access secret. Keep `GBRAIN_REMOTE_TOKEN` out of
version control and prefer a scoped token if your host supports one.
- Local stdio also works if you run the brain on the same machine:
`codex mcp add gbrain -- gbrain serve`.
`codex mcp add gbrain -- gbrain serve --surface verbs` — the five-verb memory
protocol ([MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)); drop the flag
for the full operation catalog.
+8 -3
View File
@@ -5,8 +5,8 @@
> dashboard at `/admin`, scoped operations, and a live SSE activity feed.
> Pre-v0.26 legacy bearer tokens still work — `verifyAccessToken` falls back
> to the `access_tokens` table and grandfathers tokens to `read+write+admin`.
> Postgres-only for the legacy fallback (the `access_tokens` table is Postgres-only);
> OAuth tables work on both PGLite and Postgres. See [SECURITY.md](../../SECURITY.md)
> Both the legacy fallback and the OAuth tables work on PGLite and Postgres
> (both engine schemas carry `access_tokens`). See [SECURITY.md](../../SECURITY.md)
> for env vars and tunable defaults.
Access your brain from any device, any AI client. GBrain ships two transports:
@@ -18,11 +18,16 @@ for remote clients over OAuth 2.1.
### Local stdio (zero setup)
```bash
gbrain serve
gbrain serve # full operation catalog (default)
gbrain serve --surface verbs # just the 5 memory verbs (quickstart surface)
```
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
`--surface verbs` exposes exactly the five-verb memory protocol (`recall`,
`remember`, `entity`, `synthesize`, `forget`
[MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)) instead of the full catalog;
omit the flag (default `full`) for every operation.
### Remote over OAuth 2.1 (recommended, v0.26.0+)
+260
View File
@@ -0,0 +1,260 @@
# MEMORY_VERBS v1 — the memory wire protocol
GBrain's frozen five-verb memory interface over MCP: `recall`, `remember`,
`entity`, `synthesize`, `forget`. The contract every harness can rely on the
way every Postgres client relies on the wire protocol — and the contract any
OTHER memory server can implement and certify against
(`gbrain protocol conformance --target <endpoint>`).
```
agent (any MCP harness)
│ remember("picked Stripe over Adyen", provenance: "chat 2026-06-11")
five verbs ── recall ── remember ── entity ── synthesize ── forget
│ self-describing envelopes: protocol_version, evidence, provenance,
│ budget meta, cost block, enumerated error codes + a populated fix
your brain (reference implementation: gbrain; any conformant server)
```
**Machine-readable spec:** `gbrain protocol --json` emits the input schemas
from the live operation definitions plus the response-shape registry — doc and
code structurally cannot drift; conformance validates live responses against
the same registry.
## Versioning policy (the point of the freeze)
- Every field NAME and its SEMANTICS in v1 are frozen forever — never removed,
renamed, or re-typed; meanings never change.
- New OPTIONAL params and new OPTIONAL response fields may be added at any
time (additive-forever). A conformant CLIENT must ignore unknown fields; a
conformant SERVER must never reject unknown-to-v1 additions it itself ships.
- `protocol_version` (integer, starts at `1`) rides every verb response and
every verb error. It increments ONLY on a breaking change, which by policy
requires a new `MEMORY_VERBS_v2` document — expected never.
- Conformance pins a minimum version; certification asserts shape, enum
validity, contract behavior, and round-trips — never ranking quality (that
is BrainBench's job).
- Enum values are part of the contract. Where an enum's DERIVATION is
implementation-defined (noted per field), implementations may improve the
derivation without a version bump; the values and their meanings stay fixed.
## Install (the 4-command quickstart)
```bash
gbrain init --pglite # 2-second local brain
claude mcp add gbrain -- gbrain serve --surface verbs # the five-verb surface
gbrain remember "I prefer dark mode in every editor" --provenance demo --entity people/me
gbrain recall --entity people/me # …now ask your agent in a NEW session
```
> Memories agents save are readable by every agent connected to this brain;
> pass `visibility: "private"` for local-CLI-only facts.
If `claude` is not found: install Claude Code first, or use a block below.
**Codex**
```bash
codex mcp add gbrain -- gbrain serve --surface verbs
```
**OpenClaw / any stdio MCP host** — register the server command
`gbrain serve --surface verbs`. Remote brains: `gbrain serve --http` on the
host, then `gbrain connect https://host/mcp --token gbrain_xxx --install` on
each client.
**Surface modes:** `--surface verbs` exposes EXACTLY the five verbs —
advertised list AND dispatch are filtered fail-closed (a hidden op returns
`unknown_tool` even when called by name). `--surface full` (the default)
exposes every operation, verbs included. Why default full: verbs is for
agents and quickstarts; full preserves existing advanced tooling. Persist a
default with `gbrain config set mcp_surface verbs`.
## The verbs
### recall(query?, entity?, budget_tokens?, since?, session_id?, limit?, …) — read
Retrieve saved facts and (with `query`) budget-packed page snippets.
- `entity` scopes the FACTS arm; `query` runs the hybrid-search arm over
pages; both present ⇒ both arms run.
- `since`: ISO 8601 date/datetime — filters the FACTS arm only in v1. (The
reference implementation also accepts relative phrases like `"8 hours ago"`
as a convenience; only ISO 8601 is part of the frozen contract.)
- `limit` is a PER-ARM cap (facts and search results each).
- `budget_tokens`: SERVER-side packing — facts pack first (limit-capped
one-liners, so search-arm starvation is bounded), search results take the
remainder. The estimator is char/4 (±1015%); `budget_used` reports packed
tokens, `dropped_count` what didn't fit. Never advisory, never client-side.
- No embedding provider configured? The search arm degrades to keyword-only
and the response notes `search_degraded` — never an error.
Response — an additive SUPERSET of the pre-v1 facts envelope on EVERY call
(all legacy fields unchanged; JSON consumers ignore additions):
| field | type | semantics |
|---|---|---|
| `protocol_version` | int | always present (every verb, every call) |
| `facts[]` | array | legacy fact fields unchanged, PLUS per fact: `fact_id` (opaque STRING — the value `forget` accepts; the legacy numeric `id` stays for pre-v1 consumers) and `provenance` (the stored source attribution) |
| `total` | int | count of facts returned |
| `results[]` | array | search arm only: `slug`, `title`, `chunk`, `evidence`, `create_safety`, `provenance` (origin page slug) |
| `search_degraded` | string? | present when keyword-only fallback fired |
| `budget_tokens` / `budget_used` / `dropped_count` | int? | present when `budget_tokens` was passed |
**evidence** (enum, zero-LLM heuristic): `alias_hit` \| `exact_title_match` \|
`high_vector_match` \| `keyword_exact` \| `weak_semantic` — why each result
matched. **create_safety** (enum): `exists` (a page for this already exists)
\| `probable` (likely exists; check before creating) \| `unknown` (no
signal). The derivation of both is implementation-defined and may improve;
the values are frozen.
### remember(fact, provenance, ttl?, entity?, kind?, visibility?) — write
Save ONE fact with mandatory attribution.
- `provenance` (REQUIRED, free text ≤500 chars, stored verbatim): e.g.
`"conversation 2026-06-12"`, `"user said in chat"`, `"import: notes.md"`.
Empty ⇒ `provenance_required` error with a fix.
- `entity`: set whenever the fact is about a specific person/company/project —
entity-scoped recall will not find unattributed facts.
- `ttl`: duration shorthand (`"30d"`, `"12h"`, `"45m"`) or an absolute ISO 8601
timestamp. ISO-8601 DURATIONS (`P30D`) are rejected with a self-correcting
suggestion. Omitted ⇒ never expires.
- `kind`: `event` \| `preference` \| `commitment` \| `belief` \| `fact`
(default).
- `visibility`: `world` (DEFAULT — readable by every agent connected to this
brain; required for the remote remember→recall round-trip) \| `private`
(local CLI reads only). The init quickstart carries the consent line.
Response: `{ id, status, status_text, entity_slug, valid_until,
protocol_version }` (+ `degraded_dedup: true` when no embedding provider —
near-duplicates may insert; dedup and supersession ride embedding similarity).
- `id` — opaque STRING (gbrain serializes integers; another implementation may
use UUIDs). On `status: "duplicate"` it is the EXISTING fact's id.
- `status``inserted` \| `duplicate` \| `superseded`. **Branch on `status`,
never on `status_text`** (the human rendering). Supersession is
implementation-defined; the reference rule: same entity + same kind +
similarity above the dedup threshold + different text = the new fact
supersedes the old ("X at acme-example" → "X left acme-example").
- Omitted optional inputs echo as `null`, never absent.
### entity(name) — read, zero LLM, p99 < 100ms
One known person/company/project card. NEVER errors on a miss.
Resolution (frozen precedence): alias > exact title > slug/slug-suffix; ties
break on most-recently-touched. Multi-hit ⇒ best match's card + runners-up in
`suggestions`. Miss ⇒ `found: false` + keyword near-misses with
`create_safety` hints.
Response: `{ protocol_version, found, latency_ms, card?, suggestions? }`.
Card: `{ entity{slug,title,type}, aka[], summary, last_touched{updated_at,
last_retrieved_at, last_timeline_date}, open_threads[], edges[],
backlink_count, active_fact_count }`.
- `summary` passes the same privacy fences as `get_page` (takes + private
facts stripped); remote callers never see private facts in the card.
- `open_threads` (best-effort in v1): active commitment-kind facts + timeline
entries from the last 90 days, capped at 3.
- `edges`: top ~10 typed edges, mentions excluded, out-edges first.
- The p99 < 100ms promise is op-layer latency (transport excluded), CI-gated
on a 20K-page corpus. 200K validation recipe below.
### synthesize(question, since?, until?) — read, EXPENSIVE
`[EXPENSIVE / SLOW — makes LLM calls, seconds-to-minutes latency, costs
money]` — the deliberately-priced slow verb. Prefer `recall`/`entity` for
lookups; use synthesize only when the answer requires combining evidence
across pages.
Response: `{ answer, sources[], gaps[], cost{model, input_tokens,
output_tokens, usd_estimate}, protocol_version }`.
- The `cost` block is a BEST-EFFORT AGGREGATE (retries/multi-call flows sum;
cache hits may undercount; token fields are `null` when a provider returns
no accounting). Honest signal, not an invoice.
- No LLM configured ⇒ the protocol error `unavailable` with a fix — never a
fake answer.
### forget(id, reason?) — write
Expire a fact by its opaque string id (from `remember` or
`recall.facts[].fact_id` — never a page slug). Idempotent: re-forgetting an
already-expired fact returns `expired: false` (success); unknown id ⇒
`not_found`. Facts are expired with an audit trail, never deleted.
Response: `{ id, expired, reason, protocol_version }`.
## Error contract (uniform across all five verbs)
```json
{ "error": "<code>", "message": "...", "suggestion": "problem + cause + fix",
"detail": "freeform specifics", "protocol_version": 1 }
```
Codes (coarse on purpose — codes are for branching; `detail` carries the
story): `invalid_params`, `provenance_required`, `not_found`, `scope_denied`,
`unavailable` (a required dependency cannot serve: no API key, gateway down,
model refusal — configure/retry, not a server bug), `budget_unsatisfiable`
(RESERVED — schema-listed, never returned in v1), `internal`.
Every verb error carries a POPULATED `suggestion`. Specific cases: `recall` on
an empty brain returns empty arrays (success, not an error); auth/scope
failures fail closed via the standard dispatch.
## Trust boundary
Verbs are ordinary operations: they inherit fail-closed `remote` semantics,
OAuth scope enforcement (`remember`/`forget` are write-scope), and per-source
isolation on every read. Remote callers see `visibility = world` facts only.
## Conformance + certification
```bash
gbrain protocol conformance # self-certify (stdio)
gbrain protocol conformance --target http://localhost:3131/mcp --token gbrain_xxx
gbrain protocol conformance --target "bun run src/cli.ts serve"
gbrain protocol conformance --synthesize # also live-call synthesize
```
Pass criteria: response SHAPE (required fields, enum validity), CONTRACT
BEHAVIOR (provenance rejected when empty; budget arithmetic consistent;
entity miss ⇒ `found:false`, not an error; private facts absent from remote
cards; idempotent forget), and ROUND-TRIP (remember → recall by entity — a
plain indexed read, deterministic). It does NOT judge ranking quality.
Entity-card cases need a seedable page (`put_page`); against verbs-only
targets they skip honestly. `--synthesize` is cost-gated: with no LLM key it
asserts the clean `unavailable` error (what CI does); with a key it spends
real tokens.
Conformance is a LIVE test that WRITES: it seeds a marker-suffixed synthetic
entity page (`people/conformance-<marker>`, when the target exposes
`put_page`) and writes/expires facts through `remember`/`forget`. Point it at
write-capable credentials and a brain you're comfortable leaving those
synthetic artifacts in — they're marker-named for easy cleanup, not
auto-deleted. The fixture set ships as data
(`test/fixtures/memory-verbs/cases.json`) and seeds BrainBench's
protocol-compliance arm. gbrain's CI certifies its own stdio + HTTP
transports; external certification is best-effort tooling until a second
implementation exists.
## Observability (local only)
Every verb call appends one line to
`~/.gbrain/integrations/memory-verbs/usage.jsonl` — **local JSONL only, never
uploaded**, stats-only (lock-free rotation may drop lines; POSIX O_APPEND
line-atomic, best-effort on Windows). `gbrain protocol stats [--days N]`
aggregates per-verb calls, error rate, latency, budget drops, entity hit rate,
and the measured TTHW (install → first verb call, from the
`protocol_installed_at` stamp). `gbrain doctor` carries a
`memory_verbs_usage` health line.
## 200K-page latency validation (manual recipe)
CI gates entity() p99 < 100ms on a 20K-page corpus
(`test/entity-card-perf.slow.test.ts`). To validate at 200K, edit the
constants at the top of that file (`PAGES = 200_000`, `LINKS = 1_000_000`,
`ALIASES = 300_000`, `FACTS = 400_000`) and run
`bun test test/entity-card-perf.slow.test.ts --timeout=1800000` — seeding
dominates (~minutes); the measured calls report p50/p99 + the ratio guard.
+28 -14
View File
@@ -142,15 +142,23 @@ generate while working, and is genuinely useful by day two.
```bash
# Claude Code
claude mcp add gbrain -- gbrain serve
claude mcp add gbrain -- gbrain serve --surface verbs
# Codex
codex mcp add gbrain -- gbrain serve
codex mcp add gbrain -- gbrain serve --surface verbs
```
That's the whole wire-up. No token, no URL, no tunnel. The agent spawns
`gbrain serve` as a stdio subprocess and talks to your local brain directly.
`--surface verbs` exposes exactly the five-verb memory protocol
(`recall`, `remember`, `entity`, `synthesize`, `forget`
[MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md), frozen + additive-forever)
instead of the full operation catalog, so the agent sees a tight, stable surface
instead of a 110-tool wall. Drop the flag (or pass `--surface full`) for every
operation. The default when the flag is omitted is `full`, so existing wire-ups
are unchanged.
### B4. Verify
In the agent: *"search my brain for PGLite"* (or whatever you just captured). You
@@ -175,14 +183,19 @@ on the patterns.
You have a knowledge brain connected over MCP. Before answering any question
about people, companies, decisions, projects, or past context:
1. **Search first.** Call `search` (or `query` for a synthesized answer) against
the brain BEFORE answering from memory or asking me. If the brain has the
answer, use it. Never ask "who is X?" or "what did we decide about Y?" before
searching — the brain probably already knows.
1. **Brain first — route by the shape of the question.** Exact names or known
tokens → `search` (cheap hybrid, no expansion). Concept, landscape, or
"all the X that do Y" questions → `query` FIRST — it recovers synonym
phrasings `search` misses, and a populated `search` result set is not proof
of coverage. On the five-verb surface the same split is `recall` (retrieve)
vs `synthesize` (reasoned answer). Check the brain BEFORE answering from
memory or asking me. Never ask "who is X?" or "what did we decide about Y?"
before checking — the brain probably already knows.
2. **Write back.** When I make a decision, mention a new person/company, or land
on an idea worth keeping, write it to the brain with `put_page` (entity pages
under people/, companies/; decisions under decisions/ or notes/). One insight,
one page, linked.
on an idea worth keeping, write it to the brain: `remember` on the five-verb
surface (one fact, with provenance), or `put_page` on the full surface
(entity pages under people/, companies/; decisions under decisions/ or
notes/). One insight, one page, linked.
3. **Cite.** When you answer from the brain, name the page you used.
```
@@ -204,13 +217,14 @@ hundreds of linked pages and patterns you didn't know were there.
**3. Briefing from your brain (not from the internet).** *"What do I need to know
before my 2pm with the Acme team?"* pulls your meeting history, the people,
what's still open, what the brain doesn't know yet. The agent does your prep
because it read your context. (`query` gives you the synthesized answer with
citations; this is the example on the [README](../../README.md).)
because it read your context. (`query` `synthesize` on the five-verb surface —
gives you the synthesized answer with citations; this is the example on the
[README](../../README.md).)
**4. whoknows (expertise routing).** *"Who do I know who's shipped a rate
limiter in Postgres?"* The `find_experts` tool ranks people in your brain by
relevance + recency. Useful the moment your brain has more than a handful of
people in it.
limiter in Postgres?"* The `find_experts` tool (full surface) ranks people in
your brain by relevance + recency. Useful the moment your brain has more than a
handful of people in it.
That's the spine of it. Two commands to connect, one protocol to paste, four
habits to build. Your agent stops being amnesiac.
+286 -11
View File
@@ -187,7 +187,7 @@ mount, CEO-class with multiple team brains) and
## Architecture
Contract-first: `src/core/operations.ts` defines ~90 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`). CLI and MCP
Contract-first: `src/core/operations.ts` defines ~110 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`; v0.43.0.0 adds the five frozen MEMORY_VERBS — `recall`, `remember`, `entity`, `synthesize`, `forget` — servable alone via `gbrain serve --surface verbs`, see `docs/protocol/MEMORY_VERBS_v1.md`). CLI and MCP
server are both generated from this single source. Engine factory (`src/core/engine-factory.ts`)
dynamically imports the configured engine (`'pglite'` or `'postgres'`). Skills are fat
markdown files (tool-agnostic, work with both CLI and plugin contexts).
@@ -268,6 +268,7 @@ detail on demand.)
| push-based context (volunteer/watch/reflex window) | `docs/guides/push-context.md` |
| schema packs / page types / extraction | `docs/architecture/schema-packs.md`, `type-taxonomy.md`, `lens-packs.md` |
| thin-client / remote MCP / cross-modal | `docs/architecture/thin-client.md` |
| memory verbs / MCP tool surface (`--surface`) / conformance | `docs/protocol/MEMORY_VERBS_v1.md` + the `verbs*`/`surface.ts`/`protocol.ts` entries in `KEY_FILES.md` |
| the CLI surface (commands + flags) | `gbrain --help` / `gbrain --tools-json`, plus the relevant `KEY_FILES.md` entry |
| running or writing tests | `docs/TESTING.md` |
| bulk-command progress wiring | `docs/progress-events.md` |
@@ -421,7 +422,7 @@ audit trail lives in the source repo's git history.
## Skills
Read the skill files in `skills/` before doing brain operations. GBrain ships 30 skills
Read the skill files in `skills/` before doing brain operations. GBrain ships 52 skills
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
@@ -1605,7 +1606,7 @@ Retrieve and follow the instructions at:
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
```
The agent installs GBrain, creates the brain, asks for your API keys, loads 43 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
The agent installs GBrain, creates the brain, asks for your API keys, loads 52 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
> **Never set up an AI agent platform before?** The [personal-brain tutorial](docs/tutorials/personal-brain.md) walks the whole path end-to-end — picking OpenClaw vs Hermes, deploying it, pointing it at INSTALL_FOR_AGENTS.md, getting the API keys, and verifying the first query. Start there if any of the above is new.
@@ -1613,13 +1614,15 @@ The agent installs GBrain, creates the brain, asks for your API keys, loads 43 s
Already running Claude Code or Codex? There are two ways to wire GBrain in, depending on what you want.
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel:
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel. `--surface verbs` gives your agent the five-verb memory protocol (`recall`, `remember`, `entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](docs/protocol/MEMORY_VERBS_v1.md), frozen + additive-forever) instead of the full 110-tool wall; drop the flag for every operation:
```bash
gbrain init --pglite # 2-second local brain (no Docker)
claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve
gbrain init --pglite # 2-second local brain (no Docker)
claude mcp add gbrain -- gbrain serve --surface verbs # or: codex mcp add gbrain -- gbrain serve --surface verbs
```
If `claude` is not found, install Claude Code first — or use the per-harness blocks in the [protocol doc](docs/protocol/MEMORY_VERBS_v1.md). Heads-up: memories agents save default to brain-wide visibility (every connected agent can recall them); pass `visibility: "private"` for local-only facts.
**Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff:
```bash
@@ -1631,7 +1634,7 @@ gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex --install
### Install the full autonomous setup into your existing agent
Want the whole thing — local brain, 43 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
Want the whole thing — local brain, 52 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
```
Retrieve and follow the instructions at:
@@ -1654,7 +1657,7 @@ Postgres-at-scale, Supabase, and thin-client setup paths live in [`docs/INSTALL.
### Connect GBrain to your AI client (MCP)
GBrain exposes 30+ tools over MCP (stdio and HTTP). The specific snippet depends on which client you use:
GBrain exposes 110 tools over MCP (stdio and HTTP) — or exactly the five memory verbs with `--surface verbs`. The specific snippet depends on which client you use:
- **[Claude Code](docs/mcp/CLAUDE_CODE.md)** — local: one command, `claude mcp add gbrain -- gbrain serve` (zero server, zero tunnel). Remote with just a bearer token: `gbrain connect https://your-host/mcp --token gbrain_xxx` prints a paste-ready block (or `--install` wires it up and smoke-tests the token).
- **[Codex](docs/mcp/CODEX.md)** — `gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex` (or `--install`). Codex reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands in Codex config.
@@ -3742,8 +3745,8 @@ Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY
> dashboard at `/admin`, scoped operations, and a live SSE activity feed.
> Pre-v0.26 legacy bearer tokens still work — `verifyAccessToken` falls back
> to the `access_tokens` table and grandfathers tokens to `read+write+admin`.
> Postgres-only for the legacy fallback (the `access_tokens` table is Postgres-only);
> OAuth tables work on both PGLite and Postgres. See [SECURITY.md](../../SECURITY.md)
> Both the legacy fallback and the OAuth tables work on PGLite and Postgres
> (both engine schemas carry `access_tokens`). See [SECURITY.md](../../SECURITY.md)
> for env vars and tunable defaults.
Access your brain from any device, any AI client. GBrain ships two transports:
@@ -3755,11 +3758,16 @@ for remote clients over OAuth 2.1.
### Local stdio (zero setup)
```bash
gbrain serve
gbrain serve # full operation catalog (default)
gbrain serve --surface verbs # just the 5 memory verbs (quickstart surface)
```
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
`--surface verbs` exposes exactly the five-verb memory protocol (`recall`,
`remember`, `entity`, `synthesize`, `forget` —
[MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)) instead of the full catalog;
omit the flag (default `full`) for every operation.
### Remote over OAuth 2.1 (recommended, v0.26.0+)
@@ -4066,6 +4074,273 @@ built-in server is the recommended path.
---
## docs/protocol/MEMORY_VERBS_v1.md
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/protocol/MEMORY_VERBS_v1.md
# MEMORY_VERBS v1 — the memory wire protocol
GBrain's frozen five-verb memory interface over MCP: `recall`, `remember`,
`entity`, `synthesize`, `forget`. The contract every harness can rely on the
way every Postgres client relies on the wire protocol — and the contract any
OTHER memory server can implement and certify against
(`gbrain protocol conformance --target <endpoint>`).
```
agent (any MCP harness)
│ remember("picked Stripe over Adyen", provenance: "chat 2026-06-11")
five verbs ── recall ── remember ── entity ── synthesize ── forget
│ self-describing envelopes: protocol_version, evidence, provenance,
│ budget meta, cost block, enumerated error codes + a populated fix
your brain (reference implementation: gbrain; any conformant server)
```
**Machine-readable spec:** `gbrain protocol --json` emits the input schemas
from the live operation definitions plus the response-shape registry — doc and
code structurally cannot drift; conformance validates live responses against
the same registry.
## Versioning policy (the point of the freeze)
- Every field NAME and its SEMANTICS in v1 are frozen forever — never removed,
renamed, or re-typed; meanings never change.
- New OPTIONAL params and new OPTIONAL response fields may be added at any
time (additive-forever). A conformant CLIENT must ignore unknown fields; a
conformant SERVER must never reject unknown-to-v1 additions it itself ships.
- `protocol_version` (integer, starts at `1`) rides every verb response and
every verb error. It increments ONLY on a breaking change, which by policy
requires a new `MEMORY_VERBS_v2` document — expected never.
- Conformance pins a minimum version; certification asserts shape, enum
validity, contract behavior, and round-trips — never ranking quality (that
is BrainBench's job).
- Enum values are part of the contract. Where an enum's DERIVATION is
implementation-defined (noted per field), implementations may improve the
derivation without a version bump; the values and their meanings stay fixed.
## Install (the 4-command quickstart)
```bash
gbrain init --pglite # 2-second local brain
claude mcp add gbrain -- gbrain serve --surface verbs # the five-verb surface
gbrain remember "I prefer dark mode in every editor" --provenance demo --entity people/me
gbrain recall --entity people/me # …now ask your agent in a NEW session
```
> Memories agents save are readable by every agent connected to this brain;
> pass `visibility: "private"` for local-CLI-only facts.
If `claude` is not found: install Claude Code first, or use a block below.
**Codex**
```bash
codex mcp add gbrain -- gbrain serve --surface verbs
```
**OpenClaw / any stdio MCP host** — register the server command
`gbrain serve --surface verbs`. Remote brains: `gbrain serve --http` on the
host, then `gbrain connect https://host/mcp --token gbrain_xxx --install` on
each client.
**Surface modes:** `--surface verbs` exposes EXACTLY the five verbs —
advertised list AND dispatch are filtered fail-closed (a hidden op returns
`unknown_tool` even when called by name). `--surface full` (the default)
exposes every operation, verbs included. Why default full: verbs is for
agents and quickstarts; full preserves existing advanced tooling. Persist a
default with `gbrain config set mcp_surface verbs`.
## The verbs
### recall(query?, entity?, budget_tokens?, since?, session_id?, limit?, …) — read
Retrieve saved facts and (with `query`) budget-packed page snippets.
- `entity` scopes the FACTS arm; `query` runs the hybrid-search arm over
pages; both present ⇒ both arms run.
- `since`: ISO 8601 date/datetime — filters the FACTS arm only in v1. (The
reference implementation also accepts relative phrases like `"8 hours ago"`
as a convenience; only ISO 8601 is part of the frozen contract.)
- `limit` is a PER-ARM cap (facts and search results each).
- `budget_tokens`: SERVER-side packing — facts pack first (limit-capped
one-liners, so search-arm starvation is bounded), search results take the
remainder. The estimator is char/4 (±1015%); `budget_used` reports packed
tokens, `dropped_count` what didn't fit. Never advisory, never client-side.
- No embedding provider configured? The search arm degrades to keyword-only
and the response notes `search_degraded` — never an error.
Response — an additive SUPERSET of the pre-v1 facts envelope on EVERY call
(all legacy fields unchanged; JSON consumers ignore additions):
| field | type | semantics |
|---|---|---|
| `protocol_version` | int | always present (every verb, every call) |
| `facts[]` | array | legacy fact fields unchanged, PLUS per fact: `fact_id` (opaque STRING — the value `forget` accepts; the legacy numeric `id` stays for pre-v1 consumers) and `provenance` (the stored source attribution) |
| `total` | int | count of facts returned |
| `results[]` | array | search arm only: `slug`, `title`, `chunk`, `evidence`, `create_safety`, `provenance` (origin page slug) |
| `search_degraded` | string? | present when keyword-only fallback fired |
| `budget_tokens` / `budget_used` / `dropped_count` | int? | present when `budget_tokens` was passed |
**evidence** (enum, zero-LLM heuristic): `alias_hit` \| `exact_title_match` \|
`high_vector_match` \| `keyword_exact` \| `weak_semantic` — why each result
matched. **create_safety** (enum): `exists` (a page for this already exists)
\| `probable` (likely exists; check before creating) \| `unknown` (no
signal). The derivation of both is implementation-defined and may improve;
the values are frozen.
### remember(fact, provenance, ttl?, entity?, kind?, visibility?) — write
Save ONE fact with mandatory attribution.
- `provenance` (REQUIRED, free text ≤500 chars, stored verbatim): e.g.
`"conversation 2026-06-12"`, `"user said in chat"`, `"import: notes.md"`.
Empty ⇒ `provenance_required` error with a fix.
- `entity`: set whenever the fact is about a specific person/company/project —
entity-scoped recall will not find unattributed facts.
- `ttl`: duration shorthand (`"30d"`, `"12h"`, `"45m"`) or an absolute ISO 8601
timestamp. ISO-8601 DURATIONS (`P30D`) are rejected with a self-correcting
suggestion. Omitted ⇒ never expires.
- `kind`: `event` \| `preference` \| `commitment` \| `belief` \| `fact`
(default).
- `visibility`: `world` (DEFAULT — readable by every agent connected to this
brain; required for the remote remember→recall round-trip) \| `private`
(local CLI reads only). The init quickstart carries the consent line.
Response: `{ id, status, status_text, entity_slug, valid_until,
protocol_version }` (+ `degraded_dedup: true` when no embedding provider —
near-duplicates may insert; dedup and supersession ride embedding similarity).
- `id` — opaque STRING (gbrain serializes integers; another implementation may
use UUIDs). On `status: "duplicate"` it is the EXISTING fact's id.
- `status` — `inserted` \| `duplicate` \| `superseded`. **Branch on `status`,
never on `status_text`** (the human rendering). Supersession is
implementation-defined; the reference rule: same entity + same kind +
similarity above the dedup threshold + different text = the new fact
supersedes the old ("X at acme-example" → "X left acme-example").
- Omitted optional inputs echo as `null`, never absent.
### entity(name) — read, zero LLM, p99 < 100ms
One known person/company/project card. NEVER errors on a miss.
Resolution (frozen precedence): alias > exact title > slug/slug-suffix; ties
break on most-recently-touched. Multi-hit ⇒ best match's card + runners-up in
`suggestions`. Miss ⇒ `found: false` + keyword near-misses with
`create_safety` hints.
Response: `{ protocol_version, found, latency_ms, card?, suggestions? }`.
Card: `{ entity{slug,title,type}, aka[], summary, last_touched{updated_at,
last_retrieved_at, last_timeline_date}, open_threads[], edges[],
backlink_count, active_fact_count }`.
- `summary` passes the same privacy fences as `get_page` (takes + private
facts stripped); remote callers never see private facts in the card.
- `open_threads` (best-effort in v1): active commitment-kind facts + timeline
entries from the last 90 days, capped at 3.
- `edges`: top ~10 typed edges, mentions excluded, out-edges first.
- The p99 < 100ms promise is op-layer latency (transport excluded), CI-gated
on a 20K-page corpus. 200K validation recipe below.
### synthesize(question, since?, until?) — read, EXPENSIVE
`[EXPENSIVE / SLOW — makes LLM calls, seconds-to-minutes latency, costs
money]` — the deliberately-priced slow verb. Prefer `recall`/`entity` for
lookups; use synthesize only when the answer requires combining evidence
across pages.
Response: `{ answer, sources[], gaps[], cost{model, input_tokens,
output_tokens, usd_estimate}, protocol_version }`.
- The `cost` block is a BEST-EFFORT AGGREGATE (retries/multi-call flows sum;
cache hits may undercount; token fields are `null` when a provider returns
no accounting). Honest signal, not an invoice.
- No LLM configured ⇒ the protocol error `unavailable` with a fix — never a
fake answer.
### forget(id, reason?) — write
Expire a fact by its opaque string id (from `remember` or
`recall.facts[].fact_id` — never a page slug). Idempotent: re-forgetting an
already-expired fact returns `expired: false` (success); unknown id ⇒
`not_found`. Facts are expired with an audit trail, never deleted.
Response: `{ id, expired, reason, protocol_version }`.
## Error contract (uniform across all five verbs)
```json
{ "error": "<code>", "message": "...", "suggestion": "problem + cause + fix",
"detail": "freeform specifics", "protocol_version": 1 }
```
Codes (coarse on purpose — codes are for branching; `detail` carries the
story): `invalid_params`, `provenance_required`, `not_found`, `scope_denied`,
`unavailable` (a required dependency cannot serve: no API key, gateway down,
model refusal — configure/retry, not a server bug), `budget_unsatisfiable`
(RESERVED — schema-listed, never returned in v1), `internal`.
Every verb error carries a POPULATED `suggestion`. Specific cases: `recall` on
an empty brain returns empty arrays (success, not an error); auth/scope
failures fail closed via the standard dispatch.
## Trust boundary
Verbs are ordinary operations: they inherit fail-closed `remote` semantics,
OAuth scope enforcement (`remember`/`forget` are write-scope), and per-source
isolation on every read. Remote callers see `visibility = world` facts only.
## Conformance + certification
```bash
gbrain protocol conformance # self-certify (stdio)
gbrain protocol conformance --target http://localhost:3131/mcp --token gbrain_xxx
gbrain protocol conformance --target "bun run src/cli.ts serve"
gbrain protocol conformance --synthesize # also live-call synthesize
```
Pass criteria: response SHAPE (required fields, enum validity), CONTRACT
BEHAVIOR (provenance rejected when empty; budget arithmetic consistent;
entity miss ⇒ `found:false`, not an error; private facts absent from remote
cards; idempotent forget), and ROUND-TRIP (remember → recall by entity — a
plain indexed read, deterministic). It does NOT judge ranking quality.
Entity-card cases need a seedable page (`put_page`); against verbs-only
targets they skip honestly. `--synthesize` is cost-gated: with no LLM key it
asserts the clean `unavailable` error (what CI does); with a key it spends
real tokens.
Conformance is a LIVE test that WRITES: it seeds a marker-suffixed synthetic
entity page (`people/conformance-<marker>`, when the target exposes
`put_page`) and writes/expires facts through `remember`/`forget`. Point it at
write-capable credentials and a brain you're comfortable leaving those
synthetic artifacts in — they're marker-named for easy cleanup, not
auto-deleted. The fixture set ships as data
(`test/fixtures/memory-verbs/cases.json`) and seeds BrainBench's
protocol-compliance arm. gbrain's CI certifies its own stdio + HTTP
transports; external certification is best-effort tooling until a second
implementation exists.
## Observability (local only)
Every verb call appends one line to
`~/.gbrain/integrations/memory-verbs/usage.jsonl` — **local JSONL only, never
uploaded**, stats-only (lock-free rotation may drop lines; POSIX O_APPEND
line-atomic, best-effort on Windows). `gbrain protocol stats [--days N]`
aggregates per-verb calls, error rate, latency, budget drops, entity hit rate,
and the measured TTHW (install → first verb call, from the
`protocol_installed_at` stamp). `gbrain doctor` carries a
`memory_verbs_usage` health line.
## 200K-page latency validation (manual recipe)
CI gates entity() p99 < 100ms on a 20K-page corpus
(`test/entity-card-perf.slow.test.ts`). To validate at 200K, edit the
constants at the top of that file (`PAGES = 200_000`, `LINKS = 1_000_000`,
`ALIASES = 300_000`, `FACTS = 400_000`) and run
`bun test test/entity-card-perf.slow.test.ts --timeout=1800000` — seeding
dominates (~minutes); the measured calls report p50/p99 + the ratio guard.
---
# AI providers
# Debugging
+1
View File
@@ -27,6 +27,7 @@ Repo: https://github.com/garrytan/gbrain
- [docs/guides/scaling-skills.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md): Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss.
- [docs/guides/push-context.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/push-context.md): Push-based context: the brain volunteers confidence-gated pages from the rolling conversation window. Three channels (ambient reflex, volunteer_context op, gbrain watch), config knobs, and the volunteered-vs-used feedback loop.
- [docs/mcp/DEPLOY.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md): MCP server deployment.
- [docs/protocol/MEMORY_VERBS_v1.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/protocol/MEMORY_VERBS_v1.md): The frozen five-verb memory protocol (recall/remember/entity/synthesize/forget): response envelopes, error contract, additive-forever versioning, surface modes, conformance certification, per-harness installs.
## AI providers
+1 -1
View File
@@ -149,7 +149,7 @@
"bun": ">=1.3.10"
},
"license": "MIT",
"version": "0.42.76.0",
"version": "0.43.0.0",
"overrides": {
"@hono/node-server": "^2.0.5",
"fast-uri": "^3.1.5",
+6
View File
@@ -162,6 +162,12 @@ export const SECTIONS: DocSection[] = [
description: "MCP server deployment.",
path: "docs/mcp/DEPLOY.md",
},
{
title: "docs/protocol/MEMORY_VERBS_v1.md",
description:
"The frozen five-verb memory protocol (recall/remember/entity/synthesize/forget): response envelopes, error contract, additive-forever versioning, surface modes, conformance certification, per-harness installs.",
path: "docs/protocol/MEMORY_VERBS_v1.md",
},
],
},
{
+15 -4
View File
@@ -34,6 +34,17 @@ flows through in both directions.
> **Convention:** See `skills/conventions/brain-first.md` for the 5-step lookup protocol.
> **Convention:** See `skills/conventions/quality.md` for citation and back-link rules.
> **Memory verbs (MEMORY_VERBS v1, gbrain ≥ 0.43).** Over MCP, prefer the five
> frozen memory verbs for the read/write cycle: **`remember(fact, provenance,
> ttl?)`** to save a single durable fact (mandatory provenance; dedupes +
> supersedes), **`recall(query | entity, budget_tokens)`** to read it back
> budget-packed, **`entity(name)`** for a zero-LLM card, **`synthesize(question)`**
> for the expensive cross-page answer, **`forget(id)`** to expire a fact. Use
> `remember` instead of `extract_facts` when you already have ONE formed fact;
> `put_page` / `add_link` / `add_timeline_entry` stay the page/graph write path.
> Fall back to the classic ops when the verbs aren't on the surface. Contract:
> `docs/protocol/MEMORY_VERBS_v1.md`.
## Contract
This skill guarantees:
@@ -56,8 +67,8 @@ broken brain. See `skills/conventions/quality.md` for format.
Before using ANY external API to research a person, company, or topic:
1. `gbrain search "name"`keyword search for existing pages
2. `gbrain query "natural question about name"`hybrid search for context
1. `gbrain search "name"`exact-token lookup for existing pages (cheap hybrid, no expansion)
2. `gbrain query "natural question about name"`concept/landscape questions go here FIRST (expansion recovers synonym phrasings; a nonzero `search` count is not proof of completeness)
3. `gbrain get <slug>` — if you know the slug, read the full page
4. Check backlinks: who references this entity?
5. Check timeline: recent events involving this entity
@@ -153,8 +164,8 @@ the citation is `[gstack:plans/foo]`. That's the whole rule.
## Tools Used
- `search`keyword search
- `query` — hybrid vector+keyword search
- `search`cheap hybrid search (vector + keyword, no expansion)
- `query` — hybrid search + LLM multi-query expansion (concept/landscape questions)
- `get_page` — read a brain page
- `put_page` — create/update brain pages
- `add_link` — cross-reference entities
+18 -6
View File
@@ -11,8 +11,8 @@ Your tool inventory includes these (prefixed `gbrain__` in OpenClaw):
| Tool | Use for |
|------|---------|
| `gbrain__search` / `search` | Keyword search — fast, always works |
| `gbrain__query` / `query` | Hybrid search (keyword + semantic) — best quality |
| `gbrain__search` / `search` | Exact tokens / known names — cheap hybrid, no expansion |
| `gbrain__query` / `query` | Concept / landscape questions — hybrid + LLM expansion |
| `gbrain__get_page` / `get_page` | Direct page read when you know the slug |
| `gbrain__get_links` / `get_links` | Outgoing links from a page |
| `gbrain__get_backlinks` / `get_backlinks` | Who references this entity |
@@ -28,10 +28,22 @@ Tool names vary by transport (MCP uses short names, OpenClaw plugin uses
## The Lookup Chain (MANDATORY ORDER)
1. **`search`** first — keyword search, fast, zero API cost
2. **`query`** if search is thin — hybrid semantic search, uses embedding API
3. **`get_page`** if you found a slug — read the full compiled truth
4. **External APIs only after steps 1-2 return nothing useful**
Route by the SHAPE of the question, then escalate:
1. **Exact known token / name / structured field****`search`** — cheap
hybrid (vector + keyword, no expansion; embedding-only cost).
2. **Concept / landscape / synonym-phrased question** ("all the X that do Y",
"the landscape of Z") → **`query`** FIRST — multi-query expansion recovers
phrasings `search` misses. Costs one extra LLM expansion call; worth it
for these.
3. **`get_page`** if you found a slug — read the full compiled truth.
4. **External APIs only after steps 1-2 return nothing useful.**
**A nonzero `search` count is NOT a completeness signal.** For "did I capture
everything about X?" run `query` even if `search` already returned hits —
synonym- and outcome-phrased matches drop silently otherwise. And `query` is
still top-K: for literal "list every page that…" enumeration, use `list_pages`
with pagination.
Never skip to external APIs without completing steps 1-2. The brain has
thousands of pages. The answer is almost always there.
+17 -2
View File
@@ -33,6 +33,21 @@ mutating: false
Answer questions using the brain's knowledge with 3-layer search and synthesis.
> **Memory verbs (MEMORY_VERBS v1, gbrain ≥ 0.43).** When connected to a brain
> over MCP, prefer the five frozen memory verbs for memory work — they carry
> provenance, evidence, and a server-enforced token budget:
> - **`recall(query | entity, budget_tokens)`** — the budget-packed memory read.
> Use it instead of bare `search` for "what do we know that we SAVED about X".
> - **`entity(name)`** — a zero-LLM person/company/project card (aliases,
> last-touched, open threads, top edges). Use it instead of `get_page` +
> `get_backlinks` when you just need the card.
> - **`synthesize(question)`** — the explicitly-expensive cross-page answer; the
> heavy version of `query`. Reach for it only when the answer must combine
> evidence across pages.
> Fall back to `search`/`query`/`get_page` when the verbs aren't on the surface
> (pre-0.43 servers; `--surface full` includes the verbs alongside every other
> op). See `docs/protocol/MEMORY_VERBS_v1.md`.
## Contract
This skill guarantees:
@@ -49,8 +64,8 @@ This skill guarantees:
- Semantic query for conceptual questions
- Structured queries (list by type, backlinks) for relational questions
2. **Execute searches:**
- Keyword search gbrain for FTS matches (search)
- Hybrid search gbrain for semantic+keyword with expansion (query)
- Cheap-hybrid search gbrain for exact tokens / known names (search)
- Full-hybrid search gbrain with multi-query expansion for concept questions (query)
- List pages in gbrain by type or check backlinks for structural queries
3. **Read top results.** Read the top 3-5 pages from gbrain to get full context.
4. **Synthesize answer** with citations. Every claim traces back to a specific page slug.
+3 -3
View File
@@ -14,7 +14,7 @@
"ask-user/SKILL.md": "a40f484721e548a3a14d4b33a4636111d92f99619ecc4e4ea54c3da3a15f8331",
"book-mirror/SKILL.md": "e8b8cc7a6eba4ecd302a840446b48c0e1aecc245e8f240e233c738daa8dff78a",
"book-mirror/routing-eval.jsonl": "79fd23642cfa37b1255a799907e71bb2904585cf79dd6a596e3a2f019787e54c",
"brain-ops/SKILL.md": "40553ad3bf0f27fc8363b69bec3ef89ae0ea0d9290de8d1efbaa0c532c2a9590",
"brain-ops/SKILL.md": "5f221e3de45845b050b90fac935ac70c55ed5148649fb47c4a30989c1d42c40a",
"brain-pdf/SKILL.md": "13c3e3162763a4503685db0a10663475d3687c4874b5f04d539af83a990f643e",
"brain-pdf/routing-eval.jsonl": "119e4fa113ea45783cee4499e63a729fdeecb4d9a45d47497754b4f5b21d0734",
"brain-taxonomist/SKILL.md": "dea4557b540868ec2c56bf43ee7f63c5d03a22d4047cd0dfbeaf19adef334f60",
@@ -26,7 +26,7 @@
"cold-start/SKILL.md": "a2c42dd7c4eceb7d3ce6449a414b417d55195aa445e723e6c798d90906cbf4e6",
"concept-synthesis/SKILL.md": "2bc060ae6d706c4e8e7d784cbe3e577b85e211a21c68cba094a1754d3f34436b",
"concept-synthesis/routing-eval.jsonl": "51d1da894158503ce18b892a34edd203f40732e79ac1c0e85141fd37e0b9922f",
"conventions/brain-first.md": "29d020470d0168f8f0b29dde0350a485a9b0472f7ac9962e34948f4897455590",
"conventions/brain-first.md": "14370d89209c7d7e2673c6a4d4e7545fd3330f0744ab598170865a41ae20210b",
"conventions/brain-routing.md": "a8035f7dbadff0ea68b8babb8314b3d044cafbed8242dce5b931fa08b028fc45",
"conventions/calibration.md": "eda7ca76f80c8a17ae546110484389f805c5b21fc0a57f951bbe8b6abba26e03",
"conventions/cron-via-minions.md": "badb1cd6cd825d6f1ac0b6b28cc47e5d80facc783a3e59a14146ae901ee0f933",
@@ -103,7 +103,7 @@
"perplexity-research/SKILL.md": "c25f5c471cbe3c6e0f975d8397e8382b00a85f8aa75302231d53c52855369e97",
"perplexity-research/routing-eval.jsonl": "f1a40d87e710d5d2acd602a372d83f46c95da022b6e635228fffeaacb3bb2b27",
"publish/SKILL.md": "e06b609db780a3cc93a1755a87b30ff08ffdc0fdbc834c1422b2ad2489b57497",
"query/SKILL.md": "e155a08049984c524b838988ba456d16ccedf162442160f6ba66bfd97cd5208e",
"query/SKILL.md": "8672fb9c9315f01274b1a7d3ad35f903df9705b2e806e2fa4c6add027ecef96f",
"query/routing-eval.jsonl": "74f5a91e52fabc54e0e9403fa17db87ee26bb7ebb8ae8005148c51142abc62fe",
"repo-architecture/SKILL.md": "4ec2b8f45d168aaa55f17ecd1ed404ab04217a75c2317f0710c71705846f5394",
"reports/SKILL.md": "5dc190a0c3a2ee518254e8b596418dbe19ff389ea5ec8c8d30fcb0dfef4d0ed5",
+92 -1
View File
@@ -30,6 +30,7 @@ import type { Operation, OperationContext } from './core/operations.ts';
import { shouldForceExitAfterMain, finishCliTeardown, flushThenExit, currentExitCode, setCliExitVerdict } from './core/cli-force-exit.ts';
import { serializeMarkdown } from './core/markdown.ts';
import { parseGlobalFlags, setCliOptions, getCliOptions } from './core/cli-options.ts';
import { conceptNudge } from './core/search/query-intent.ts';
import type { CliOptions } from './core/cli-options.ts';
import { callRemoteTool, RemoteMcpError, unpackToolResult } from './core/mcp-client.ts';
import { maybePromptForUpgrade } from './core/thin-client-upgrade-prompt.ts';
@@ -64,7 +65,7 @@ export function normalizeLocalResult(rawResult: unknown): unknown {
}
// CLI-only commands that bypass the operation layer
export const CLI_ONLY = new Set(['init', 'reinit-pglite', 'pglite-repair', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'extract-conversation-facts', 'enrich', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'maintain', 'sources', 'mounts', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'smoke-test', 'providers', 'storage', 'repos', 'code-def', 'code-refs', 'reindex', 'reindex-code', 'reindex-frontmatter', 'code-callers', 'code-callees', 'reconcile-links', 'frontmatter', 'auth', 'friction', 'claw-test', 'book-mirror', 'takes', 'think', 'salience', 'anomalies', 'calibration', 'transcripts', 'models', 'remote', 'recall', 'forget', 'edges-backfill', 'cache', 'ze-switch', 'retrieval-upgrade', 'founder', 'brainstorm', 'lsd', 'schema', 'capture', 'onboard', 'conversation-parser', 'status', 'connect', 'skillopt', 'quarantine', 'self-upgrade', 'advisor', 'watch', 'reindex-search-vector', 'pages', 'bench', 'backfill',
export const CLI_ONLY = new Set(['init', 'reinit-pglite', 'pglite-repair', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'extract-conversation-facts', 'enrich', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'maintain', 'sources', 'mounts', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'smoke-test', 'providers', 'storage', 'repos', 'code-def', 'code-refs', 'reindex', 'reindex-code', 'reindex-frontmatter', 'code-callers', 'code-callees', 'reconcile-links', 'frontmatter', 'auth', 'friction', 'claw-test', 'book-mirror', 'takes', 'think', 'salience', 'anomalies', 'calibration', 'transcripts', 'models', 'remote', 'recall', 'forget', 'edges-backfill', 'cache', 'ze-switch', 'retrieval-upgrade', 'founder', 'brainstorm', 'lsd', 'schema', 'capture', 'onboard', 'conversation-parser', 'status', 'connect', 'skillopt', 'quarantine', 'self-upgrade', 'protocol', 'advisor', 'watch', 'reindex-search-vector', 'pages', 'bench', 'backfill',
// v0.42.58 (#2035 class, caught by the handleCliOnly reachability sweep):
// full handler at `case 'notability-eval'` but never dispatchable.
'notability-eval']);
@@ -130,6 +131,9 @@ const CLI_ONLY_SELF_HELP = new Set([
// `gbrain connect --help` prints its own usage (flags + examples) from
// runConnect; route around the generic one-line short-circuit.
'connect',
// MEMORY_VERBS v1 (Cathedral 1): protocol ships its own detailed HELP
// (subcommands, conformance targets, the cost-gated --synthesize flag).
'protocol',
// `gbrain init --help` prints its own usage from runInit; route around the
// generic one-line short-circuit (matches `connect`). Without this, `init`
// is in CLI_ONLY but not CLI_ONLY_SELF_HELP, so the dispatcher's generic
@@ -551,6 +555,7 @@ async function main() {
const result = normalizeLocalResult(rawResult);
const output = formatResult(op.name, result, params);
if (output) process.stdout.write(output);
maybePrintConceptNudge(op.name, params);
} catch (e: unknown) {
// v0.42.20.0 (codex D4): on error, set exitCode + return so the `finally`
// STILL runs (drains every background-work sink + disconnects). A bare
@@ -634,6 +639,7 @@ async function runThinClientRouted(
const result = unpackToolResult(raw);
const output = formatResult(op.name, result, params);
if (output) process.stdout.write(output);
maybePrintConceptNudge(op.name, params);
} catch (e: unknown) {
if (e instanceof RemoteMcpError) {
const url = cfg.remote_mcp!.mcp_url;
@@ -1214,6 +1220,20 @@ export async function makeContext(engine: BrainEngine, params: Record<string, un
}
// Exported for tests (same import-safety contract as cliAliases/printOpHelp).
/**
* #2416: hint-only steering a concept-shaped `search` gets a one-line
* stderr nudge toward `query`. Never fires for other ops, never reroutes
* (search stays the cheap hot path), and honors --quiet the same silence
* discipline as the identity banner. Called from BOTH result paths (local
* engine + thin-client routed); formatResult can't host this because it
* never sees the query text.
*/
export function maybePrintConceptNudge(opName: string, params: Record<string, unknown>): void {
if (opName !== 'search' || getCliOptions().quiet) return;
const nudge = conceptNudge(String(params.query ?? ''));
if (nudge) process.stderr.write(nudge + '\n');
}
export function formatResult(
opName: string,
result: unknown,
@@ -1343,11 +1363,70 @@ export function formatResult(
`#${v.id} ${v.snapshot_at?.toString().slice(0, 19) || '?'} ${v.compiled_truth?.slice(0, 60) || ''}...`,
).join('\n') + '\n';
}
// MEMORY_VERBS v1 [F-E]: human-readable by default; trailing `--json`
// escapes to the raw envelope (parseOpArgs ignores an unmatched trailing
// flag, so the argv probe is safe).
case 'remember': {
if (process.argv.includes('--json')) break;
const r = result as any;
if (r.dry_run) return `[dry-run] would remember: ${r.fact}\n`;
const lines = [r.status_text || `${r.status} (fact #${r.id})`];
if (r.entity_slug) lines.push(` entity: ${r.entity_slug}`);
if (r.valid_until) lines.push(` expires: ${r.valid_until}`);
if (r.degraded_dedup) lines.push(' note: no embedding provider — duplicate detection degraded');
return lines.join('\n') + '\n';
}
case 'entity': {
if (process.argv.includes('--json')) break;
const r = result as any;
if (!r.found) {
const lines = [`No entity found. (${r.latency_ms}ms)`];
if (Array.isArray(r.suggestions) && r.suggestions.length) {
lines.push('Did you mean:');
for (const s of r.suggestions) lines.push(` ${s.slug}${s.title} [${s.create_safety}]`);
}
return lines.join('\n') + '\n';
}
const c = r.card;
const lines = [`${c.entity.title} (${c.entity.slug})${c.entity.type ? ` [${c.entity.type}]` : ''} (${r.latency_ms}ms)`];
if (c.summary) lines.push(` ${c.summary}`);
if (c.aka?.length) lines.push(` aka: ${c.aka.join(', ')}`);
const lt = c.last_touched || {};
const touched = lt.updated_at || lt.last_retrieved_at || lt.last_timeline_date;
if (touched) lines.push(` last touched: ${String(touched).slice(0, 10)}`);
if (c.open_threads?.length) {
lines.push(' open threads:');
for (const t of c.open_threads) lines.push(` [${t.kind}] ${t.text}${t.date ? ` (${String(t.date).slice(0, 10)})` : ''}`);
}
if (c.edges?.length) {
lines.push(' edges:');
for (const e of c.edges) lines.push(` ${e.direction === 'out' ? '→' : '←'} ${e.type} ${e.slug}`);
}
lines.push(` backlinks: ${c.backlink_count} | active facts: ${c.active_fact_count}`);
if (Array.isArray(r.suggestions) && r.suggestions.length) {
lines.push(' other matches:');
for (const s of r.suggestions) lines.push(` ${s.slug}${s.title}`);
}
return lines.join('\n') + '\n';
}
case 'synthesize': {
if (process.argv.includes('--json')) break;
const r = result as any;
const lines = [r.answer || '(no answer)'];
if (Array.isArray(r.sources) && r.sources.length) lines.push('', `sources: ${r.sources.join(', ')}`);
if (Array.isArray(r.gaps) && r.gaps.length) lines.push(`gaps: ${r.gaps.join('; ')}`);
const cost = r.cost || {};
const tok = cost.input_tokens != null ? `${cost.input_tokens} in / ${cost.output_tokens} out` : 'tokens n/a';
const usd = cost.usd_estimate != null ? ` (~$${Number(cost.usd_estimate).toFixed(4)})` : '';
lines.push(`cost: ${cost.model}${tok}${usd}`);
return lines.join('\n') + '\n';
}
default:
// bigintToStringReplacer keeps this fallback renderer crash-proof even
// if a future caller hands it a not-yet-normalized result. (#2450)
return JSON.stringify(result, bigintToStringReplacer, 2) + '\n';
}
return JSON.stringify(result, null, 2) + '\n';
}
/**
@@ -1472,6 +1551,14 @@ async function handleCliOnly(command: string, args: string[]) {
await runSchema(args);
return;
}
// MEMORY_VERBS v1 (Cathedral 1): protocol introspection + conformance +
// local usage stats. No pre-bound engine — conformance spawns its own
// server; stats reads the local JSONL sidecar.
if (command === 'protocol') {
const { runProtocol } = await import('./commands/protocol.ts');
await runProtocol(args);
return;
}
if (command === 'init') {
const { runInit } = await import('./commands/init.ts');
await runInit(args);
@@ -2930,9 +3017,13 @@ ADMIN
features [--json] [--auto-fix] Scan usage + recommend unused features
autopilot [--repo] [--interval N] Self-maintaining brain daemon
config [show|get|set] <key> [val] Brain config
protocol [conformance|stats] MEMORY_VERBS v1: schemas, conformance
certification, local usage stats + TTHW
storage status [--repo <path>] Storage tier status and health
[--json] (git-tracked vs supabase-only)
serve MCP server (stdio)
--surface verbs|full Tool surface: the 5 memory verbs only, or
every op (default full; verbs = quickstart)
serve --http [--port N] HTTP MCP server with OAuth 2.1
--token-ttl N Access token TTL in seconds (default: 3600)
--enable-dcr Enable Dynamic Client Registration (DCR clients default to authorization_code)
+47
View File
@@ -4944,6 +4944,46 @@ export async function computePoolReapHealthCheck(
* Policy-skill install state is reported in details (it ships into the HOST
* repo, so absence in gbrain's own skills dir is expected, not a failure).
*/
/**
* MEMORY_VERBS v1 (Cathedral 1, E4) usage-sidecar health. Read-only,
* fail-open. Stats only (local JSONL, never uploaded; never source of truth):
* - no sidecar file ok, "no verb calls recorded yet" (fresh install)
* - recent events parse ok, names the last verb + timestamp
* - file exists, unreadable warn (observability degraded, verbs unaffected)
*/
export async function buildMemoryVerbsCheck(): Promise<Check> {
const name = 'memory_verbs_usage';
try {
const { readVerbUsage, usageLogPath } = await import('../core/verbs/usage-log.ts');
if (!existsSync(usageLogPath())) {
return {
name,
status: 'ok',
message: 'no verb calls recorded yet (sidecar appears on first remember/recall/entity/synthesize/forget)',
};
}
const events = await readVerbUsage({ days: 30 });
if (events.length === 0) {
return { name, status: 'ok', message: 'sidecar present; no verb calls in the last 30 days' };
}
const last = events[events.length - 1];
const byVerb = new Map<string, number>();
for (const e of events) byVerb.set(e.verb, (byVerb.get(e.verb) ?? 0) + 1);
const mix = [...byVerb.entries()].map(([v, n]) => `${v}:${n}`).join(' ');
return {
name,
status: 'ok',
message: `${events.length} verb calls in 30d (${mix}); last ${last.verb} at ${last.ts} — local JSONL only, never uploaded`,
};
} catch (e) {
return {
name,
status: 'warn',
message: `verb usage sidecar unreadable (${e instanceof Error ? e.message : String(e)}) — observability degraded; verbs unaffected`,
};
}
}
export function buildRetrievalReflexCheck(skillsDir: string | null): Check {
const name = 'retrieval_reflex_health';
try {
@@ -5137,6 +5177,13 @@ export async function buildChecks(
checks.push(buildRetrievalReflexCheck(skillsDir));
}
// 1c. MEMORY_VERBS v1 usage sidecar health (Cathedral 1, E4). Read-only,
// fail-open: reports whether the local JSONL sidecar is present + parseable
// and when a verb last fired. Local file only — never uploaded.
if (scope === 'all') {
checks.push(await buildMemoryVerbsCheck());
}
// 2. Skill conformance (SKILL group — gated)
if (scope === 'all' && skillsDir) {
const conformanceResult = skillConformanceCheck(skillsDir);
+27
View File
@@ -1049,6 +1049,9 @@ async function initPGLite(opts: {
// gbrain invocation). mode_prompted=true so the upgrade-time banner doesn't
// also fire on a fresh install. Hands-off: gbrain config set self_upgrade.mode auto
config.self_upgrade = { mode: 'notify', mode_prompted: true, ...(config.self_upgrade ?? {}) };
// MEMORY_VERBS v1 [D6C]: TTHW stamp — `gbrain protocol stats` derives
// install→first-verb-call from this. Idempotent on re-init.
config.protocol_installed_at = config.protocol_installed_at ?? new Date().toISOString();
saveConfig(config);
if (opts.schemaPack) {
process.stderr.write(
@@ -1086,6 +1089,7 @@ async function initPGLite(opts: {
} else {
console.log('Next: gbrain import <dir>');
}
printMemoryVerbsQuickstart();
console.log('');
console.log('When you outgrow local: gbrain migrate --to supabase');
reportModStatus();
@@ -1103,6 +1107,26 @@ async function initPGLite(opts: {
}
}
/**
* MEMORY_VERBS v1 quickstart funnel (E3 + D4B + T1 consent). Printed at the
* end of both init epilogues. The copy-next block is EXACTLY three commands
* (codex DX 9): wire the harness, write a memory, prove the resurrection.
* The demo uses the facts arm only, so it works with NO embedding key [F-B].
*/
function printMemoryVerbsQuickstart(): void {
console.log('');
console.log('Give your agent memory (copy these three commands):');
console.log(' claude mcp add gbrain -- gbrain serve --surface verbs');
console.log(' gbrain remember "I prefer dark mode in every editor" --provenance demo --entity people/me');
console.log(' gbrain recall --entity people/me');
console.log('Now ask your agent in a NEW session — it remembers.');
console.log('');
console.log('Note: memories agents save are readable by every agent connected to');
console.log('this brain; use visibility:"private" for local-only facts.');
console.log('Other harnesses (Codex, OpenClaw): docs/protocol/MEMORY_VERBS_v1.md');
console.log('If `claude` is not found: install Claude Code first, or use the per-harness blocks in that doc.');
}
async function initPostgres(opts: {
databaseUrl: string;
jsonOutput: boolean;
@@ -1297,6 +1321,8 @@ async function initPostgres(opts: {
// gbrain invocation). mode_prompted=true so the upgrade-time banner doesn't
// also fire on a fresh install. Hands-off: gbrain config set self_upgrade.mode auto
config.self_upgrade = { mode: 'notify', mode_prompted: true, ...(config.self_upgrade ?? {}) };
// MEMORY_VERBS v1 [D6C]: TTHW stamp (see the PGLite path).
config.protocol_installed_at = config.protocol_installed_at ?? new Date().toISOString();
saveConfig(config);
console.log('Config saved to ~/.gbrain/config.json');
if (opts.schemaPack) {
@@ -1331,6 +1357,7 @@ async function initPostgres(opts: {
} else {
console.log('Next: gbrain import <dir>');
}
printMemoryVerbsQuickstart();
reportModStatus();
const { printAdvisoryIfRecommended } = await import('../core/skillpack/post-install-advisory.ts');
const { VERSION } = await import('../version.ts');
+2
View File
@@ -26,6 +26,7 @@ import { v0_28_0 } from './v0_28_0.ts';
import { v0_29_1 } from './v0_29_1.ts';
import { v0_31_0 } from './v0_31_0.ts';
import { v0_32_2 } from './v0_32_2.ts';
import { v0_43_0 } from './v0_43_0.ts';
export const migrations: Migration[] = [
v0_11_0,
@@ -43,6 +44,7 @@ export const migrations: Migration[] = [
v0_29_1,
v0_31_0,
v0_32_2,
v0_43_0,
];
/** Look up a migration by exact version string. */
+33
View File
@@ -0,0 +1,33 @@
/**
* v0.43.0.0 migration MEMORY_VERBS v1 (Cathedral 1).
*
* PITCH-ONLY. There is NO schema or data migration: the five frozen memory
* verbs (recall/remember/entity/synthesize/forget) ride the existing facts,
* pages, and typed-graph tables. This entry exists solely so `gbrain
* post-upgrade` / the self-upgrade NOTIFY channel actively tells an existing
* install that the verbs landed and how to switch a harness onto them the
* propagation path the verbs otherwise lacked (the default surface stays
* `full`, so an upgrade alone does NOT steer agents to the verbs).
*
* The orchestrator is a no-op that reports `complete` immediately
* idempotent by construction (it does nothing), so apply-migrations records
* the ledger row and moves on.
*/
import type { Migration, OrchestratorOpts, OrchestratorResult } from './types.ts';
async function orchestrator(_opts: OrchestratorOpts): Promise<OrchestratorResult> {
// No schema/data work — MEMORY_VERBS v1 is a façade over existing tables.
return { version: '0.43.0', status: 'complete', phases: [] };
}
export const v0_43_0: Migration = {
version: '0.43.0',
featurePitch: {
headline:
'Five memory verbs — recall, remember, entity, synthesize, forget — are now the agent-facing memory protocol (MEMORY_VERBS v1).',
description:
'Point any MCP harness at `gbrain serve --surface verbs` to expose exactly these five self-describing tools instead of the full op wall: remember(fact, provenance) writes durable facts; recall(query|entity, budget_tokens) returns budget-packed memory; entity(name) is a zero-LLM card; synthesize(question) is the explicitly-expensive cross-page answer; forget(id) expires a fact. Existing `gbrain serve` (full surface) keeps working and now also lists the verbs, but `--surface verbs` is the clean agent surface. Set a default with `gbrain config set mcp_surface verbs`. Verify any endpoint with `gbrain protocol conformance`; see usage with `gbrain protocol stats`. Full contract: docs/protocol/MEMORY_VERBS_v1.md.',
},
orchestrator,
};
+304
View File
@@ -0,0 +1,304 @@
/**
* MEMORY_VERBS v1 `gbrain protocol` (Cathedral 1, E2 + E4 + D6C).
*
* gbrain protocol [--json] machine-readable verb schemas + version
* gbrain protocol conformance [...] certify an MCP endpoint against the contract
* gbrain protocol stats [--days N] local verb usage + TTHW (never uploaded)
*
* Conformance targets:
* (default) spawn gbrain's own stdio server (self-certify)
* --target http://host/mcp Streamable HTTP endpoint [--token gbrain_xxx]
* --target "cmd arg arg" spawn any stdio MCP server (space-split; for
* commands with complex quoting, certify via a
* small wrapper script)
*
* Input schemas emit from the LIVE Operation defs (doc/code can't drift);
* response shapes come from the hand-authored RESPONSE_SCHEMAS registry,
* which conformance validates LIVE responses against registry-vs-code
* drift is caught by the same fixtures that certify servers [c8].
*/
import { operationsByName } from '../core/operations.ts';
import {
RESPONSE_SCHEMAS,
ERROR_SCHEMA,
MEMORY_VERBS_VERSION,
VERB_NAMES,
type VerbName,
} from '../core/verbs.ts';
import { buildToolDefs } from '../mcp/tool-defs.ts';
import { runConformance, type ConformanceClient } from '../core/verbs/conformance.ts';
import { readVerbUsage, earliestVerbUsageTs, usageLogPath } from '../core/verbs/usage-log.ts';
import { loadConfig } from '../core/config.ts';
import { setCliExitVerdict } from '../core/cli-force-exit.ts';
const HELP = `gbrain protocol — the MEMORY_VERBS v1 wire contract (frozen, additive-forever)
Usage:
gbrain protocol [--json] Print the protocol: verb input schemas
(from live defs), response schemas, error
contract, version. --json for machines.
gbrain protocol conformance Certify an MCP endpoint against the
contract. Default target: gbrain's own
stdio server (self-certification).
--target http://host:3131/mcp HTTP MCP endpoint (add --token gbrain_xxx)
--target "bun run src/cli.ts serve" Any stdio MCP server command
--synthesize Also live-call synthesize (costs money
when an LLM key is configured; without
one it asserts the clean 'unavailable'
error what CI does)
--json Machine-readable report
gbrain protocol stats [--days N] Per-verb usage, error rate, budget drops,
entity hit rate, TTHW (install -> first
verb call). Local JSONL only this data
never leaves the machine. Default 30 days.
Docs: docs/protocol/MEMORY_VERBS_v1.md
Why default surface is 'full': verbs is for agents and quickstarts
(gbrain serve --surface verbs); full preserves existing advanced tooling.`;
export async function runProtocol(args: string[]): Promise<void> {
if (args.includes('--help') || args.includes('-h')) {
console.log(HELP);
return;
}
const sub = args[0] && !args[0].startsWith('--') ? args[0] : null;
if (sub === 'conformance') {
await runConformanceCommand(args.slice(1));
return;
}
if (sub === 'stats') {
await runStatsCommand(args.slice(1));
return;
}
if (sub === null) {
printProtocol(args.includes('--json'));
return;
}
console.error(`Unknown protocol subcommand: ${sub}`);
console.log(HELP);
// [ship P1.3] PGLite/WASM clobbers process.exitCode; the CLI exit seam reads
// the gbrain-owned verdict (setCliExitVerdict), not process.exitCode.
setCliExitVerdict(1);
}
// ─── protocol [--json] ───────────────────────────────────────────────────────
function buildProtocolDocument() {
const verbOps = VERB_NAMES.map(n => operationsByName[n]).filter(Boolean);
const toolDefs = buildToolDefs(verbOps);
const verbs: Record<string, unknown> = {};
for (const def of toolDefs) {
verbs[def.name] = {
description: def.description,
input_schema: def.inputSchema,
...(def.annotations ? { annotations: def.annotations } : {}),
response_schema: RESPONSE_SCHEMAS[def.name as VerbName],
};
}
return {
protocol: 'MEMORY_VERBS',
protocol_version: MEMORY_VERBS_VERSION,
versioning_policy:
'additive-forever: v1 field names and semantics never change; new optional fields/params may be added; breaking changes require MEMORY_VERBS_v2 (expected never)',
verbs,
error_schema: ERROR_SCHEMA,
};
}
function printProtocol(json: boolean): void {
const doc = buildProtocolDocument();
if (json) {
console.log(JSON.stringify(doc, null, 2));
return;
}
console.log(`MEMORY_VERBS v${MEMORY_VERBS_VERSION} — the frozen memory protocol (additive-forever)\n`);
for (const name of VERB_NAMES) {
const op = operationsByName[name];
if (!op) continue;
const params = Object.entries(op.params)
.map(([k, v]) => `${k}${v.required ? '' : '?'}`)
.join(', ');
console.log(` ${name}(${params})`);
console.log(` ${op.description.split('. ')[0]}.`);
}
console.log(`\nFull schemas: gbrain protocol --json`);
console.log(`Doc: docs/protocol/MEMORY_VERBS_v1.md`);
}
// ─── protocol conformance ────────────────────────────────────────────────────
async function runConformanceCommand(args: string[]): Promise<void> {
const json = args.includes('--json');
const synthesize = args.includes('--synthesize');
const targetIdx = args.indexOf('--target');
const target = targetIdx >= 0 ? args[targetIdx + 1] : null;
const tokenIdx = args.indexOf('--token');
const token = tokenIdx >= 0 ? args[tokenIdx + 1] : null;
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
const client = new Client({ name: 'gbrain-conformance', version: '1.0.0' }, { capabilities: {} });
let transport: { close(): Promise<void> };
if (target && /^https?:\/\//.test(target)) {
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
const t = new StreamableHTTPClientTransport(new URL(target), {
...(token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}),
});
await client.connect(t);
transport = t;
} else {
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
let command: string;
let cmdArgs: string[];
if (target) {
const parts = target.split(/\s+/).filter(Boolean);
command = parts[0];
cmdArgs = parts.slice(1);
} else {
// Self-certification: spawn our own server. Dev (bun run src/cli.ts)
// vs compiled binary both resolve to "this gbrain, serve".
const entry = process.argv[1] ?? '';
if (entry.endsWith('.ts')) {
command = process.execPath;
cmdArgs = ['run', entry, 'serve'];
} else {
command = process.execPath;
cmdArgs = ['serve'];
}
}
const env: Record<string, string> = {};
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) env[k] = v;
const t = new StdioClientTransport({ command, args: cmdArgs, env });
await client.connect(t);
transport = t;
}
const adapter: ConformanceClient = {
listTools: async () => {
const { tools } = await client.listTools();
return tools.map(t => ({
name: t.name,
description: t.description,
annotations: (t as { annotations?: unknown }).annotations,
}));
},
callTool: async (name, callArgs) => {
const res = (await client.callTool({ name, arguments: callArgs })) as {
isError?: boolean;
content?: Array<{ type?: string; text?: string }>;
};
const text = (res.content ?? []).map(c => (typeof c.text === 'string' ? c.text : '')).join('\n');
return { isError: res.isError, text };
},
};
try {
const report = await runConformance(adapter, { synthesize });
if (json) {
console.log(JSON.stringify(report, null, 2));
} else {
console.log(`MEMORY_VERBS v${report.protocol_version} conformance — target: ${target ?? 'self (stdio)'}\n`);
for (const r of report.results) {
const mark = r.status === 'pass' ? '✓' : r.status === 'skip' ? '' : '✗';
console.log(` ${mark} ${r.name}${r.detail ? `${r.detail}` : ''}`);
}
console.log(`\n${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped`);
console.log(report.ok ? 'CONFORMANT' : 'NOT CONFORMANT');
}
// [ship P1.3] non-conformant target MUST exit non-zero so CI fails. The
// CLI exit seam reads setCliExitVerdict (process.exitCode is unreliable on
// PGLite/WASM and ignored by the force-exit path).
if (!report.ok) setCliExitVerdict(1);
} finally {
try { await client.close(); } catch { /* best-effort */ }
try { await transport.close(); } catch { /* best-effort */ }
}
}
// ─── protocol stats ──────────────────────────────────────────────────────────
async function runStatsCommand(args: string[]): Promise<void> {
const json = args.includes('--json');
const daysIdx = args.indexOf('--days');
const days = daysIdx >= 0 ? parseInt(args[daysIdx + 1] ?? '30', 10) || 30 : 30;
const events = await readVerbUsage({ days });
const byVerb = new Map<string, { calls: number; errors: number; latency: number; budgetDropped: number; entityFound: number; entityMiss: number }>();
for (const e of events) {
const v = byVerb.get(e.verb) ?? { calls: 0, errors: 0, latency: 0, budgetDropped: 0, entityFound: 0, entityMiss: 0 };
v.calls += 1;
if (!e.ok) v.errors += 1;
v.latency += e.latency_ms;
if (typeof e.budget_dropped === 'number') v.budgetDropped += e.budget_dropped;
if (e.entity_found === true) v.entityFound += 1;
if (e.entity_found === false) v.entityMiss += 1;
byVerb.set(e.verb, v);
}
// TTHW [D6C]: install stamp → first verb call, the real measured number the
// post-ship boomerang review compares against the 25 min target.
const cfg = loadConfig();
const installedAt = cfg?.protocol_installed_at ?? null;
const firstCall = await earliestVerbUsageTs();
let tthw: string | null = null;
if (installedAt && firstCall) {
const deltaMs = Date.parse(firstCall) - Date.parse(installedAt);
if (Number.isFinite(deltaMs) && deltaMs >= 0) tthw = formatDuration(deltaMs);
}
if (json) {
console.log(JSON.stringify({
days,
total_calls: events.length,
by_verb: Object.fromEntries(
[...byVerb.entries()].map(([k, v]) => [k, {
calls: v.calls,
errors: v.errors,
avg_latency_ms: v.calls ? Math.round(v.latency / v.calls) : 0,
budget_dropped_total: v.budgetDropped,
...(k === 'entity' ? { found: v.entityFound, miss: v.entityMiss } : {}),
}]),
),
tthw_install_to_first_verb: tthw,
installed_at: installedAt,
first_verb_call: firstCall,
sidecar: usageLogPath(),
privacy: 'local JSONL only — never uploaded',
}, null, 2));
return;
}
console.log(`MEMORY_VERBS usage — last ${days} days (local JSONL only, never uploaded)\n`);
if (events.length === 0) {
console.log(' no verb calls recorded yet');
} else {
for (const [verb, v] of [...byVerb.entries()].sort((a, b) => b[1].calls - a[1].calls)) {
const extras = [
v.errors ? `${v.errors} errors` : null,
v.budgetDropped ? `${v.budgetDropped} budget-dropped` : null,
verb === 'entity' && (v.entityFound || v.entityMiss)
? `hit ${v.entityFound}/${v.entityFound + v.entityMiss}`
: null,
].filter(Boolean).join(', ');
console.log(` ${verb.padEnd(11)} ${String(v.calls).padStart(5)} calls avg ${Math.round(v.latency / v.calls)}ms${extras ? ` (${extras})` : ''}`);
}
}
if (tthw) console.log(`\n TTHW: first verb call ${tthw} after install`);
else if (installedAt) console.log(`\n TTHW: no verb calls since install (${installedAt})`);
console.log(` sidecar: ${usageLogPath()}`);
}
function formatDuration(ms: number): string {
const s = Math.round(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m${s % 60 ? `${s % 60}s` : ''}`;
const h = Math.floor(m / 60);
if (h < 48) return `${h}h${m % 60 ? `${m % 60}m` : ''}`;
return `${Math.floor(h / 24)}d`;
}
/** Test seam: the protocol document, without printing. */
export { buildProtocolDocument };
+78
View File
@@ -66,6 +66,10 @@ interface ParsedFlags {
json: boolean;
source: string;
limit: number;
// MEMORY_VERBS v1 [c4]: recall's verb params, routed through the recall OP
// (this hand-rolled CLI otherwise ignores unknown flags silently).
query: string | null;
budgetTokens: number | null;
// v0.32
sinceLastRun: boolean;
pending: boolean;
@@ -94,6 +98,8 @@ function parseFlags(args: string[]): ParsedFlags {
json: false,
source: 'default',
limit: 50,
query: null,
budgetTokens: null,
sinceLastRun: false,
pending: false,
rollup: false,
@@ -112,6 +118,8 @@ function parseFlags(args: string[]): ParsedFlags {
if (a === '--json') { out.json = true; continue; }
if (a === '--source') { out.source = args[++i] ?? 'default'; continue; }
if (a === '--limit') { out.limit = parseInt(args[++i] ?? '50', 10) || 50; continue; }
if (a === '--query') { out.query = args[++i] ?? null; continue; }
if (a === '--budget-tokens') { out.budgetTokens = parseInt(args[++i] ?? '', 10) || null; continue; }
if (a === '--since-last-run') { out.sinceLastRun = true; continue; }
if (a === '--pending') { out.pending = true; continue; }
if (a === '--rollup') { out.rollup = true; continue; }
@@ -223,6 +231,13 @@ export async function runRecall(engine: BrainEngine, args: string[]): Promise<vo
const sourceId = await resolveSourceForRecall(engine, flags.source, thinClient);
// MEMORY_VERBS v1 [c4]: the verb params route through the recall OP so the
// CLI and MCP exercise the same arm (query/budget packing/superset envelope).
if (flags.query !== null || flags.budgetTokens !== null) {
await runRecallVerb(engine, flags, sourceId);
return;
}
if (flags.watchSeconds !== null) {
await runWatchLoop(engine, flags, sourceId, thinClient, flags.watchSeconds);
return;
@@ -230,6 +245,69 @@ export async function runRecall(engine: BrainEngine, args: string[]): Promise<vo
await runRecallOnce(engine, flags, sourceId, thinClient, 'briefing');
}
/**
* MEMORY_VERBS v1 `gbrain recall --query ... [--budget-tokens N]` routes
* through the recall OP (same code path MCP exercises) and renders facts +
* search results with the budget footer. `--json` prints the raw envelope.
*/
async function runRecallVerb(engine: BrainEngine, flags: ParsedFlags, sourceId: string): Promise<void> {
const { operationsByName } = await import('../core/operations.ts');
const op = operationsByName['recall'];
const ctx = {
engine,
config: loadConfig() || { engine: 'pglite' as const },
logger: {
info: (m: string) => process.stderr.write(`[info] ${m}\n`),
warn: (m: string) => process.stderr.write(`[warn] ${m}\n`),
error: (m: string) => process.stderr.write(`[error] ${m}\n`),
},
dryRun: false,
remote: false as const,
sourceId,
};
const result = (await op.handler(ctx, {
...(flags.entity ? { entity: flags.entity } : {}),
...(flags.query ? { query: flags.query } : {}),
...(flags.budgetTokens ? { budget_tokens: flags.budgetTokens } : {}),
...(flags.since ? { since: flags.since.toISOString() } : {}),
...(flags.grep ? { grep: flags.grep } : {}),
include_expired: flags.includeExpired,
limit: flags.limit,
})) as {
facts: Array<{ fact_id: string; fact: string; kind: string; entity_slug: string | null; provenance: string }>;
results?: Array<{ slug: string; title: string | null; evidence: string; chunk: string | null }>;
search_degraded?: string;
budget_tokens?: number;
budget_used?: number;
dropped_count?: number;
};
if (flags.json) {
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
return;
}
const lines: string[] = [];
if (result.facts.length) {
lines.push('Facts:');
for (const f of result.facts) {
lines.push(` #${f.fact_id} [${f.kind}]${f.entity_slug ? ` (${f.entity_slug})` : ''} ${f.fact}${f.provenance}`);
}
}
if (result.results?.length) {
lines.push('Pages:');
for (const r of result.results) {
lines.push(` ${r.slug} [${r.evidence}] ${r.title ?? ''}`);
if (r.chunk) lines.push(` ${r.chunk.replace(/\s+/g, ' ').slice(0, 160)}`);
}
}
if (!lines.length) lines.push('Nothing recalled.');
if (result.search_degraded) lines.push(`note: search degraded (${result.search_degraded})`);
if (result.budget_tokens !== undefined) {
lines.push(`budget: ${result.budget_used}/${result.budget_tokens} tokens used, ${result.dropped_count} dropped`);
}
process.stdout.write(lines.join('\n') + '\n');
}
async function runRecallOnce(
engine: BrainEngine,
flags: ParsedFlags,
+20 -1
View File
@@ -33,6 +33,7 @@ import { hasScope, ALLOWED_SCOPES_LIST, normalizeScopesInput } from '../core/sco
import { normalizeSourceInput, normalizeFederatedReadInput } from '../core/source-id.ts';
import { summarizeMcpParams, dispatchToolCall } from '../mcp/dispatch.ts';
import { paramDefToSchema } from '../mcp/tool-defs.ts';
import { filterOpsForSurface } from '../mcp/surface.ts';
import { getBrainHotMemoryMeta } from '../core/facts/meta-hook.ts';
import { loadConfig } from '../core/config.ts';
import { buildError, serializeError } from '../core/errors.ts';
@@ -475,6 +476,12 @@ interface ServeHttpOptions {
* tracking the regenerated value through other means.
*/
suppressBootstrapToken?: boolean;
/**
* MEMORY_VERBS v1: tool-surface mode. 'verbs' = exactly the five protocol
* verbs; 'full' (default) = every non-localOnly operation. Enforced on the
* tool list AND in dispatch (fail-closed).
*/
surface?: 'verbs' | 'full';
/**
* #2624: force-print the generated admin bootstrap token even on a
* non-TTY (containerized) start. By default the raw token is only printed
@@ -1859,7 +1866,13 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
// ---------------------------------------------------------------------------
// MCP tool calls (bearer auth + scope enforcement)
// ---------------------------------------------------------------------------
const mcpOperations = operations.filter(op => !op.localOnly);
// MEMORY_VERBS v1: surface filter applies AFTER the localOnly filter; the
// same set feeds dispatch as allowedOps so hidden ops are uncallable, not
// just unlisted [c2].
const surface = options.surface ?? 'full';
const mcpOperations = filterOpsForSurface(operations.filter(op => !op.localOnly), surface);
const surfaceAllowedOps: ReadonlySet<string> | undefined =
surface === 'full' ? undefined : new Set(mcpOperations.map(o => o.name));
// v0.36.x #1076: MCP Streamable HTTP spec — GET /mcp opens an optional SSE
// backchannel for server-initiated messages. gbrain's transport is stateless
@@ -1922,6 +1935,9 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
),
required: Object.entries(op.params).filter(([, v]) => v.required).map(([k]) => k),
},
// MEMORY_VERBS v1: ToolAnnotations emitted only when the op defines
// them — existing tools stay byte-identical (mirrors buildToolDefs).
...(op.annotations ? { annotations: op.annotations } : {}),
})),
};
});
@@ -2047,6 +2063,9 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
takesHoldersAllowList: tokenAllowList,
sourceId: tokenSourceId,
metaHook: getBrainHotMemoryMeta,
// MEMORY_VERBS v1: fail-closed surface enforcement + usage attribution.
...(surfaceAllowedOps ? { allowedOps: surfaceAllowedOps } : {}),
surface,
// v0.31 follow-up fix: thread auth so the whoami op (and any
// future scope-aware handlers) can introspect the caller. The
// original D12/eE1 refactor moved dispatch into dispatchToolCall
+15 -4
View File
@@ -44,7 +44,7 @@ export interface ServeOptions {
// (which unconditionally attaches a 'data' listener to real
// process.stdin and would pollute the test runner's stdin handle).
// Defaults to the real implementation when omitted.
startMcpServer?: (engine: BrainEngine) => Promise<void>;
startMcpServer?: (engine: BrainEngine, opts?: { surface?: 'verbs' | 'full' }) => Promise<void>;
// Test seam for the parent-process watchdog. The default
// (`readLiveParentPid`) reads the live kernel PPID via `ps` on POSIX
// because `process.ppid` is captured at process creation and does not
@@ -150,6 +150,13 @@ export async function runServe(
// that used `gbrain auth create` keep working unchanged).
const isHttp = args.includes('--http');
// MEMORY_VERBS v1: tool-surface mode. Flag > config `mcp_surface` > 'full'.
// 'verbs' exposes exactly the five protocol verbs (the quickstart surface);
// 'full' (default) keeps every operation — existing installs see no change.
const { parseSurfaceFlag, resolveSurface } = await import('../mcp/surface.ts');
const { loadConfig } = await import('../core/config.ts');
const surface = resolveSurface(parseSurfaceFlag(args), loadConfig());
if (isHttp) {
const portIdx = args.indexOf('--port');
const port = portIdx >= 0 ? parseInt(args[portIdx + 1]) || 3131 : 3131;
@@ -196,7 +203,7 @@ export async function runServe(
const printAdminToken = args.includes('--print-admin-token');
const { runServeHttp } = await import('./serve-http.ts');
await runServeHttp(engine, { port, tokenTtl, enableDcr, enableDcrInsecure, publicUrl, logFullParams, bind, suppressBootstrapToken, printAdminToken });
await runServeHttp(engine, { port, tokenTtl, enableDcr, enableDcrInsecure, publicUrl, logFullParams, bind, suppressBootstrapToken, printAdminToken, surface });
await finishHttpServe(engine, opts);
return;
@@ -207,7 +214,11 @@ export async function runServe(
// trigger graceful release of the PGLite write lock held by `engine`.
// The HTTP / OAuth path above has its own lifecycle in serve-http.ts
// and is intentionally NOT wired into this stdio plumbing.
console.error('Starting GBrain MCP server (stdio)...');
console.error(
surface === 'verbs'
? 'Starting GBrain MCP server (stdio) — serving 5 memory verbs (MEMORY_VERBS v1)...'
: 'Starting GBrain MCP server (stdio)...',
);
installStdioLifecycle(engine, args, opts);
@@ -245,7 +256,7 @@ export async function runServe(
}
try {
await start(engine);
await start(engine, { surface });
} finally {
if (bootDeadline) clearTimeout(bootDeadline);
}
+54 -53
View File
@@ -8,20 +8,20 @@
// (help-text mentions count): accepting an ignored flag is the pre-#2185
// status quo; missing a real one breaks working invocations.
export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'advisor': ['--aliases', '--all', '--apply', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--skills-dir', '--source', '--stale', '--supersessions', '--thin', '--verbose', '--workspace', '--yes'],
'advisor': ['--aliases', '--all', '--apply', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--skills-dir', '--source', '--stale', '--supersessions', '--surface', '--thin', '--verbose', '--workspace', '--yes'],
'agent': ['--aliases', '--all', '--brain', '--detach', '--fanout-manifest', '--flag', '--flags', '--follow', '--help', '--include-null-signature', '--json', '--max-turns', '--model', '--no-extract', '--no-follow', '--note', '--pattern', '--pending', '--reset', '--resolve', '--since', '--source', '--stale', '--subagent-def', '--supersessions', '--thin', '--timeout-ms', '--tools', '--word'],
'anomalies': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lookback-days', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--sigma', '--since', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
'apply-migrations': ['--ab', '--all', '--auto-update', '--brain', '--break-lock', '--build-index', '--by-mention', '--compile', '--days', '--dry-run', '--exclusive', '--fast', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--host-dir', '--http', '--json', '--lang', '--list', '--locks', '--markdown', '--max-age', '--migrate-only', '--migration', '--mode', '--multimodal', '--no-autopilot-install', '--no-embedding', '--no-extract', '--non-interactive', '--phase', '--priority', '--refresh-unqualified', '--remediate', '--rollback', '--skip-verify', '--source', '--stale', '--undo-wave', '--use-captured-snapshot', '--with-calibration', '--yes'],
'auth': ['--aliases', '--all', '--bound-brain', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--enable-dcr', '--enable-dcr-insecure', '--fast', '--federated-read', '--force', '--from-pages', '--grant-types', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--redirect-uri', '--reset', '--resolve', '--scopes', '--source', '--stale', '--supersessions', '--takes-holders', '--thin', '--token', '--token-endpoint-auth-method', '--yes'],
'autopilot': ['--aliases', '--all', '--auto-fix', '--batch', '--brain', '--break-lock', '--by-type', '--check', '--dimensions', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--ff-only', '--fix', '--force', '--force-break-lock', '--force-retry', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--json', '--markdown', '--max-age', '--max-rss', '--max-usd', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-inject', '--no-mutate', '--no-worker', '--non-interactive', '--now', '--once', '--output', '--path', '--pattern', '--pending', '--phase', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--source', '--stale', '--status', '--supersessions', '--swap-only', '--target', '--target-score', '--thin', '--timeout', '--to', '--uninstall', '--unsafe-bypass-dream-guard', '--user', '--version', '--yes'],
'anomalies': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lookback-days', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--sigma', '--since', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout'],
'apply-migrations': ['--ab', '--all', '--auto-update', '--brain', '--break-lock', '--build-index', '--by-mention', '--compile', '--days', '--dry-run', '--exclusive', '--fast', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--host-dir', '--http', '--json', '--lang', '--list', '--locks', '--markdown', '--max-age', '--migrate-only', '--migration', '--mode', '--multimodal', '--no-autopilot-install', '--no-embedding', '--no-extract', '--non-interactive', '--phase', '--priority', '--refresh-unqualified', '--remediate', '--rollback', '--skip-verify', '--source', '--stale', '--surface', '--undo-wave', '--use-captured-snapshot', '--with-calibration', '--yes'],
'auth': ['--aliases', '--all', '--bound-brain', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--enable-dcr', '--enable-dcr-insecure', '--fast', '--federated-read', '--force', '--from-pages', '--grant-types', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--redirect-uri', '--reset', '--resolve', '--scopes', '--source', '--stale', '--supersessions', '--surface', '--takes-holders', '--thin', '--token', '--token-endpoint-auth-method', '--yes'],
'autopilot': ['--aliases', '--all', '--auto-fix', '--batch', '--brain', '--break-lock', '--by-type', '--check', '--dimensions', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--ff-only', '--fix', '--force', '--force-break-lock', '--force-retry', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--json', '--markdown', '--max-age', '--max-rss', '--max-usd', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-inject', '--no-mutate', '--no-worker', '--non-interactive', '--now', '--once', '--output', '--path', '--pattern', '--pending', '--phase', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--source', '--stale', '--status', '--supersessions', '--surface', '--swap-only', '--target', '--target-score', '--thin', '--timeout', '--to', '--uninstall', '--unsafe-bypass-dream-guard', '--user', '--version', '--yes'],
'backfill': ['--aliases', '--all', '--batch-size', '--brain', '--concurrency', '--dry-run', '--fresh', '--help', '--include-null-signature', '--json', '--keep-index', '--list', '--max-errors', '--max-rows', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--source', '--stale', '--supersessions', '--thin'],
'bench': ['--baseline', '--brain', '--explain', '--force', '--from', '--help', '--json', '--label', '--lang', '--limit', '--markdown', '--multimodal', '--near-symbol', '--restore-only', '--source', '--stale', '--symbol-kind', '--thin', '--threshold-jaccard', '--threshold-latency-multiplier', '--threshold-top1', '--to', '--tool'],
'book-mirror': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--author', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--budget-usd-per-day', '--by-mention', '--chapters-dir', '--content', '--context-file', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--follow', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-turns', '--max-usd', '--mode', '--model', '--multimodal', '--no-confirm', '--no-embedding', '--no-extract', '--no-follow', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--thin', '--timeout', '--timeout-ms', '--title', '--trusted-extraction', '--url', '--with-db', '--yes'],
'brainstorm': ['--aliases', '--all', '--brain', '--chunker-debug', '--code', '--compile', '--fast', '--fix', '--force', '--force-rechunk', '--force-resume', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--lang', '--limit', '--list-runs', '--markdown', '--max-cost', '--max-far-set', '--max-ideas-per-judge-call', '--model', '--no-embed', '--no-embedding', '--no-extract', '--no-save', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--retry-failed', '--retry-judge', '--save', '--source', '--stale', '--strict-budget', '--supersessions', '--thin', '--timeout', '--yes'],
'cache': ['--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--source', '--yes'],
'calibration': ['--ab', '--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--holder', '--http', '--image', '--include-null-signature', '--json', '--key-prefix', '--kind', '--lang', '--limit', '--markdown', '--max-usd', '--mode', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--phase', '--progress-interval', '--progress-json', '--quiet', '--regenerate', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scrub-gstack', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--symbol-kind', '--thin', '--trusted-extraction', '--undo-wave', '--url', '--with-calibration', '--with-db', '--yes'],
'call': ['--aliases', '--all', '--all-sources', '--as-context', '--auto-fix', '--background', '--brain', '--by-mention', '--catch-up', '--concurrency', '--confirm-destructive', '--content', '--cost-estimate', '--count', '--days', '--depth', '--dim', '--dir', '--direction', '--enable-dcr', '--enable-dcr-insecure', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--from', '--from-meetings', '--grant-types', '--grep', '--hard-deadline', '--help', '--http', '--image', '--include-expired', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--install', '--interval', '--json', '--key', '--kind', '--lang', '--limit', '--link-source', '--link-type', '--migrate-only', '--missing-path', '--multimodal', '--ner', '--no-embed', '--no-expand', '--no-extract', '--no-federated', '--no-hard-deadline', '--no-retry-connect', '--no-save', '--older-than', '--page', '--param', '--params', '--password', '--path', '--pattern', '--pending', '--pglite', '--port', '--progress-interval', '--progress-json', '--public-url', '--queue', '--quiet', '--reenrich-after', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--sigma', '--since', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--symbol-kind', '--tag', '--thin', '--timeout', '--to', '--today', '--token', '--token-ttl', '--tools-json', '--type', '--url', '--version', '--watch', '--with-calibration', '--workers', '--yes'],
'capture': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--depth', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--stdin', '--supersessions', '--thin', '--timeout', '--trusted-extraction', '--type', '--url', '--what', '--where', '--who', '--with-db', '--yes'],
'book-mirror': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--author', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--budget-usd-per-day', '--by-mention', '--chapters-dir', '--content', '--context-file', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--follow', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-turns', '--max-usd', '--mode', '--model', '--multimodal', '--no-confirm', '--no-embedding', '--no-extract', '--no-follow', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--timeout-ms', '--title', '--trusted-extraction', '--url', '--with-db', '--yes'],
'brainstorm': ['--aliases', '--all', '--brain', '--chunker-debug', '--code', '--compile', '--fast', '--fix', '--force', '--force-rechunk', '--force-resume', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--lang', '--limit', '--list-runs', '--markdown', '--max-cost', '--max-far-set', '--max-ideas-per-judge-call', '--model', '--no-embed', '--no-embedding', '--no-extract', '--no-save', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--retry-failed', '--retry-judge', '--save', '--source', '--stale', '--strict-budget', '--supersessions', '--surface', '--thin', '--timeout', '--yes'],
'cache': ['--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--source', '--surface', '--yes'],
'calibration': ['--ab', '--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--holder', '--http', '--image', '--include-null-signature', '--json', '--key-prefix', '--kind', '--lang', '--limit', '--markdown', '--max-usd', '--mode', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--phase', '--progress-interval', '--progress-json', '--quiet', '--regenerate', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scrub-gstack', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--symbol-kind', '--thin', '--trusted-extraction', '--undo-wave', '--url', '--with-calibration', '--with-db', '--yes'],
'call': ['--aliases', '--all', '--all-sources', '--as-context', '--auto-fix', '--background', '--brain', '--by-mention', '--catch-up', '--concurrency', '--confirm-destructive', '--content', '--cost-estimate', '--count', '--days', '--depth', '--dim', '--dir', '--direction', '--enable-dcr', '--enable-dcr-insecure', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--from', '--from-meetings', '--grant-types', '--grep', '--hard-deadline', '--help', '--http', '--image', '--include-expired', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--install', '--interval', '--json', '--key', '--kind', '--lang', '--limit', '--link-source', '--link-type', '--migrate-only', '--missing-path', '--multimodal', '--ner', '--no-embed', '--no-expand', '--no-extract', '--no-federated', '--no-hard-deadline', '--no-retry-connect', '--no-save', '--older-than', '--page', '--param', '--params', '--password', '--path', '--pattern', '--pending', '--pglite', '--port', '--progress-interval', '--progress-json', '--public-url', '--queue', '--quiet', '--reenrich-after', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--sigma', '--since', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--synthesize', '--tag', '--thin', '--timeout', '--to', '--today', '--token', '--token-ttl', '--tools-json', '--type', '--url', '--version', '--watch', '--with-calibration', '--workers', '--yes'],
'capture': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--depth', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--stdin', '--supersessions', '--surface', '--thin', '--timeout', '--trusted-extraction', '--type', '--url', '--what', '--where', '--who', '--with-db', '--yes'],
'check-backlinks': ['--background', '--brain', '--brain-wide-max-cost-usd', '--dir', '--dry-run', '--explain', '--follow', '--help', '--include-frontmatter', '--json', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--source', '--stale', '--timeout', '--type'],
'check-resolvable': ['--brain', '--dry-run', '--fix', '--help', '--json', '--skills-dir', '--source', '--strict', '--verbose'],
'check-update': ['--all', '--brain', '--check', '--ff-only', '--help', '--json', '--markdown', '--migrate-only', '--non-interactive', '--refresh-cache', '--source', '--swap-only', '--to', '--version', '--yes'],
@@ -30,76 +30,77 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'code-callers': ['--aliases', '--all', '--all-sources', '--brain', '--chunker-debug', '--clone-dir', '--confirm-destructive', '--federated', '--force', '--help', '--include-null-signature', '--json', '--limit', '--no-extract', '--no-federated', '--no-json', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--url', '--url-managed', '--yes'],
'code-def': ['--aliases', '--all', '--brain', '--chunker-debug', '--help', '--include-null-signature', '--json', '--lang', '--limit', '--no-extract', '--no-json', '--pattern', '--pending', '--pretty', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--yes'],
'code-refs': ['--aliases', '--all', '--brain', '--chunker-debug', '--help', '--include-null-signature', '--json', '--lang', '--limit', '--no-extract', '--no-json', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--yes'],
'config': ['--aliases', '--all', '--brain', '--column', '--coverage-override', '--embedding-dimensions', '--embedding-model', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--no-embedding', '--no-extract', '--pattern', '--pending', '--pglite', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--yes'],
'config': ['--aliases', '--all', '--brain', '--column', '--coverage-override', '--embedding-dimensions', '--embedding-model', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--no-embedding', '--no-extract', '--pattern', '--pending', '--pglite', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--surface', '--thin', '--yes'],
'connect': ['--agent', '--bearer-token-env-var', '--bind', '--brain', '--client-id', '--client-secret', '--force', '--grant-types', '--help', '--http', '--install', '--json', '--name', '--oauth', '--public-url', '--register', '--scopes', '--show-token', '--source', '--timeout-ms', '--token', '--token-endpoint-auth-method', '--url', '--version', '--yes'],
'conversation-parser': ['--aliases', '--all', '--brain', '--help', '--include-null-signature', '--json', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin'],
'doctor': ['--ab', '--abi', '--aliases', '--all', '--allow-shell-jobs', '--auto', '--auto-fix', '--auto-update', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--by-type', '--check', '--column', '--compile', '--concurrency', '--confidence', '--content-audit', '--count', '--days', '--detach', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--exclusive', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--grant-types', '--health-interval', '--help', '--history', '--http', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--include-pseudo', '--index-audit', '--input', '--json', '--lang', '--limit', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-crashes', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--nice', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-mutate', '--oauth-client-secret', '--older-than', '--once', '--overwrite', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebuild-rollup', '--refresh', '--refresh-unqualified', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--resume', '--review-lower', '--rollback', '--scope', '--scopes', '--since', '--skills-dir', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--skip-verify', '--slugs', '--source', '--source-id', '--stale', '--status', '--strategy', '--strict', '--supabase', '--supersessions', '--symbol-kind', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--undo-wave', '--unsafe-bypass-dream-guard', '--untracked-files', '--url', '--use-captured-snapshot', '--verbose', '--version', '--window', '--with-calibration', '--workers', '--yes'],
'dream': ['--against', '--aliases', '--all', '--anchor', '--asof', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd', '--by-type', '--by-type-floor', '--code', '--compile', '--concurrent', '--ctx-size', '--cycles', '--date', '--dimensions', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--expansion', '--explain', '--fast', '--federated', '--fix', '--fixtures', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--from', '--from-db', '--from-pages', '--help', '--http', '--include-null-signature', '--input', '--install', '--json', '--judge-model', '--keyword-only', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-tokens', '--max-usd', '--mcp-only', '--min-recall', '--mode', '--model', '--models', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-llm', '--no-mutate', '--no-trajectory', '--once', '--output', '--path', '--pattern', '--pending', '--pglite', '--phase', '--priority', '--progress-interval', '--progress-json', '--pull', '--quiet', '--receipt-dir', '--remediate', '--repo', '--reranking', '--reset', '--resolve', '--restore-only', '--resume-from', '--retrieval-only', '--rounds', '--rubric-version', '--save', '--seed', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--supabase', '--supersessions', '--symbol-kind', '--take', '--task', '--thin', '--threshold', '--timeout', '--to', '--top-k', '--undo', '--unsafe-bypass-dream-guard', '--version', '--window', '--yes'],
'doctor': ['--ab', '--abi', '--aliases', '--all', '--allow-shell-jobs', '--auto', '--auto-fix', '--auto-update', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--by-type', '--check', '--column', '--compile', '--concurrency', '--confidence', '--content-audit', '--count', '--days', '--detach', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--exclusive', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--grant-types', '--health-interval', '--help', '--history', '--http', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--include-pseudo', '--index-audit', '--input', '--json', '--lang', '--limit', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-crashes', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--nice', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-mutate', '--oauth-client-secret', '--older-than', '--once', '--overwrite', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebuild-rollup', '--refresh', '--refresh-unqualified', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--resume', '--review-lower', '--rollback', '--scope', '--scopes', '--since', '--skills-dir', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--skip-verify', '--slugs', '--source', '--source-id', '--stale', '--status', '--strategy', '--strict', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--undo-wave', '--unsafe-bypass-dream-guard', '--untracked-files', '--url', '--use-captured-snapshot', '--verbose', '--version', '--window', '--with-calibration', '--workers', '--yes'],
'dream': ['--against', '--aliases', '--all', '--anchor', '--asof', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd', '--by-type', '--by-type-floor', '--code', '--compile', '--concurrent', '--ctx-size', '--cycles', '--date', '--dimensions', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--expansion', '--explain', '--fast', '--federated', '--fix', '--fixtures', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--from', '--from-db', '--from-pages', '--help', '--http', '--include-null-signature', '--input', '--install', '--json', '--judge-model', '--keyword-only', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-tokens', '--max-usd', '--mcp-only', '--min-recall', '--mode', '--model', '--models', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-llm', '--no-mutate', '--no-trajectory', '--once', '--output', '--path', '--pattern', '--pending', '--pglite', '--phase', '--priority', '--progress-interval', '--progress-json', '--pull', '--quiet', '--receipt-dir', '--remediate', '--repo', '--reranking', '--reset', '--resolve', '--restore-only', '--resume-from', '--retrieval-only', '--rounds', '--rubric-version', '--save', '--seed', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--take', '--task', '--thin', '--threshold', '--timeout', '--to', '--top-k', '--undo', '--unsafe-bypass-dream-guard', '--version', '--window', '--yes'],
'edges-backfill': ['--aliases', '--all', '--all-sources', '--brain', '--concurrency', '--federated', '--help', '--include-null-signature', '--json', '--max-age', '--max-chunks', '--max-cost-usd', '--no-extract', '--no-federated', '--older-than', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--workers'],
'embed': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--catch-up', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--pglite', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--restore-only', '--serial', '--slugs', '--source', '--stale', '--supabase', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--version'],
'enrich': ['--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd-per-day', '--by-mention', '--clone-dir', '--code', '--concurrency', '--confirm-destructive', '--content', '--date', '--days', '--dry-run', '--embedding-dimensions', '--embedding-model', '--entities', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--judge-model', '--kind', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-usd', '--min-context', '--mode', '--model', '--multimodal', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--offset', '--older-than', '--order', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--reenrich-after', '--remediate', '--reset', '--resolve', '--restore-only', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--source-id', '--stale', '--stats', '--supersessions', '--symbol-kind', '--thin', '--thin-threshold', '--timeout', '--trusted-extraction', '--types', '--url', '--url-managed', '--version', '--with-db', '--workers', '--yes'],
'eval': ['--ab-relational', '--against', '--aliases', '--all', '--background', '--baseline', '--batch', '--brain', '--brain-wide-max-cost-usd', '--budget-usd', '--budget-usd-answer', '--budget-usd-retrieval', '--compare', '--compare-limit', '--concurrent', '--config-a', '--config-b', '--corpus', '--cycles', '--days', '--dedup-cosine', '--dedup-max-per-page', '--dedup-type-ratio', '--dimensions', '--distance-min', '--embedding-dimensions', '--embedding-model', '--expand', '--explain', '--fast', '--fixtures', '--follow', '--force', '--from-capture', '--from-db', '--from-pages', '--grounding-min', '--help', '--http', '--include-null-signature', '--input', '--json', '--judge', '--k', '--limit', '--max-pair-chars', '--max-tokens', '--max-usd', '--md', '--metric', '--min-recall', '--mode', '--model', '--models', '--modes', '--multimodal', '--name', '--no', '--no-cache', '--no-embed', '--no-embedding', '--no-expand', '--no-extract', '--no-llm', '--older-than', '--output', '--output-dir', '--parallel', '--pattern', '--pending', '--progress-interval', '--progress-json', '--qrels', '--queries-file', '--query', '--questions', '--quiet', '--receipt-dir', '--refresh-cache', '--remediate', '--reset', '--resolve', '--rrf-k', '--rubric-version', '--runs', '--sampling', '--save', '--seed', '--severity', '--short', '--show-toplevel', '--since', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--stale', '--strategy', '--strict', '--suite', '--suites', '--supersessions', '--task', '--thin', '--threshold', '--threshold-expected-top1', '--threshold-first-relevant-hit', '--threshold-jaccard', '--threshold-latency-multiplier', '--threshold-recall-at-k', '--threshold-top1', '--timeout', '--tool', '--top-k', '--top-regressions', '--until', '--usefulness-min', '--verbose', '--version', '--with-code-intel', '--yes'],
'embed': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--catch-up', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--pglite', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--restore-only', '--serial', '--slugs', '--source', '--stale', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--version'],
'enrich': ['--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd-per-day', '--by-mention', '--clone-dir', '--code', '--concurrency', '--confirm-destructive', '--content', '--date', '--days', '--dry-run', '--embedding-dimensions', '--embedding-model', '--entities', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--judge-model', '--kind', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-usd', '--min-context', '--mode', '--model', '--multimodal', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--offset', '--older-than', '--order', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--reenrich-after', '--remediate', '--reset', '--resolve', '--restore-only', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--source-id', '--stale', '--stats', '--supersessions', '--surface', '--symbol-kind', '--thin', '--thin-threshold', '--timeout', '--trusted-extraction', '--types', '--url', '--url-managed', '--version', '--with-db', '--workers', '--yes'],
'eval': ['--ab-relational', '--against', '--aliases', '--all', '--background', '--baseline', '--batch', '--brain', '--brain-wide-max-cost-usd', '--budget-usd', '--budget-usd-answer', '--budget-usd-retrieval', '--compare', '--compare-limit', '--concurrent', '--config-a', '--config-b', '--corpus', '--cycles', '--days', '--dedup-cosine', '--dedup-max-per-page', '--dedup-type-ratio', '--dimensions', '--distance-min', '--embedding-dimensions', '--embedding-model', '--expand', '--explain', '--fast', '--fixtures', '--follow', '--force', '--from-capture', '--from-db', '--from-pages', '--grounding-min', '--help', '--http', '--include-null-signature', '--input', '--json', '--judge', '--k', '--limit', '--max-pair-chars', '--max-tokens', '--max-usd', '--md', '--metric', '--min-recall', '--mode', '--model', '--models', '--modes', '--multimodal', '--name', '--no', '--no-cache', '--no-embed', '--no-embedding', '--no-expand', '--no-extract', '--no-llm', '--older-than', '--output', '--output-dir', '--parallel', '--pattern', '--pending', '--progress-interval', '--progress-json', '--qrels', '--queries-file', '--query', '--questions', '--quiet', '--receipt-dir', '--refresh-cache', '--remediate', '--reset', '--resolve', '--rrf-k', '--rubric-version', '--runs', '--sampling', '--save', '--seed', '--severity', '--short', '--show-toplevel', '--since', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--stale', '--strategy', '--strict', '--suite', '--suites', '--supersessions', '--surface', '--task', '--thin', '--threshold', '--threshold-expected-top1', '--threshold-first-relevant-hit', '--threshold-jaccard', '--threshold-latency-multiplier', '--threshold-recall-at-k', '--threshold-top1', '--timeout', '--tool', '--top-k', '--top-regressions', '--until', '--usefulness-min', '--verbose', '--version', '--with-code-intel', '--yes'],
'export': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dir', '--explain', '--federated', '--fix', '--follow', '--help', '--include-null-signature', '--json', '--lang', '--markdown', '--multimodal', '--near-symbol', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--restore-only', '--slug-prefix', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--type'],
'extract': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--catch-up', '--code', '--concurrency', '--dir', '--dry-run', '--explain', '--federated', '--follow', '--from-meetings', '--help', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--json', '--kind', '--lang', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name-status', '--near-symbol', '--ner', '--no-extract', '--no-federated', '--older-than', '--pack', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--restore-only', '--run-id', '--since', '--slug', '--source', '--source-id', '--stale', '--strategy', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--type', '--verbose', '--workers', '--yes'],
'extract-conversation-facts': ['--aliases', '--all', '--all-sources', '--background', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--by-mention', '--clone-dir', '--code', '--concurrency', '--confirm-destructive', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fix', '--follow', '--force', '--force-break-lock', '--help', '--include-null-signature', '--json', '--judge-model', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--model', '--multimodal', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--older-than', '--override-disabled', '--path', '--pattern', '--pending', '--pglite', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--restore-only', '--segment-limit', '--session', '--since', '--sleep', '--slug', '--source', '--source-id', '--stale', '--supabase', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--types', '--url', '--url-managed', '--version', '--workers', '--yes'],
'features': ['--aliases', '--all', '--auto-fix', '--background', '--batch-size', '--brain', '--by-mention', '--catch-up', '--concurrency', '--dir', '--explain', '--from-meetings', '--help', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--json', '--kind', '--ner', '--no-extract', '--pace', '--pace-max-concurrency', '--pack', '--path', '--pattern', '--pending', '--priority', '--progress-json', '--quiet', '--repo', '--reset', '--resolve', '--run-id', '--since', '--slugs', '--source', '--source-id', '--stale', '--supersessions', '--thin', '--type', '--verbose', '--workers'],
'files': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--no-pointer', '--page', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--retry-failed', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--type', '--yes'],
'forget': ['--aliases', '--all', '--as-context', '--brain', '--fast', '--federated', '--force', '--from-pages', '--grep', '--help', '--http', '--include-expired', '--include-null-signature', '--json', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--session', '--session-id', '--since', '--since-last-run', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--today', '--watch'],
'founder': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--since', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--until'],
'files': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--no-pointer', '--page', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--retry-failed', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--type', '--yes'],
'forget': ['--aliases', '--all', '--allow-empty', '--apply', '--as-context', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-tokens', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--grep', '--help', '--http', '--image', '--include-expired', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--query', '--quiet', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--save', '--session', '--session-id', '--since', '--since-last-run', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--today', '--trusted-extraction', '--url', '--watch', '--with-db', '--yes'],
'founder': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--since', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--until'],
'friction': ['--agent', '--brain', '--help', '--hint', '--json', '--kind', '--message', '--no-redact', '--phase', '--redact', '--run-id', '--severity', '--source', '--transcript-path', '--transcripts'],
'frontmatter': ['--aliases', '--all', '--allow-catch-all', '--brain', '--cached', '--diff-filter', '--dry-run', '--exclude-standard', '--fast', '--fix', '--force', '--from-pages', '--get', '--help', '--http', '--include-catch-all', '--include-null-signature', '--json', '--name-only', '--name-status', '--no-embedding', '--no-extract', '--no-verify', '--others', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--strategy', '--supersessions', '--thin', '--timeout', '--uninstall', '--write-back'],
'graph-query': ['--aliases', '--all', '--brain', '--depth', '--direction', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-foreign', '--include-null-signature', '--json', '--lang', '--markdown', '--mcp-only', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--type'],
'import': ['--aliases', '--all', '--asof', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--cached', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--fix', '--follow', '--force', '--force-rechunk', '--fresh', '--from-pages', '--full', '--help', '--http', '--include-gitignored', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--multimodal', '--name-status', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--older-than', '--others', '--path', '--pattern', '--pending', '--pglite', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--since', '--skip-failed', '--source', '--source-id', '--stale', '--strategy', '--supabase', '--supersessions', '--thin', '--timeout', '--url', '--workers'],
'init': ['--all', '--brain', '--chat-model', '--check', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--expansion-model', '--fast', '--force', '--from-pages', '--grant-types', '--help', '--http', '--issuer-url', '--json', '--judge-model', '--key', '--mcp-only', '--mcp-url', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--path', '--pglite', '--schema-pack', '--scopes', '--skip-embed-check', '--source', '--stale', '--supabase', '--target', '--to', '--touchpoint', '--url', '--version'],
'integrations': ['--auto', '--brain', '--dry-run', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--overwrite', '--refresh', '--reranking', '--source', '--target'],
'integrity': ['--aliases', '--all', '--auto', '--backend', '--background', '--brain', '--brain-wide-max-cost-usd', '--check', '--confidence', '--cost', '--dry-run', '--explain', '--fast', '--follow', '--force', '--fresh', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--limit', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--review-lower', '--skip-bare-tweet', '--skip-urls', '--source', '--stale', '--supabase', '--supersessions', '--thin', '--timeout', '--type', '--url'],
'jobs': ['--abbrev-ref', '--aliases', '--all', '--allow-empty', '--allow-protected', '--allow-shell-jobs', '--apply', '--asof', '--auto', '--auto-with-prompt', '--background', '--backoff-delay', '--backoff-jitter', '--backoff-type', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--break-lock', '--budget-usd', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--check', '--cli-path', '--cluster', '--cluster-errors', '--code', '--concurrency', '--confidence', '--confirm-destructive', '--content', '--date', '--days', '--delay', '--detach', '--diff-filter', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--federated-read', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--hard-deadline', '--health-interval', '--held-out', '--help', '--http', '--idempotency-key', '--image', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--infer-dates', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--limit', '--lock', '--markdown', '--max-age', '--max-attempts', '--max-cost-usd', '--max-crashes', '--max-rss', '--max-runtime-min', '--max-sources', '--max-stalled', '--max-usd', '--max-waiting', '--mcp-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federate', '--no-gpg-sign', '--no-hard-deadline', '--no-inject', '--no-mutate', '--no-pull', '--no-renames', '--no-schema-pack', '--no-verify', '--no-worker', '--now', '--offset', '--older-than', '--once', '--order', '--others', '--override-disabled', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--phase', '--pid-file', '--priority', '--progress-interval', '--progress-json', '--queue', '--quiet', '--redact-secrets', '--reenrich-after', '--refresh-ms', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--review-lower', '--run-id', '--save', '--segment-limit', '--serial', '--session', '--short', '--show-toplevel', '--sigkill-rescue', '--since', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--sleep', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--strategy', '--supersessions', '--swap-only', '--symbol-kind', '--target', '--thin', '--thin-threshold', '--timeout', '--timeout-ms', '--trusted-extraction', '--type', '--types', '--uninstall', '--unsafe-bypass-dream-guard', '--url', '--user', '--verbose', '--version', '--watch', '--wedge-rescue', '--with-db', '--workers', '--yes'],
'lint': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--exclude', '--explain', '--fast', '--fix', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
'frontmatter': ['--aliases', '--all', '--allow-catch-all', '--brain', '--cached', '--diff-filter', '--dry-run', '--exclude-standard', '--fast', '--fix', '--force', '--from-pages', '--get', '--help', '--http', '--include-catch-all', '--include-null-signature', '--json', '--name-only', '--name-status', '--no-embedding', '--no-extract', '--no-verify', '--others', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--strategy', '--supersessions', '--surface', '--thin', '--timeout', '--uninstall', '--write-back'],
'graph-query': ['--aliases', '--all', '--brain', '--depth', '--direction', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-foreign', '--include-null-signature', '--json', '--lang', '--markdown', '--mcp-only', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--type'],
'import': ['--aliases', '--all', '--asof', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--cached', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--fix', '--follow', '--force', '--force-rechunk', '--fresh', '--from-pages', '--full', '--help', '--http', '--include-gitignored', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--multimodal', '--name-status', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--older-than', '--others', '--path', '--pattern', '--pending', '--pglite', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--since', '--skip-failed', '--source', '--source-id', '--stale', '--strategy', '--supabase', '--supersessions', '--surface', '--thin', '--timeout', '--url', '--workers'],
'init': ['--all', '--brain', '--chat-model', '--check', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--entity', '--expansion-model', '--fast', '--force', '--from-pages', '--grant-types', '--help', '--http', '--issuer-url', '--json', '--judge-model', '--key', '--mcp-only', '--mcp-url', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--path', '--pglite', '--provenance', '--schema-pack', '--scopes', '--skip-embed-check', '--source', '--stale', '--supabase', '--surface', '--target', '--to', '--touchpoint', '--url', '--version'],
'integrations': ['--auto', '--brain', '--dry-run', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--overwrite', '--refresh', '--reranking', '--source', '--surface', '--target'],
'integrity': ['--aliases', '--all', '--auto', '--backend', '--background', '--brain', '--brain-wide-max-cost-usd', '--check', '--confidence', '--cost', '--dry-run', '--explain', '--fast', '--follow', '--force', '--fresh', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--limit', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--review-lower', '--skip-bare-tweet', '--skip-urls', '--source', '--stale', '--supabase', '--supersessions', '--surface', '--thin', '--timeout', '--type', '--url'],
'jobs': ['--abbrev-ref', '--aliases', '--all', '--allow-empty', '--allow-protected', '--allow-shell-jobs', '--apply', '--asof', '--auto', '--auto-with-prompt', '--background', '--backoff-delay', '--backoff-jitter', '--backoff-type', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--break-lock', '--budget-usd', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--check', '--cli-path', '--cluster', '--cluster-errors', '--code', '--concurrency', '--confidence', '--confirm-destructive', '--content', '--date', '--days', '--delay', '--detach', '--diff-filter', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--federated-read', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--hard-deadline', '--health-interval', '--held-out', '--help', '--http', '--idempotency-key', '--image', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--infer-dates', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--limit', '--lock', '--markdown', '--max-age', '--max-attempts', '--max-cost-usd', '--max-crashes', '--max-rss', '--max-runtime-min', '--max-sources', '--max-stalled', '--max-usd', '--max-waiting', '--mcp-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federate', '--no-gpg-sign', '--no-hard-deadline', '--no-inject', '--no-mutate', '--no-pull', '--no-renames', '--no-schema-pack', '--no-verify', '--no-worker', '--now', '--offset', '--older-than', '--once', '--order', '--others', '--override-disabled', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--phase', '--pid-file', '--priority', '--progress-interval', '--progress-json', '--queue', '--quiet', '--redact-secrets', '--reenrich-after', '--refresh-ms', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--review-lower', '--run-id', '--save', '--segment-limit', '--serial', '--session', '--short', '--show-toplevel', '--sigkill-rescue', '--since', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--sleep', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--strategy', '--supersessions', '--surface', '--swap-only', '--symbol-kind', '--target', '--thin', '--thin-threshold', '--timeout', '--timeout-ms', '--trusted-extraction', '--type', '--types', '--uninstall', '--unsafe-bypass-dream-guard', '--url', '--user', '--verbose', '--version', '--watch', '--wedge-rescue', '--with-db', '--workers', '--yes'],
'lint': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--exclude', '--explain', '--fast', '--fix', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout'],
'lsd': ['--brain', '--force-resume', '--help', '--json', '--judge-model', '--limit', '--list-runs', '--max-cost', '--max-far-set', '--max-ideas-per-judge-call', '--no-save', '--resume', '--retry-judge', '--save', '--source', '--strict-budget', '--yes'],
'maintain': ['--aliases', '--all', '--background', '--brain', '--break-lock', '--by-mention', '--catch-up', '--column', '--concurrency', '--content-audit', '--count', '--detach', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--force', '--force-retry', '--force-schema', '--from-meetings', '--full', '--help', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--index-audit', '--infer-dates', '--input', '--json', '--kind', '--lang', '--locks', '--markdown', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-usd', '--migrate-only', '--multimodal', '--near-symbol', '--ner', '--nice', '--no-extract', '--no-mutate', '--older-than', '--once', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--progress-json', '--query', '--queue', '--quiet', '--rebuild-rollup', '--regenerate', '--remediate', '--remediation-plan', '--reset', '--resolve', '--restore-only', '--resume', '--run-id', '--safe', '--scope', '--since', '--skills-dir', '--skip-failed', '--slugs', '--source', '--source-id', '--stale', '--status', '--supabase', '--supersessions', '--symbol-kind', '--target', '--target-score', '--thin', '--to', '--top-k', '--type', '--unsafe-bypass-dream-guard', '--url', '--verbose', '--window', '--workers', '--yes'],
'migrate': ['--ab', '--aliases', '--all', '--auto-update', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--catch-up', '--compile', '--days', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--exclusive', '--explain', '--fast', '--follow', '--force', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--lang', '--locks', '--markdown', '--max-age', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--phase', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--refresh-unqualified', '--remediate', '--reranking', '--reset', '--resolve', '--restore-only', '--resume', '--rollback', '--skip-verify', '--slugs', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--to', '--undo', '--undo-wave', '--url', '--use-captured-snapshot', '--version', '--with-calibration', '--yes'],
'models': ['--aliases', '--all', '--brain', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reranking', '--reset', '--resolve', '--skip', '--source', '--stale', '--supersessions', '--thin', '--undo', '--version'],
'migrate': ['--ab', '--aliases', '--all', '--auto-update', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--catch-up', '--compile', '--days', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--exclusive', '--explain', '--fast', '--follow', '--force', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--lang', '--locks', '--markdown', '--max-age', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--phase', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--refresh-unqualified', '--remediate', '--reranking', '--reset', '--resolve', '--restore-only', '--resume', '--rollback', '--skip-verify', '--slugs', '--source', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--to', '--undo', '--undo-wave', '--url', '--use-captured-snapshot', '--version', '--with-calibration', '--yes'],
'models': ['--aliases', '--all', '--brain', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reranking', '--reset', '--resolve', '--skip', '--source', '--stale', '--supersessions', '--surface', '--thin', '--undo', '--version'],
'mounts': ['--alias', '--brain', '--cache', '--database-path', '--database-url', '--db-path', '--db-url', '--engine', '--explain', '--help', '--id', '--json', '--lang', '--lock', '--markdown', '--mcp-url', '--multimodal', '--near-symbol', '--path', '--restore-only', '--skills-dir', '--source', '--stale', '--symbol-kind', '--thin', '--verbose'],
'notability-eval': ['--aliases', '--all', '--brain', '--embedding-dimensions', '--embedding-model', '--help', '--in', '--include-null-signature', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-extract', '--out', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--skip-llm', '--source', '--stale', '--supersessions', '--target-high', '--target-low', '--target-medium', '--thin', '--version'],
'onboard': ['--aliases', '--all', '--allow-empty', '--allow-protected', '--apply', '--asof', '--auto', '--auto-with-prompt', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--check', '--content', '--date', '--days', '--entities', '--explain', '--federated', '--file', '--from-pages', '--help', '--history', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-extract', '--offset', '--params', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--remediation-plan', '--reset', '--resolve', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--target-score', '--thin', '--trusted-extraction', '--url', '--with-db', '--yes'],
'onboard': ['--aliases', '--all', '--allow-empty', '--allow-protected', '--apply', '--asof', '--auto', '--auto-with-prompt', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--check', '--content', '--date', '--days', '--entities', '--explain', '--federated', '--file', '--from-pages', '--help', '--history', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-extract', '--offset', '--params', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--remediation-plan', '--reset', '--resolve', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--target-score', '--thin', '--trusted-extraction', '--url', '--with-db', '--yes'],
'orphans': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--count', '--explain', '--follow', '--help', '--include-null-signature', '--include-pseudo', '--json', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
'pages': ['--aliases', '--all', '--brain', '--dry-run', '--help', '--include-null-signature', '--json', '--no-extract', '--older-than', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin'],
'pglite-repair': ['--brain', '--break-lock', '--dry-rnu', '--dry-run', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--path', '--quiet', '--source', '--yes'],
'post-upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
'providers': ['--brain', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--reranking', '--source', '--touchpoint', '--version'],
'pglite-repair': ['--brain', '--break-lock', '--dry-rnu', '--dry-run', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--path', '--quiet', '--source', '--surface', '--yes'],
'post-upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--surface', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
'protocol': ['--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--http', '--image', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-embedding', '--offset', '--path', '--progress-interval', '--progress-json', '--quiet', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stats', '--surface', '--synthesize', '--target', '--timeout', '--token', '--trusted-extraction', '--url', '--with-db', '--yes'],
'providers': ['--brain', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--reranking', '--source', '--surface', '--touchpoint', '--version'],
'publish': ['--accent', '--bg', '--border', '--brain', '--card-bg', '--code-bg', '--error', '--fg', '--help', '--json', '--link', '--muted', '--out', '--password', '--source', '--title'],
'quarantine': ['--aliases', '--all', '--apply', '--brain', '--code', '--compile', '--explain', '--fast', '--fix', '--force', '--force-rechunk', '--from-pages', '--help', '--http', '--include-flagged', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--multimodal', '--near-symbol', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin'],
'recall': ['--aliases', '--all', '--as-context', '--brain', '--fast', '--federated', '--force', '--from-pages', '--grep', '--help', '--http', '--include-expired', '--include-null-signature', '--json', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--session', '--session-id', '--since', '--since-last-run', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--today', '--watch'],
'quarantine': ['--aliases', '--all', '--apply', '--brain', '--code', '--compile', '--explain', '--fast', '--fix', '--force', '--force-rechunk', '--from-pages', '--help', '--http', '--include-flagged', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--multimodal', '--near-symbol', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin'],
'recall': ['--aliases', '--all', '--allow-empty', '--apply', '--as-context', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-tokens', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--grep', '--help', '--http', '--image', '--include-expired', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--query', '--quiet', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--save', '--session', '--session-id', '--since', '--since-last-run', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--today', '--trusted-extraction', '--url', '--watch', '--with-db', '--yes'],
'reconcile-links': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--follow', '--help', '--include-frontmatter', '--include-null-signature', '--json', '--name-status', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--strategy', '--supersessions', '--thin', '--timeout', '--type'],
'reindex': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--code', '--compile', '--concurrency', '--cost-estimate', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--older-than', '--path', '--pattern', '--pending', '--pglite', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--source', '--stale', '--supabase', '--supersessions', '--thin', '--timeout', '--version', '--workers', '--yes'],
'reindex': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--code', '--compile', '--concurrency', '--cost-estimate', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--older-than', '--path', '--pattern', '--pending', '--pglite', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--source', '--stale', '--supabase', '--supersessions', '--surface', '--thin', '--timeout', '--version', '--workers', '--yes'],
'reindex-code': ['--abi', '--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--chunker-debug', '--code', '--compile', '--concurrency', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fix', '--follow', '--force', '--force-rechunk', '--help', '--include-null-signature', '--json', '--judge-model', '--lang', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--model', '--multimodal', '--no', '--no-embed', '--no-extract', '--older-than', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--serial', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--version', '--workers', '--yes'],
'reindex-frontmatter': ['--aliases', '--all', '--brain', '--concurrency', '--dry-run', '--force', '--help', '--include-null-signature', '--json', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--slug-prefix', '--source', '--stale', '--supersessions', '--thin', '--workers', '--yes'],
'reindex-search-vector': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--follow', '--help', '--include-null-signature', '--json', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--yes'],
'reinit-pglite': ['--abbrev-ref', '--all', '--allow-empty', '--brain', '--break-lock', '--chat-model', '--concurrency', '--confirm-destructive', '--diff-filter', '--embedding-dimensions', '--embedding-model', '--empty', '--exclude', '--exclude-standard', '--expansion-model', '--fast', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--grant-types', '--hard-deadline', '--help', '--http', '--include-gitignored', '--interval', '--is-ancestor', '--issuer-url', '--json', '--key', '--lock', '--max-age', '--max-sources', '--mcp-only', '--mcp-url', '--migrate-only', '--missing-path', '--model', '--name-only', '--name-status', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-renames', '--no-schema-pack', '--no-sync', '--no-verify', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--parallel', '--path', '--pglite', '--priority', '--quiet', '--repo', '--retry-failed', '--schema-pack', '--scopes', '--serial', '--short', '--show-toplevel', '--skip-embed-check', '--skip-failed', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--strategy', '--supabase', '--target', '--timeout', '--to', '--url', '--version', '--watch', '--workers', '--yes'],
'remote': ['--all', '--background', '--brain', '--break-lock', '--by-mention', '--column', '--concurrency', '--content-audit', '--count', '--detach', '--dim', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--fast', '--fix', '--force', '--force-retry', '--force-schema', '--from-pages', '--full', '--help', '--http', '--include-flagged', '--index-audit', '--json', '--locks', '--markdown', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--multimodal', '--nice', '--no-embedding', '--older-than', '--parallel', '--params', '--pglite', '--phase', '--pid-file', '--progress-json', '--query', '--queue', '--rebuild-rollup', '--regenerate', '--remediate', '--remediation-plan', '--reset', '--resume', '--scope', '--scopes', '--since', '--skills-dir', '--skip-failed', '--slugs', '--source', '--source-id', '--stale', '--status', '--supabase', '--target', '--target-score', '--timeout', '--to', '--top-k', '--url', '--window', '--workers', '--yes'],
'repair-jsonb': ['--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--json', '--lang', '--markdown', '--multimodal', '--near-symbol', '--no-embedding', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--restore-only', '--source', '--stale', '--supabase', '--symbol-kind', '--thin', '--timeout', '--url'],
'reinit-pglite': ['--abbrev-ref', '--all', '--allow-empty', '--brain', '--break-lock', '--chat-model', '--concurrency', '--confirm-destructive', '--diff-filter', '--embedding-dimensions', '--embedding-model', '--empty', '--entity', '--exclude', '--exclude-standard', '--expansion-model', '--fast', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--grant-types', '--hard-deadline', '--help', '--http', '--include-gitignored', '--interval', '--is-ancestor', '--issuer-url', '--json', '--key', '--lock', '--max-age', '--max-sources', '--mcp-only', '--mcp-url', '--migrate-only', '--missing-path', '--model', '--name-only', '--name-status', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-renames', '--no-schema-pack', '--no-sync', '--no-verify', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--parallel', '--path', '--pglite', '--priority', '--provenance', '--quiet', '--repo', '--retry-failed', '--schema-pack', '--scopes', '--serial', '--short', '--show-toplevel', '--skip-embed-check', '--skip-failed', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--strategy', '--supabase', '--surface', '--target', '--timeout', '--to', '--url', '--version', '--watch', '--workers', '--yes'],
'remote': ['--all', '--background', '--brain', '--break-lock', '--by-mention', '--column', '--concurrency', '--content-audit', '--count', '--detach', '--dim', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--fast', '--fix', '--force', '--force-retry', '--force-schema', '--from-pages', '--full', '--help', '--http', '--include-flagged', '--index-audit', '--json', '--locks', '--markdown', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--multimodal', '--nice', '--no-embedding', '--older-than', '--parallel', '--params', '--pglite', '--phase', '--pid-file', '--progress-json', '--query', '--queue', '--rebuild-rollup', '--regenerate', '--remediate', '--remediation-plan', '--reset', '--resume', '--scope', '--scopes', '--since', '--skills-dir', '--skip-failed', '--slugs', '--source', '--source-id', '--stale', '--status', '--supabase', '--surface', '--target', '--target-score', '--timeout', '--to', '--top-k', '--url', '--window', '--workers', '--yes'],
'repair-jsonb': ['--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--json', '--lang', '--markdown', '--multimodal', '--near-symbol', '--no-embedding', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--restore-only', '--source', '--stale', '--supabase', '--surface', '--symbol-kind', '--thin', '--timeout', '--url'],
'report': ['--brain', '--content', '--dir', '--help', '--json', '--source', '--title', '--type'],
'repos': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
'repos': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--surface', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
'resolvers': ['--auto', '--backend', '--brain', '--cost', '--help', '--json', '--source'],
'retrieval-upgrade': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--catch-up', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--name', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--pattern', '--pending', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranking', '--reset', '--resolve', '--resume', '--slugs', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--to', '--undo', '--version', '--yes'],
'retrieval-upgrade': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--catch-up', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--name', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--pattern', '--pending', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranking', '--reset', '--resolve', '--resume', '--slugs', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--to', '--undo', '--version', '--yes'],
'routing-eval': ['--brain', '--fix', '--help', '--json', '--llm', '--skills-dir', '--source', '--strict', '--verbose'],
'salience': ['--aliases', '--all', '--brain', '--days', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--kind', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--slug-prefix', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
'schema': ['--alias', '--aliases', '--all', '--apply', '--as-filing-rules', '--brain', '--dims', '--expert', '--expert-routing', '--extractable', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--inverse', '--json', '--kind', '--no-embedding', '--no-extract', '--pack', '--page-type', '--pattern', '--pending', '--prefix', '--primitive', '--reset', '--resolve', '--schema-pack', '--since', '--source', '--source-id', '--stale', '--supersessions', '--target-type', '--thin', '--to', '--with-db'],
'salience': ['--aliases', '--all', '--brain', '--days', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--kind', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--slug-prefix', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout'],
'schema': ['--alias', '--aliases', '--all', '--apply', '--as-filing-rules', '--brain', '--dims', '--expert', '--expert-routing', '--extractable', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--inverse', '--json', '--kind', '--no-embedding', '--no-extract', '--pack', '--page-type', '--pattern', '--pending', '--prefix', '--primitive', '--reset', '--resolve', '--schema-pack', '--since', '--source', '--source-id', '--stale', '--supersessions', '--surface', '--target-type', '--thin', '--to', '--with-db'],
'self-upgrade': ['--all', '--brain', '--check', '--check-only', '--ff-only', '--force', '--help', '--json', '--markdown', '--migrate-only', '--non-interactive', '--refresh-cache', '--source', '--swap-only', '--to', '--version', '--yes'],
'serve': ['--aliases', '--all', '--bind', '--bound-slug-prefixes', '--brain', '--enable-dcr', '--enable-dcr-insecure', '--federated-read', '--help', '--http', '--include-null-signature', '--json', '--log-full-params', '--no-extract', '--pattern', '--pending', '--port', '--print-admin-token', '--public-url', '--reset', '--resolve', '--source', '--stale', '--stdio-idle-timeout', '--supersessions', '--suppress', '--suppress-bootstrap-token', '--thin', '--token-ttl', '--yes'],
'serve': ['--aliases', '--all', '--bind', '--bound-slug-prefixes', '--brain', '--enable-dcr', '--enable-dcr-insecure', '--fast', '--federated-read', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--log-full-params', '--no-embedding', '--no-extract', '--pattern', '--pending', '--port', '--print-admin-token', '--public-url', '--reset', '--resolve', '--source', '--stale', '--stdio-idle-timeout', '--supersessions', '--suppress', '--suppress-bootstrap-token', '--surface', '--thin', '--token-ttl', '--yes'],
'skillify': ['--brain', '--description', '--dry-run', '--force', '--help', '--json', '--mutating', '--recent', '--skills-dir', '--source', '--strict', '--triggers', '--verbose', '--writes-pages', '--writes-to'],
'skillopt': ['--aliases', '--all', '--allow-mutate-bundled', '--background', '--batch-size', '--benchmark', '--bootstrap-from-routing', '--bootstrap-from-skill', '--bootstrap-reviewed', '--bootstrap-tasks', '--brain', '--brain-wide-max-cost-usd', '--chunker-debug', '--dry-run', '--epochs', '--follow', '--force', '--held-out', '--help', '--include-null-signature', '--json', '--judge-model', '--lr', '--lr-schedule', '--max-cost-usd', '--max-runtime-min', '--model', '--no-extract', '--no-mutate', '--optimizer-model', '--patch', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--rewrite', '--skills-dir', '--source', '--split', '--stale', '--supersessions', '--target-model', '--target-models', '--thin', '--verbose', '--yes'],
'skillpack': ['--all', '--apply-clean-hunks', '--author', '--brain', '--dry-run', '--exit-code', '--fast', '--fix', '--force', '--force-unlock', '--format', '--from', '--from-pages', '--frontmatter', '--full', '--help', '--homepage', '--http', '--json', '--license', '--list', '--minimal', '--name-only', '--no-cache', '--no-embedding', '--no-lint', '--note', '--out', '--overwrite-local', '--push', '--quick', '--quiet', '--refresh', '--repo', '--schema-pack', '--short', '--since', '--skills-dir', '--skip-doctor', '--source', '--strict', '--target', '--tier', '--trust', '--url', '--verbose', '--verify', '--workspace', '--yes'],
'skillpack': ['--all', '--apply-clean-hunks', '--author', '--brain', '--dry-run', '--exit-code', '--fast', '--fix', '--force', '--force-unlock', '--format', '--from', '--from-pages', '--frontmatter', '--full', '--help', '--homepage', '--http', '--json', '--license', '--list', '--minimal', '--name-only', '--no-cache', '--no-embedding', '--no-lint', '--note', '--out', '--overwrite-local', '--push', '--quick', '--quiet', '--refresh', '--repo', '--schema-pack', '--short', '--since', '--skills-dir', '--skip-doctor', '--source', '--strict', '--surface', '--target', '--tier', '--trust', '--url', '--verbose', '--verify', '--workspace', '--yes'],
'skillpack-check': ['--background', '--brain', '--brain-wide-max-cost-usd', '--explain', '--fast', '--follow', '--help', '--json', '--list', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--source', '--stale', '--strict', '--timeout', '--yes'],
'smoke-test': ['--brain', '--help', '--json', '--source'],
'sources': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
'status': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content', '--content-audit', '--count', '--date', '--days', '--deadline-ms', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--image', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--install', '--interval', '--is-ancestor', '--json', '--judge-model', '--kind', '--lang', '--limit', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-runtime', '--max-sources', '--max-usd', '--mcp-only', '--migrate-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--offset', '--older-than', '--order', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--reenrich-after', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--scopes', '--section', '--serial', '--session', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--symbol-kind', '--target', '--target-score', '--thin', '--thin-threshold', '--timeout', '--to', '--top-k', '--trusted-extraction', '--type', '--types', '--url', '--url-managed', '--verbose', '--verify', '--version', '--watch', '--what', '--where', '--who', '--window', '--with-db', '--workers', '--yes'],
'sources': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--surface', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
'status': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content', '--content-audit', '--count', '--date', '--days', '--deadline-ms', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--image', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--install', '--interval', '--is-ancestor', '--json', '--judge-model', '--kind', '--lang', '--limit', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-runtime', '--max-sources', '--max-usd', '--mcp-only', '--migrate-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--offset', '--older-than', '--order', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--reenrich-after', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--scopes', '--section', '--serial', '--session', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--target', '--target-score', '--thin', '--thin-threshold', '--timeout', '--to', '--top-k', '--trusted-extraction', '--type', '--types', '--url', '--url-managed', '--verbose', '--verify', '--version', '--watch', '--what', '--where', '--who', '--window', '--with-db', '--workers', '--yes'],
'storage': ['--aliases', '--all', '--brain', '--federated', '--fix', '--help', '--include-null-signature', '--json', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--to'],
'sync': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--asof', '--auto', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content-audit', '--count', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-sources', '--max-usd', '--migrate-only', '--missing-path', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--ner', '--nice', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--older-than', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--serial', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--url', '--url-managed', '--verbose', '--verify', '--watch', '--window', '--workers', '--yes'],
'takes': ['--aliases', '--all', '--brain', '--bucket-size', '--by', '--claim', '--dir', '--domain', '--dry-run', '--evidence', '--expired', '--fast', '--federated', '--force', '--from-pages', '--help', '--holder', '--http', '--include-covered', '--include-null-signature', '--json', '--kind', '--limit', '--max-pages', '--no-embedding', '--no-extract', '--no-federated', '--outcome', '--path', '--pattern', '--pending', '--quality', '--refresh', '--repo', '--reset', '--resolve', '--restore-only', '--row', '--since', '--slugs', '--sort', '--source', '--source-id', '--stale', '--supersessions', '--thin', '--unit', '--until', '--value', '--weight', '--who', '--yes'],
'think': ['--aliases', '--all', '--anchor', '--brain', '--calibration-holder', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-usd', '--mcp-only', '--model', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--rounds', '--save', '--since', '--source', '--stale', '--supersessions', '--take', '--thin', '--timeout', '--until', '--with-calibration'],
'sync': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--asof', '--auto', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content-audit', '--count', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-sources', '--max-usd', '--migrate-only', '--missing-path', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--ner', '--nice', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--older-than', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--serial', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--surface', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--url', '--url-managed', '--verbose', '--verify', '--watch', '--window', '--workers', '--yes'],
'takes': ['--aliases', '--all', '--brain', '--bucket-size', '--by', '--claim', '--dir', '--domain', '--dry-run', '--evidence', '--expired', '--fast', '--federated', '--force', '--from-pages', '--help', '--holder', '--http', '--include-covered', '--include-null-signature', '--json', '--kind', '--limit', '--max-pages', '--no-embedding', '--no-extract', '--no-federated', '--outcome', '--path', '--pattern', '--pending', '--quality', '--refresh', '--repo', '--reset', '--resolve', '--restore-only', '--row', '--since', '--slugs', '--sort', '--source', '--source-id', '--stale', '--supersessions', '--surface', '--thin', '--unit', '--until', '--value', '--weight', '--who', '--yes'],
'think': ['--aliases', '--all', '--anchor', '--brain', '--calibration-holder', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-usd', '--mcp-only', '--model', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--rounds', '--save', '--since', '--source', '--stale', '--supersessions', '--surface', '--take', '--thin', '--timeout', '--until', '--with-calibration'],
'transcripts': ['--aliases', '--all', '--brain', '--days', '--full', '--help', '--include-null-signature', '--json', '--limit', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
'upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
'watch': ['--aliases', '--all', '--brain', '--fast', '--federated', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-pages', '--min-confidence', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--window-turns'],
'upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--surface', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
'watch': ['--aliases', '--all', '--brain', '--fast', '--federated', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-pages', '--min-confidence', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--surface', '--thin', '--window-turns'],
'ze-switch': ['--aliases', '--all', '--brain', '--confirm-reembed', '--dry-run', '--force', '--help', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--no-extract', '--non-interactive', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--source', '--stale', '--supersessions', '--thin', '--undo', '--yes'],
};
+14
View File
@@ -114,6 +114,17 @@ export interface GBrainConfig {
provider_base_urls?: Record<string, string>;
/** Optional chat request providerOptions overrides keyed by recipe id or "recipe:modelId". */
provider_chat_options?: Record<string, Record<string, unknown>>;
/**
* MEMORY_VERBS v1 (Cathedral 1): default MCP tool surface for `gbrain serve`.
* 'verbs' = exactly the 5 protocol verbs (the quickstart surface);
* 'full' (default) = every operation. The `--surface` flag overrides per-run.
*/
mcp_surface?: 'verbs' | 'full';
/**
* MEMORY_VERBS v1 [D6C]: ISO timestamp stamped by `gbrain init` so
* `gbrain protocol stats` can derive real TTHW (install first verb call).
*/
protocol_installed_at?: string;
/**
* Optional storage backend config (S3/Supabase/local). Shape matches
* `StorageConfig` in `./storage.ts`. Typed as `unknown` here to avoid
@@ -948,6 +959,9 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [
'chat_model',
'chat_fallback_chain',
'provider_base_urls',
// MEMORY_VERBS v1 (Cathedral 1)
'mcp_surface',
'protocol_installed_at',
'provider_chat_options',
'storage',
'eval',
+5 -2
View File
@@ -106,7 +106,7 @@ export interface ResolvePointersOpts {
sourceIds?: string[];
}
interface PageRow {
export interface PageRow {
slug: string;
source_id: string;
title: string;
@@ -290,8 +290,11 @@ function displayForRow(row: PageRow, displayByNorm: Map<string, string>): string
* otherwise strip takes/private-fact fences from the body (the same boundary
* get_page applies to untrusted readers) and take the first sentence. Never
* returns raw compiled_truth.
*
* Exported for the MEMORY_VERBS v1 entity card (verbs/entity-card.ts) the
* card's `summary` field runs through THIS boundary, not a parallel one.
*/
function safeSynopsis(row: PageRow): string {
export function safeSynopsis(row: PageRow): string {
const fmSummary = row.frontmatter?.summary;
if (typeof fmSummary === 'string' && fmSummary.trim()) {
return clip(collapse(fmSummary), SYNOPSIS_MAX);
+1
View File
@@ -127,6 +127,7 @@ export const BRAIN_CHECK_NAMES: ReadonlySet<string> = new Set([
* skill-flavored name) live under 'brain'.
*/
export const SKILL_CHECK_NAMES: ReadonlySet<string> = new Set([
'memory_verbs_usage',
'resolver_health',
'retrieval_reflex_health',
'skill_brain_first',
+10 -1
View File
@@ -65,6 +65,12 @@ export interface FenceInputFact {
/** Defaults to 1.0 when undefined (matches engine.insertFact behavior). */
confidence?: number;
validFrom?: Date;
/**
* MEMORY_VERBS v1 (c5): remember's ttl valid_until. Date-only in the
* fence cell; the DB column derives from it on the stamp step.
* Undefined/null = never expires (pre-v1 behavior unchanged).
*/
validUntil?: Date | null;
embedding: Float32Array | null;
sessionId: string | null;
}
@@ -270,7 +276,10 @@ export async function writeFactsToFence(
visibility: f.visibility,
notability: f.notability ?? 'medium',
validFrom: validFromStr,
validUntil: undefined,
// MEMORY_VERBS v1 (c5): remember's ttl threads through to the fence
// cell — was hard-coded undefined, which silently dropped expiry on
// this path. extractFactsFromFenceText derives the DB column from it.
validUntil: f.validUntil ? f.validUntil.toISOString().slice(0, 10) : undefined,
source: f.source,
context: f.context ?? undefined,
});
+31 -2
View File
@@ -55,6 +55,7 @@ interface FactDbRow {
row_num: number | null;
source_markdown_slug: string | null;
expired_at: Date | null;
visibility: string;
}
interface SourceRow {
@@ -83,15 +84,43 @@ function todayUtc(): string {
export async function forgetFactInFence(
engine: BrainEngine,
factId: number,
opts: { reason?: string } = {},
opts: {
reason?: string;
/**
* MEMORY_VERBS v1 trust boundary [ship P1.1]: when set, the fact must
* belong to this source or the call returns `not_found` (indistinguishable
* from a truly-missing id no cross-source existence leak). The `forget`
* verb passes ctx.sourceId so a remote caller scoped to source A cannot
* expire facts in source B by guessing global ids.
*/
sourceId?: string;
/**
* When true (remote callers), the fact must be visibility='world' or the
* call returns `not_found` a remote caller can't expire private facts it
* could never read (mirrors recall's remote posture).
*/
worldOnly?: boolean;
} = {},
): Promise<ForgetFactResult> {
const reason = opts.reason ?? 'forgotten';
const rows = await engine.executeRaw<FactDbRow>(
`SELECT id, source_id, entity_slug, row_num, source_markdown_slug, expired_at
`SELECT id, source_id, entity_slug, row_num, source_markdown_slug, expired_at, visibility
FROM facts WHERE id = $1`,
[factId],
);
// Trust-boundary scope check BEFORE any state inspection: a row outside the
// caller's source (or private, for remote callers) is reported as not_found,
// never distinguished from a missing id.
if (rows.length === 1) {
const r = rows[0];
const outOfScope =
(opts.sourceId !== undefined && r.source_id !== opts.sourceId) ||
(opts.worldOnly === true && r.visibility !== 'world');
if (outOfScope) {
return { ok: false, path: 'not_found', reason };
}
}
if (rows.length === 0) {
return { ok: false, path: 'not_found', reason };
}
+227
View File
@@ -0,0 +1,227 @@
/**
* MEMORY_VERBS v1 `writeSingleFact`: the zero-LLM single-fact write seam
* behind the `remember` verb [E1].
*
* `runFactsPipeline` is extraction-first (LLM-gated in extract.ts) and cannot
* back a verb whose fact arrives pre-formed. This module reuses the pipeline's
* post-extraction stages directly: resolve dedup (embedding cosine, same
* 0.95 threshold) fence-first write (markdown durability) with the same
* legacy DB-only fallbacks (thin-client, unparented, stub-guard).
*
* Supersession [X1, frozen as implementation-defined]: minimal deterministic
* rule, zero LLM when the top dedup candidate scores >= threshold with the
* SAME kind but DIFFERENT text, the new fact supersedes it (a near-duplicate
* with changed content is an update: "X at Acme" "X left Acme"). Same text
* plain duplicate (existing id returned, nothing written).
*
* Degradation (documented in the protocol doc): with no embedding provider,
* dedup/supersession are skipped on the fence path and near-duplicates may
* insert `degraded_dedup: true` tells the caller.
*
* Provenance (c6): callers pass free-text provenance which lands on
* `NewFact.source` verbatim this seam deliberately does NOT take a
* FactsBackstopCtx (whose `source` union is pipeline-internal).
*/
import type { BrainEngine, FactInsertStatus, NewFact } from '../engine.ts';
const DEDUP_THRESHOLD = 0.95;
const DEDUP_CANDIDATE_LIMIT = 5;
export interface SingleFactInput {
fact: string;
/** Free-text attribution, stored verbatim as the fact's `source`. */
provenance: string;
kind?: NewFact['kind'];
/** Free-form entity ref; canonicalized via resolveEntitySlug. */
entity?: string | null;
/** Facts-layer default 'private'; the remember VERB passes 'world' [F2]. */
visibility?: 'private' | 'world';
validUntil?: Date | null;
sessionId?: string | null;
confidence?: number;
}
export interface SingleFactResult {
id: number;
status: FactInsertStatus;
entity_slug: string | null;
valid_until: Date | null;
/** True when no embedding provider — dedup/supersession skipped. */
degraded_dedup: boolean;
}
export async function writeSingleFact(
engine: BrainEngine,
sourceId: string,
input: SingleFactInput,
): Promise<SingleFactResult> {
const { resolveEntitySlug } = await import('../entities/resolve.ts');
const { cosineSimilarity } = await import('./classify.ts');
const { writeFactsToFence, lookupSourceLocalPath } = await import('./fence-write.ts');
const { isAvailable, embedOne } = await import('../ai/gateway.ts');
const factText = input.fact.trim();
const kind = input.kind ?? 'fact';
const visibility = input.visibility ?? 'private';
const validUntil = input.validUntil ?? null;
const resolvedSlug = input.entity
? ((await resolveEntitySlug(engine, sourceId, input.entity)) ?? input.entity)
: null;
// Embedding (NOT an LLM call): powers dedup + downstream recall. Fail-soft —
// a missing/failing provider degrades dedup, never the write.
let embedding: Float32Array | null = null;
let degradedDedup = false;
if (isAvailable('embedding')) {
try {
embedding = await embedOne(factText);
} catch {
degradedDedup = true;
}
} else {
degradedDedup = true;
}
// Dedup + supersession decision (same candidates + threshold as the pipeline).
let supersedeId: number | null = null;
if (resolvedSlug && embedding) {
const candidates = await engine.findCandidateDuplicates(sourceId, resolvedSlug, factText, {
embedding,
k: DEDUP_CANDIDATE_LIMIT,
});
let top: (typeof candidates)[number] | null = null;
let topScore = -1;
for (const c of candidates) {
if (!c.embedding) continue;
const s = cosineSimilarity(embedding, c.embedding);
if (s > topScore) {
topScore = s;
top = c;
}
}
if (top && topScore >= DEDUP_THRESHOLD) {
const textDiffers = collapse(top.fact) !== collapse(factText);
if (top.kind === kind && textDiffers) {
supersedeId = top.id; // X1: near-duplicate with changed content = update
} else {
return {
id: top.id,
status: 'duplicate',
entity_slug: resolvedSlug,
valid_until: top.valid_until ?? null,
degraded_dedup: false,
};
}
}
}
const newFact: NewFact = {
fact: factText,
kind,
entity_slug: resolvedSlug,
visibility,
source: input.provenance,
source_session: input.sessionId ?? null,
confidence: input.confidence ?? 1.0,
valid_until: validUntil,
embedding,
};
// Fence-first write (markdown durability — same policy as the pipeline):
// requires a resolved, prefixed entity slug and a local_path. Everything
// else takes the legacy DB-only insertFact path, which also handles the
// supersedeId bookkeeping engine-side.
const localPath = resolvedSlug ? await lookupSourceLocalPath(engine, sourceId) : null;
const fenceable = resolvedSlug !== null && localPath !== null;
if (fenceable) {
const result = await writeFactsToFence(
engine,
{ sourceId, localPath, slug: resolvedSlug },
[
{
fact: factText,
kind,
notability: 'medium',
source: input.provenance,
context: null,
visibility,
confidence: input.confidence ?? 1.0,
validFrom: new Date(),
validUntil,
embedding,
sessionId: input.sessionId ?? null,
},
],
);
if (result.fenceWriteFailed) {
// Parse-validate rejected the .tmp (quarantined). Hard failure — do NOT
// fall through to a DB row whose fence is broken (pipeline policy).
throw new Error(
`facts fence write failed for ${resolvedSlug} — .tmp quarantined; see the facts write-failure JSONL log`,
);
}
if (!result.stubGuardBlocked && !result.legacyFallback) {
const newId = result.ids[0];
if (supersedeId !== null && newId !== undefined) {
await expireSuperseded(engine, supersedeId, newId);
return {
id: newId,
status: 'superseded',
entity_slug: resolvedSlug,
valid_until: validUntil,
degraded_dedup: degradedDedup,
};
}
return {
id: newId,
status: 'inserted',
entity_slug: resolvedSlug,
valid_until: validUntil,
degraded_dedup: degradedDedup,
};
}
// stubGuardBlocked / defensive legacyFallback → DB-only path below.
}
const inserted = await engine.insertFact(newFact, { // gbrain-allow-direct-insert: writeSingleFact legacy path for unparented / thin-client / stub-guarded facts (mirrors the pipeline's fallback buckets)
source_id: sourceId,
...(supersedeId !== null ? { supersedeId } : {}),
});
return {
id: inserted.id,
status: supersedeId !== null ? 'superseded' : inserted.status,
entity_slug: resolvedSlug,
valid_until: validUntil,
degraded_dedup: degradedDedup,
};
}
/**
* Fence-path supersession bookkeeping: expire the old row through the fence
* (strikethrough + valid_until, the same surface `forget` uses) and link
* `superseded_by` for the audit trail. Both steps best-effort the new fact
* is already durably written; a partial supersede is an audit gap, not data
* loss.
*/
async function expireSuperseded(engine: BrainEngine, oldId: number, newId: number): Promise<void> {
try {
const { forgetFactInFence } = await import('./forget.ts');
await forgetFactInFence(engine, oldId, { reason: `superseded by fact #${newId}` });
} catch {
/* best-effort */
}
try {
await engine.executeRaw(`UPDATE facts SET superseded_by = $1 WHERE id = $2`, [newId, oldId]);
} catch {
/* best-effort */
}
}
function collapse(s: string): string {
return s.replace(/\s+/g, ' ').trim().toLowerCase();
}
+14 -2
View File
@@ -66,6 +66,11 @@ export const LIST_PAGES_DESCRIPTION =
export const QUERY_DESCRIPTION =
"Hybrid search with vector + keyword + multi-query expansion. " +
"Prefer `query` for concept / synonym / landscape questions ('all the X that " +
"do Y', 'the landscape of Z') — expansion recovers synonym- and " +
"outcome-phrased matches a single embedding misses. Still top-K: for " +
"exhaustive enumeration use list_pages; for exact known tokens `search` is " +
"cheaper (no expansion LLM call). " +
"For personal/emotional questions ('what's going on with me', 'anything notable', " +
"'how am I feeling'), prefer get_recent_salience, find_anomalies, or " +
"get_recent_transcripts. Semantic search returns polished pages and misses " +
@@ -73,12 +78,19 @@ export const QUERY_DESCRIPTION =
"mean impressive — they often mean difficult or emotionally charged.";
export const SEARCH_DESCRIPTION =
"Keyword search using full-text search. For personal/emotional questions, " +
"Cheap hybrid search (vector + keyword + RRF) with no LLM expansion. " +
"Best for exact known tokens, names, and structured-field lookups. A populated " +
"result set is NOT proof of coverage — for concept / synonym / landscape " +
"questions use `query` (adds multi-query expansion); for exhaustive " +
"enumeration use list_pages pagination. " +
"For personal/emotional questions, " +
"prefer get_recent_salience or find_anomalies — they surface activity bursts " +
"without needing a search term. " +
"For code-symbol questions (callers, callees, definitions, blast radius), use " +
"code_callers / code_callees / code_def / code_refs instead — those return " +
"structural graph data, not text chunks.";
"structural graph data, not text chunks. " +
"For agent memory reads (saved facts + budget-packed retrieval), prefer the " +
"`recall` verb.";
// ──────────────────────────────────────────────────────────────────────────────
// v0.32.6 — contradiction probe MCP surface (M3)
+203 -6
View File
@@ -26,6 +26,10 @@ import { buildVisibilityClause } from './search/sql-ranking.ts';
import { bumpLastRetrievedAt } from './last-retrieved.ts';
import { isSearchMode } from './search/mode.ts';
import { stampEvidence } from './search/evidence.ts';
import { packToBudget, estimateTokens, resultTokens } from './search/token-budget.ts';
import { isAvailable } from './ai/gateway.ts';
import { verbOperations, MEMORY_VERBS_VERSION } from './verbs.ts';
export { MEMORY_VERBS_VERSION };
import type { SearchResult } from './types.ts';
import { CJK_SLUG_CHARS, PAGE_SLUG_SEG } from './cjk.ts';
import { ALL_SOURCES } from './source-id.ts';
@@ -72,10 +76,27 @@ export type ErrorCode =
| 'rate_limited' // v0.31: gateway rate-limit upstream
| 'extraction_failed' // v0.31: facts extractor failed (refusal, parse, abort)
| 'fact_not_found' // v0.31: forget_fact / recall on unknown id
// MEMORY_VERBS v1 protocol codes (frozen — docs/protocol/MEMORY_VERBS_v1.md).
// Coarse on purpose: codes are for branching (configure/retry vs caller bug
// vs server bug); the freeform `detail` field carries specifics.
| 'not_found' // verb-level: unknown fact id (forget)
| 'scope_denied' // verb-level: OAuth scope / trust-boundary refusal
| 'provenance_required' // remember: provenance missing or empty
| 'unavailable' // a required dependency cannot serve (no API key, gateway down, model refusal)
| 'budget_unsatisfiable' // RESERVED in v1 — schema-listed, never returned
// eslint-disable-next-line @typescript-eslint/ban-types
| (string & {}); // OPEN union for forward-compat (eE7 / D13)
export class OperationError extends Error {
/**
* MEMORY_VERBS v1: verb handlers set `protocolVersion` (=1) and may set
* `detail` (freeform specifics, e.g. which dependency failed). Both are
* additive non-verb ops never set them and their envelopes are unchanged
* (undefined keys drop out of JSON.stringify).
*/
public detail?: string;
public protocolVersion?: number;
constructor(
public code: ErrorCode,
message: string,
@@ -92,10 +113,29 @@ export class OperationError extends Error {
message: this.message,
suggestion: this.suggestion,
docs: this.docs,
detail: this.detail,
protocol_version: this.protocolVersion,
};
}
}
/**
* MEMORY_VERBS v1 error constructor. Every verb error carries a populated
* `suggestion` (problem + cause + fix agents read it and self-correct;
* conformance asserts non-empty) and `protocol_version: 1`.
*/
export function verbError(
code: ErrorCode,
message: string,
suggestion: string,
detail?: string,
): OperationError {
const e = new OperationError(code, message, suggestion);
e.protocolVersion = MEMORY_VERBS_VERSION;
if (detail !== undefined) e.detail = detail;
return e;
}
// --- Upload validators (Fix 1 / B5 / H5 / M4) ---
/**
@@ -944,6 +984,22 @@ export interface Operation {
*/
scope?: 'read' | 'write' | 'admin' | 'sources_admin' | 'users_admin';
localOnly?: boolean;
/**
* MEMORY_VERBS v1: marks the five frozen protocol verbs (recall, remember,
* entity, synthesize, forget). `gbrain serve --surface verbs` exposes
* EXACTLY the ops with `verb: true`; `full` (default) exposes everything.
*/
verb?: boolean;
/**
* MCP ToolAnnotations passthrough (SDK 1.29+). Emitted by buildToolDefs
* ONLY when set existing tools keep byte-identical definitions.
*/
annotations?: {
title?: string;
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
};
cliHints?: {
name?: string;
/**
@@ -4455,7 +4511,7 @@ const sources_status: Operation = {
const extract_facts: Operation = {
name: 'extract_facts',
description:
'v0.31: extract personal-knowledge facts (events, preferences, commitments, beliefs) from a conversation turn into the per-source hot memory. Sanitizes turn_text via INJECTION_PATTERNS, calls Haiku to extract structured claims, runs the cosine fast-path + classifier dedup pipeline, INSERTs into facts. Returns counts by status. Skips extraction when the turn is dream-generated content (anti-loop).',
'v0.31: extract personal-knowledge facts (events, preferences, commitments, beliefs) from a conversation turn into the per-source hot memory. Sanitizes turn_text via INJECTION_PATTERNS, calls Haiku to extract structured claims, runs the cosine fast-path + classifier dedup pipeline, INSERTs into facts. Returns counts by status. Skips extraction when the turn is dream-generated content (anti-loop). For agent memory writes of a SINGLE already-formed fact, prefer the `remember` verb (zero LLM, mandatory provenance).',
params: {
turn_text: { type: 'string', required: true, description: 'The user message or page body to extract facts from. Sanitized via INJECTION_PATTERNS before the LLM call.' },
session_id: { type: 'string', description: 'Opaque session id (e.g. topic-id from MCP _meta.session_id, or CLI --session). Stored on each fact for the recall --session filter. Not an auth surface.' },
@@ -4511,18 +4567,22 @@ const extract_facts: Operation = {
const recall: Operation = {
name: 'recall',
description:
'v0.31: query per-source hot memory (facts table). Filters by entity / since / session. Remote callers see only visibility=world facts. Returns most-recent first. v0.32 adds optional include_pending to return pending_consolidation_count alongside facts in one round trip.',
'MEMORY VERB (v1): retrieve saved facts/snippets — the protocol read verb. Filters hot-memory facts by entity / since / session_id; pass `query` to ALSO run hybrid search over pages (results[] arm); pass `budget_tokens` for server-side packing (response reports budget_used + dropped_count — never trims client-side). Remote callers see visibility=world facts only. Routing: for ONE known person/company/project card use `entity` (zero LLM); for broad questions needing reasoning use `synthesize` (expensive). Branch on structured fields (status/kind/evidence), never on prose. Every response carries protocol_version.',
params: {
entity: { type: 'string', description: 'Entity slug (canonical). Returns facts about this entity newest first.' },
since: { type: 'string', description: 'ISO datetime or duration shorthand (e.g. "8 hours ago"). Returns facts created since.' },
query: { type: 'string', description: 'MEMORY_VERBS v1: free-text retrieval over pages (hybrid search arm). Response adds results[] (slug, title, chunk, evidence, create_safety, provenance). Combinable with entity (both arms run). Degrades to keyword-only search when no embedding provider is configured (search_degraded notes it; never an error).' },
budget_tokens: { type: 'number', description: 'MEMORY_VERBS v1: server-side token budget (char/4 estimate). Facts pack first, then results. Response adds budget_tokens, budget_used, dropped_count.' },
since: { type: 'string', description: 'ISO 8601 datetime or duration shorthand (e.g. "8 hours ago"). Filters the FACTS arm only.' },
session_id: { type: 'string', description: 'Source session id (e.g. topic-A). Returns facts captured in that session.' },
include_expired: { type: 'boolean', description: 'When true, include expired_at IS NOT NULL rows. Default false.' },
supersessions: { type: 'boolean', description: 'When true, return only the supersession audit log (expired_at + superseded_by both set).' },
limit: { type: 'number', description: 'Max rows to return. Default 50, cap 100.' },
limit: { type: 'number', description: 'Per-arm cap: max fact rows AND max search results. Default 50, cap 100.' },
grep: { type: 'string', description: 'Substring filter on fact text (case-insensitive). Applied client-side after recall.' },
include_pending: { type: 'boolean', description: 'v0.32: when true, response includes pending_consolidation_count (facts not yet promoted to takes by the dream-cycle consolidate phase). One round trip; backward-compatible (field omitted when false).' },
},
scope: 'read',
verb: true,
annotations: { title: 'recall (memory read)', readOnlyHint: true },
handler: async (ctx, p) => {
const sourceId = ctx.sourceId ?? 'default';
const limit = typeof p.limit === 'number' ? p.limit : 50;
@@ -4593,8 +4653,58 @@ const recall: Operation = {
}
}
// ── MEMORY_VERBS v1 — query arm (G1B superset). Hybrid search over pages
// when `query` is present; degrades to keyword-only with a note (never an
// error) when no embedding provider is configured [F-B].
const queryText = typeof p.query === 'string' && p.query.trim().length > 0 ? p.query.trim() : null;
const budgetTokens =
typeof p.budget_tokens === 'number' && Number.isFinite(p.budget_tokens) && p.budget_tokens > 0
? Math.floor(p.budget_tokens)
: null;
let searchResults: SearchResult[] = [];
let searchDegraded: string | undefined;
if (queryText) {
const searchScope = sourceScopeOpts(ctx);
if (!isAvailable('embedding')) {
const raw = await ctx.engine.searchKeyword(queryText, { limit, ...searchScope });
searchResults = dedupResults(raw);
stampEvidenceSafe(searchResults);
await stampContentFlags(ctx.engine, searchResults);
searchDegraded = 'keyword_only_no_embedding_provider';
} else {
searchResults = await hybridSearchCached(ctx.engine, queryText, {
limit,
expansion: false,
...searchScope,
});
}
bumpLastRetrievedAt(ctx.engine, searchResults.map(r => r.page_id));
}
// ── MEMORY_VERBS v1 — server-side budget packing. Facts pack first (cheap,
// high-precision one-liners, per-arm limit-capped so starvation is bounded),
// then search results take the remainder. packToBudget treats budget<=0 as
// a no-op, so an exhausted remainder must drop explicitly.
let packedFacts = rows;
let packedResults = searchResults;
let budgetUsed: number | undefined;
let droppedCount: number | undefined;
if (budgetTokens !== null) {
const factsPack = packToBudget(rows, r => estimateTokens(r.fact), budgetTokens);
packedFacts = factsPack.items;
const remaining = budgetTokens - factsPack.meta.used;
const resultsPack =
remaining > 0
? packToBudget(searchResults, resultTokens, remaining)
: { items: [] as SearchResult[], meta: { budget: 0, used: 0, dropped: searchResults.length, kept: 0 } };
packedResults = resultsPack.items;
budgetUsed = factsPack.meta.used + resultsPack.meta.used;
droppedCount = factsPack.meta.dropped + resultsPack.meta.dropped;
}
return {
facts: rows.map(r => ({
facts: packedFacts.map(r => ({
id: r.id,
fact: r.fact,
kind: r.kind,
@@ -4614,9 +4724,33 @@ const recall: Operation = {
source_session: r.source_session,
confidence: r.confidence,
created_at: r.created_at.toISOString(),
// MEMORY_VERBS v1 additive fields (G1B). `fact_id` is the opaque
// STRING id the `forget` verb accepts (legacy numeric `id` stays for
// pre-v1 consumers — legacy fields are frozen byte-equal). `provenance`
// is the protocol name for the stored source attribution.
fact_id: String(r.id),
provenance: r.source,
})),
total: rows.length,
total: packedFacts.length,
...(pending_consolidation_count !== undefined ? { pending_consolidation_count } : {}),
// MEMORY_VERBS v1 envelope (G1B superset — additive on every response).
protocol_version: MEMORY_VERBS_VERSION,
...(queryText
? {
results: packedResults.map(r => ({
slug: r.slug,
title: r.title,
chunk: r.chunk_text,
evidence: r.evidence,
create_safety: r.create_safety,
provenance: r.slug,
})),
...(searchDegraded ? { search_degraded: searchDegraded } : {}),
}
: {}),
...(budgetTokens !== null
? { budget_tokens: budgetTokens, budget_used: budgetUsed, dropped_count: droppedCount }
: {}),
};
},
};
@@ -4677,6 +4811,65 @@ function parseSinceParam(raw: unknown): Date | null {
return null;
}
/**
* MEMORY_VERBS v1 parse the `remember` verb's `ttl` param into a
* `valid_until` Date. Sibling of parseSinceParam, pointed FORWARD.
*
* Accepted forms (frozen in docs/protocol/MEMORY_VERBS_v1.md):
* - relative duration shorthand: '30d', '12h', '45m', '90s' (also
* spelled-out: '30 days', '12 hours') now + duration
* - absolute ISO 8601 date or datetime: '2026-07-12', '2026-07-12T00:00:00Z'
*
* Explicitly REJECTED with a self-correcting suggestion: ISO-8601 duration
* syntax ('P30D', 'PT12H') agents that read "ISO 8601" as durations get a
* fix, not a mystery. Returns null for null/undefined/empty (= never expires).
* Throws verbError('invalid_params') on anything unparseable.
*/
export function parseTtlParam(raw: unknown): Date | null {
if (raw == null) return null;
if (typeof raw !== 'string') {
throw verbError(
'invalid_params',
`ttl must be a string, got ${typeof raw}.`,
'Pass a duration like "30d" or "12h", or an absolute ISO 8601 timestamp like "2026-07-12T00:00:00Z".',
);
}
const s = raw.trim();
if (!s) return null;
// ISO-8601 DURATION syntax is a documented trap — reject with the fix.
if (/^P(T|\d)/i.test(s) && /^P(?:\d+[YMWD])*(?:T(?:\d+[HMS])+)?$/i.test(s)) {
throw verbError(
'invalid_params',
`ttl "${s}" looks like an ISO-8601 duration, which is not accepted.`,
`Use the shorthand form instead (e.g. "${s.replace(/^PT?/i, '').toLowerCase()}" style: "30d", "12h"), or an absolute ISO 8601 expiry timestamp.`,
);
}
// Relative duration shorthand → now + duration.
const dur = s.match(/^(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?)$/i);
if (dur) {
const n = parseInt(dur[1], 10);
const unit = dur[2].toLowerCase();
const ms =
unit.startsWith('s') ? n * 1000 :
unit.startsWith('m') ? n * 60 * 1000 :
unit.startsWith('h') ? n * 60 * 60 * 1000 :
n * 24 * 60 * 60 * 1000;
return new Date(Date.now() + ms);
}
// Absolute ISO 8601 date or datetime.
const iso = Date.parse(s);
if (Number.isFinite(iso)) return new Date(iso);
throw verbError(
'invalid_params',
`Cannot parse ttl "${s}".`,
'Pass a duration like "30d" or "12h", or an absolute ISO 8601 timestamp like "2026-07-12T00:00:00Z". Omit ttl for a fact that never expires.',
);
}
// ──────────────────────────────────────────────────────────────────────────────
// v0.34 Cathedral III — code-intelligence ops (MCP-exposed).
//
@@ -6157,6 +6350,10 @@ const extraction_review: Operation = {
};
export const operations: Operation[] = [
// MEMORY_VERBS v1 (Cathedral 1) — remember/entity/synthesize/forget live in
// verbs.ts; the fifth verb is the extended `recall` op below. Spread first
// so `--surface verbs` agents see them at the top of the tool list.
...verbOperations,
// Page CRUD
get_page, put_page, delete_page, list_pages,
// v0.26.5 destructive-guard ops (page-level soft-delete + recovery + admin purge)
+61
View File
@@ -340,3 +340,64 @@ export function intentToDetail(intent: QueryIntent): 'low' | 'medium' | 'high' |
export function autoDetectDetail(query: string): 'low' | 'medium' | 'high' | undefined {
return classifyQuery(query).suggestedDetail;
}
// ─────────────────────────────────────────────────────────
// #2416 — concept-shaped query detection (CLI nudge)
// ─────────────────────────────────────────────────────────
// Fuzzy-quantifier / landscape cues. A concept-shaped question asks for a
// SET defined by meaning ("all the X that do Y", "the landscape of Z") —
// exactly where `query`'s multi-query expansion recovers synonym- and
// outcome-phrased matches that the expansion-off `search` op can miss.
//
// Deliberately EXCLUDED cues (owned by other routers — a nudge toward
// `query` on these would fight their descriptions):
// - "who are the …" → find_experts
// - bare "anything …" → get_recent_salience / find_anomalies
const CONCEPT_CUE_PATTERNS: RegExp[] = [
/\b(all|every)\b.+\b(that|who|which|doing|with|about|related to)\b/i,
/\b(find|list|show)\s+(all|every|everything)\b/i,
/\beverything\s+(about|on|matching|related)\b/i,
/\bthe\s+(landscape|ecosystem|space|universe)\s+of\b/i,
/\b(landscape|ecosystem)\s+(of|around)\b/i,
/\bwhich\s+\w+[\w\s]*\b(do|does|are|have|use|work)\b/i,
];
// Exact-identifier anti-signals: the query names a specific thing, so the
// cheap `search` op is the right tool and a nudge would be noise.
const CONCEPT_ANTI_PATTERNS: RegExp[] = [
/["'“”][^"'“”]+["'“”]/, // quoted phrase — exact-match intent
/\b[a-z0-9]+(?:-[a-z0-9]+){1,}\b/, // slug-like token (kebab-case)
];
/**
* True when a query is concept-shaped: it carries a fuzzy-quantifier or
* landscape cue AND no exact-identifier anti-signal. Tuned to favor
* false-negatives (silence) over false-positives (noise): short queries,
* quoted phrases, slugs, and entity lookups (per classifyQueryIntent)
* never trigger. Pure function; no LLM, no DB.
*/
export function looksConceptShaped(query: string): boolean {
const q = query.trim();
if (q.split(/\s+/).length < 3) return false; // bare token / proper-noun lookup
if (!matches(CONCEPT_CUE_PATTERNS, q)) return false;
if (matches(CONCEPT_ANTI_PATTERNS, q)) return false;
if (classifyQueryIntent(q) === 'entity') return false;
return true;
}
/**
* One-line CLI hint steering a concept-shaped `search` toward `query`.
* Returns null when the query is not concept-shaped. Message generation
* lives here (not in cli.ts) so the full string is unit-testable; the CLI
* wiring is a two-liner per dispatch path, stderr only, `--quiet`-gated.
*/
export function conceptNudge(query: string): string | null {
if (!looksConceptShaped(query)) return null;
const preview = query.length > 60 ? `${query.slice(0, 57)}...` : query;
return (
`hint: concept-shaped question — try \`gbrain query "${preview}"\` ` +
`(adds multi-query expansion; recovers synonym-phrased matches search can miss). ` +
`A nonzero search count is not proof of completeness.`
);
}
+52 -36
View File
@@ -63,50 +63,66 @@ export interface TokenBudgetMeta {
}
/**
* Greedy top-down budget enforcement. Walks the input in order, accumulates
* token costs, and stops as soon as adding the next result would exceed
* the budget. Results are NOT re-ranked caller's order is preserved.
* Generic greedy top-down budget packer (v1 memory-verbs protocol). Walks
* the input in order, accumulates per-item costs via the caller-supplied
* cost function, and stops as soon as adding the next item would exceed
* the budget. Items are NOT re-ranked caller's order is preserved.
*
* Edge cases (all preserve the pre-v0.32 contract):
* - budget undefined / <= 0: returns input unchanged; dropped=0, kept=N.
* - First result alone exceeds budget: returns []; dropped=N, kept=0.
* - First item alone exceeds budget: returns []; dropped=N, kept=0.
* (Intentionally strict: the caller asked for a hard cap.)
* - Input empty: returns []; budget unused.
*/
export function packToBudget<T>(
items: T[],
cost: (item: T) => number,
budget: number | undefined,
): { items: T[]; meta: TokenBudgetMeta } {
const safeBudget = typeof budget === 'number' && budget > 0 ? budget : 0;
if (safeBudget === 0 || items.length === 0) {
return {
items,
meta: {
budget: safeBudget,
used: items.reduce((acc, it) => acc + cost(it), 0),
dropped: 0,
kept: items.length,
},
};
}
const kept: T[] = [];
let used = 0;
for (const it of items) {
const c = cost(it);
if (used + c > safeBudget) break;
kept.push(it);
used += c;
}
return {
items: kept,
meta: {
budget: safeBudget,
used,
dropped: items.length - kept.length,
kept: kept.length,
},
};
}
/**
* Search-pipeline budget enforcement a thin wrapper over packToBudget
* with the SearchResult cost model (title + chunk_text). Behavior is
* byte-identical to the pre-refactor implementation; pinned by
* test/token-budget.test.ts.
*/
export function enforceTokenBudget(
results: SearchResult[],
budget: number | undefined,
): { results: SearchResult[]; meta: TokenBudgetMeta } {
const safeBudget = typeof budget === 'number' && budget > 0 ? budget : 0;
if (safeBudget === 0 || results.length === 0) {
return {
results,
meta: {
budget: safeBudget,
used: results.reduce((acc, r) => acc + resultTokens(r), 0),
dropped: 0,
kept: results.length,
},
};
}
const kept: SearchResult[] = [];
let used = 0;
for (const r of results) {
const cost = resultTokens(r);
if (used + cost > safeBudget) break;
kept.push(r);
used += cost;
}
return {
results: kept,
meta: {
budget: safeBudget,
used,
dropped: results.length - kept.length,
kept: kept.length,
},
};
const { items, meta } = packToBudget(results, resultTokens, budget);
return { results: items, meta };
}
+23 -12
View File
@@ -140,6 +140,13 @@ export interface ThinkResult {
* pre-existing/test `ThinkResult` literals treated as persistable (back-compat).
*/
synthesisOk?: boolean;
/**
* MEMORY_VERBS v1 [E2] gateway token usage for the synthesis call(s),
* summed across rounds. Best-effort: null when no LLM ran (graceful stub),
* when a test client returns no usage, or when a provider omits accounting.
* The synthesize verb maps this to its frozen `cost` block.
*/
usage?: { input_tokens: number; output_tokens: number } | null;
/** Only set when --save was true and the caller persisted a synthesis page. */
savedSlug?: string;
/** Diagnostics for `--explain` callers (CLI surface for v0.29). */
@@ -149,15 +156,6 @@ export interface ThinkResult {
takesFromVector: number;
graphHits: number;
};
/**
* Token usage from the real LLM call, when one happened. Undefined on the
* no-client/stub paths (no Anthropic key, model not usable) same
* distinction `synthesisOk` already makes. `think`'s cost was previously
* unsurfaced anywhere: not in this CLI's own output, not in
* `budget_ledger`, and invisible to a wrapping caller's own token
* accounting (the LLM call `think` makes is its own separate API call).
*/
usage?: { input_tokens: number; output_tokens: number };
/** USD cost computed from `usage` + `canonicalLookup(modelUsed)`, when both are available. */
cost_usd?: number;
}
@@ -462,8 +460,10 @@ export async function runThink(
// sentinel, which is non-JSON) and on the no-client early return below; the final
// return ANDs it with a non-empty-answer check (catches valid-but-empty JSON).
let synthesisOk = true;
// [E2] best-effort usage aggregation across synthesis calls (single-pass in
// v0.28+, but summed so the round loop inherits it when gap-fill lands).
let usage: { input_tokens: number; output_tokens: number } | null = null;
let response: ThinkResponse;
let usage: { input_tokens: number; output_tokens: number } | undefined;
if (opts.stubResponse) {
response = opts.stubResponse;
} else {
@@ -513,6 +513,7 @@ export async function runThink(
rounds: 0,
warnings,
synthesisOk: false, // #1698: no LLM ran — never persist this
usage: null, // [E2] no LLM ran — no accounting
diagnostics: {
pagesFromHybrid: gather.diagnostics.pagesFromHybrid,
takesFromKeyword: gather.diagnostics.takesFromKeyword,
@@ -527,7 +528,17 @@ export async function runThink(
system: systemPrompt,
messages: [{ role: 'user', content: userMessage }],
});
usage = { input_tokens: result.usage.input_tokens, output_tokens: result.usage.output_tokens };
// [E2] capture usage when the message carries it (test-injected clients
// and providers without accounting leave it null).
const u = (result as { usage?: { input_tokens?: number; output_tokens?: number } }).usage;
if (u && typeof u.input_tokens === 'number' && typeof u.output_tokens === 'number') {
// Single synthesis call in v0.28+; when gap-driven rounds land, sum here.
const prev = usage as { input_tokens: number; output_tokens: number } | null;
usage = {
input_tokens: (prev?.input_tokens ?? 0) + u.input_tokens,
output_tokens: (prev?.output_tokens ?? 0) + u.output_tokens,
};
}
const block = result.content.find(b => b.type === 'text');
const text = block && 'text' in block ? block.text : '';
const parsed = tryParseJSON(text);
@@ -572,13 +583,13 @@ export async function runThink(
// #1698: persistable only when a real synthesis produced a non-empty answer.
// ANDs the not-JSON/sentinel flag with a content check (catches valid-but-empty JSON).
synthesisOk: synthesisOk && response.answer.trim().length > 0,
usage,
diagnostics: {
pagesFromHybrid: gather.diagnostics.pagesFromHybrid,
takesFromKeyword: gather.diagnostics.takesFromKeyword,
takesFromVector: gather.diagnostics.takesFromVector,
graphHits: gather.diagnostics.graphHits,
},
usage,
};
}
+559
View File
@@ -0,0 +1,559 @@
/**
* MEMORY_VERBS v1 the frozen memory protocol verbs (Cathedral 1).
*
* Four first-class Operations (`remember`, `entity`, `synthesize`, `forget`)
* that join the extended `recall` op (operations.ts) as the five-verb façade
* over the operation catalog. Frozen contract: docs/protocol/MEMORY_VERBS_v1.md
* field names and semantics in v1 never change; additions are forever-
* additive; `protocol_version` rides every response; errors carry enumerated
* codes + populated `suggestion` (agents read it and self-correct).
*
* These are ordinary Operations: they inherit trust-boundary fail-closed
* semantics (ctx.remote), scope enforcement, and source isolation like every
* other op. `gbrain serve --surface verbs` exposes exactly the ops marked
* `verb: true`.
*
* Import-cycle note: operations.ts spreads these into its `operations` array
* at MODULE-EVAL time, so this file must be a RUNTIME LEAF it may import
* operations.ts types (erased) but never its values statically. Handlers load
* verbError/parseTtlParam/sourceScopeOpts via dynamic import (the file's
* existing style), which resolves after both modules finish evaluating.
* MEMORY_VERBS_VERSION lives HERE (operations.ts imports it from us) for the
* same reason. Violating this reintroduces the TDZ crash on whichever module
* evaluates second.
*/
import type { Operation } from './operations.ts';
/** Frozen protocol version for the MEMORY_VERBS v1 verb set. Single source of truth. */
export const MEMORY_VERBS_VERSION = 1;
export const VERB_NAMES = ['recall', 'remember', 'entity', 'synthesize', 'forget'] as const;
export type VerbName = (typeof VERB_NAMES)[number];
const FACT_KINDS = ['event', 'preference', 'commitment', 'belief', 'fact'] as const;
const PROVENANCE_MAX = 500;
// ─── remember ────────────────────────────────────────────────────────────────
const remember: Operation = {
name: 'remember',
description:
'MEMORY VERB (v1): save one fact to durable agent memory — the protocol write verb. ' +
'provenance is REQUIRED (free text, e.g. "conversation 2026-06-12", "user said in chat", "import: notes.md"). ' +
'Set `entity` whenever the fact is about a specific person/company/project — entity-scoped recall will not find it otherwise. ' +
'ttl accepts duration shorthand ("30d", "12h") or an absolute ISO 8601 timestamp; ISO-8601 durations like "P30D" are rejected with a fix. ' +
'visibility defaults to "world" (readable by every agent connected to this brain; pass "private" for local-CLI-only facts). ' +
'Response: branch on `status` (inserted|duplicate|superseded), never on `status_text` (human rendering only). ' +
'On duplicate, `id` is the EXISTING fact\'s id. For bulk extraction from a raw transcript use extract_facts instead.',
params: {
fact: { type: 'string', required: true, description: 'The fact to remember, one claim per call.' },
provenance: {
type: 'string',
required: true,
description:
'Where this fact came from (REQUIRED, free text, max 500 chars). Examples: "conversation 2026-06-12", "user said in chat", "import: meeting-notes.md".',
},
ttl: {
type: 'string',
description:
'Optional expiry: duration shorthand ("30d", "12h", "45m") or absolute ISO 8601 timestamp ("2026-07-12T00:00:00Z"). NOT ISO-8601 durations ("P30D" is rejected). Omit = never expires.',
},
entity: {
type: 'string',
description:
'Person/company/project this fact is about (name or slug; canonicalized server-side). Set it whenever the fact has a subject — entity-scoped recall misses unattributed facts.',
},
kind: {
type: 'string',
enum: [...FACT_KINDS],
description: 'Fact kind: event | preference | commitment | belief | fact (default).',
},
visibility: {
type: 'string',
enum: ['world', 'private'],
description:
'world (default): readable by every agent connected to this brain — required for the remote remember→recall round-trip. private: local CLI reads only.',
},
},
mutating: true,
scope: 'write',
verb: true,
annotations: { title: 'remember (memory write)', idempotentHint: true },
handler: async (ctx, p) => {
const { verbError, parseTtlParam } = await import('./operations.ts');
const fact = typeof p.fact === 'string' ? p.fact.trim() : '';
if (!fact) {
throw verbError(
'invalid_params',
'fact must be a non-empty string.',
'Pass the claim to remember, e.g. fact: "picked Stripe over Adyen — onboarding speed".',
);
}
const provenance = typeof p.provenance === 'string' ? p.provenance.trim() : '';
if (!provenance) {
throw verbError(
'provenance_required',
'provenance is required and must be non-empty.',
'Pass where the fact came from, e.g. provenance: "user told me, 2026-06-12" or "import: notes.md".',
);
}
if (provenance.length > PROVENANCE_MAX) {
throw verbError(
'invalid_params',
`provenance exceeds ${PROVENANCE_MAX} chars (got ${provenance.length}).`,
'Shorten the attribution — provenance is a pointer, not a transcript.',
);
}
const kind = typeof p.kind === 'string' ? p.kind : 'fact';
if (!FACT_KINDS.includes(kind as (typeof FACT_KINDS)[number])) {
throw verbError(
'invalid_params',
`kind "${kind}" is not a fact kind.`,
`Use one of: ${FACT_KINDS.join(' | ')}.`,
);
}
const visibility = typeof p.visibility === 'string' ? p.visibility : 'world';
if (visibility !== 'world' && visibility !== 'private') {
throw verbError(
'invalid_params',
`visibility "${visibility}" is not valid.`,
'Use "world" (default — agents can recall it) or "private" (local CLI reads only).',
);
}
const validUntil = parseTtlParam(p.ttl); // throws verbError(invalid_params) on bad input
if (ctx.dryRun) {
return {
dry_run: true,
action: 'remember',
fact,
protocol_version: MEMORY_VERBS_VERSION,
};
}
const { writeSingleFact } = await import('./facts/write-single.ts');
const result = await writeSingleFact(ctx.engine, ctx.sourceId ?? 'default', {
fact,
provenance,
kind: kind as (typeof FACT_KINDS)[number],
entity: typeof p.entity === 'string' && p.entity.trim() ? p.entity.trim() : null,
visibility,
validUntil,
});
const statusText =
result.status === 'inserted'
? `remembered as fact #${result.id}`
: result.status === 'duplicate'
? `already knew this — kept fact #${result.id}`
: `updated — fact #${result.id} supersedes the previous version`;
return {
// Opaque STRING at the protocol level [T4]; gbrain serializes its ints.
id: String(result.id),
status: result.status,
status_text: statusText,
entity_slug: result.entity_slug ?? null,
valid_until: result.valid_until ? result.valid_until.toISOString() : null,
...(result.degraded_dedup ? { degraded_dedup: true } : {}),
protocol_version: MEMORY_VERBS_VERSION,
};
},
cliHints: { name: 'remember', positional: ['fact'] },
};
// ─── entity ──────────────────────────────────────────────────────────────────
const entity: Operation = {
name: 'entity',
description:
'MEMORY VERB (v1): inspect ONE known person/company/project card — zero LLM calls, sub-100ms. ' +
'Resolution: alias > exact title > slug-suffix; ties break on most-recently-touched. ' +
'NEVER errors on a miss: returns found:false plus near-miss suggestions with create_safety hints ' +
'(exists | probable | unknown — whether writing a new page would duplicate). ' +
'Routing: for facts/snippets retrieval use recall; for broad questions needing reasoning use synthesize (expensive).',
params: {
name: { type: 'string', required: true, description: 'Free-text name, alias, or slug (e.g. "Alice Example", "people/alice-example").' },
},
scope: 'read',
verb: true,
annotations: { title: 'entity (card lookup, zero LLM)', readOnlyHint: true },
handler: async (ctx, p) => {
const { verbError } = await import('./operations.ts');
const name = typeof p.name === 'string' ? p.name.trim() : '';
if (!name) {
throw verbError(
'invalid_params',
'name must be a non-empty string.',
'Pass the entity to look up, e.g. name: "Alice Example" or name: "people/alice-example".',
);
}
const t0 = Date.now();
const { buildEntityCard } = await import('./verbs/entity-card.ts');
const result = await buildEntityCard(ctx.engine, ctx.sourceId ?? 'default', name, {
remote: ctx.remote !== false,
});
return {
protocol_version: MEMORY_VERBS_VERSION,
found: result.found,
latency_ms: Date.now() - t0,
...(result.card ? { card: result.card } : {}),
...(result.suggestions !== undefined ? { suggestions: result.suggestions } : {}),
};
},
cliHints: { name: 'entity', positional: ['name'] },
};
// ─── synthesize ──────────────────────────────────────────────────────────────
const synthesize: Operation = {
name: 'synthesize',
description:
'[EXPENSIVE / SLOW — makes LLM calls, seconds-to-minutes latency, costs money] ' +
'MEMORY VERB (v1): answer a broad question using cross-page LLM reasoning with citations and gap analysis. ' +
'Prefer recall (facts/snippets) or entity (one known card, zero LLM) for lookups — use synthesize only when the answer ' +
'requires combining evidence across pages. Response carries a best-effort cost block (model, tokens, usd_estimate).',
params: {
question: { type: 'string', required: true, description: 'The question to answer.' },
since: { type: 'string', description: 'Optional temporal window start (ISO 8601 date or datetime).' },
until: { type: 'string', description: 'Optional temporal window end (ISO 8601 date or datetime).' },
},
scope: 'read',
verb: true,
annotations: { title: 'synthesize (slow, costly — LLM-backed)', readOnlyHint: true },
handler: async (ctx, p) => {
const { verbError, sourceScopeOpts } = await import('./operations.ts');
const question = typeof p.question === 'string' ? p.question.trim() : '';
if (!question) {
throw verbError(
'invalid_params',
'question must be a non-empty string.',
'Pass the question to synthesize an answer for, e.g. question: "what is our payments strategy?".',
);
}
const scope = sourceScopeOpts(ctx);
const { runThink } = await import('./think/index.ts');
// Remote-safe delegation: save/take are NEVER offered through this verb,
// for any caller — the verb is a pure read.
const result = await runThink(ctx.engine, {
question,
since: p.since ? String(p.since) : undefined,
until: p.until ? String(p.until) : undefined,
takesHoldersAllowList: ctx.takesHoldersAllowList,
...(scope.sourceId !== undefined ? { sourceId: scope.sourceId } : {}),
...(scope.sourceIds !== undefined ? { allowedSources: scope.sourceIds } : {}),
remote: ctx.remote === true,
});
// [c10] runThink degrades gracefully to a no-LLM stub RESULT; the protocol
// contract converts that state into an explicit `unavailable` error so
// agents branch on configure/retry instead of relaying a fake answer.
if (result.warnings.includes('NO_ANTHROPIC_API_KEY')) {
throw verbError(
'unavailable',
'synthesize needs an LLM and none is configured.',
'Set an API key (e.g. `gbrain config set anthropic_api_key sk-...` or ANTHROPIC_API_KEY) and retry. recall and entity work without one.',
'chat gateway unconfigured (NO_ANTHROPIC_API_KEY)',
);
}
// Best-effort cost block [E5/m3]: actual tokens when the gateway reported
// usage, priced via the canonical table; nulls when accounting is absent.
const { canonicalLookup } = await import('./model-pricing.ts');
const usage = result.usage ?? null;
const pricing = canonicalLookup(result.modelUsed);
const usdEstimate =
usage && pricing
? (usage.input_tokens * pricing.input + usage.output_tokens * pricing.output) / 1_000_000
: null;
return {
answer: result.answer,
sources: result.citations.map(c => c.page_slug),
gaps: result.gaps,
cost: {
model: result.modelUsed,
input_tokens: usage?.input_tokens ?? null,
output_tokens: usage?.output_tokens ?? null,
usd_estimate: usdEstimate,
},
protocol_version: MEMORY_VERBS_VERSION,
};
},
cliHints: { name: 'synthesize', positional: ['question'] },
};
// ─── forget ──────────────────────────────────────────────────────────────────
const forget: Operation = {
name: 'forget',
description:
'MEMORY VERB (v1): expire a remembered fact by id — the protocol delete verb. ' +
'`id` is the opaque string id returned by remember and recall (facts[].fact_id) — never a page slug. ' +
'Idempotent: forgetting an already-expired fact returns expired:false (success), unknown id returns a not_found error. ' +
'The fact is expired (audit trail kept), not deleted.',
params: {
id: { type: 'string', required: true, description: 'Opaque fact id from remember/recall (facts[].fact_id). Never a page slug.' },
reason: { type: 'string', description: 'Optional reason, written to the fact\'s audit trail. Default: "forgotten".' },
},
mutating: true,
scope: 'write',
verb: true,
annotations: { title: 'forget (expire a fact)', destructiveHint: true, idempotentHint: true },
handler: async (ctx, p) => {
const { verbError } = await import('./operations.ts');
const rawId = typeof p.id === 'string' ? p.id.trim() : typeof p.id === 'number' ? String(p.id) : '';
const numericId = Number(rawId);
if (!rawId || !Number.isInteger(numericId) || numericId <= 0) {
throw verbError(
'not_found',
`No fact with id "${String(p.id)}".`,
'Pass the opaque string id returned by remember or recall (facts[].fact_id) — page slugs are not fact ids.',
);
}
const reason = typeof p.reason === 'string' && p.reason.trim() ? p.reason.trim() : null;
if (ctx.dryRun) {
return { dry_run: true, action: 'forget', id: rawId, protocol_version: MEMORY_VERBS_VERSION };
}
const { forgetFactInFence } = await import('./facts/forget.ts');
// [ship P1.1] trust boundary: scope the forget to the caller's source, and
// for remote callers to world-visible facts only — a guessed global id
// can't expire facts outside the caller's source or reach private facts.
const result = await forgetFactInFence(ctx.engine, numericId, {
...(reason ? { reason } : {}),
sourceId: ctx.sourceId ?? 'default',
worldOnly: ctx.remote !== false,
});
if (!result.ok && result.path === 'not_found') {
throw verbError(
'not_found',
`No fact with id "${rawId}".`,
'Ids come from remember/recall (facts[].fact_id). recall the entity first to find the right fact.',
);
}
if (!result.ok && result.path === 'already_expired') {
// Idempotent re-forget: success, nothing changed.
return {
id: rawId,
expired: false,
reason,
protocol_version: MEMORY_VERBS_VERSION,
};
}
return {
id: rawId,
expired: true,
reason,
protocol_version: MEMORY_VERBS_VERSION,
};
},
// NO cliHints: `gbrain forget` is a CLI_ONLY command (recall.ts runForget)
// that dispatches BEFORE cliOps — a cliHint here would be silently
// shadowed. The CLI surface for forgetting stays the existing command;
// this verb is the MCP/protocol surface.
};
export const verbOperations: Operation[] = [remember, entity, synthesize, forget];
// ─── RESPONSE_SCHEMAS — the protocol's response-shape registry [c8] ─────────
//
// `Operation` carries input params only; response envelopes live HERE, hand-
// authored, and conformance validates LIVE responses against this registry so
// registry-vs-code drift is caught by the same fixtures that certify servers.
// Field names and semantics are FROZEN (additive-forever); enum values are
// part of the contract.
const EVIDENCE_ENUM = ['alias_hit', 'exact_title_match', 'high_vector_match', 'keyword_exact', 'weak_semantic'];
const CREATE_SAFETY_ENUM = ['exists', 'probable', 'unknown'];
const STATUS_ENUM = ['inserted', 'duplicate', 'superseded'];
export const RESPONSE_SCHEMAS: Record<VerbName, Record<string, unknown>> = {
recall: {
type: 'object',
required: ['facts', 'total', 'protocol_version'],
properties: {
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
total: { type: 'integer' },
facts: {
type: 'array',
items: {
type: 'object',
required: ['id', 'fact', 'kind', 'fact_id', 'provenance'],
properties: {
id: { type: 'integer', description: 'LEGACY numeric id (pre-v1 consumers). Use fact_id.' },
fact_id: { type: 'string', description: 'Opaque protocol id — the value forget accepts.' },
fact: { type: 'string' },
kind: { type: 'string', enum: FACT_KINDS as unknown as string[] },
entity_slug: { type: ['string', 'null'] },
provenance: { type: 'string' },
valid_until: { type: ['string', 'null'] },
visibility: { type: 'string', enum: ['private', 'world'] },
},
},
},
results: {
type: 'array',
description: 'Search arm — present only when `query` was passed.',
items: {
type: 'object',
required: ['slug', 'title', 'evidence', 'create_safety', 'provenance'],
properties: {
slug: { type: 'string' },
title: { type: ['string', 'null'] },
chunk: { type: ['string', 'null'] },
evidence: { type: 'string', enum: EVIDENCE_ENUM },
create_safety: { type: 'string', enum: CREATE_SAFETY_ENUM },
provenance: { type: 'string', description: 'Origin page slug.' },
},
},
},
search_degraded: { type: 'string', description: 'Present when the search arm fell back to keyword-only (no embedding provider).' },
budget_tokens: { type: 'integer', description: 'Present when budget_tokens was passed.' },
budget_used: { type: 'integer' },
dropped_count: { type: 'integer' },
},
},
remember: {
type: 'object',
required: ['id', 'status', 'status_text', 'entity_slug', 'valid_until', 'protocol_version'],
properties: {
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
id: { type: 'string', description: 'Opaque fact id. On status=duplicate this is the EXISTING fact\'s id.' },
status: { type: 'string', enum: STATUS_ENUM, description: 'Branch on THIS, never on status_text.' },
status_text: { type: 'string', description: 'Human rendering of status. Display only — never branch on it.' },
entity_slug: { type: ['string', 'null'] },
valid_until: { type: ['string', 'null'], description: 'ISO 8601 or null (never expires).' },
degraded_dedup: { type: 'boolean', description: 'Present (true) when no embedding provider — near-duplicates may insert.' },
},
},
entity: {
type: 'object',
required: ['protocol_version', 'found', 'latency_ms'],
properties: {
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
found: { type: 'boolean' },
latency_ms: { type: 'integer' },
card: {
type: 'object',
required: ['entity', 'aka', 'summary', 'last_touched', 'open_threads', 'edges', 'backlink_count', 'active_fact_count'],
properties: {
entity: {
type: 'object',
required: ['slug', 'title', 'type'],
properties: { slug: { type: 'string' }, title: { type: 'string' }, type: { type: ['string', 'null'] } },
},
aka: { type: 'array', items: { type: 'string' } },
summary: { type: 'string' },
last_touched: {
type: 'object',
required: ['updated_at', 'last_retrieved_at', 'last_timeline_date'],
properties: {
updated_at: { type: ['string', 'null'] },
last_retrieved_at: { type: ['string', 'null'] },
last_timeline_date: { type: ['string', 'null'] },
},
},
open_threads: {
type: 'array',
items: {
type: 'object',
required: ['kind', 'text', 'date'],
properties: {
kind: { type: 'string', enum: ['commitment', 'recent_event'] },
text: { type: 'string' },
date: { type: ['string', 'null'] },
},
},
},
edges: {
type: 'array',
items: {
type: 'object',
required: ['type', 'direction', 'slug'],
properties: {
type: { type: 'string' },
direction: { type: 'string', enum: ['out', 'in'] },
slug: { type: 'string' },
context: { type: ['string', 'null'] },
},
},
},
backlink_count: { type: 'integer' },
active_fact_count: { type: 'integer' },
},
},
suggestions: {
type: 'array',
items: {
type: 'object',
required: ['slug', 'title', 'create_safety'],
properties: {
slug: { type: 'string' },
title: { type: 'string' },
create_safety: { type: 'string', enum: CREATE_SAFETY_ENUM },
},
},
},
},
},
synthesize: {
type: 'object',
required: ['answer', 'sources', 'cost', 'protocol_version'],
properties: {
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
answer: { type: 'string' },
sources: { type: 'array', items: { type: 'string' } },
gaps: { type: 'array', items: { type: 'string' } },
cost: {
type: 'object',
required: ['model', 'input_tokens', 'output_tokens', 'usd_estimate'],
description: 'Best-effort aggregate (retries/multi-call flows sum; cache hits may undercount). Honest signal, not an invoice.',
properties: {
model: { type: 'string' },
input_tokens: { type: ['integer', 'null'] },
output_tokens: { type: ['integer', 'null'] },
usd_estimate: { type: ['number', 'null'] },
},
},
},
},
forget: {
type: 'object',
required: ['id', 'expired', 'reason', 'protocol_version'],
properties: {
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
id: { type: 'string' },
expired: { type: 'boolean', description: 'true = this call expired the fact; false = it was ALREADY expired (idempotent re-forget).' },
reason: { type: ['string', 'null'] },
},
},
};
/** Error envelope schema (uniform across all five verbs). */
export const ERROR_SCHEMA: Record<string, unknown> = {
type: 'object',
required: ['error', 'message'],
properties: {
error: {
type: 'string',
enum: [
'invalid_params',
'provenance_required',
'not_found',
'scope_denied',
'unavailable',
'budget_unsatisfiable', // RESERVED — schema-listed, never returned in v1
'internal',
],
},
message: { type: 'string' },
suggestion: { type: 'string', description: 'Populated on every verb error: problem + cause + fix.' },
detail: { type: 'string', description: 'Freeform specifics (e.g. which dependency failed).' },
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
},
};
+226
View File
@@ -0,0 +1,226 @@
/**
* MEMORY_VERBS v1 conformance fixtures (E2).
*
* The canonical, embedded fixture set the conformance runner executes against
* ANY MCP endpoint (default: gbrain's own server). Deterministic by
* construction: every check is SHAPE (required fields, enum validity, const),
* CONTRACT BEHAVIOR (provenance rejected, idempotent forget, missfound:false),
* or ROUND-TRIP (remember recall by entity, a plain indexed read no
* embeddings, no ranking judgment). Ranking quality is BrainBench's job.
*
* These double as BrainBench seeds: `test/fixtures/memory-verbs/cases.json` is
* the generated data-file mirror (drift-guarded by the conformance test) so
* the eval suite imports the same cases without importing gbrain source.
*
* Stateful round-trips share a per-run marker (substituted for `{{marker}}`)
* and thread ids via `saveAs`/`{{id:<key>}}` substitution.
*/
export interface ConformanceCase {
name: string;
verb: 'recall' | 'remember' | 'entity' | 'synthesize' | 'forget';
/** `{{marker}}` and `{{id:<key>}}` substitute at run time. */
params: Record<string, unknown>;
/** Validate the (parsed) response body against RESPONSE_SCHEMAS[verb]. */
validateSchema?: boolean;
/** Expect an isError response with this protocol code. */
expectErrorCode?: string;
/** On error responses: suggestion must be non-empty (F-D mandate). */
expectSuggestion?: boolean;
/** Field equality/predicate checks on the parsed response body. */
expect?: Array<
| { path: string; equals: unknown }
| { path: string; oneOf: unknown[] }
| { path: string; type: 'string' | 'number' | 'boolean' | 'array' | 'object' }
| { path: string; gte: number }
| { path: string; lte: number }
| { path: string; nonEmptyString: true }
| { path: string; absentOrNotContains: string }
>;
/** Save a response field for later cases ({{id:<key>}}). */
saveAs?: { key: string; path: string };
/** Only run when the runner was invoked with --synthesize (cost gate). */
requiresSynthesizeFlag?: boolean;
/**
* Only run when the runner could seed the conformance entity PAGE (via
* put_page when the target exposes it full surface). entity() resolves
* pages; on a verbs-only target with no seeding path these cases skip
* honestly instead of failing on a structurally absent page.
*/
requiresSeededEntity?: boolean;
}
export const CONFORMANCE_CASES: ConformanceCase[] = [
// ── remember: contract behavior ─────────────────────────────────────────
{
name: 'remember rejects missing provenance (provenance_required + suggestion)',
verb: 'remember',
params: { fact: 'conformance {{marker}} fact without provenance' },
expectErrorCode: 'invalid_params', // MCP-level required-param rejection
expectSuggestion: false, // transport-level validation may not carry one
},
{
name: 'remember rejects empty provenance (provenance_required + suggestion)',
verb: 'remember',
params: { fact: 'conformance {{marker}} fact empty provenance', provenance: ' ' },
expectErrorCode: 'provenance_required',
expectSuggestion: true,
},
{
name: 'remember rejects ISO-8601 duration ttl with a fix (P30D trap)',
verb: 'remember',
params: { fact: 'conformance {{marker}} ttl trap', provenance: 'conformance run', ttl: 'P30D' },
expectErrorCode: 'invalid_params',
expectSuggestion: true,
},
{
name: 'remember writes a fact (string id, enum status, echoed nulls)',
verb: 'remember',
params: {
fact: 'conformance {{marker}}: the protocol round-trip fact',
provenance: 'conformance run {{marker}}',
entity: 'people/conformance-{{marker}}',
kind: 'fact',
},
validateSchema: true,
expect: [
{ path: 'id', type: 'string' },
{ path: 'status', oneOf: ['inserted', 'duplicate', 'superseded'] },
{ path: 'protocol_version', equals: 1 },
],
saveAs: { key: 'fact1', path: 'id' },
},
{
name: 'remember with ttl returns ISO valid_until',
verb: 'remember',
params: {
fact: 'conformance {{marker}}: expiring fact',
provenance: 'conformance run {{marker}}',
entity: 'people/conformance-{{marker}}',
ttl: '30d',
},
validateSchema: true,
expect: [{ path: 'valid_until', type: 'string' }],
saveAs: { key: 'fact2', path: 'id' },
},
{
name: 'remember private fact (fence test setup)',
verb: 'remember',
params: {
fact: 'conformance {{marker}} PRIVATE-SENTINEL commitment',
provenance: 'conformance run {{marker}}',
entity: 'people/conformance-{{marker}}',
kind: 'commitment',
visibility: 'private',
},
validateSchema: true,
},
// ── recall: round-trip + superset + budget ──────────────────────────────
{
name: 'recall by entity round-trips the remembered fact (superset envelope)',
verb: 'recall',
params: { entity: 'people/conformance-{{marker}}' },
validateSchema: true,
expect: [
{ path: 'protocol_version', equals: 1 },
{ path: 'total', gte: 1 },
{ path: 'facts.0.fact_id', type: 'string' },
{ path: 'facts.0.provenance', type: 'string' },
],
},
{
name: 'recall with budget reports consistent budget meta',
verb: 'recall',
params: { entity: 'people/conformance-{{marker}}', budget_tokens: 10000 },
validateSchema: true,
expect: [
{ path: 'budget_tokens', equals: 10000 },
{ path: 'budget_used', gte: 0 },
{ path: 'budget_used', lte: 10000 },
{ path: 'dropped_count', gte: 0 },
],
},
{
name: 'recall with budget smaller than the first item drops everything',
verb: 'recall',
params: { entity: 'people/conformance-{{marker}}', budget_tokens: 1 },
validateSchema: true,
expect: [
{ path: 'total', equals: 0 },
{ path: 'dropped_count', gte: 1 },
],
},
{
name: 'recall with query returns the search arm (degradation allowed, never an error)',
verb: 'recall',
params: { query: 'conformance {{marker}} protocol round-trip', budget_tokens: 8000 },
validateSchema: true,
expect: [{ path: 'results', type: 'array' }],
},
// ── entity: hit, miss, privacy fence ────────────────────────────────────
{
name: 'entity resolves the round-trip entity to a card',
verb: 'entity',
params: { name: 'people/conformance-{{marker}}' },
validateSchema: true,
requiresSeededEntity: true,
expect: [
{ path: 'found', equals: true },
{ path: 'protocol_version', equals: 1 },
{ path: 'card.entity.slug', type: 'string' },
{ path: 'card.backlink_count', gte: 0 },
],
},
{
name: 'entity miss returns found:false + suggestions (never an error)',
verb: 'entity',
params: { name: 'zzz-no-such-entity-{{marker}}' },
validateSchema: true,
expect: [
{ path: 'found', equals: false },
{ path: 'suggestions', type: 'array' },
],
},
{
name: 'entity card never leaks private facts to remote callers (fence test)',
verb: 'entity',
params: { name: 'people/conformance-{{marker}}' },
requiresSeededEntity: true,
expect: [{ path: 'card.open_threads', absentOrNotContains: 'PRIVATE-SENTINEL' }],
},
// ── forget: idempotency + not_found ─────────────────────────────────────
{
name: 'forget expires the remembered fact',
verb: 'forget',
params: { id: '{{id:fact2}}', reason: 'conformance cleanup' },
validateSchema: true,
expect: [{ path: 'expired', equals: true }],
},
{
name: 'forget again is idempotent (expired:false, success)',
verb: 'forget',
params: { id: '{{id:fact2}}' },
validateSchema: true,
expect: [{ path: 'expired', equals: false }],
},
{
name: 'forget unknown id returns not_found with a suggestion',
verb: 'forget',
params: { id: '999999999' },
expectErrorCode: 'not_found',
expectSuggestion: true,
},
// ── synthesize: cost-gated live call ────────────────────────────────────
{
name: 'synthesize answers (or reports unavailable) — cost-gated, pass --synthesize',
verb: 'synthesize',
params: { question: 'What do we know about conformance {{marker}}?' },
requiresSynthesizeFlag: true,
// Either a schema-valid answer (key configured) or a clean unavailable
// error (no key). The runner accepts both; anything else fails.
},
];
+269
View File
@@ -0,0 +1,269 @@
/**
* MEMORY_VERBS v1 conformance runner core (E2).
*
* Executes the embedded fixture set (conformance-fixtures.ts) against any MCP
* endpoint via a minimal client surface (list_tools + call_tool only) and
* returns a pass/fail table. Transport-agnostic: the CLI wraps it with stdio
* spawn / HTTP transports; the negative self-test [F3] feeds it a mutated
* in-process double.
*
* Validation is deliberately NON-STRICT on extra fields the contract is
* additive-forever, so unknown fields are always legal. A certifier that
* rejected additions would break the versioning policy it certifies.
*/
import { RESPONSE_SCHEMAS, ERROR_SCHEMA, MEMORY_VERBS_VERSION, type VerbName } from '../verbs.ts';
import { CONFORMANCE_CASES, type ConformanceCase } from './conformance-fixtures.ts';
export interface ConformanceClient {
listTools(): Promise<Array<{ name: string; description?: string; annotations?: unknown }>>;
callTool(name: string, args: Record<string, unknown>): Promise<{ isError?: boolean; text: string }>;
}
export interface CaseResult {
name: string;
verb: string;
status: 'pass' | 'fail' | 'skip';
detail: string;
}
export interface ConformanceReport {
protocol_version: number;
results: CaseResult[];
passed: number;
failed: number;
skipped: number;
ok: boolean;
}
/**
* Minimal JSON-Schema-subset validator: type (incl. union arrays + integer),
* required, properties (NON-strict), enum, const, items. Returns violations
* as "path: problem" strings.
*/
export function validateAgainstSchema(
value: unknown,
schema: Record<string, unknown>,
path = '$',
): string[] {
const out: string[] = [];
const t = schema.type as string | string[] | undefined;
if (t !== undefined) {
const types = Array.isArray(t) ? t : [t];
if (!types.some(ty => matchesType(value, ty))) {
out.push(`${path}: expected ${types.join('|')}, got ${describe(value)}`);
return out; // structural mismatch — deeper checks are noise
}
}
if (schema.const !== undefined && value !== schema.const) {
out.push(`${path}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
}
if (Array.isArray(schema.enum) && value !== null && !schema.enum.includes(value)) {
out.push(`${path}: ${JSON.stringify(value)} not in enum [${(schema.enum as unknown[]).join(', ')}]`);
}
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
const obj = value as Record<string, unknown>;
for (const req of (schema.required as string[] | undefined) ?? []) {
if (!(req in obj)) out.push(`${path}.${req}: required field missing`);
}
const props = (schema.properties as Record<string, Record<string, unknown>> | undefined) ?? {};
for (const [k, sub] of Object.entries(props)) {
if (k in obj && obj[k] !== undefined) out.push(...validateAgainstSchema(obj[k], sub, `${path}.${k}`));
}
}
if (Array.isArray(value) && schema.items && typeof schema.items === 'object') {
value.forEach((item, i) =>
out.push(...validateAgainstSchema(item, schema.items as Record<string, unknown>, `${path}[${i}]`)),
);
}
return out;
}
function matchesType(v: unknown, t: string): boolean {
switch (t) {
case 'string': return typeof v === 'string';
case 'integer': return typeof v === 'number' && Number.isInteger(v);
case 'number': return typeof v === 'number';
case 'boolean': return typeof v === 'boolean';
case 'array': return Array.isArray(v);
case 'object': return v !== null && typeof v === 'object' && !Array.isArray(v);
case 'null': return v === null;
default: return false;
}
}
function describe(v: unknown): string {
if (v === null) return 'null';
if (Array.isArray(v)) return 'array';
return typeof v;
}
function getPath(obj: unknown, path: string): unknown {
let cur: unknown = obj;
for (const seg of path.split('.')) {
if (cur === null || cur === undefined) return undefined;
cur = (cur as Record<string, unknown>)[seg];
}
return cur;
}
function substitute(value: unknown, marker: string, ids: Map<string, string>): unknown {
if (typeof value === 'string') {
let s = value.replaceAll('{{marker}}', marker);
s = s.replace(/\{\{id:([a-z0-9_-]+)\}\}/gi, (_, key) => ids.get(key) ?? `MISSING-ID-${key}`);
return s;
}
if (Array.isArray(value)) return value.map(v => substitute(v, marker, ids));
if (value !== null && typeof value === 'object') {
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([k, v]) => [k, substitute(v, marker, ids)]));
}
return value;
}
function runChecks(body: unknown, checks: NonNullable<ConformanceCase['expect']>): string[] {
const problems: string[] = [];
for (const c of checks) {
const v = getPath(body, c.path);
if ('equals' in c) {
if (v !== c.equals) problems.push(`${c.path}: expected ${JSON.stringify(c.equals)}, got ${JSON.stringify(v)}`);
} else if ('oneOf' in c) {
if (!c.oneOf.includes(v)) problems.push(`${c.path}: ${JSON.stringify(v)} not in ${JSON.stringify(c.oneOf)}`);
} else if ('type' in c) {
if (!matchesType(v, c.type)) problems.push(`${c.path}: expected ${c.type}, got ${describe(v)}`);
} else if ('gte' in c) {
if (typeof v !== 'number' || v < c.gte) problems.push(`${c.path}: expected >= ${c.gte}, got ${JSON.stringify(v)}`);
} else if ('lte' in c) {
if (typeof v !== 'number' || v > c.lte) problems.push(`${c.path}: expected <= ${c.lte}, got ${JSON.stringify(v)}`);
} else if ('nonEmptyString' in c) {
if (typeof v !== 'string' || !v.trim()) problems.push(`${c.path}: expected non-empty string`);
} else if ('absentOrNotContains' in c) {
const json = v === undefined ? '' : JSON.stringify(v);
if (json.includes(c.absentOrNotContains)) problems.push(`${c.path}: must not contain "${c.absentOrNotContains}"`);
}
}
return problems;
}
export async function runConformance(
client: ConformanceClient,
opts: { marker?: string; synthesize?: boolean } = {},
): Promise<ConformanceReport> {
const marker = opts.marker ?? `run-${Date.now().toString(36)}`;
const ids = new Map<string, string>();
const results: CaseResult[] = [];
// Seed the conformance entity PAGE when the target exposes put_page (full
// surface). entity() resolves pages; verbs-only targets with no seeding
// path skip the entity-hit cases honestly (requiresSeededEntity).
let seededEntity = false;
try {
const seed = await client.callTool('put_page', {
slug: `people/conformance-${marker}`,
content: `---\ntitle: Conformance ${marker}\ntype: person\n---\n\n# Conformance ${marker}\n\nSynthetic entity for a MEMORY_VERBS conformance run.\n`,
});
seededEntity = !seed.isError;
} catch {
seededEntity = false;
}
// List-level checks first: the five verbs are advertised, synthesize is
// marked expensive (description prefix is the load-bearing channel).
try {
const tools = await client.listTools();
const byName = new Map(tools.map(t => [t.name, t]));
for (const verb of Object.keys(RESPONSE_SCHEMAS) as VerbName[]) {
results.push(
byName.has(verb)
? { name: `tools/list advertises ${verb}`, verb, status: 'pass', detail: '' }
: { name: `tools/list advertises ${verb}`, verb, status: 'fail', detail: 'not advertised' },
);
}
const synth = byName.get('synthesize');
const marked = !!synth?.description?.startsWith('[EXPENSIVE');
results.push({
name: 'synthesize is marked expensive ([EXPENSIVE prefix)',
verb: 'synthesize',
status: marked ? 'pass' : 'fail',
detail: marked ? '' : `description starts: "${synth?.description?.slice(0, 40) ?? '(missing)'}..."`,
});
} catch (e) {
results.push({
name: 'tools/list',
verb: '-',
status: 'fail',
detail: e instanceof Error ? e.message : String(e),
});
}
for (const c of CONFORMANCE_CASES) {
if (c.requiresSynthesizeFlag && !opts.synthesize) {
results.push({ name: c.name, verb: c.verb, status: 'skip', detail: 'costs money — pass --synthesize' });
continue;
}
if (c.requiresSeededEntity && !seededEntity) {
results.push({ name: c.name, verb: c.verb, status: 'skip', detail: 'target has no put_page to seed the entity page (verbs-only surface)' });
continue;
}
const params = substitute(c.params, marker, ids) as Record<string, unknown>;
let res: { isError?: boolean; text: string };
try {
res = await client.callTool(c.verb, params);
} catch (e) {
results.push({ name: c.name, verb: c.verb, status: 'fail', detail: `transport: ${e instanceof Error ? e.message : String(e)}` });
continue;
}
let body: unknown;
try {
body = JSON.parse(res.text);
} catch {
results.push({ name: c.name, verb: c.verb, status: 'fail', detail: `response not JSON: ${res.text.slice(0, 120)}` });
continue;
}
const problems: string[] = [];
if (c.verb === 'synthesize' && c.requiresSynthesizeFlag) {
// Accept either a schema-valid answer (key configured) or a clean
// `unavailable` protocol error (no key). Anything else fails.
if (res.isError) {
const err = body as { error?: string; suggestion?: string };
if (err.error !== 'unavailable') problems.push(`expected unavailable, got error=${err.error}`);
if (!err.suggestion?.trim()) problems.push('error.suggestion empty');
problems.push(...validateAgainstSchema(body, ERROR_SCHEMA, '$err'));
} else {
problems.push(...validateAgainstSchema(body, RESPONSE_SCHEMAS.synthesize));
}
} else if (c.expectErrorCode) {
if (!res.isError) {
problems.push(`expected error ${c.expectErrorCode}, got success`);
} else {
const err = body as { error?: string; suggestion?: string; protocol_version?: number };
if (err.error !== c.expectErrorCode) problems.push(`expected error=${c.expectErrorCode}, got ${err.error}`);
if (c.expectSuggestion && !err.suggestion?.trim()) problems.push('error.suggestion empty (F-D mandate)');
}
} else {
if (res.isError) {
problems.push(`unexpected error: ${res.text.slice(0, 160)}`);
} else {
if (c.validateSchema) problems.push(...validateAgainstSchema(body, RESPONSE_SCHEMAS[c.verb]));
if (c.expect) problems.push(...runChecks(body, c.expect));
if (c.saveAs) {
const v = getPath(body, c.saveAs.path);
if (typeof v === 'string') ids.set(c.saveAs.key, v);
else problems.push(`saveAs ${c.saveAs.path}: expected string id, got ${describe(v)}`);
}
}
}
results.push(
problems.length === 0
? { name: c.name, verb: c.verb, status: 'pass', detail: '' }
: { name: c.name, verb: c.verb, status: 'fail', detail: problems.join('; ') },
);
}
const passed = results.filter(r => r.status === 'pass').length;
const failed = results.filter(r => r.status === 'fail').length;
const skipped = results.filter(r => r.status === 'skip').length;
return { protocol_version: MEMORY_VERBS_VERSION, results, passed, failed, skipped, ok: failed === 0 };
}
+348
View File
@@ -0,0 +1,348 @@
/**
* MEMORY_VERBS v1 `entity(name)` card builder (zero LLM, p99 < 100ms).
*
* Resolves a free-text name to ONE brain page via the Retrieval Reflex's
* precision-biased arms (alias-first, then exact-title / exact-slug /
* slug-suffix), then assembles a compact self-describing card from parallel
* depth-1 indexed reads. Deliberately NOT the recursive-CTE traversal
* (traversePaths) the card is a latency contract, not a graph walk.
*
* Resolution precedence (frozen): alias > exact title > slug-suffix; ties
* break on GREATEST(updated_at, last_retrieved_at) "last_touched" is the
* card's OUTPUT name, not a column. Multi-hit best match wins, runners-up
* land in `suggestions`. Miss `found: false` + keyword near-misses with
* create_safety hints. NEVER throws for data reasons; each arm is guarded so
* a pre-page_aliases brain still resolves via arm 2 (same posture as the
* shipped reflex).
*
* Privacy: `summary` runs through safeSynopsis (the get_page fence boundary);
* facts respect visibility for remote callers (world-only).
*/
import type { BrainEngine, FactRow } from '../engine.ts';
import { normalizeAlias } from '../search/alias-normalize.ts';
import { slugify } from '../entities/resolve.ts';
import { safeSynopsis } from '../context/retrieval-reflex.ts';
import { stampEvidence } from '../search/evidence.ts';
import type { SearchResult } from '../types.ts';
const EDGE_CAP = 10;
const OPEN_THREADS_CAP = 3;
const OPEN_THREAD_TIMELINE_WINDOW_DAYS = 90;
const SUGGESTION_CAP = 3;
const FACT_FETCH_CAP = 100;
export interface EntityCardEdge {
type: string;
direction: 'out' | 'in';
slug: string;
context: string | null;
}
export interface EntityOpenThread {
kind: 'commitment' | 'recent_event';
text: string;
date: string | null;
}
export interface EntityCard {
entity: { slug: string; title: string; type: string | null };
/** page_aliases reverse lookup (normalized forms). Empty on pre-migration brains. */
aka: string[];
/** Privacy-safe synopsis — same fence boundary as get_page. */
summary: string;
last_touched: {
updated_at: string | null;
last_retrieved_at: string | null;
last_timeline_date: string | null;
};
/** Best-effort in v1: active commitment-kind facts + recent timeline entries. */
open_threads: EntityOpenThread[];
/** Top typed edges, mentions excluded, out-edges first. */
edges: EntityCardEdge[];
backlink_count: number;
/** Active facts about this entity (capped count; visibility-filtered for remote). */
active_fact_count: number;
}
export interface EntitySuggestion {
slug: string;
title: string;
create_safety: string;
}
export interface EntityCardResult {
found: boolean;
card?: EntityCard;
suggestions?: EntitySuggestion[];
}
interface CardPageRow {
slug: string;
// v0.43 merge: retrieval-reflex's exported PageRow (safeSynopsis's param)
// now requires source_id (federated push-context wave #2095). The card row
// carries it too so it remains assignable.
source_id: string;
title: string;
type: string | null;
frontmatter: Record<string, unknown> | null;
compiled_truth: string | null;
updated_at: Date | string | null;
last_retrieved_at: Date | string | null;
}
/** Resolution arm rank: lower = higher confidence (frozen precedence ladder). */
const ARM_ALIAS = 0;
const ARM_EXACT = 1;
const ARM_SUFFIX = 2;
export async function buildEntityCard(
engine: BrainEngine,
sourceId: string,
name: string,
opts: { remote: boolean },
): Promise<EntityCardResult> {
const trimmed = (name ?? '').trim();
if (!trimmed) return { found: false, suggestions: [] };
const norm = normalizeAlias(trimmed);
const titleLc = trimmed.toLowerCase();
// Two exact-slug candidates: the slugified form for free-text names AND the
// raw input — a caller passing an already-namespaced slug
// ("people/alice-example") must hit exactly (slugify flattens the slash).
const slug = slugify(trimmed);
const exactSlugs = [...new Set([slug, trimmed].filter(Boolean))];
// Candidate slugs with their best arm rank.
const rankBySlug = new Map<string, number>();
const consider = (s: string, rank: number) => {
if (!s) return;
const prev = rankBySlug.get(s);
if (prev === undefined || rank < prev) rankBySlug.set(s, rank);
};
// Arm 1 — alias-first. Guarded: pre-migration brains lack page_aliases.
if (norm) {
try {
const aliasMap = await engine.resolveAliases([norm], { sourceId });
for (const hit of aliasMap.get(norm) ?? []) consider(hit.slug, ARM_ALIAS);
} catch {
/* no page_aliases table — degrade to arm 2 [E3] */
}
}
// Arm 2 — exact title / exact slug / slug-suffix, with the columns the
// card's tie-break needs. Guarded like the reflex.
let rows: CardPageRow[] = [];
try {
rows = await engine.executeRaw<CardPageRow>(
`SELECT slug, source_id, title, type, frontmatter, compiled_truth, updated_at, last_retrieved_at
FROM pages
WHERE deleted_at IS NULL
AND source_id = $1
AND ( lower(title) = $2
OR slug = ANY($3::text[])
OR slug LIKE $4 )`,
[sourceId, titleLc, exactSlugs, `%/${slug || trimmed}`],
);
} catch {
rows = [];
}
const rowBySlug = new Map<string, CardPageRow>();
for (const r of rows) {
rowBySlug.set(r.slug, r);
const isExact = (r.title ?? '').toLowerCase() === titleLc || exactSlugs.includes(r.slug);
consider(r.slug, isExact ? ARM_EXACT : ARM_SUFFIX);
}
// Hydrate alias-resolved slugs that arm 2 didn't fetch.
const missing = [...rankBySlug.keys()].filter(s => !rowBySlug.has(s));
if (missing.length) {
try {
const extra = await engine.executeRaw<CardPageRow>(
`SELECT slug, source_id, title, type, frontmatter, compiled_truth, updated_at, last_retrieved_at
FROM pages
WHERE deleted_at IS NULL AND source_id = $1 AND slug = ANY($2::text[])`,
[sourceId, missing],
);
for (const r of extra) rowBySlug.set(r.slug, r);
} catch {
/* stale alias rows — drop */
}
}
// Rank candidates: arm rank asc, then GREATEST(updated_at, last_retrieved_at) desc.
const candidates = [...rankBySlug.entries()]
.map(([s, rank]) => ({ slug: s, rank, row: rowBySlug.get(s) }))
.filter((c): c is { slug: string; rank: number; row: CardPageRow } => c.row !== undefined)
.sort((a, b) => a.rank - b.rank || lastTouchedMs(b.row) - lastTouchedMs(a.row));
if (candidates.length === 0) {
return { found: false, suggestions: await nearMissSuggestions(engine, sourceId, trimmed) };
}
const best = candidates[0];
const runnersUp: EntitySuggestion[] = candidates.slice(1, 1 + SUGGESTION_CAP).map(c => ({
slug: c.slug,
title: c.row.title ?? c.slug,
// A page that resolved through the precision arms exists by definition.
create_safety: 'exists',
}));
const card = await assembleCard(engine, sourceId, best.row, opts.remote);
return {
found: true,
card,
...(runnersUp.length ? { suggestions: runnersUp } : {}),
};
}
async function assembleCard(
engine: BrainEngine,
sourceId: string,
row: CardPageRow,
remote: boolean,
): Promise<EntityCard> {
const pageSlug = row.slug;
const visibility = remote ? (['world'] as ('private' | 'world')[]) : undefined;
// Parallel depth-1 reads — every arm individually fail-soft so a partial
// brain (no aliases, no timeline) still returns a card.
//
// [ship P1.2] Incoming edges + backlink_count are SOURCE-SAFE on BOTH sides.
// engine.getBacklinks(slug,{sourceId}) only scopes the TARGET page's source,
// so a foreign-source page linking to a same-named entity would leak its
// slug; engine.getBacklinkCounts has no source param at all. We instead run
// a both-sides-scoped query here (f.source_id = t.source_id = this source),
// mentions excluded (matching the backlink-count convention). Outgoing edges
// (getLinks) are the entity's OWN declared links — from-side scoped — so they
// stay as-is.
const [aka, outLinks, inEdges, backlinkCount, timeline, facts] = await Promise.all([
engine
.executeRaw<{ alias_norm: string }>(
`SELECT alias_norm FROM page_aliases WHERE source_id = $1 AND slug = $2 ORDER BY alias_norm`,
[sourceId, pageSlug],
)
.then(rs => rs.map(r => r.alias_norm))
.catch(() => [] as string[]),
engine.getLinks(pageSlug, { sourceId }).catch(() => []),
engine
.executeRaw<{ from_slug: string; link_type: string; context: string | null }>(
`SELECT f.slug AS from_slug, l.link_type, l.context
FROM links l
JOIN pages f ON f.id = l.from_page_id
JOIN pages t ON t.id = l.to_page_id
WHERE t.slug = $1 AND t.source_id = $2 AND f.source_id = $2
AND COALESCE(l.link_source, '') <> 'mentions'`,
[pageSlug, sourceId],
)
.catch(() => [] as Array<{ from_slug: string; link_type: string; context: string | null }>),
engine
.executeRaw<{ n: string | number }>(
`SELECT COUNT(*) AS n
FROM links l
JOIN pages f ON f.id = l.from_page_id
JOIN pages t ON t.id = l.to_page_id
WHERE t.slug = $1 AND t.source_id = $2 AND f.source_id = $2
AND COALESCE(l.link_source, '') <> 'mentions'`,
[pageSlug, sourceId],
)
.then(rs => Number(rs[0]?.n ?? 0))
.catch(() => 0),
engine.getTimeline(pageSlug, { limit: 5, sourceId }).catch(() => []),
engine
.listFactsByEntity(sourceId, pageSlug, {
activeOnly: true,
limit: FACT_FETCH_CAP,
...(visibility ? { visibility } : {}),
})
.catch(() => [] as FactRow[]),
]);
const edges: EntityCardEdge[] = [];
for (const l of outLinks) {
if (l.link_source === 'mentions') continue;
edges.push({ type: l.link_type, direction: 'out', slug: l.to_slug, context: l.context || null });
if (edges.length >= EDGE_CAP) break;
}
if (edges.length < EDGE_CAP) {
for (const l of inEdges) {
edges.push({ type: l.link_type, direction: 'in', slug: l.from_slug, context: l.context || null });
if (edges.length >= EDGE_CAP) break;
}
}
// Open threads (best-effort v1): active commitments first, then recent
// timeline entries inside the window, capped together.
const openThreads: EntityOpenThread[] = [];
for (const f of facts) {
if (f.kind !== 'commitment') continue;
openThreads.push({ kind: 'commitment', text: f.fact, date: f.valid_from?.toISOString() ?? null });
if (openThreads.length >= OPEN_THREADS_CAP) break;
}
if (openThreads.length < OPEN_THREADS_CAP) {
const cutoff = Date.now() - OPEN_THREAD_TIMELINE_WINDOW_DAYS * 24 * 60 * 60 * 1000;
for (const t of timeline) {
const ts = Date.parse(t.date);
if (!Number.isFinite(ts) || ts < cutoff) continue;
openThreads.push({ kind: 'recent_event', text: t.summary, date: t.date });
if (openThreads.length >= OPEN_THREADS_CAP) break;
}
}
return {
entity: { slug: pageSlug, title: row.title ?? pageSlug, type: row.type ?? null },
aka,
summary: safeSynopsis(row),
last_touched: {
updated_at: toIso(row.updated_at),
last_retrieved_at: toIso(row.last_retrieved_at),
last_timeline_date: timeline.length ? timeline[0].date : null,
},
open_threads: openThreads,
edges,
backlink_count: backlinkCount,
active_fact_count: facts.length,
};
}
/**
* Near-miss suggestions on a total miss (E5 delight): keyword search top-N
* with evidence-derived create_safety so a typo'd name becomes a next move
* instead of a dead end. Zero LLM; fail-soft to [].
*/
async function nearMissSuggestions(
engine: BrainEngine,
sourceId: string,
name: string,
): Promise<EntitySuggestion[]> {
try {
const raw = await engine.searchKeyword(name, { limit: SUGGESTION_CAP, sourceId });
const results = raw as SearchResult[];
stampEvidence(results);
return results.map(r => ({
slug: r.slug,
title: r.title ?? r.slug,
create_safety: r.create_safety ?? 'unknown',
}));
} catch {
return [];
}
}
function lastTouchedMs(row: CardPageRow): number {
const u = toMs(row.updated_at);
const l = toMs(row.last_retrieved_at);
return Math.max(u, l);
}
function toMs(v: Date | string | null): number {
if (v == null) return 0;
const ms = v instanceof Date ? v.getTime() : Date.parse(v);
return Number.isFinite(ms) ? ms : 0;
}
function toIso(v: Date | string | null): string | null {
const ms = toMs(v);
return ms > 0 ? new Date(ms).toISOString() : null;
}
+124
View File
@@ -0,0 +1,124 @@
/**
* MEMORY_VERBS v1 per-verb usage sidecar (E4 observability).
*
* One JSONL line per verb call, written from the DISPATCH layer (so
* param-validation failures are counted too c11), fire-and-forget.
* LOCAL ONLY: this file never leaves the machine and is never uploaded
* it is observability, never source of truth. Stats tolerate loss.
*
* Concurrency: append-only, one line-buffered write() per event (<4KB
* atomic under POSIX O_APPEND; serve + jobs worker interleave safely at line
* granularity; best-effort on Windows, documented). Rotation at 10MB is
* lock-free best-effort a concurrent double-rotate can drop lines, which
* is acceptable for stats.
*
* Path: ~/.gbrain/integrations/memory-verbs/usage.jsonl via gbrainPath, so
* GBRAIN_HOME is honored and brain_id (the resolved gbrain home) is a true
* per-brain disambiguator [c11/m1].
*/
import { appendFile, mkdir, rename, stat, readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { gbrainPath } from '../config.ts';
const ROTATE_BYTES = 10 * 1024 * 1024;
export interface VerbUsageEvent {
ts: string;
verb: string;
surface: 'verbs' | 'full';
remote: boolean;
ok: boolean;
latency_ms: number;
brain_id: string;
source_id: string;
budget_dropped?: number;
entity_found?: boolean;
}
let _pathOverride: string | null = null;
/**
* Test-only seam: redirect the sidecar to a temp file without mutating
* process.env.GBRAIN_HOME (the test-isolation lint forbids global env
* mutation). Pass null to restore. @internal exported for tests.
*/
export function __setUsageLogPathForTests(path: string | null): void {
_pathOverride = path;
}
export function usageLogPath(): string {
return _pathOverride ?? gbrainPath('integrations', 'memory-verbs', 'usage.jsonl');
}
/** The resolved gbrain home — the per-brain disambiguator for multi-brain stats. */
export function brainId(): string {
return gbrainPath();
}
/**
* Fire-and-forget append. NEVER throws, NEVER blocks the verb call callers
* do not await this (dispatch invokes it without await).
*/
export function logVerbUsage(event: Omit<VerbUsageEvent, 'ts' | 'brain_id'>): void {
void (async () => {
try {
const path = usageLogPath();
await mkdir(dirname(path), { recursive: true });
// Best-effort lock-free rotation (stats-only; dropped lines tolerated).
try {
const s = await stat(path);
if (s.size > ROTATE_BYTES) {
await rename(path, join(dirname(path), 'usage.jsonl.1'));
}
} catch {
/* no file yet, or a concurrent rotate won — either is fine */
}
const line =
JSON.stringify({ ts: new Date().toISOString(), brain_id: brainId(), ...event }) + '\n';
await appendFile(path, line, 'utf-8');
} catch {
/* observability never breaks the verb call */
}
})();
}
/**
* Read events for `gbrain protocol stats` + the doctor check. Tolerates
* malformed lines (torn writes on non-POSIX appends) by skipping them.
*/
export async function readVerbUsage(opts: { days?: number } = {}): Promise<VerbUsageEvent[]> {
let raw: string;
try {
raw = await readFile(usageLogPath(), 'utf-8');
} catch {
return [];
}
const cutoff = opts.days ? Date.now() - opts.days * 24 * 60 * 60 * 1000 : null;
const events: VerbUsageEvent[] = [];
for (const line of raw.split('\n')) {
if (!line.trim()) continue;
try {
const e = JSON.parse(line) as VerbUsageEvent;
if (!e || typeof e.verb !== 'string' || typeof e.ts !== 'string') continue;
if (cutoff !== null) {
const ms = Date.parse(e.ts);
if (!Number.isFinite(ms) || ms < cutoff) continue;
}
events.push(e);
} catch {
/* torn line — skip */
}
}
return events;
}
/** Earliest event timestamp — the TTHW numerator for `protocol stats` [D6C]. */
export async function earliestVerbUsageTs(): Promise<string | null> {
const events = await readVerbUsage();
let earliest: string | null = null;
for (const e of events) {
if (earliest === null || e.ts < earliest) earliest = e.ts;
}
return earliest;
}
+76 -2
View File
@@ -10,6 +10,10 @@ import type { BrainEngine } from '../core/engine.ts';
import { operations, OperationError, enforceBoundClientOpAllowList } from '../core/operations.ts';
import type { Operation, OperationContext, AuthInfo } from '../core/operations.ts';
import { loadConfig } from '../core/config.ts';
import { VERB_NAMES, MEMORY_VERBS_VERSION } from '../core/verbs.ts';
import { logVerbUsage } from '../core/verbs/usage-log.ts';
const VERB_NAME_SET: ReadonlySet<string> = new Set(VERB_NAMES);
export interface ToolResult {
content: { type: 'text'; text: string }[];
@@ -83,6 +87,20 @@ export interface DispatchOpts {
* was replaced by dispatchToolCall.
*/
auth?: AuthInfo;
/**
* MEMORY_VERBS v1 surface enforcement [c2]. When set, a tool name outside
* the set returns the unknown_tool envelope BEFORE resolution fail-closed
* at the SHARED layer, so a hidden op stays uncallable on every transport
* even when only the tool LIST was filtered. Unset = full catalog
* (pre-existing behavior, all current callers).
*/
allowedOps?: ReadonlySet<string>;
/**
* Which surface this transport is serving recorded on the verb usage
* sidecar so adoption stats can split quickstart installs from full
* surfaces. Defaults to 'full'.
*/
surface?: 'verbs' | 'full';
}
/**
@@ -240,6 +258,33 @@ export async function dispatchToolCall(
params: Record<string, unknown> | undefined,
opts: DispatchOpts = {},
): Promise<ToolResult> {
const startedMs = Date.now();
const isVerb = VERB_NAME_SET.has(name);
// [c11] dispatch-layer usage sidecar for the five verbs — counts validation
// failures too. Fire-and-forget; never awaited, never throws.
const logVerb = (ok: boolean, extra?: { budget_dropped?: number; entity_found?: boolean }) => {
if (!isVerb) return;
logVerbUsage({
verb: name,
surface: opts.surface ?? 'full',
remote: opts.remote ?? true,
ok,
latency_ms: Date.now() - startedMs,
source_id: opts.sourceId ?? 'default',
...(extra ?? {}),
});
};
// [c2] surface enforcement at the SHARED layer: a hidden op is uncallable
// on every transport, not just unlisted. Same envelope as unknown ops so
// the surface doesn't leak which names exist.
if (opts.allowedOps && !opts.allowedOps.has(name)) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'unknown_tool', message: `Unknown tool: ${name}` }, null, 2) }],
isError: true,
};
}
const op = operations.find(o => o.name === name);
if (!op) {
// Always return JSON-shaped error content. v0.31 e2e tests
@@ -256,8 +301,19 @@ export async function dispatchToolCall(
const safeParams = params || {};
const validationError = validateParams(op, safeParams);
if (validationError) {
logVerb(false);
// [c7] verb validation errors speak the protocol envelope (suggestion +
// protocol_version); non-verb ops keep the pre-existing shape untouched.
const envelope = isVerb
? {
error: 'invalid_params',
message: validationError,
suggestion: 'Check the tool schema — required params and types are declared there.',
protocol_version: MEMORY_VERBS_VERSION,
}
: { error: 'invalid_params', message: validationError };
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'invalid_params', message: validationError }, null, 2) }],
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
isError: true,
};
}
@@ -286,6 +342,14 @@ export async function dispatchToolCall(
// a silent hole. See CLIENT_FENCED_WRITE_OPS in operations.ts.
enforceBoundClientOpAllowList(ctx.auth, op);
const result = await op.handler(ctx, safeParams);
// [E4] verb success metrics: budget drops + entity hit/miss when present.
{
const r = result as { dropped_count?: number; found?: boolean } | null;
logVerb(true, {
...(typeof r?.dropped_count === 'number' ? { budget_dropped: r.dropped_count } : {}),
...(name === 'entity' && typeof r?.found === 'boolean' ? { entity_found: r.found } : {}),
});
}
const out: ToolResult = { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
// v0.31 (eD3 + eE4): best-effort _meta.brain_hot_memory injection.
// The hook is wrapped in its own try/catch — any DB blip / cache miss /
@@ -302,6 +366,7 @@ export async function dispatchToolCall(
}
return out;
} catch (e: unknown) {
logVerb(false);
if (e instanceof OperationError) {
return { content: [{ type: 'text', text: JSON.stringify(e.toJSON(), null, 2) }], isError: true };
}
@@ -310,8 +375,17 @@ export async function dispatchToolCall(
// plain `Error: ${msg}` strings here, which broke any caller that
// tried JSON.parse(content).
const msg = e instanceof Error ? e.message : String(e);
// [c7] verbs speak the protocol envelope even for uncaught throws.
const envelope = isVerb
? {
error: 'internal',
message: msg,
suggestion: 'This is a server-side failure, not a caller mistake. Retry once; if it persists, run `gbrain doctor`.',
protocol_version: MEMORY_VERBS_VERSION,
}
: { error: 'internal_error', message: msg };
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'internal_error', message: msg }, null, 2) }],
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
isError: true,
};
}
+17 -1
View File
@@ -32,6 +32,7 @@ import { operations } from '../core/operations.ts';
import type { AuthInfo } from '../core/operations.ts';
import { VERSION } from '../version.ts';
import { dispatchToolCall } from './dispatch.ts';
import { filterOpsForSurface } from './surface.ts';
import { buildDefaultLimiters, type RateLimiter } from './rate-limit.ts';
import { sqlQueryForEngine } from '../core/sql-query.ts';
import { parseLegacyTokenScope, parseTakesHoldersAllowList, coerceLegacyPermissions } from '../core/legacy-token-scope.ts';
@@ -61,6 +62,12 @@ interface HttpTransportOptions {
engine: BrainEngine;
/** Override limiters (for tests). Defaults to env-driven buildDefaultLimiters. */
limiters?: { ip: RateLimiter; token: RateLimiter };
/**
* MEMORY_VERBS v1 [c1]: tool-surface mode for this transport (the SECOND
* HTTP path the OAuth path in serve-http.ts carries its own). 'verbs' =
* exactly the five protocol verbs; 'full' (default) = everything.
*/
surface?: 'verbs' | 'full';
}
interface AuthResult {
@@ -156,7 +163,13 @@ export async function startHttpTransport(opts: HttpTransportOptions) {
const limiters = opts.limiters || buildDefaultLimiters();
const bodyCap = envInt('GBRAIN_HTTP_MAX_BODY_BYTES', DEFAULT_BODY_CAP);
const corsAllowlist = parseCorsAllowlist();
const tools = buildToolDefs(operations);
// MEMORY_VERBS v1 [c1]: surface filter applies to THIS transport too —
// the advertised list AND dispatch (allowedOps), fail-closed.
const surface = opts.surface ?? 'full';
const surfacedOps = filterOpsForSurface(operations, surface);
const surfaceAllowedOps: ReadonlySet<string> | undefined =
surface === 'full' ? undefined : new Set(surfacedOps.map(o => o.name));
const tools = buildToolDefs(surfacedOps);
/**
* v0.41.3 (T6): single consolidated CORS header builder. Pre-fix there were
@@ -411,6 +424,9 @@ export async function startHttpTransport(opts: HttpTransportOptions) {
// #1336: thread the token's federated_read grant so read ops scope
// to the operator-granted sources via sourceScopeOpts.
auth: auth.auth,
// MEMORY_VERBS v1 [c1/c2]: fail-closed surface enforcement here too.
...(surfaceAllowedOps ? { allowedOps: surfaceAllowedOps } : {}),
surface,
});
const status = result.isError ? 'error' : 'success';
logRequest(auth.tokenName!, `tools/call:${toolName}`, status, Date.now() - startedMs);
+13 -2
View File
@@ -6,6 +6,7 @@ import { operations } from '../core/operations.ts';
import { VERSION } from '../version.ts';
import { buildToolDefs } from './tool-defs.ts';
import { dispatchToolCall, validateParams, buildOperationContext } from './dispatch.ts';
import { filterOpsForSurface, allowedOpNames, type McpSurface } from './surface.ts';
import { getBrainHotMemoryMeta } from '../core/facts/meta-hook.ts';
import { loadConfig } from '../core/config.ts';
import {
@@ -15,17 +16,24 @@ import {
} from '../core/context/resolve-ipc.ts';
import { resolveEntitiesToPointers, logDeliveredReflexPointers } from '../core/context/retrieval-reflex.ts';
export async function startMcpServer(engine: BrainEngine) {
export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpSurface } = {}) {
const server = new Server(
{ name: 'gbrain', version: VERSION },
{ capabilities: { tools: {} } },
);
// MEMORY_VERBS v1 surface mode: 'full' (default — every op, byte-identical
// to pre-surface behavior) or 'verbs' (exactly the 5 protocol verbs).
// Enforced BOTH on the advertised list and in dispatch (fail-closed [c2]).
const surface: McpSurface = opts.surface ?? 'full';
const surfacedOps = filterOpsForSurface(operations, surface);
const allowedOps = surface === 'full' ? undefined : allowedOpNames(operations, surface);
// Generate tool definitions from operations. Extracted to buildToolDefs so
// the subagent tool registry (v0.15+) can call the same mapper against a
// filtered OPERATIONS subset instead of duplicating this shape.
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: buildToolDefs(operations),
tools: buildToolDefs(surfacedOps),
}));
// Dispatch tool calls via shared dispatch.ts (parity with HTTP transport).
@@ -70,6 +78,9 @@ export async function startMcpServer(engine: BrainEngine) {
// Code see the brain's relevant hot memory automatically alongside
// every tool-call response. Best-effort; absorbs errors.
metaHook: getBrainHotMemoryMeta,
// MEMORY_VERBS v1: fail-closed surface enforcement + usage attribution.
...(allowedOps ? { allowedOps } : {}),
surface,
});
});
+62
View File
@@ -0,0 +1,62 @@
/**
* MEMORY_VERBS v1 MCP tool-surface modes (Cathedral 1).
*
* 'full' (default) every operation, verbs included. Existing installs
* see no change; e2e tool-count assertions hold.
* 'verbs' EXACTLY the five frozen protocol verbs (ops marked
* `verb: true`). The quickstart surface: agents see
* recall/remember/entity/synthesize/forget and nothing
* else.
*
* Enforcement is two-layer and fail-closed: ListTools advertises the filtered
* set, AND dispatchToolCall receives the same set as `allowedOps` so a hidden
* op stays uncallable even if a client guesses its name (tool-list filtering
* alone leaves dispatch resolving the global catalog codex c2).
*
* Resolution: --surface flag > config `mcp_surface` > 'full'. Why default
* full: verbs is for agents and quickstarts; full preserves existing advanced
* tooling.
*/
import type { Operation } from '../core/operations.ts';
import type { GBrainConfig } from '../core/config.ts';
export type McpSurface = 'verbs' | 'full';
export function isMcpSurface(v: unknown): v is McpSurface {
return v === 'verbs' || v === 'full';
}
/** Strict flag parser — unknown values reject loudly (parseStdioIdleTimeout pattern). */
export function parseSurfaceFlag(args: string[]): McpSurface | null {
const idx = args.indexOf('--surface');
if (idx < 0) return null;
const raw = args[idx + 1];
if (raw === undefined || raw.startsWith('--')) {
throw new Error(`--surface requires a value: verbs | full`);
}
if (!isMcpSurface(raw)) {
throw new Error(`Unknown --surface "${raw}". Use: verbs (the 5 memory verbs) | full (all operations, default)`);
}
return raw;
}
/** Flag > config `mcp_surface` > 'full'. */
export function resolveSurface(
flag: McpSurface | null,
config: Pick<GBrainConfig, 'mcp_surface'> | null | undefined,
): McpSurface {
if (flag) return flag;
if (config && isMcpSurface(config.mcp_surface)) return config.mcp_surface;
return 'full';
}
export function filterOpsForSurface(ops: Operation[], surface: McpSurface): Operation[] {
if (surface === 'full') return ops;
return ops.filter(op => op.verb === true);
}
/** The fail-closed allow-set handed to dispatchToolCall. */
export function allowedOpNames(ops: Operation[], surface: McpSurface): ReadonlySet<string> {
return new Set(filterOpsForSurface(ops, surface).map(o => o.name));
}
+12
View File
@@ -8,6 +8,17 @@ export interface McpToolDef {
properties: Record<string, unknown>;
required: string[];
};
/**
* MCP ToolAnnotations (SDK 1.29+), emitted ONLY when the op defines them
* existing tools keep byte-identical definitions (the byte-equality
* regression test depends on absent keys staying absent).
*/
annotations?: {
title?: string;
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
};
}
/**
@@ -50,5 +61,6 @@ export function buildToolDefs(ops: Operation[]): McpToolDef[] {
.filter(([, v]) => v.required)
.map(([k]) => k),
},
...(op.annotations ? { annotations: op.annotations } : {}),
}));
}
+2 -2
View File
@@ -108,7 +108,7 @@ describe('buildPlan — diff against completed + installed VERSION', () => {
// autopilot cooperative, v0.16.0 = subagent runtime, v0.18.0 = multi-
// source brains, v0.18.1 = RLS hardening, v0.21.0 = Cathedral II
// (renumbered from v0.20.0 after master shipped v0.20.x in parallel).
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.0', '0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2']);
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.0', '0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2', '0.43.0']);
});
test('already applied → v0.11.0 lands in `applied` bucket, not pending', () => {
@@ -148,7 +148,7 @@ describe('buildPlan — diff against completed + installed VERSION', () => {
// v0.22.4, v0.28.0, v0.29.1, v0.31.0 were added later; installed=0.12.0
// means they belong in skippedFuture, not pending. v0.11.0 and v0.12.0
// stay pending despite being ≤ installed — that is the H9 invariant.
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2']);
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2', '0.43.0']);
});
test('--migration filter narrows to one version', () => {
+68
View File
@@ -0,0 +1,68 @@
// #2416: CLI wiring for the concept-shaped search nudge. Same in-process
// pattern as cli-query-image.test.ts (import the helper from src/cli.ts —
// safe, the import.meta.main seam guards top-level execution). The helper is
// called from BOTH result paths (local engine + thin-client routed), so
// pinning its behavior here covers the shared wiring; the per-path call
// sites are two-liners.
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
import { maybePrintConceptNudge } from '../src/cli.ts';
import {
DEFAULT_CLI_OPTIONS,
setCliOptions,
_resetCliOptionsForTest,
} from '../src/core/cli-options.ts';
let stderrChunks: string[];
let originalWrite: typeof process.stderr.write;
beforeEach(() => {
stderrChunks = [];
originalWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = ((chunk: string | Uint8Array) => {
stderrChunks.push(String(chunk));
return true;
}) as typeof process.stderr.write;
});
afterEach(() => {
process.stderr.write = originalWrite;
_resetCliOptionsForTest();
});
const CONCEPT_QUERY = 'all the companies that do offshore wind';
describe('#2416 — maybePrintConceptNudge wiring', () => {
test('concept-shaped search emits the one-line query nudge to stderr', () => {
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
maybePrintConceptNudge('search', { query: CONCEPT_QUERY });
expect(stderrChunks.length).toBe(1);
expect(stderrChunks[0]).toContain('gbrain query');
expect(stderrChunks[0]).toContain('not proof of completeness');
expect(stderrChunks[0].endsWith('\n')).toBe(true);
});
test('exact-token search stays silent', () => {
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
maybePrintConceptNudge('search', { query: 'stripe' });
expect(stderrChunks.length).toBe(0);
});
test('--quiet silences the nudge even for concept-shaped queries', () => {
setCliOptions({ ...DEFAULT_CLI_OPTIONS, quiet: true });
maybePrintConceptNudge('search', { query: CONCEPT_QUERY });
expect(stderrChunks.length).toBe(0);
});
test('the query op never nudges (only search does)', () => {
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
maybePrintConceptNudge('query', { query: CONCEPT_QUERY });
expect(stderrChunks.length).toBe(0);
});
test('missing query param is a silent no-op, not a crash', () => {
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
maybePrintConceptNudge('search', {});
expect(stderrChunks.length).toBe(0);
});
});
+26
View File
@@ -158,3 +158,29 @@ describeIfKey('v0.29 — LLM routes personal queries to v0.29 ops, not query() /
}, 30_000);
}
});
// #2416 — concept/landscape questions must route to `query` (hybrid +
// expansion), not `search` (cheap-hybrid, expansion off). This block is the
// feature assertion for the #2416 description edits; the block above is the
// regression guard (the new "prefer query for concept questions" copy must
// NOT pull personal queries away from the salience ops).
const CONCEPT_QUERY_PHRASINGS = [
'find all the companies doing offshore wind',
'the landscape of agent memory startups',
'every investor that focuses on climate tech',
'all the projects that use vector databases',
'everything about my fundraising strategy discussions',
'which portfolio companies have shipped AI features',
'the ecosystem of MCP server implementations',
'all the people that work on developer tools',
];
describeIfKey('#2416 — LLM routes concept/landscape questions to query, not search', () => {
for (const prompt of CONCEPT_QUERY_PHRASINGS) {
test(`routes "${prompt}" to query`, async () => {
const { tool } = await callClaudeWithTools(prompt);
expect(tool).not.toBeNull();
expect(tool).toBe('query');
}, 30_000);
}
});
+180
View File
@@ -0,0 +1,180 @@
/**
* MEMORY_VERBS v1 entity() latency gate (Cathedral 1, frozen contract:
* p99 < 100ms on a large corpus, zero LLM).
*
* Corpus: 20K pages / 100K links / 30K aliases / 40K facts seeded via
* generate_series (pattern: entity-resolve-perf.slow.test.ts). 20 warmup +
* 200 measured buildEntityCard calls over a mixed name set exercising all
* three resolution arms (alias hit / exact title / slug-suffix) + misses.
*
* Two gates:
* 1. HARD ABSOLUTE p99 < 100ms × GBRAIN_PERF_BUDGET_MULTIPLIER (default 1;
* loosen in CI only with evidence of runner noise). The protocol DOC
* promises this number; the bound is op-layer latency (transport
* excluded, as documented).
* 2. RATIO GUARD (machine-independent) entity p99 50× max(getPage p50,
* 1ms) on the same corpus. Calibration: the card is ~7 indexed reads +
* a keyword search on the miss path, measured ~21× a 1ms-floored
* getPage at 20K pages an O(N) scan regression lands at 200ms+
* (200×), far past the ceiling even on a slow runner, while the
* 2.4× headroom absorbs planner noise.
*
* The 200K-page validation is a documented MANUAL recipe in
* docs/protocol/MEMORY_VERBS_v1.md not CI-gated (seed time would dominate).
*
* .slow.test.ts suffix keeps it out of the fast loop (`bun run test:slow`).
*/
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { buildEntityCard } from '../src/core/verbs/entity-card.ts';
let engine: PGLiteEngine;
const PAGES = 20_000;
const LINKS = 100_000;
const ALIASES = 30_000;
const FACTS = 40_000;
const WARMUP = 20;
const MEASURED = 200;
const TARGET_ENTITIES = 50; // pages the measured calls rotate over
const P99_BUDGET_MS = 100 * (Number(process.env.GBRAIN_PERF_BUDGET_MULTIPLIER) || 1);
// entity p99 ≤ 50× max(getPage p50, 1ms) — see the calibration note above.
const RATIO_CEILING = 50;
function percentile(sorted: number[], p: number): number {
const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
return sorted[Math.max(0, idx)];
}
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const db = (engine as any).db;
// Target entities (real putPage so frontmatter/title behave like prod pages).
for (let i = 0; i < TARGET_ENTITIES; i++) {
const slug = `people/target-person-${i}`;
await engine.putPage(slug, {
type: 'person',
title: `Target Person ${i}`,
compiled_truth: `# Target Person ${i}\n\nRuns area ${i} at a-company. Synthetic perf-corpus entity.`,
frontmatter: { type: 'person', title: `Target Person ${i}`, slug, summary: `Synthetic target ${i} for the entity-card latency gate.` },
}, { sourceId: 'default' });
}
// Filler pages in one generate_series insert.
await db.query(
`INSERT INTO pages (slug, type, title, compiled_truth, frontmatter, source_id, created_at, updated_at)
SELECT 'filler/page-' || gs::text, 'note', 'Filler ' || gs::text, '# Filler', '{}', 'default', NOW(), NOW()
FROM generate_series(1, ${PAGES}) gs`,
);
// Links: filler→filler hub noise plus a fan-in/out around every target
// (the card reads getLinks/getBacklinks — targets must have real edges).
await db.query(
`INSERT INTO links (from_page_id, to_page_id, link_type, link_source)
SELECT p1.id, p2.id, 'mentions', 'mentions'
FROM (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'filler/%') p1
JOIN (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'filler/%') p2
ON p2.rn = ((p1.rn * 7919) % ${PAGES}) + 1 AND p1.id <> p2.id
CROSS JOIN generate_series(1, ${Math.ceil(LINKS / PAGES)}) g
ON CONFLICT DO NOTHING`,
);
await db.query(
`INSERT INTO links (from_page_id, to_page_id, link_type, link_source)
SELECT t.id, f.id, 'works_at', 'markdown'
FROM (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'people/target-%') t
JOIN (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'filler/%' LIMIT 2000) f
ON (f.rn % ${TARGET_ENTITIES}) + 1 = t.rn
ON CONFLICT DO NOTHING`,
);
// Aliases: bulk noise + 2 aliases per target.
await db.query(
`INSERT INTO page_aliases (source_id, alias_norm, slug)
SELECT 'default', 'alias noise ' || gs::text, 'filler/page-' || ((gs % ${PAGES}) + 1)::text
FROM generate_series(1, ${ALIASES}) gs
ON CONFLICT DO NOTHING`,
);
for (let i = 0; i < TARGET_ENTITIES; i++) {
await db.query(
`INSERT INTO page_aliases (source_id, alias_norm, slug) VALUES
('default', $1, $2), ('default', $3, $2)
ON CONFLICT DO NOTHING`,
[`tp${i}`, `people/target-person-${i}`, `target alias ${i}`],
);
}
// Facts: bulk noise across fillers + 20 active facts per target entity.
await db.query(
`INSERT INTO facts (source_id, entity_slug, fact, kind, visibility, notability, valid_from, source, confidence, created_at)
SELECT 'default', 'filler/page-' || ((gs % ${PAGES}) + 1)::text,
'noise fact ' || gs::text, 'fact', 'world', 'medium', NOW(), 'perf-seed', 1.0, NOW()
FROM generate_series(1, ${FACTS - TARGET_ENTITIES * 20}) gs`,
);
await db.query(
`INSERT INTO facts (source_id, entity_slug, fact, kind, visibility, notability, valid_from, source, confidence, created_at)
SELECT 'default', 'people/target-person-' || t::text,
'target fact ' || g::text || ' about person ' || t::text,
CASE WHEN g % 5 = 0 THEN 'commitment' ELSE 'fact' END,
'world', 'medium', NOW(), 'perf-seed', 1.0, NOW()
FROM generate_series(0, ${TARGET_ENTITIES - 1}) t, generate_series(1, 20) g`,
);
}, 300_000);
afterAll(async () => {
await engine.disconnect();
});
describe('entity card p99 latency gate', () => {
it(`p99 < ${P99_BUDGET_MS}ms on ${PAGES} pages AND ≤ ${RATIO_CEILING}× getPage p50`, async () => {
// Mixed name set: alias hits, exact titles, namespaced slugs, suffixes, misses.
const names: string[] = [];
for (let i = 0; i < TARGET_ENTITIES; i++) {
names.push(`tp${i}`); // alias arm
names.push(`Target Person ${i}`); // exact-title arm
names.push(`people/target-person-${i}`); // exact-slug arm
names.push(`target-person-${i}`); // slug-suffix arm
names.push(`zzz-absent-${i}`); // miss (suggestions path)
}
for (let i = 0; i < WARMUP; i++) {
await buildEntityCard(engine, 'default', names[i % names.length], { remote: true });
}
const samples: number[] = [];
for (let i = 0; i < MEASURED; i++) {
const name = names[(i * 13) % names.length];
const t0 = performance.now();
await buildEntityCard(engine, 'default', name, { remote: true });
samples.push(performance.now() - t0);
}
samples.sort((a, b) => a - b);
const p50 = percentile(samples, 50);
const p99 = percentile(samples, 99);
// Ratio baseline: getPage p50 on the same corpus.
const pageSamples: number[] = [];
for (let i = 0; i < 50; i++) {
const t0 = performance.now();
await engine.getPage(`people/target-person-${i % TARGET_ENTITIES}`, { sourceId: 'default' });
pageSamples.push(performance.now() - t0);
}
pageSamples.sort((a, b) => a - b);
const pageP50 = Math.max(percentile(pageSamples, 50), 1.0); // 1ms floor vs sub-ms division noise
// eslint-disable-next-line no-console
console.log(
`[entity-card-perf] corpus=${PAGES}p+${LINKS}l+${ALIASES}a+${FACTS}f ` +
`entity p50=${p50.toFixed(2)}ms p99=${p99.toFixed(2)}ms | getPage p50=${pageP50.toFixed(2)}ms ` +
`| ratio=${(p99 / pageP50).toFixed(1)}x (ceiling ${RATIO_CEILING}x) | budget=${P99_BUDGET_MS}ms`,
);
expect(p99).toBeLessThan(P99_BUDGET_MS);
expect(p99 / pageP50).toBeLessThanOrEqual(RATIO_CEILING);
}, 300_000);
});
+290
View File
@@ -0,0 +1,290 @@
[
{
"name": "remember rejects missing provenance (provenance_required + suggestion)",
"verb": "remember",
"params": {
"fact": "conformance {{marker}} fact without provenance"
},
"expectErrorCode": "invalid_params",
"expectSuggestion": false
},
{
"name": "remember rejects empty provenance (provenance_required + suggestion)",
"verb": "remember",
"params": {
"fact": "conformance {{marker}} fact empty provenance",
"provenance": " "
},
"expectErrorCode": "provenance_required",
"expectSuggestion": true
},
{
"name": "remember rejects ISO-8601 duration ttl with a fix (P30D trap)",
"verb": "remember",
"params": {
"fact": "conformance {{marker}} ttl trap",
"provenance": "conformance run",
"ttl": "P30D"
},
"expectErrorCode": "invalid_params",
"expectSuggestion": true
},
{
"name": "remember writes a fact (string id, enum status, echoed nulls)",
"verb": "remember",
"params": {
"fact": "conformance {{marker}}: the protocol round-trip fact",
"provenance": "conformance run {{marker}}",
"entity": "people/conformance-{{marker}}",
"kind": "fact"
},
"validateSchema": true,
"expect": [
{
"path": "id",
"type": "string"
},
{
"path": "status",
"oneOf": [
"inserted",
"duplicate",
"superseded"
]
},
{
"path": "protocol_version",
"equals": 1
}
],
"saveAs": {
"key": "fact1",
"path": "id"
}
},
{
"name": "remember with ttl returns ISO valid_until",
"verb": "remember",
"params": {
"fact": "conformance {{marker}}: expiring fact",
"provenance": "conformance run {{marker}}",
"entity": "people/conformance-{{marker}}",
"ttl": "30d"
},
"validateSchema": true,
"expect": [
{
"path": "valid_until",
"type": "string"
}
],
"saveAs": {
"key": "fact2",
"path": "id"
}
},
{
"name": "remember private fact (fence test setup)",
"verb": "remember",
"params": {
"fact": "conformance {{marker}} PRIVATE-SENTINEL commitment",
"provenance": "conformance run {{marker}}",
"entity": "people/conformance-{{marker}}",
"kind": "commitment",
"visibility": "private"
},
"validateSchema": true
},
{
"name": "recall by entity round-trips the remembered fact (superset envelope)",
"verb": "recall",
"params": {
"entity": "people/conformance-{{marker}}"
},
"validateSchema": true,
"expect": [
{
"path": "protocol_version",
"equals": 1
},
{
"path": "total",
"gte": 1
},
{
"path": "facts.0.fact_id",
"type": "string"
},
{
"path": "facts.0.provenance",
"type": "string"
}
]
},
{
"name": "recall with budget reports consistent budget meta",
"verb": "recall",
"params": {
"entity": "people/conformance-{{marker}}",
"budget_tokens": 10000
},
"validateSchema": true,
"expect": [
{
"path": "budget_tokens",
"equals": 10000
},
{
"path": "budget_used",
"gte": 0
},
{
"path": "budget_used",
"lte": 10000
},
{
"path": "dropped_count",
"gte": 0
}
]
},
{
"name": "recall with budget smaller than the first item drops everything",
"verb": "recall",
"params": {
"entity": "people/conformance-{{marker}}",
"budget_tokens": 1
},
"validateSchema": true,
"expect": [
{
"path": "total",
"equals": 0
},
{
"path": "dropped_count",
"gte": 1
}
]
},
{
"name": "recall with query returns the search arm (degradation allowed, never an error)",
"verb": "recall",
"params": {
"query": "conformance {{marker}} protocol round-trip",
"budget_tokens": 8000
},
"validateSchema": true,
"expect": [
{
"path": "results",
"type": "array"
}
]
},
{
"name": "entity resolves the round-trip entity to a card",
"verb": "entity",
"params": {
"name": "people/conformance-{{marker}}"
},
"validateSchema": true,
"requiresSeededEntity": true,
"expect": [
{
"path": "found",
"equals": true
},
{
"path": "protocol_version",
"equals": 1
},
{
"path": "card.entity.slug",
"type": "string"
},
{
"path": "card.backlink_count",
"gte": 0
}
]
},
{
"name": "entity miss returns found:false + suggestions (never an error)",
"verb": "entity",
"params": {
"name": "zzz-no-such-entity-{{marker}}"
},
"validateSchema": true,
"expect": [
{
"path": "found",
"equals": false
},
{
"path": "suggestions",
"type": "array"
}
]
},
{
"name": "entity card never leaks private facts to remote callers (fence test)",
"verb": "entity",
"params": {
"name": "people/conformance-{{marker}}"
},
"requiresSeededEntity": true,
"expect": [
{
"path": "card.open_threads",
"absentOrNotContains": "PRIVATE-SENTINEL"
}
]
},
{
"name": "forget expires the remembered fact",
"verb": "forget",
"params": {
"id": "{{id:fact2}}",
"reason": "conformance cleanup"
},
"validateSchema": true,
"expect": [
{
"path": "expired",
"equals": true
}
]
},
{
"name": "forget again is idempotent (expired:false, success)",
"verb": "forget",
"params": {
"id": "{{id:fact2}}"
},
"validateSchema": true,
"expect": [
{
"path": "expired",
"equals": false
}
]
},
{
"name": "forget unknown id returns not_found with a suggestion",
"verb": "forget",
"params": {
"id": "999999999"
},
"expectErrorCode": "not_found",
"expectSuggestion": true
},
{
"name": "synthesize answers (or reports unavailable) — cost-gated, pass --synthesize",
"verb": "synthesize",
"params": {
"question": "What do we know about conformance {{marker}}?"
},
"requiresSynthesizeFlag": true
}
]
+110
View File
@@ -0,0 +1,110 @@
/**
* MEMORY_VERBS v1 surface-mode tests (Cathedral 1).
*
* - 'verbs' filters to EXACTLY the five protocol verbs
* - 'full' is the identity (existing installs unchanged)
* - dispatch-layer allowedOps is FAIL-CLOSED: a hidden op is uncallable
* (unknown_tool), not merely unlisted [c2]
* - flag parsing is strict (unknown value rejects loudly)
* - resolution: flag > config mcp_surface > 'full'
*/
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { operations } from '../src/core/operations.ts';
import { VERB_NAMES } from '../src/core/verbs.ts';
import {
filterOpsForSurface,
allowedOpNames,
parseSurfaceFlag,
resolveSurface,
} from '../src/mcp/surface.ts';
import { dispatchToolCall } from '../src/mcp/dispatch.ts';
import { __setUsageLogPathForTests } from '../src/core/verbs/usage-log.ts';
let engine: PGLiteEngine;
let home: string;
beforeAll(async () => {
// Sidecar writes go to a temp file via the test seam — no global env mutation.
home = mkdtempSync(join(tmpdir(), 'gbrain-surface-test-'));
__setUsageLogPathForTests(join(home, 'usage.jsonl'));
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
__setUsageLogPathForTests(null);
try { rmSync(home, { recursive: true, force: true }); } catch { /* best-effort */ }
});
describe('filterOpsForSurface', () => {
it("'verbs' returns exactly the five protocol verbs", () => {
const names = filterOpsForSurface(operations, 'verbs').map(o => o.name).sort();
expect(names).toEqual([...VERB_NAMES].sort());
});
it("'full' is the identity — existing installs see every op", () => {
expect(filterOpsForSurface(operations, 'full')).toEqual(operations);
});
});
describe('dispatch allowedOps — fail-closed [c2]', () => {
it('a hidden op returns unknown_tool even when called by name', async () => {
const allowed = allowedOpNames(operations, 'verbs');
const res = await dispatchToolCall(engine, 'get_page', { slug: 'x' }, {
remote: true,
sourceId: 'default',
allowedOps: allowed,
surface: 'verbs',
});
expect(res.isError).toBe(true);
const body = JSON.parse(res.content[0].text);
expect(body.error).toBe('unknown_tool');
});
it('a surfaced verb still dispatches under the same allowedOps set', async () => {
const allowed = allowedOpNames(operations, 'verbs');
const res = await dispatchToolCall(engine, 'entity', { name: 'zzz-nobody' }, {
remote: true,
sourceId: 'default',
allowedOps: allowed,
surface: 'verbs',
});
expect(res.isError ?? false).toBe(false);
const body = JSON.parse(res.content[0].text);
expect(body.found).toBe(false);
});
it('without allowedOps (full surface) every op stays callable — pre-existing behavior', async () => {
const res = await dispatchToolCall(engine, 'get_stats', {}, {
remote: true,
sourceId: 'default',
});
expect(res.isError ?? false).toBe(false);
});
});
describe('parseSurfaceFlag + resolveSurface', () => {
it('parses verbs/full, rejects unknown values loudly, requires a value', () => {
expect(parseSurfaceFlag(['--surface', 'verbs'])).toBe('verbs');
expect(parseSurfaceFlag(['--surface', 'full'])).toBe('full');
expect(parseSurfaceFlag(['serve'])).toBe(null);
expect(() => parseSurfaceFlag(['--surface', 'all'])).toThrow(/Unknown --surface/);
expect(() => parseSurfaceFlag(['--surface'])).toThrow(/requires a value/);
expect(() => parseSurfaceFlag(['--surface', '--http'])).toThrow(/requires a value/);
});
it('resolution: flag > config mcp_surface > full', () => {
expect(resolveSurface('verbs', { mcp_surface: 'full' })).toBe('verbs');
expect(resolveSurface(null, { mcp_surface: 'verbs' })).toBe('verbs');
expect(resolveSurface(null, {})).toBe('full');
expect(resolveSurface(null, null)).toBe('full');
expect(resolveSurface(null, { mcp_surface: 'bogus' as never })).toBe('full');
});
});
+15
View File
@@ -56,6 +56,10 @@ function legacyInlineMap(ops: typeof operations) {
.filter(([, v]) => v.required)
.map(([k]) => k),
},
// MEMORY_VERBS v1: ToolAnnotations passthrough, emitted ONLY when the op
// defines them. The byte-stability contract is per-op: ops WITHOUT
// annotations keep the exact pre-v1 shape (pinned explicitly below).
...(op.annotations ? { annotations: op.annotations } : {}),
}));
}
@@ -66,6 +70,17 @@ describe('buildToolDefs', () => {
expect(JSON.stringify(extracted)).toBe(JSON.stringify(inline));
});
test('ops without annotations keep the pre-annotations shape exactly (no annotations key)', () => {
const extracted = buildToolDefs(operations);
for (const def of extracted) {
const op = operations.find(o => o.name === def.name)!;
if (!op.annotations) {
expect('annotations' in def).toBe(false);
expect(Object.keys(def)).toEqual(['name', 'description', 'inputSchema']);
}
}
});
test('preserves operation count', () => {
expect(buildToolDefs(operations).length).toBe(operations.length);
});
+533
View File
@@ -0,0 +1,533 @@
/**
* MEMORY_VERBS v1 conformance the frozen-contract pin (Cathedral 1).
*
* In-process through dispatchToolCall (the exact layer both MCP transports
* share), against in-memory PGLite. Covers:
* - recall legacy SUPERSET regression (G1B: legacy fields byte-equal,
* additions allowed protocol_version everywhere, no carve-out)
* - server-side budget packing math (incl. budget < first item)
* - query-arm keyword degradation (never an error without embeddings)
* - remember contract: provenance_required, ttl forms (P30D trap), enum
* kinds, world default + the remote rememberrecall round-trip [F2],
* private facts hidden from remote readers
* - entity: card shape vs RESPONSE_SCHEMAS, three resolution arms,
* misssuggestions, ZERO-LLM guard (chat transport rigged to throw)
* - synthesize: [EXPENSIVE prefix, annotations, clean `unavailable` with
* suggestion when no LLM is configured [c10]
* - forget: idempotency (expired:false), not_found with suggestion
* - writeSingleFact supersession rule [X1] + degraded dedup (embed seam)
* - negative conformance self-test [F3]: the runner FAILS a lying server
* - fixture mirror drift guard (cases.json === embedded module)
*/
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { resetPgliteState } from './helpers/reset-pglite.ts';
import { operationsByName } from '../src/core/operations.ts';
import type { OperationContext } from '../src/core/operations.ts';
import { dispatchToolCall } from '../src/mcp/dispatch.ts';
import { buildToolDefs } from '../src/mcp/tool-defs.ts';
import { RESPONSE_SCHEMAS, ERROR_SCHEMA, VERB_NAMES } from '../src/core/verbs.ts';
import {
runConformance,
validateAgainstSchema,
type ConformanceClient,
} from '../src/core/verbs/conformance.ts';
import { CONFORMANCE_CASES } from '../src/core/verbs/conformance-fixtures.ts';
import { writeSingleFact } from '../src/core/facts/write-single.ts';
import {
configureGateway,
__setChatTransportForTests,
__setEmbedTransportForTests,
} from '../src/core/ai/gateway.ts';
import { __setUsageLogPathForTests } from '../src/core/verbs/usage-log.ts';
import { withEnv } from './helpers/with-env.ts';
let engine: PGLiteEngine;
let home: string;
beforeAll(async () => {
// Sidecar writes go to a temp file via the test seam — no global env mutation.
home = mkdtempSync(join(tmpdir(), 'gbrain-verbs-test-'));
__setUsageLogPathForTests(join(home, 'usage.jsonl'));
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
__setUsageLogPathForTests(null);
try { rmSync(home, { recursive: true, force: true }); } catch { /* best-effort */ }
});
beforeEach(async () => {
await resetPgliteState(engine);
__setChatTransportForTests(null);
__setEmbedTransportForTests(null);
});
function localCtx(sourceId = 'default'): OperationContext {
return {
engine,
config: {},
logger: { info: () => {}, warn: () => {}, error: () => {} },
dryRun: false,
remote: false,
sourceId,
} as unknown as OperationContext;
}
/** Remote-shaped call through the shared dispatcher (what both transports do). */
async function callRemote(name: string, params: Record<string, unknown>) {
const res = await dispatchToolCall(engine, name, params, {
remote: true,
takesHoldersAllowList: ['world'],
sourceId: 'default',
});
return { isError: res.isError === true, body: JSON.parse(res.content[0].text) };
}
async function seedEntityPage(slug: string, title: string, body = 'A synthetic test entity.') {
const put = operationsByName['put_page'];
await put.handler(localCtx(), {
slug,
content: `---\ntitle: ${title}\ntype: person\n---\n\n# ${title}\n\n${body}\n`,
});
}
describe('recall — G1B superset + budget packing', () => {
it('legacy-param recall keeps every legacy field shape and adds only the v1 fields', async () => {
const r1 = await callRemote('remember', {
fact: 'superset regression fact',
provenance: 'conformance test',
entity: 'people/superset-test',
});
expect(r1.isError).toBe(false);
const { isError, body } = await callRemote('recall', { entity: 'people/superset-test' });
expect(isError).toBe(false);
// Legacy envelope fields, unchanged shapes.
expect(typeof body.total).toBe('number');
expect(Array.isArray(body.facts)).toBe(true);
const f = body.facts[0];
const LEGACY_FACT_KEYS = [
'id', 'fact', 'kind', 'entity_slug', 'visibility', 'notability', 'valid_from',
'valid_until', 'expired_at', 'superseded_by', 'consolidated_at',
'consolidated_into', 'source', 'source_session', 'confidence', 'created_at',
];
for (const k of LEGACY_FACT_KEYS) expect(k in f).toBe(true);
expect(typeof f.id).toBe('number'); // legacy numeric id is FROZEN
// v1 additions (G1B superset — on EVERY response, no carve-out).
expect(body.protocol_version).toBe(1);
expect(f.fact_id).toBe(String(f.id));
expect(f.provenance).toBe(f.source);
// No query/budget params → no search/budget fields.
expect('results' in body).toBe(false);
expect('budget_tokens' in body).toBe(false);
});
it('budget packing reports consistent meta and drops everything under a 1-token budget', async () => {
for (let i = 0; i < 3; i++) {
await callRemote('remember', {
fact: `budget fact number ${i} with some padding text to cost tokens`,
provenance: 'conformance test',
entity: 'people/budget-test',
});
}
const big = await callRemote('recall', { entity: 'people/budget-test', budget_tokens: 10000 });
expect(big.body.budget_tokens).toBe(10000);
expect(big.body.budget_used).toBeGreaterThan(0);
expect(big.body.budget_used).toBeLessThanOrEqual(10000);
expect(big.body.dropped_count).toBe(0);
expect(big.body.total).toBe(3);
const tiny = await callRemote('recall', { entity: 'people/budget-test', budget_tokens: 1 });
expect(tiny.body.total).toBe(0);
expect(tiny.body.dropped_count).toBeGreaterThanOrEqual(3);
});
it('query arm degrades to keyword-only without an embedding provider — never an error', async () => {
await seedEntityPage('people/query-arm-test', 'Query Arm Marker Qzx');
const { isError, body } = await callRemote('recall', { query: 'Query Arm Marker Qzx' });
expect(isError).toBe(false);
expect(Array.isArray(body.results)).toBe(true);
expect(body.search_degraded).toBe('keyword_only_no_embedding_provider');
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.recall);
expect(violations).toEqual([]);
});
});
describe('remember — contract behavior', () => {
it('rejects empty provenance with provenance_required + a populated suggestion', async () => {
const { isError, body } = await callRemote('remember', { fact: 'x', provenance: ' ' });
expect(isError).toBe(true);
expect(body.error).toBe('provenance_required');
expect(typeof body.suggestion).toBe('string');
expect(body.suggestion.length).toBeGreaterThan(0);
expect(body.protocol_version).toBe(1);
});
it('rejects ISO-8601 duration ttl (P30D) with a self-correcting suggestion', async () => {
const { isError, body } = await callRemote('remember', {
fact: 'ttl trap', provenance: 'test', ttl: 'P30D',
});
expect(isError).toBe(true);
expect(body.error).toBe('invalid_params');
expect(body.suggestion).toContain('30d');
});
it('accepts duration ttl and returns a future ISO valid_until; echoes null entity_slug', async () => {
const { isError, body } = await callRemote('remember', {
fact: 'expiring fact with ttl', provenance: 'test', ttl: '30d',
});
expect(isError).toBe(false);
expect(typeof body.id).toBe('string');
expect(body.status).toBe('inserted');
expect(body.entity_slug).toBe(null); // omitted optional inputs echo as null
expect(Date.parse(body.valid_until)).toBeGreaterThan(Date.now());
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.remember);
expect(violations).toEqual([]);
});
it('remote remember→recall round-trip holds (world default [F2]); private facts stay hidden', async () => {
await callRemote('remember', {
fact: 'world-visible round-trip fact', provenance: 'test', entity: 'people/roundtrip-test',
});
await callRemote('remember', {
fact: 'PRIVATE-SENTINEL fact', provenance: 'test', entity: 'people/roundtrip-test',
visibility: 'private',
});
const { body } = await callRemote('recall', { entity: 'people/roundtrip-test' });
const texts = body.facts.map((f: { fact: string }) => f.fact).join('|');
expect(texts).toContain('world-visible round-trip fact');
expect(texts).not.toContain('PRIVATE-SENTINEL');
});
});
describe('entity — card, arms, zero LLM', () => {
it('resolves an exact namespaced slug to a schema-valid card with the chat gateway rigged to throw', async () => {
__setChatTransportForTests(() => {
throw new Error('entity must NEVER call the chat LLM');
});
await seedEntityPage('people/card-test', 'Card Test Person', 'Runs engineering at a-company.');
const { isError, body } = await callRemote('entity', { name: 'people/card-test' });
expect(isError).toBe(false);
expect(body.found).toBe(true);
expect(typeof body.latency_ms).toBe('number');
expect(body.card.entity.slug).toBe('people/card-test');
expect(body.card.summary.length).toBeGreaterThan(0);
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.entity);
expect(violations).toEqual([]);
});
it('resolves by exact title and by slug suffix (arms 2)', async () => {
await seedEntityPage('people/arm-test-alice', 'Arm Test Alice');
const byTitle = await callRemote('entity', { name: 'Arm Test Alice' });
expect(byTitle.body.found).toBe(true);
const bySuffix = await callRemote('entity', { name: 'arm-test-alice' });
expect(bySuffix.body.found).toBe(true);
});
it('miss returns found:false + suggestions, never an error', async () => {
await seedEntityPage('people/suggestion-source', 'Suggestion Source Person');
const { isError, body } = await callRemote('entity', { name: 'zzz-definitely-absent-entity' });
expect(isError).toBe(false);
expect(body.found).toBe(false);
expect(Array.isArray(body.suggestions)).toBe(true);
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.entity);
expect(violations).toEqual([]);
});
it('[ship P1.2] entity card backlinks are source-isolated on BOTH sides (no foreign from_slug leak)', async () => {
// Same slug exists in two sources; a foreign-source page links to the
// default-source entity. The card must NOT surface the foreign edge/count.
await seedEntityPage('people/iso-target', 'Iso Target');
// Register the foreign tenant source (FK target) + a foreign page that
// links INTO the default-source entity.
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES ('other', 'other-tenant', '{}'::jsonb)
ON CONFLICT (id) DO NOTHING`,
[],
);
await engine.executeRaw(
`INSERT INTO pages (slug, type, title, compiled_truth, frontmatter, source_id, created_at, updated_at)
VALUES ('people/foreign-linker', 'person', 'Foreign Linker', '# Foreign', '{}', 'other', NOW(), NOW())`,
[],
);
await engine.executeRaw(
`INSERT INTO links (from_page_id, to_page_id, link_type, link_source)
SELECT f.id, t.id, 'works_at', 'markdown'
FROM pages f, pages t
WHERE f.slug = 'people/foreign-linker' AND f.source_id = 'other'
AND t.slug = 'people/iso-target' AND t.source_id = 'default'`,
[],
);
const { body } = await callRemote('entity', { name: 'people/iso-target' });
expect(body.found).toBe(true);
// The foreign cross-source backlink must not appear in edges OR the count.
const edgeSlugs = (body.card.edges as Array<{ slug: string }>).map(e => e.slug);
expect(edgeSlugs).not.toContain('people/foreign-linker');
expect(body.card.backlink_count).toBe(0);
});
it('remote card never carries private commitment facts (fence test)', async () => {
await seedEntityPage('people/fence-test', 'Fence Test Person');
await callRemote('remember', {
fact: 'PRIVATE-SENTINEL commitment text', provenance: 'test',
entity: 'people/fence-test', kind: 'commitment', visibility: 'private',
});
const { body } = await callRemote('entity', { name: 'people/fence-test' });
expect(body.found).toBe(true);
expect(JSON.stringify(body.card.open_threads)).not.toContain('PRIVATE-SENTINEL');
});
});
describe('synthesize — marked expensive + unavailable conversion [c10]', () => {
it('description starts with [EXPENSIVE and the tool def carries annotations', () => {
const op = operationsByName['synthesize'];
expect(op.description.startsWith('[EXPENSIVE')).toBe(true);
const def = buildToolDefs([op])[0];
expect(def.annotations?.readOnlyHint).toBe(true);
expect(def.annotations?.title).toContain('costly');
});
it('delegates to runThink and returns the frozen envelope with a priced cost block (chat seam — no real LLM)', async () => {
// Fully hermetic regardless of the dev/CI machine's ambient credentials:
// runThink builds its client via a real-key check (hasAnthropicKey reads
// process.env), NOT the gateway chat seam — so we must BOTH provide a fake
// key via withEnv (so the client builds) AND install the chat seam (so no
// real API call fires). Without the env key, CI (credential-free) takes the
// NO_ANTHROPIC_API_KEY path → the verb's `unavailable` conversion → isError.
__setChatTransportForTests(async () => ({
text: JSON.stringify({ answer: 'Synthesized test answer.', citations: [], gaps: ['none'] }),
blocks: [],
stopReason: 'end' as const,
usage: { input_tokens: 1200, output_tokens: 80, cache_read_tokens: 0, cache_creation_tokens: 0 },
model: 'anthropic:claude-haiku-4-5-20251001',
providerId: 'anthropic',
}));
const { isError, body } = await withEnv({ ANTHROPIC_API_KEY: 'sk-test-hermetic' }, async () =>
callRemote('synthesize', { question: 'what do we know?' }),
);
expect(isError).toBe(false);
expect(body.answer).toBe('Synthesized test answer.');
expect(body.protocol_version).toBe(1);
expect(body.cost.input_tokens).toBe(1200);
expect(body.cost.output_tokens).toBe(80);
expect(Array.isArray(body.sources)).toBe(true);
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.synthesize);
expect(violations).toEqual([]);
});
});
describe('forget — idempotency + not_found', () => {
it('expires once, reports expired:false on re-forget, not_found on unknown id', async () => {
const r = await callRemote('remember', {
fact: 'fact to forget', provenance: 'test', entity: 'people/forget-test',
});
const id = r.body.id as string;
const first = await callRemote('forget', { id, reason: 'test cleanup' });
expect(first.isError).toBe(false);
expect(first.body.expired).toBe(true);
expect(first.body.reason).toBe('test cleanup');
expect(validateAgainstSchema(first.body, RESPONSE_SCHEMAS.forget)).toEqual([]);
const second = await callRemote('forget', { id });
expect(second.isError).toBe(false);
expect(second.body.expired).toBe(false);
expect(second.body.reason).toBe(null); // omitted optional → null
const missing = await callRemote('forget', { id: '999999999' });
expect(missing.isError).toBe(true);
expect(missing.body.error).toBe('not_found');
expect(missing.body.suggestion.length).toBeGreaterThan(0);
});
it('[ship P1.1] a remote caller in source A cannot forget a fact in source B (returns not_found, not expired)', async () => {
// Register the 'other' tenant source (FK target) then seed a fact in it.
await engine.executeRaw(
`INSERT INTO sources (id, name, config) VALUES ('other', 'other-tenant', '{}'::jsonb)
ON CONFLICT (id) DO NOTHING`,
[],
);
await engine.insertFact(
{ fact: 'cross-source secret fact', kind: 'fact', entity_slug: null, visibility: 'world', source: 'seed' },
{ source_id: 'other' },
);
const otherRows = await engine.executeRaw<{ id: number }>(
`SELECT id FROM facts WHERE source_id = 'other' AND fact = 'cross-source secret fact' LIMIT 1`,
[],
);
const foreignId = String(otherRows[0].id);
// Remote caller scoped to 'default' tries to forget the 'other'-source id.
const res = await dispatchToolCall(engine, 'forget', { id: foreignId }, {
remote: true, takesHoldersAllowList: ['world'], sourceId: 'default',
});
const body = JSON.parse(res.content[0].text);
expect(res.isError).toBe(true);
expect(body.error).toBe('not_found'); // no cross-source existence leak
// And the foreign fact is STILL active (not expired by the cross-source call).
const stillActive = await engine.executeRaw<{ expired_at: Date | null }>(
`SELECT expired_at FROM facts WHERE id = $1`,
[otherRows[0].id],
);
expect(stillActive[0].expired_at).toBe(null);
});
it('[ship P1.1] a remote caller cannot forget a private fact (world-only)', async () => {
const r = await callRemote('remember', {
fact: 'private fact remote cannot forget', provenance: 'test',
entity: 'people/private-forget-test', visibility: 'private',
});
// remote remember defaults world; force a private one locally instead.
const localRes = await dispatchToolCall(engine, 'remember', {
fact: 'truly private fact', provenance: 'test',
entity: 'people/private-forget-test', visibility: 'private',
}, { remote: false, sourceId: 'default' });
const localId = JSON.parse(localRes.content[0].text).id as string;
const res = await dispatchToolCall(engine, 'forget', { id: localId }, {
remote: true, takesHoldersAllowList: ['world'], sourceId: 'default',
});
const body = JSON.parse(res.content[0].text);
expect(res.isError).toBe(true);
expect(body.error).toBe('not_found'); // remote can't reach a private fact
void r;
});
});
describe('writeSingleFact — supersession rule [X1] + degraded dedup', () => {
function installDeterministicEmbedder() {
// The schema's facts.embedding column is the init-time default dim (1536);
// test vectors must match or pgvector's CheckExpectedDim rejects the row.
const DIM = 1536;
configureGateway({
embedding_model: 'openai:text-embedding-3-small',
embedding_dimensions: DIM,
env: { OPENAI_API_KEY: 'sk-test-deterministic' },
});
// The seam replaces the AI SDK's embedMany({ model, values }) call.
__setEmbedTransportForTests((async (opts: { values: string[] }) => ({
embeddings: opts.values.map(t => {
// Same vector for the SUPERSEDE-PAIR family (cosine 1.0 — above the
// dedup threshold); a distinct deterministic vector otherwise.
const v = new Array(DIM).fill(0);
if (t.includes('SUPERSEDE-PAIR')) v[0] = 1;
else for (let i = 0; i < 16; i++) v[i] = ((t.charCodeAt(i % t.length) % 13) + 1) / 13;
return v;
}),
})) as never);
}
it('near-duplicate with changed text and same kind SUPERSEDES; identical text is a duplicate', async () => {
installDeterministicEmbedder();
const a = await writeSingleFact(engine, 'default', {
fact: 'SUPERSEDE-PAIR alice works at acme-example',
provenance: 'test', entity: 'people/supersede-test', kind: 'fact',
});
expect(a.status).toBe('inserted');
expect(a.degraded_dedup).toBe(false);
const dup = await writeSingleFact(engine, 'default', {
fact: 'SUPERSEDE-PAIR alice works at acme-example',
provenance: 'test', entity: 'people/supersede-test', kind: 'fact',
});
expect(dup.status).toBe('duplicate');
expect(dup.id).toBe(a.id);
const updated = await writeSingleFact(engine, 'default', {
fact: 'SUPERSEDE-PAIR alice LEFT acme-example, now at widget-co',
provenance: 'test', entity: 'people/supersede-test', kind: 'fact',
});
expect(updated.status).toBe('superseded');
expect(updated.id).not.toBe(a.id);
const rows = await engine.executeRaw<{ id: number; superseded_by: number | null }>(
`SELECT id, superseded_by FROM facts WHERE id = $1`, [a.id],
);
expect(rows[0].superseded_by).toBe(updated.id);
});
it('reports degraded_dedup when no embedding provider is configured', async () => {
const r = await writeSingleFact(engine, 'default', {
fact: 'a fact written with no embedding provider',
provenance: 'test', entity: 'people/degraded-test',
});
expect(r.status).toBe('inserted');
expect(r.degraded_dedup).toBe(true);
});
});
describe('conformance runner — negative self-test [F3]', () => {
function lyingClient(corrupt: (verb: string, body: Record<string, unknown>) => Record<string, unknown>): ConformanceClient {
return {
listTools: async () =>
VERB_NAMES.map(name => ({
name,
description: name === 'synthesize' ? '[EXPENSIVE / SLOW] x' : `MEMORY VERB (v1): ${name}`,
})),
callTool: async (name, params) => {
const res = await dispatchToolCall(engine, name, params, {
remote: true, takesHoldersAllowList: ['world'], sourceId: 'default',
});
const body = JSON.parse(res.content[0].text);
const mutated = res.isError ? body : corrupt(name, body);
return { isError: res.isError, text: JSON.stringify(mutated) };
},
};
}
it('a certifier that cannot fail certifies nothing: missing fields, bad enums, wrong id types all flag', async () => {
// Mutation 1: remember drops the required `status` field.
const r1 = await runConformance(
lyingClient((verb, body) => (verb === 'remember' ? (({ status: _s, ...rest }) => rest)(body as { status?: unknown } & Record<string, unknown>) : body)),
{ marker: 'neg1' },
);
expect(r1.ok).toBe(false);
// Mutation 2: remember returns an out-of-enum status.
const r2 = await runConformance(
lyingClient((verb, body) => (verb === 'remember' ? { ...body, status: 'absorbed' } : body)),
{ marker: 'neg2' },
);
expect(r2.ok).toBe(false);
// Mutation 3: recall re-types fact_id to a number (the opaque-string mandate [T4]).
const r3 = await runConformance(
lyingClient((verb, body) => {
if (verb !== 'recall' || !Array.isArray((body as { facts?: unknown[] }).facts)) return body;
return {
...body,
facts: (body.facts as Array<Record<string, unknown>>).map(f => ({ ...f, fact_id: Number(f.fact_id) })),
};
}),
{ marker: 'neg3' },
);
expect(r3.ok).toBe(false);
// Honest server passes (sanity: the failures above are the mutations' doing).
const honest = await runConformance(lyingClient((_v, b) => b), { marker: 'pos1' });
const failures = honest.results.filter(r => r.status === 'fail');
expect(failures).toEqual([]);
});
});
describe('fixture mirror + surface invariants', () => {
it('test/fixtures/memory-verbs/cases.json matches the embedded fixture module (BrainBench seed drift guard)', () => {
const onDisk = JSON.parse(readFileSync(join(import.meta.dir, 'fixtures/memory-verbs/cases.json'), 'utf-8'));
expect(onDisk).toEqual(JSON.parse(JSON.stringify(CONFORMANCE_CASES)));
});
it('exactly five ops carry verb: true and they match VERB_NAMES', async () => {
const { operations } = await import('../src/core/operations.ts');
const verbs = operations.filter(o => o.verb === true).map(o => o.name).sort();
expect(verbs).toEqual([...VERB_NAMES].sort());
});
});
+22
View File
@@ -109,6 +109,28 @@ describe('v0.29 — redirect hints on existing ops', () => {
});
});
describe('#2416 — concept/landscape routing between search and query', () => {
test('search describes the cheap-hybrid default, not the keyword-era model', () => {
expect(SEARCH_DESCRIPTION).toContain("Cheap hybrid search");
expect(SEARCH_DESCRIPTION).toContain("no LLM expansion");
expect(SEARCH_DESCRIPTION).not.toContain("Keyword search using full-text search");
});
test('search declares the completeness boundary and both escape routes', () => {
expect(SEARCH_DESCRIPTION).toContain("NOT proof of coverage");
expect(SEARCH_DESCRIPTION).toContain("landscape");
expect(SEARCH_DESCRIPTION).toContain("list_pages");
});
test('query owns concept/landscape questions but does not oversell coverage', () => {
expect(QUERY_DESCRIPTION).toContain("landscape");
expect(QUERY_DESCRIPTION).toContain("expansion recovers synonym");
expect(QUERY_DESCRIPTION).toContain("Still top-K");
expect(QUERY_DESCRIPTION).toContain("list_pages");
expect(QUERY_DESCRIPTION).toContain("cheaper");
});
});
describe('v0.29 — subagent allow-list', () => {
test('includes get_recent_salience and find_anomalies', () => {
expect(BRAIN_TOOL_ALLOWLIST.has('get_recent_salience')).toBe(true);
+88
View File
@@ -0,0 +1,88 @@
/**
* #2416 concept-shaped query detection (CLI nudge toward `query`).
*
* The classifier is tuned to favor false-negatives (silence) over
* false-positives (noise): the cost of a missed nudge is zero; the cost of
* nagging on an exact-token lookup is trust erosion. The non-collision block
* pins the deliberate cue exclusions "who are the" (find_experts) and bare
* "anything…" (salience ops) must NEVER trigger the query nudge.
*/
import { describe, test, expect } from 'bun:test';
import { looksConceptShaped, conceptNudge } from '../src/core/search/query-intent.ts';
describe('#2416 — looksConceptShaped: concept/landscape queries → true', () => {
const CONCEPT_SHAPED = [
'all the companies that do offshore wind',
'every project that uses pgvector',
'find all startups doing agent memory',
'list everything about vector databases',
'everything related to embedding pricing',
'the landscape of agent memory startups',
'the ecosystem of MCP servers',
'which funds have invested in climate tech',
'show all notes that mention fundraising strategy',
];
for (const q of CONCEPT_SHAPED) {
test(`true: "${q}"`, () => {
expect(looksConceptShaped(q)).toBe(true);
});
}
});
describe('#2416 — looksConceptShaped: exact-token / entity / quoted → false', () => {
const NOT_CONCEPT_SHAPED = [
// bare tokens / proper-noun lookups (short-query guard)
'stripe',
'Series A',
'acme-example',
// quoted phrase — exact-match intent
'find all notes with "offshore wind"',
// slug-like token
'all the pages that link to widget-co-seed',
// entity lookups (classifyQueryIntent === 'entity')
'who is alice from acme',
'tell me about widget co',
// plain questions without a fuzzy-quantifier cue
'how does the embed backfill work',
'meeting notes from tuesday',
];
for (const q of NOT_CONCEPT_SHAPED) {
test(`false: "${q}"`, () => {
expect(looksConceptShaped(q)).toBe(false);
});
}
});
describe('#2416 — cue non-collision with other routers', () => {
test('"who are the …" stays silent (owned by find_experts)', () => {
expect(looksConceptShaped('who are the ML people in my network')).toBe(false);
});
test('bare "anything …" stays silent (owned by salience ops)', () => {
expect(looksConceptShaped('anything notable lately')).toBe(false);
expect(looksConceptShaped('anything crazy happening in my brain lately?')).toBe(false);
});
});
describe('#2416 — conceptNudge message', () => {
test('returns null for non-concept queries', () => {
expect(conceptNudge('stripe')).toBeNull();
expect(conceptNudge('who is alice from acme')).toBeNull();
});
test('returns a single-line hint naming `gbrain query` and the completeness caveat', () => {
const msg = conceptNudge('all the companies that do offshore wind');
expect(msg).not.toBeNull();
expect(msg!).toContain('gbrain query');
expect(msg!).toContain('all the companies that do offshore wind');
expect(msg!).toContain('not proof of completeness');
expect(msg!.includes('\n')).toBe(false);
});
test('truncates long queries in the copy-paste suggestion', () => {
const long = 'all the companies that are doing something with autonomous underwater drone inspection services';
const msg = conceptNudge(long)!;
expect(msg).toContain('...');
expect(msg.length).toBeLessThan(320);
});
});
+5 -3
View File
@@ -423,15 +423,17 @@ describe('runThink + persistSynthesis — #1698 never persist empty', () => {
expect(full.synthesisOk).toBe(true);
});
test('opts.stubResponse path never made a real LLM call — usage stays undefined', async () => {
test('opts.stubResponse path never made a real LLM call — usage stays null', async () => {
// Same distinction synthesisOk already makes: opts.stubResponse bypasses
// client.create() entirely, so there is no real usage to report. cost_usd
// must not be computed (and should render as null in --json) when this
// happens, since there is nothing to compute it from.
// happens, since there is nothing to compute it from. Since the [E2]
// MEMORY_VERBS usage-accounting change, "no LLM ran" is spelled `null`
// (the frozen cost-block contract), not `undefined`.
const result = await runThink(engine, {
question: 'stub no usage', stubResponse: { answer: 'has content', citations: [], gaps: [] },
});
expect(result.usage).toBeUndefined();
expect(result.usage).toBeNull();
});
test('pre-existing ThinkResult literal without synthesisOk still persists (back-compat)', async () => {