From 76bf72d89fbf126146b74e858a79a827f7d76eee Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 3 Oct 2026 21:26:00 +0000 Subject: [PATCH] A7: run only the resolve_symbol_edges dream phase, detected first /sync-gbrain --dream and the --full auto-build ran gbrain's full dream cycle (~35 minutes, LLM phases) when the call graph needs only the resolve_symbol_edges phase. The stage now runs gbrain dream [--source ] --phase resolve_symbol_edges when the installed gbrain accepts that phase (detected by behavior: gbrain validates --phase before printing --help). Otherwise it skips the forced dream and says the full cycle costs about 35 minutes and how to run it, instead of starting it. Co-authored-by: liutiming --- bin/gstack-gbrain-sync.ts | 33 ++++++++++++++++++-- docs/gbrain-sync-errors.md | 19 ++++++++++++ sync-gbrain/SKILL.md.tmpl | 9 +++--- test/gbrain-dream-stage.test.ts | 53 ++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 8 deletions(-) diff --git a/bin/gstack-gbrain-sync.ts b/bin/gstack-gbrain-sync.ts index 4990d5b9b..826ccbc10 100644 --- a/bin/gstack-gbrain-sync.ts +++ b/bin/gstack-gbrain-sync.ts @@ -1690,6 +1690,16 @@ export function shouldRunDream(args: CliArgs, cycle: CycleStatus | null): boolea * binary / malformed env) — a broken install must be visible, not disguised as * optional maintenance. */ +/** + * A7: detected, not assumed. gbrain validates --phase values while parsing, + * before --help prints, so a gbrain without the phase exits non-zero and one + * without --phase prints help that never mentions it. + */ +export function gbrainSupportsSymbolEdgePhase(env: NodeJS.ProcessEnv = process.env): boolean { + const r = spawnGbrain(["dream", "--phase", "resolve_symbol_edges", "--help"], { baseEnv: env, timeout: 15_000 }); + return r.status === 0 && /--phase\b/.test(`${r.stdout || ""}${r.stderr || ""}`); +} + export async function runDream(args: CliArgs): Promise { const t0 = Date.now(); @@ -1702,8 +1712,8 @@ export async function runDream(args: CliArgs): Promise { ok: true, duration_ms: 0, summary: sourceId - ? `would: gbrain dream --source ${sourceId} (build this source's call graph)` - : "would: gbrain dream (call-graph build)", + ? `would: gbrain dream --source ${sourceId} --phase resolve_symbol_edges (build this source's call graph; skipped if gbrain cannot scope the phase)` + : "would: gbrain dream --phase resolve_symbol_edges (call-graph build; skipped if gbrain cannot scope the phase)", }; } @@ -1744,7 +1754,24 @@ export async function runDream(args: CliArgs): Promise { // only when we can't derive the source id (not in a git repo). const root = repoRoot(); const sourceId = root ? resolveCodeSourceId(root, gbrainEnv) : null; - const dreamArgs = sourceId ? ["dream", "--source", sourceId] : ["dream"]; + // A7 (#2783): the call graph needs only the resolve_symbol_edges phase. A + // full `gbrain dream` runs every maintenance phase, LLM ones included + // (~35 min). Scope it when the installed gbrain can; never fall back to + // the full cycle on its own. + if (!gbrainSupportsSymbolEdgePhase()) { + return { + name: "dream", + ran: false, + ok: true, + duration_ms: Date.now() - t0, + summary: + "skipped — the installed gbrain cannot run only the resolve_symbol_edges phase, and the full dream cycle " + + `costs about 35 minutes (LLM phases). Upgrade with gstack-gbrain-install, or run it yourself: gbrain dream${sourceId ? ` --source ${sourceId}` : ""}`, + }; + } + const dreamArgs = sourceId + ? ["dream", "--source", sourceId, "--phase", "resolve_symbol_edges"] + : ["dream", "--phase", "resolve_symbol_edges"]; // spawnGbrain seeds DATABASE_URL from gbrain's config via buildGbrainEnv. // diff --git a/docs/gbrain-sync-errors.md b/docs/gbrain-sync-errors.md index 01ee3703d..b7275210e 100644 --- a/docs/gbrain-sync-errors.md +++ b/docs/gbrain-sync-errors.md @@ -453,6 +453,25 @@ with `gbrain sources remove ` only if you are sure. --- +## `dream: skipped — the installed gbrain cannot run only the resolve_symbol_edges phase, and the full dream cycle costs about 35 minutes (LLM phases).` + +**Problem.** `/sync-gbrain --dream` (or `--full`) did not build the call +graph. + +**Cause.** The call graph needs only gbrain's `resolve_symbol_edges` phase. +The installed gbrain cannot run that phase alone (no `dream --phase`, or no +such phase), and gstack no longer starts gbrain's full maintenance cycle on +its own: it takes about 35 minutes and runs LLM phases. + +**Fix.** +```bash +gstack-gbrain-install # upgrade gbrain, then: +/sync-gbrain --dream +``` +Or run the full cycle yourself when you have the time: `gbrain dream --source `. + +--- + ## Nothing is syncing but I expect it to **Not an error, but a common gotcha.** Check in order: diff --git a/sync-gbrain/SKILL.md.tmpl b/sync-gbrain/SKILL.md.tmpl index 5732996e4..accc5d66d 100644 --- a/sync-gbrain/SKILL.md.tmpl +++ b/sync-gbrain/SKILL.md.tmpl @@ -47,8 +47,8 @@ When the user types `/sync-gbrain`, run this skill. Argument modes (parsed by the skill itself, not a dispatcher binary): - `/sync-gbrain` — incremental sync (default; mtime fast-path; ~50ms steady-state) -- `/sync-gbrain --full` — full code reindex via `gbrain reindex-code` (~25-35 min on a big repo). Auto-builds the call graph (`gbrain dream`) **only when it was never built**. -- `/sync-gbrain --dream` — build this source's call graph (`gbrain code-callers`/`code-callees`) via a source-scoped `gbrain dream --source ` cycle; ~minutes; runs lock-free after the sync stages. Always forces, even if already built. Only produces a graph on a code-aware schema pack; otherwise the run reports a WARN explaining why the graph is still empty. +- `/sync-gbrain --full` — full code reindex via `gbrain reindex-code` (~25-35 min on a big repo). Auto-builds the call graph (`gbrain dream --phase resolve_symbol_edges`) **only when it was never built**. +- `/sync-gbrain --dream` — build this source's call graph (`gbrain code-callers`/`code-callees`) via `gbrain dream --source --phase resolve_symbol_edges`; ~minutes; runs lock-free after the sync stages. Always forces, even if already built. Runs only that phase, never gbrain's full maintenance cycle (about 35 minutes with LLM phases); if the installed gbrain cannot scope the phase, the dream row says so and nothing runs. Only produces a graph on a code-aware schema pack; otherwise the run reports a WARN explaining why the graph is still empty. - `/sync-gbrain --no-dream` — skip the dream cycle that `--full` would otherwise auto-run. - `/sync-gbrain --code-only` — only run the code stage; skip memory + brain-sync - `/sync-gbrain --dry-run` — preview what would sync; no writes anywhere @@ -346,8 +346,9 @@ If `CYCLE == never` AND the user did NOT pass `--dream`/`--full` AND Step 3 > > ELI10: `gbrain code-callers`/`code-callees` (who calls this function / what it > calls) return nothing until the `resolve_symbol_edges` phase runs for this -> source. `gbrain dream --source ` runs it (scoped to this -> worktree's code, takes a few minutes). It only produces a graph if this +> source. `gbrain dream --source --phase resolve_symbol_edges` +> runs only that phase (scoped to this worktree's code, takes a few minutes, +> not the full ~35-minute maintenance cycle). It only produces a graph if this > source's schema pack extracts code symbols; if it doesn't, the run completes > but the graph stays empty and the dream row will say so. > diff --git a/test/gbrain-dream-stage.test.ts b/test/gbrain-dream-stage.test.ts index 886f370b2..9aa4ef104 100644 --- a/test/gbrain-dream-stage.test.ts +++ b/test/gbrain-dream-stage.test.ts @@ -14,7 +14,7 @@ */ import { describe, it, expect, afterEach } from "bun:test"; -import { mkdtempSync, existsSync, writeFileSync, utimesSync, rmSync } from "fs"; +import { mkdtempSync, existsSync, writeFileSync, utimesSync, rmSync, mkdirSync, readFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; import { usePrivateStateRoot } from "./helpers/private-state-root"; @@ -279,3 +279,54 @@ describe("formatStage — WARN render", () => { expect(formatStage({ ...base, ran: false, ok: true })).toContain("SKIP"); }); }); + +// A7 (#2783): the forced dream runs only the resolve_symbol_edges phase, and +// only when the installed gbrain can scope it (detected, not assumed). +describe("dream stage scopes gbrain dream to resolve_symbol_edges (A7)", () => { + function sandbox(phaseSupport: "phase" | "no-phase-flag" | "unknown-phase") { + const home = mkdtempSync(join(tmpdir(), "gstack-dream-phase-")); + const bin = join(home, "bin"); + const cwd = join(home, "not-a-repo"); + mkdirSync(join(home, ".gbrain"), { recursive: true }); + mkdirSync(bin, { recursive: true }); + mkdirSync(cwd, { recursive: true }); + writeFileSync(join(home, ".gbrain", "config.json"), JSON.stringify({ engine: "pglite", database_path: join(home, ".gbrain", "brain.pglite") })); + const help = phaseSupport === "no-phase-flag" ? "Usage: gbrain dream [--source ]" : " --phase Run only the named phase(s)."; + writeFileSync(join(bin, "gbrain"), `#!/bin/sh +echo "$*" >> "${home}/calls.log" +case "$1" in + --version) echo "gbrain 0.60.37.0" ;; + sources) echo '{"sources":[]}' ;; + dream) + if [ "${phaseSupport}" = "unknown-phase" ]; then echo 'Unknown phase "resolve_symbol_edges".' >&2; exit 1; fi + case " $* " in *" --help "*) echo "${help}"; exit 0 ;; esac + echo "[cycle] resolve_symbol_edges: resolved 4 edges" ;; + *) exit 1 ;; +esac +`, { mode: 0o755 }); + const r = spawnSync(process.execPath, [SCRIPT, "--dream", "--no-code", "--no-memory", "--no-brain-sync"], { + cwd, + encoding: "utf-8", + timeout: 60_000, + env: { ...process.env, HOME: home, GSTACK_HOME: join(home, ".gstack"), GBRAIN_HOME: "", PATH: `${bin}:/usr/bin:/bin` }, + }); + const calls = readFileSync(join(home, "calls.log"), "utf-8").trim().split("\n").filter((l) => l.startsWith("dream")); + rmSync(home, { recursive: true, force: true }); + return { out: (r.stdout || "") + (r.stderr || ""), calls }; + } + + it("runs `gbrain dream --phase resolve_symbol_edges` when gbrain supports it, never the full cycle", () => { + const { calls } = sandbox("phase"); + const cycles = calls.filter((c) => !c.includes("--help")); + expect(cycles).toEqual(["dream --phase resolve_symbol_edges"]); + }); + + for (const kind of ["no-phase-flag", "unknown-phase"] as const) { + it(`skips the forced dream and names the 35-minute full cycle when gbrain cannot scope it (${kind})`, () => { + const { out, calls } = sandbox(kind); + expect(calls.every((c) => c.includes("--help"))).toBe(true); + expect(out).toContain("full dream cycle costs about 35 minutes"); + expect(out).toContain("gstack-gbrain-install"); + }); + } +});