mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-07 14:06:42 +02:00
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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user