v1.26.3.0 feat: /sync-gbrain skill + native code-surface orchestrator (#1314)

* feat: native gbrain code-surface orchestrator + ensureSourceRegistered helper

Replaces gbrain import (markdown only) with gbrain sources add + sync
--strategy code (or reindex-code on --full). Adds lib/gbrain-sources.ts
exporting ensureSourceRegistered/probeSource/sourcePageCount, plus lock
file + tmp-rename atomicity + dry-run write skip in the orchestrator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: setup-gbrain Step 8 writes ## GBrain Search Guidance after smoke test

Extends Step 8 to write a machine-agnostic guidance block that teaches
the agent when to prefer gbrain CLI (search/query/code-def/code-refs/
code-callers/code-callees) over Grep. Gated on smoke test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: /sync-gbrain skill — keep gbrain current and refresh agent guidance

New top-level skill that wraps gstack-gbrain-sync with state probing,
capability check (write+search round-trip, not gbrain doctor), CLAUDE.md
guidance lifecycle (write iff healthy, remove iff broken), and a
per-source verdict block. Re-runnable, idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: preamble emits gbrain-availability block when capability ok

Extends generate-brain-sync-block.ts to emit Variant A (steady-state, 4
lines) when cwd page_count > 0 or Variant B (empty-corpus emergency, 3
lines) when 0; empty string otherwise. Reads cached page_count from
.gbrain-sync-state.json (handles pretty + compact JSON). Refreshes ship
golden fixtures and bumps the plan-review preamble byte budget to 35K
to absorb the new block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: register /sync-gbrain in AGENTS.md and docs/skills.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate SKILL.md across all hosts (gen:skill-docs)

Mechanical regeneration after preamble + setup-gbrain template + new
sync-gbrain skill. Run via: bun run gen:skill-docs --host all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v1.26.3.0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add /sync-gbrain to README skills table and gbrain section

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-04 09:29:48 -07:00
committed by GitHub
parent 30fe6bb11c
commit db9447c333
59 changed files with 3412 additions and 79 deletions
+81 -3
View File
@@ -55,7 +55,11 @@ describe("gstack-gbrain-sync CLI", () => {
const r = runScript(["--dry-run", "--code-only", "--quiet"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("would: gbrain import");
// Code stage now uses native code surface: sources add + sync --strategy code
// (NOT gbrain import — that's the markdown-only path that was rejected post-codex).
expect(r.stdout).toContain("would: gbrain sources add");
expect(r.stdout).toContain("gbrain sync --strategy code");
expect(r.stdout).not.toContain("gbrain import");
// memory + brain-sync stages should not appear
expect(r.stdout).not.toContain("gstack-memory-ingest --probe");
expect(r.stdout).not.toContain("gstack-brain-sync --discover-new");
@@ -69,7 +73,8 @@ describe("gstack-gbrain-sync CLI", () => {
const r = runScript(["--dry-run"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).toContain("would: gbrain import");
expect(r.stdout).toContain("would: gbrain sources add");
expect(r.stdout).toContain("gbrain sync --strategy code");
expect(r.stdout).toContain("would: gstack-memory-ingest");
expect(r.stdout).toContain("would: gstack-brain-sync");
rmSync(home, { recursive: true, force: true });
@@ -82,11 +87,84 @@ describe("gstack-gbrain-sync CLI", () => {
const r = runScript(["--dry-run", "--no-code"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).not.toContain("would: gbrain import");
expect(r.stdout).not.toContain("would: gbrain sources add");
expect(r.stdout).toContain("would: gstack-memory-ingest");
rmSync(home, { recursive: true, force: true });
});
it("dry-run derives a stable source id from the canonical git remote", () => {
// The source id pattern is `gstack-code-<canonicalized-remote>`. For this
// repo (github.com/garrytan/gstack), the slug should appear in the dry-run
// preview line. We don't pin the exact slug — just verify the prefix +
// that the preview command would target a source with id gstack-code-*.
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(["--dry-run", "--code-only", "--quiet"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
expect(r.stdout).toMatch(/gbrain sources add gstack-code-[a-z0-9-]+/);
expect(r.stdout).toMatch(/gbrain sync --strategy code --source gstack-code-[a-z0-9-]+/);
rmSync(home, { recursive: true, force: true });
});
it("dry-run does NOT acquire the lock file (lock is for write paths only)", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
const r = runScript(["--dry-run"], { HOME: home, GSTACK_HOME: gstackHome });
expect(r.exitCode).toBe(0);
// Lock file should not exist after a dry-run (it's a write-only safety primitive).
const lockPath = join(gstackHome, ".sync-gbrain.lock");
expect(existsSync(lockPath)).toBe(false);
rmSync(home, { recursive: true, force: true });
});
it("a stale lock file (older than 5 min) is taken over, not blocking", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
// Plant a stale lock file (mtime 6 min ago).
const lockPath = join(gstackHome, ".sync-gbrain.lock");
writeFileSync(lockPath, JSON.stringify({ pid: 99999, started_at: new Date(Date.now() - 6 * 60 * 1000).toISOString() }));
const sixMinAgo = (Date.now() - 6 * 60 * 1000) / 1000;
// Set mtime explicitly via Bun's fs.utimes
const fs = require("fs");
fs.utimesSync(lockPath, sixMinAgo, sixMinAgo);
// Run with all stages disabled so we don't actually invoke anything heavy.
const r = runScript(["--incremental", "--no-code", "--no-memory", "--no-brain-sync", "--quiet"], {
HOME: home,
GSTACK_HOME: gstackHome,
});
expect(r.exitCode).toBe(0);
// Lock should be cleared after the run (we took it over and released).
expect(existsSync(lockPath)).toBe(false);
rmSync(home, { recursive: true, force: true });
});
it("a fresh lock file (less than 5 min old) blocks a second invocation with exit 2", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");
mkdirSync(gstackHome, { recursive: true });
// Plant a fresh lock file (mtime now).
const lockPath = join(gstackHome, ".sync-gbrain.lock");
writeFileSync(lockPath, JSON.stringify({ pid: 99999, started_at: new Date().toISOString() }));
const r = runScript(["--incremental", "--no-code", "--no-memory", "--no-brain-sync", "--quiet"], {
HOME: home,
GSTACK_HOME: gstackHome,
});
expect(r.exitCode).toBe(2);
expect(r.stderr).toContain("another /sync-gbrain is running");
// Lock should still be there — the second invocation didn't take it over.
expect(existsSync(lockPath)).toBe(true);
rmSync(home, { recursive: true, force: true });
});
it("writes a state file with schema_version: 1 after a non-dry run", () => {
const home = makeTestHome();
const gstackHome = join(home, ".gstack");