mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-27 13:34:25 +02:00
65972f6a15
* docs: drop ~/.zshrc env note in favor of GSTACK_* env-shim reference
The CLAUDE.md "Where the keys live on this machine" block hand-rolled a
`grep ~/.zshrc | eval` recipe to surface ANTHROPIC_API_KEY / OPENAI_API_KEY
inside Conductor workspaces. That predates the GSTACK_* env-shim
(`lib/conductor-env-shim.ts`, v1.39.2.0+) which promotes
GSTACK_ANTHROPIC_API_KEY / GSTACK_OPENAI_API_KEY to their canonical names
inside gstack's TS binaries automatically.
The zshrc recipe is now an obsolete workaround. Replace with a short note
pointing at the env-shim as the canonical answer. Keep the Agent SDK
\`env: {...}\` gotcha (still real, unrelated to where the key comes from).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: default PGLite to voyage-code-3 when VOYAGE_API_KEY set
When gstack inits a local PGLite engine for code search, use Voyage's
code-specialized `voyage-code-3` (1024-dim) embedding model if
\`VOYAGE_API_KEY\` is present. Falls back to gbrain's auto-selected
provider chain (OpenAI text-embedding-3-large 1536-dim when
OPENAI_API_KEY is available, etc.) when the Voyage key is unset.
Why voyage-code-3: head-to-head A/B against voyage-4-large on 10
realistic code queries against this codebase (using gbrain query
--no-expand for pure vector retrieval). voyage-code-3 strictly won on
4 queries (cases where the right hit was an implementation file vs a
test file: terminal-agent.ts over terminal-agent-integration.test.ts,
sanitizeReplacer over sanitize.test.ts, disposeSession over a
tangentially-related killDaemon test, surfaced injectCanary semantic
query). Tied on 5 with consistently +0.03 to +0.06 higher confidence.
Zero losses for voyage-4-large.
Touches 3 init sites in setup-gbrain/SKILL.md.tmpl:
- Step 1.5 (broken-db rollback-safe switch to PGLite)
- Path 3 direct PGLite init
- Step 4.5 split-engine local code index (Path 4 Yes branch)
Plus 2 manual-repair hints in sync-gbrain/SKILL.md.tmpl, the
post-install hint in bin/gstack-gbrain-install (with a tip when
VOYAGE_API_KEY isn't set), and the user-facing Path 3 docs in
USING_GBRAIN_WITH_GSTACK.md.
Cost is trivial: voyage-code-3 at \$0.18/1M tokens means a full reindex
of a 100K-LOC repo runs about \$0.20. Incremental syncs are pennies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate SKILL.md after voyage-code-3 default
Mechanical regen via \`bun run gen:skill-docs --host all\` after the
template changes in the previous commit. Single-host regen leaves
other-host outputs stale and trips gen-skill-docs.test.ts; --host all
keeps every adapter (claude, codex, kiro, opencode, slate, cursor,
openclaw, hermes, gbrain) in sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: gbrain PGLite + voyage-code-3 init contract + sync integration
Two test files cover the voyage-code-3 default landed in the previous
commits:
test/gbrain-init-voyage-code-3.test.ts — free, deterministic, gate-tier.
Mirrors gbrain-init-rollback.test.ts: runs the skill template's
PGLite-init bash against a fake \`gbrain\` that logs argv to a sentinel
file, asserts the right flags pass under VOYAGE_API_KEY set/unset/empty.
Also includes belt-and-suspenders grep checks that the template literally
contains the voyage gate at all 3 PGLite init sites.
test/gbrain-sync-voyage-code-3-integration.test.ts — real, paid,
skip-if-no-key. Inits a sandbox PGLite with voyage-code-3 in a tempdir,
registers a 3-file fixture git repo as a source, runs
\`gbrain sync --strategy code --skip-failed\`, asserts pages imported +
embedded > 0. Also asserts \`gbrain doctor\` reports no dimension
mismatch and the column width is 1024d. \`gbrain code-def\` smoke test
confirms symbol extraction works against the embedded fixture.
The integration test deliberately omits a \`gbrain query\` assertion:
query produces correct output but \`gbrain query\` hangs ~2 min on a
fresh PGLite before exiting. The smoking-gun assertion for "embeddings
worked" is the "N pages embedded" line from sync output. Symbol-aware
correctness is covered by the code-def assertion.
Caught one real bug during test development: gbrain reads
\`.gbrain-source\` from CWD and tries to sync that source too. The test
sets cwd to the sandbox root to avoid the parent worktree's pin
polluting the sandbox brain. Documented in the runGbrain() helper.
Runtime: ~22s when VOYAGE_API_KEY is set, instant skip otherwise.
Cost: ~\$0.001 per run (3 tiny fixture files, ~500 tokens of Voyage
embeddings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump to v1.43.1.0 with voyage-code-3 default + tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: update USING_GBRAIN_WITH_GSTACK for v1.43.1.0 voyage-code-3 default
Add VOYAGE_API_KEY row to the env-var table; clarify the OPENAI_API_KEY row as
the fallback path. Refresh the "search returns nothing semantic" troubleshooting
to mention both providers and clarify that the env-shim only promotes
ANTHROPIC/OPENAI from GSTACK_ — VOYAGE_API_KEY must be set directly in Conductor
workspace env.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: drop em-dashes + replace phantom embedding-migrations.md ref with inline recipe
CHANGELOG release-summary prose used em-dashes (violates voice rule) and
linked to docs/embedding-migrations.md which is gbrain's doc, not gstack's.
Replace with periods/commas and inline the dimension-mismatch recovery
recipe directly (mv + re-init).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
/**
|
|
* Tests the voyage-code-3 default contract in setup-gbrain's PGLite init
|
|
* sequences. The contract lives in the skill TEMPLATE (.tmpl), not in a TS
|
|
* helper — the skill follows AI-readable instructions.
|
|
*
|
|
* Contract (asserted here):
|
|
* 1. When VOYAGE_API_KEY is set, gstack's PGLite init passes
|
|
* --embedding-model voyage:voyage-code-3 --embedding-dimensions 1024
|
|
* 2. When VOYAGE_API_KEY is unset, those flags are omitted (gbrain's
|
|
* auto-selected provider chain takes over)
|
|
*
|
|
* Why a separate file from gbrain-init-rollback.test.ts: that file owns the
|
|
* .bak-rollback contract (Step 1.5 / 4.5 plan D7). This file owns the
|
|
* embedding-model selection contract. Both extract bash from the skill
|
|
* template and execute it against a fake gbrain.
|
|
*
|
|
* The fake gbrain records argv to a sentinel file so the test can assert
|
|
* exact flags. No Voyage API calls are made.
|
|
*/
|
|
|
|
import { describe, it, expect } from "bun:test";
|
|
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
existsSync,
|
|
rmSync,
|
|
chmodSync,
|
|
} from "fs";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { spawnSync } from "child_process";
|
|
|
|
interface FakeEnv {
|
|
tmp: string;
|
|
home: string;
|
|
bindir: string;
|
|
argvLog: string;
|
|
cleanup: () => void;
|
|
}
|
|
|
|
function makeFakeEnv(): FakeEnv {
|
|
const tmp = mkdtempSync(join(tmpdir(), "gbrain-voyage-init-"));
|
|
const home = join(tmp, "home");
|
|
const bindir = join(tmp, "bin");
|
|
const argvLog = join(tmp, "gbrain-argv.log");
|
|
mkdirSync(join(home, ".gbrain"), { recursive: true });
|
|
mkdirSync(bindir, { recursive: true });
|
|
|
|
// Fake gbrain logs every argv invocation to argvLog (one line per call),
|
|
// succeeds on init (writes a sentinel pglite config), and returns canned
|
|
// output for --version. Nothing else is needed for the shape test.
|
|
const fake = `#!/bin/sh
|
|
echo "$@" >> "${argvLog}"
|
|
case "$1" in
|
|
--version)
|
|
echo "gbrain 0.37.1.0"
|
|
exit 0
|
|
;;
|
|
init)
|
|
cat > "${home}/.gbrain/config.json" <<JSON
|
|
{"engine":"pglite","database_path":"${home}/.gbrain/brain.pglite"}
|
|
JSON
|
|
echo '{"status":"success","engine":"pglite","pages":0}'
|
|
exit 0
|
|
;;
|
|
esac
|
|
exit 0
|
|
`;
|
|
writeFileSync(join(bindir, "gbrain"), fake);
|
|
chmodSync(join(bindir, "gbrain"), 0o755);
|
|
|
|
return {
|
|
tmp,
|
|
home,
|
|
bindir,
|
|
argvLog,
|
|
cleanup: () => rmSync(tmp, { recursive: true, force: true }),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verbatim reimplementation of the skill template's voyage-code-3
|
|
* conditional. The template (setup-gbrain/SKILL.md.tmpl Path 3, Step 1.5
|
|
* inside the rollback wrapper, Step 4.5 Path 4 Yes branch) instructs the
|
|
* model to execute this bash; we execute the same bash here and assert the
|
|
* argv passed to gbrain matches the contract.
|
|
*
|
|
* If the template changes the flag set or the env-var name, this test
|
|
* should fail until the shell here is updated too — by design.
|
|
*/
|
|
function runInitWithVoyageGate(env: FakeEnv, voyageKey: string | undefined): string[] {
|
|
const script = `
|
|
set -u
|
|
GBRAIN_EMBED_FLAGS=""
|
|
if [ -n "\${VOYAGE_API_KEY:-}" ]; then
|
|
GBRAIN_EMBED_FLAGS="--embedding-model voyage:voyage-code-3 --embedding-dimensions 1024"
|
|
fi
|
|
gbrain init --pglite --json $GBRAIN_EMBED_FLAGS
|
|
`;
|
|
const baseEnv: Record<string, string> = {
|
|
...process.env,
|
|
HOME: env.home,
|
|
PATH: `${env.bindir}:/usr/bin:/bin`,
|
|
};
|
|
if (voyageKey === undefined) {
|
|
delete baseEnv.VOYAGE_API_KEY;
|
|
} else {
|
|
baseEnv.VOYAGE_API_KEY = voyageKey;
|
|
}
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
encoding: "utf-8",
|
|
env: baseEnv,
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`init script exited ${result.status}: ${result.stderr}`);
|
|
}
|
|
return readFileSync(env.argvLog, "utf-8").trim().split("\n");
|
|
}
|
|
|
|
describe("voyage-code-3 default for gstack-driven PGLite init", () => {
|
|
it("passes voyage-code-3 flags when VOYAGE_API_KEY is set", () => {
|
|
const env = makeFakeEnv();
|
|
try {
|
|
const calls = runInitWithVoyageGate(env, "vk_test_set");
|
|
expect(calls.length).toBe(1);
|
|
const argv = calls[0];
|
|
expect(argv).toContain("init --pglite --json");
|
|
expect(argv).toContain("--embedding-model voyage:voyage-code-3");
|
|
expect(argv).toContain("--embedding-dimensions 1024");
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
it("omits voyage flags when VOYAGE_API_KEY is unset", () => {
|
|
const env = makeFakeEnv();
|
|
try {
|
|
const calls = runInitWithVoyageGate(env, undefined);
|
|
expect(calls.length).toBe(1);
|
|
const argv = calls[0];
|
|
expect(argv).toContain("init --pglite --json");
|
|
expect(argv).not.toContain("voyage");
|
|
expect(argv).not.toContain("--embedding-model");
|
|
expect(argv).not.toContain("--embedding-dimensions");
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
|
|
it("treats empty-string VOYAGE_API_KEY the same as unset (no false positive)", () => {
|
|
const env = makeFakeEnv();
|
|
try {
|
|
const calls = runInitWithVoyageGate(env, "");
|
|
expect(calls.length).toBe(1);
|
|
expect(calls[0]).not.toContain("voyage");
|
|
} finally {
|
|
env.cleanup();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("template alignment: the .tmpl actually contains the voyage gate", () => {
|
|
// Belt-and-suspenders: if someone edits the template and drops the
|
|
// VOYAGE_API_KEY conditional without updating the test above, this catches
|
|
// it. The shell snippet under test must literally appear in the .tmpl.
|
|
const TEMPLATE_PATH = join(import.meta.dir, "..", "setup-gbrain", "SKILL.md.tmpl");
|
|
const tmpl = readFileSync(TEMPLATE_PATH, "utf-8");
|
|
|
|
it("setup-gbrain template gates the embedding-model flag on VOYAGE_API_KEY", () => {
|
|
// Should appear at least once (currently 3 init sites use the same gate).
|
|
expect(tmpl).toContain('if [ -n "${VOYAGE_API_KEY:-}" ]; then');
|
|
expect(tmpl).toContain("--embedding-model voyage:voyage-code-3");
|
|
expect(tmpl).toContain("--embedding-dimensions 1024");
|
|
});
|
|
|
|
it("setup-gbrain template uses the conditional gate at all 3 PGLite init sites", () => {
|
|
// Count the gate occurrences. If a future edit adds/removes a PGLite
|
|
// init site, update this expectation deliberately.
|
|
const matches = tmpl.match(/if \[ -n "\$\{VOYAGE_API_KEY:-\}" \]; then/g);
|
|
expect(matches?.length).toBe(3);
|
|
});
|
|
});
|