From 16aed1ced5ad16b8be5864ac1abbe7fead870ff6 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 21:19:35 +0000 Subject: [PATCH] A5: brain-sync stage fails loud when the artifacts source indexed zero pages The stage reported "curated artifacts pushed" whenever gstack-brain-sync exited 0, but a git push is not gbrain indexing. After a successful push the stage now reads the maintained artifacts source's page count (GSTACK_BRAIN_SOURCE_ID, else the source whose path is the artifacts worktree or state root): zero pages is ERR with the fix command; a missing source or an unavailable count says so instead of claiming indexing. Co-authored-by: Jimmy Zeisweiss <4261258+jzeisweiss@users.noreply.github.com> --- bin/gstack-gbrain-sync.ts | 51 ++++++++++++++++++++++++++------- docs/gbrain-sync-errors.md | 19 ++++++++++++ test/gstack-gbrain-sync.test.ts | 30 +++++++++++++++++++ 3 files changed, 90 insertions(+), 10 deletions(-) diff --git a/bin/gstack-gbrain-sync.ts b/bin/gstack-gbrain-sync.ts index 04cf99ef2..6af701b2f 100644 --- a/bin/gstack-gbrain-sync.ts +++ b/bin/gstack-gbrain-sync.ts @@ -214,6 +214,13 @@ export function resolveMemoryIngestSources( * state root itself), if any. Its pages already cover FEDERATED_CURATED_TYPES. */ export function federatedCuratedSourceId(env?: NodeJS.ProcessEnv): string | null { + const rows = parseSourcesList(execGbrainJson(["sources", "list", "--json"], { baseEnv: env, timeout: 10_000 })); + const row = rows.find((r) => (r as { federated?: boolean }).federated === true && isArtifactsSourceRow(r, env)); + return row?.id ?? null; +} + +/** A source this installation maintains for its curated artifacts (the worktree or the state root). */ +function isArtifactsSourceRow(r: { local_path?: string }, env?: NodeJS.ProcessEnv): boolean { const realOrSelf = (p: string): string => { try { return realpathSync(p); @@ -226,9 +233,36 @@ export function federatedCuratedSourceId(env?: NodeJS.ProcessEnv): string | null realOrSelf((env ?? process.env).GSTACK_BRAIN_WORKTREE || join(home, ".gstack-brain-worktree")), realOrSelf(GSTACK_HOME), ]); - const rows = parseSourcesList(execGbrainJson(["sources", "list", "--json"], { baseEnv: env, timeout: 10_000 })); - const row = rows.find((r) => (r as { federated?: boolean }).federated === true && r.local_path && owned.has(realOrSelf(r.local_path))); - return row?.id ?? null; + return !!r.local_path && owned.has(realOrSelf(r.local_path)); +} + +/** + * A5 (#2670): after a successful artifacts push, report what gbrain actually + * holds. A git push is not indexing; a maintained artifacts source with zero + * pages is the one provably broken state and fails the stage. + */ +export function brainSyncIndexVerdict(env: NodeJS.ProcessEnv = process.env): { ok: boolean; summary: string } { + const raw = execGbrainJson(["sources", "list", "--json"], { baseEnv: env, timeout: 10_000 }); + if (raw === null) { + return { ok: true, summary: "curated artifacts pushed to git; gbrain page count unavailable (gbrain sources list failed)" }; + } + const explicit = env.GSTACK_BRAIN_SOURCE_ID; + const row = parseSourcesList(raw).find((r) => (explicit ? r.id === explicit : isArtifactsSourceRow(r, env))); + if (!row?.id) { + return { ok: true, summary: "curated artifacts pushed to git (no gbrain artifacts source on this machine, so indexing is not checked here)" }; + } + if (typeof row.page_count !== "number") { + return { ok: true, summary: `curated artifacts pushed to git; gbrain source ${row.id} did not report a page count` }; + } + if (row.page_count === 0) { + return { + ok: false, + summary: + `curated artifacts pushed to git, but gbrain source ${row.id} has 0 indexed pages. ` + + `Fix: gbrain sync --source ${row.id}, then re-run /sync-gbrain`, + }; + } + return { ok: true, summary: `curated artifacts pushed; gbrain source ${row.id} has ${row.page_count} pages` }; } export interface MemorySourceSelection { @@ -1598,13 +1632,10 @@ function runBrainSyncPush(args: CliArgs): StageResult { spawnSync(discover.cmd, discover.argv, { stdio, timeout: 60 * 1000, shell: discover.shell }); const result = spawnSync(once.cmd, once.argv, { stdio, timeout: 60 * 1000, shell: once.shell }); - return { - name: "brain-sync", - ran: true, - ok: result.status === 0, - duration_ms: Date.now() - t0, - summary: result.status === 0 ? "curated artifacts pushed" : `gstack-brain-sync exited ${result.status}`, - }; + if (result.status !== 0) { + return { name: "brain-sync", ran: true, ok: false, duration_ms: Date.now() - t0, summary: `gstack-brain-sync exited ${result.status}` }; + } + return { name: "brain-sync", ran: true, duration_ms: Date.now() - t0, ...brainSyncIndexVerdict() }; } /** diff --git a/docs/gbrain-sync-errors.md b/docs/gbrain-sync-errors.md index 15206d83a..79e573a6d 100644 --- a/docs/gbrain-sync-errors.md +++ b/docs/gbrain-sync-errors.md @@ -408,6 +408,25 @@ gstack-gbrain-install # upgrade gbrain --- +## `curated artifacts pushed to git, but gbrain source has 0 indexed pages. Fix: gbrain sync --source , then re-run /sync-gbrain` + +**Problem.** `/sync-gbrain`'s brain-sync stage is `ERR` even though the git +push worked. + +**Cause.** The push only moves your curated artifacts to git. gbrain indexes +them separately, and the artifacts source this machine maintains reports zero +pages, so searches return nothing from it. Before this release the stage said +"curated artifacts pushed" and passed. + +**Fix.** +```bash +gbrain sync --source +/sync-gbrain +``` +If it stays at zero, re-register the source with `gstack-gbrain-source-wireup`. + +--- + ## Nothing is syncing but I expect it to **Not an error, but a common gotcha.** Check in order: diff --git a/test/gstack-gbrain-sync.test.ts b/test/gstack-gbrain-sync.test.ts index 322be574b..ac0091bdf 100644 --- a/test/gstack-gbrain-sync.test.ts +++ b/test/gstack-gbrain-sync.test.ts @@ -544,6 +544,36 @@ esac rmSync(home, { recursive: true, force: true }); }); + // A5 (#2670): a successful git push is not gbrain indexing. + for (const [pageCount, ok, expected] of [ + [0, false, "gbrain source fixture-artifacts has 0 indexed pages. Fix: gbrain sync --source fixture-artifacts"], + [7, true, "curated artifacts pushed; gbrain source fixture-artifacts has 7 pages"], + ] as const) { + it(`brain-sync stage reports the artifacts source's ${pageCount} indexed pages (ok=${ok})`, () => { + const home = makeTestHome(); + const gstackHome = join(home, ".gstack"); + const fakeBin = join(home, "fake-bin"); + mkdirSync(gstackHome, { recursive: true }); + mkdirSync(fakeBin, { recursive: true }); + writeFileSync(join(fakeBin, "gbrain"), `#!/bin/sh +case "$*" in + "sources list --json") printf '%s\\n' '{"sources":[{"id":"fixture-artifacts","local_path":"${gstackHome}","page_count":${pageCount}}]}' ;; + *) exit 1 ;; +esac +`); + chmodSync(join(fakeBin, "gbrain"), 0o755); + const r = runScript(["--incremental", "--no-code", "--no-memory", "--quiet"], { + HOME: home, GSTACK_HOME: gstackHome, PATH: `${fakeBin}:${process.env.PATH || ""}`, + }); + const state = JSON.parse(readFileSync(join(gstackHome, ".gbrain-sync-state.json"), "utf-8")); + const stage = state.last_stages.find((entry: { name: string }) => entry.name === "brain-sync"); + expect(stage.summary).toContain(expected); + expect(stage.ok).toBe(ok); + expect(r.exitCode).toBe(ok ? 0 : 1); + rmSync(home, { recursive: true, force: true }); + }); + } + it("brain-sync stage resolves the sibling binary, not a HOME-rooted path", () => { // Regression for Codex M9: pre-fix the orchestrator looked up // ~/.claude/skills/gstack/bin/gstack-brain-sync, which silently no-op'd