diff --git a/README.md b/README.md index d89b8d998..68807e958 100644 --- a/README.md +++ b/README.md @@ -395,7 +395,7 @@ Four paths, pick one: - **PGLite local** — zero accounts, zero network, ~30 seconds. Isolated brain on this Mac only. Great for try-first; migrate to Supabase later with `/setup-gbrain --switch`. - **Remote gbrain MCP** — your brain runs on another machine (Tailscale, ngrok, internal LAN) or a teammate's server; paste an MCP URL and bearer token. Optionally pair with a local PGLite for symbol-aware code search in split-engine mode. Best for cross-machine memory without standing up a local DB. -After init, the skill offers to register gbrain as an MCP server for Claude Code (`claude mcp add gbrain -- gbrain serve`) so `gbrain search`, `gbrain put_page`, etc. show up as first-class typed tools — not bash shell-outs. +After init, the skill offers to register gbrain as an MCP server for Claude Code (`claude mcp add gbrain -- gbrain serve`) so `gbrain search`, `gbrain put`, etc. show up as first-class typed tools — not bash shell-outs. **Keeping the brain current.** Run `/sync-gbrain` from any repo to re-index its code into gbrain (incremental by default, `--full` for a full reindex, `--dry-run` to preview). The skill registers the cwd as a federated source via `gbrain sources add`, runs `gbrain sync --strategy code`, and writes a `## GBrain Search Guidance` block to your project's CLAUDE.md so the agent prefers `gbrain search`/`code-def`/`code-refs` over Grep. The block is removed automatically if the capability check fails — no stale guidance pointing at tools that aren't installed. diff --git a/USING_GBRAIN_WITH_GSTACK.md b/USING_GBRAIN_WITH_GSTACK.md index ef8052c2f..7507f3be0 100644 --- a/USING_GBRAIN_WITH_GSTACK.md +++ b/USING_GBRAIN_WITH_GSTACK.md @@ -82,7 +82,7 @@ By default the skill asks "Give Claude Code a typed tool surface for gbrain?" If claude mcp add gbrain -- gbrain serve ``` -That registers gbrain's stdio MCP server with Claude Code. Now `gbrain search`, `gbrain put_page`, `gbrain get_page`, etc. show up as first-class tools in every session, not bash shell-outs. +That registers gbrain's stdio MCP server with Claude Code. Now `gbrain search`, `gbrain put`, `gbrain get`, etc. show up as first-class tools in every session, not bash shell-outs. **If `claude` is not on PATH**, the skill skips MCP registration gracefully with a manual-register hint. The CLI resolver still works from any skill that shells out to `gbrain` — MCP is an upgrade, not a prerequisite. @@ -224,8 +224,8 @@ Gbrain itself ships with these that gstack wraps: | `gbrain migrate --to supabase --url ...` | Move a PGLite brain to Supabase (lossless, preserves source as backup) | | `gbrain migrate --to pglite` | Reverse migration | | `gbrain search "query"` | Search the brain | -| `gbrain put_page --title "..." --tags "a,b" <<<"content"` | Write a page | -| `gbrain get_page ""` | Fetch a page | +| `gbrain put "" --content ""` | Write a page (title/tags go in YAML frontmatter inside `--content`) | +| `gbrain get ""` | Fetch a page | | `gbrain serve` | Start the MCP stdio server (used by `claude mcp add`) | ### Config files + state diff --git a/scripts/resolvers/gbrain.ts b/scripts/resolvers/gbrain.ts index c6e54423b..cf6e6f791 100644 --- a/scripts/resolvers/gbrain.ts +++ b/scripts/resolvers/gbrain.ts @@ -37,18 +37,22 @@ Any non-zero exit code from gbrain commands should be treated as a transient fai } export function generateGBrainSaveResults(ctx: TemplateContext): string { + // gbrain v0.18+ renamed `put_page` → `put ` and moved --title/--tags + // into YAML frontmatter inside --content. These templates render into + // SKILL.md files as user-facing instructions; using the old subcommand + // ships broken copy-paste to every gstack user. const skillSaveMap: Record = { - 'office-hours': 'Save the design document as a brain page:\n```bash\ngbrain put_page --title "Office Hours: " --tags "design-doc," <<\'EOF\'\n\nEOF\n```', - 'investigate': 'Save the root cause analysis as a brain page:\n```bash\ngbrain put_page --title "Investigation: " --tags "investigation," <<\'EOF\'\n\nEOF\n```', - 'plan-ceo-review': 'Save the CEO plan as a brain page:\n```bash\ngbrain put_page --title "CEO Plan: " --tags "ceo-plan," <<\'EOF\'\n\nEOF\n```', - 'retro': 'Save the retrospective as a brain page:\n```bash\ngbrain put_page --title "Retro: " --tags "retro," <<\'EOF\'\n\nEOF\n```', - 'plan-eng-review': 'Save the architecture decisions as a brain page:\n```bash\ngbrain put_page --title "Eng Review: " --tags "eng-review," <<\'EOF\'\n\nEOF\n```', - 'ship': 'Save the release notes as a brain page:\n```bash\ngbrain put_page --title "Release: " --tags "release," <<\'EOF\'\n\nEOF\n```', - 'cso': 'Save the security audit as a brain page:\n```bash\ngbrain put_page --title "Security Audit: " --tags "security-audit," <<\'EOF\'\n\nEOF\n```', - 'design-consultation': 'Save the design system as a brain page:\n```bash\ngbrain put_page --title "Design System: " --tags "design-system," <<\'EOF\'\n\nEOF\n```', + 'office-hours': 'Save the design document as a brain page:\n```bash\ngbrain put "office-hours/" --content "$(cat <<\'EOF\'\n---\ntitle: "Office Hours: "\ntags: [design-doc, ]\n---\n\nEOF\n)"\n```', + 'investigate': 'Save the root cause analysis as a brain page:\n```bash\ngbrain put "investigations/" --content "$(cat <<\'EOF\'\n---\ntitle: "Investigation: "\ntags: [investigation, ]\n---\n\nEOF\n)"\n```', + 'plan-ceo-review': 'Save the CEO plan as a brain page:\n```bash\ngbrain put "ceo-plans/" --content "$(cat <<\'EOF\'\n---\ntitle: "CEO Plan: "\ntags: [ceo-plan, ]\n---\n\nEOF\n)"\n```', + 'retro': 'Save the retrospective as a brain page:\n```bash\ngbrain put "retros/" --content "$(cat <<\'EOF\'\n---\ntitle: "Retro: "\ntags: [retro, ]\n---\n\nEOF\n)"\n```', + 'plan-eng-review': 'Save the architecture decisions as a brain page:\n```bash\ngbrain put "eng-reviews/" --content "$(cat <<\'EOF\'\n---\ntitle: "Eng Review: "\ntags: [eng-review, ]\n---\n\nEOF\n)"\n```', + 'ship': 'Save the release notes as a brain page:\n```bash\ngbrain put "releases/" --content "$(cat <<\'EOF\'\n---\ntitle: "Release: "\ntags: [release, ]\n---\n\nEOF\n)"\n```', + 'cso': 'Save the security audit as a brain page:\n```bash\ngbrain put "security-audits/" --content "$(cat <<\'EOF\'\n---\ntitle: "Security Audit: "\ntags: [security-audit, ]\n---\n\nEOF\n)"\n```', + 'design-consultation': 'Save the design system as a brain page:\n```bash\ngbrain put "design-systems/" --content "$(cat <<\'EOF\'\n---\ntitle: "Design System: "\ntags: [design-system, ]\n---\n\nEOF\n)"\n```', }; - const saveInstruction = skillSaveMap[ctx.skillName] || 'Save the skill output as a brain page if the results are worth preserving:\n```bash\ngbrain put_page --title "" --tags "" <<\'EOF\'\n\nEOF\n```'; + const saveInstruction = skillSaveMap[ctx.skillName] || 'Save the skill output as a brain page if the results are worth preserving:\n```bash\ngbrain put "" --content "$(cat <<\'EOF\'\n---\ntitle: ""\ntags: [, ]\n---\n\nEOF\n)"\n```'; return `## Save Results to Brain @@ -58,7 +62,14 @@ ${saveInstruction} After saving the page, extract and enrich mentioned entities: for each actual person name or company/organization name found in the output, \`gbrain search ""\` to check if a page exists. If not, create a stub page: \`\`\`bash -gbrain put_page --title "" --tags "entity,person" --content "Stub page. Mentioned in output." +gbrain put "entities/" --content "$(cat <<'EOF' +--- +title: "" +tags: [entity, person] +--- +Stub page. Mentioned in output. +EOF +)" \`\`\` Only extract actual person names and company/organization names. Skip product names, section headings, technical terms, and file paths. diff --git a/test/resolvers-gbrain-put-rewrite.test.ts b/test/resolvers-gbrain-put-rewrite.test.ts new file mode 100644 index 000000000..1f9cac82a --- /dev/null +++ b/test/resolvers-gbrain-put-rewrite.test.ts @@ -0,0 +1,63 @@ +/** + * Regression pin: scripts/resolvers/gbrain.ts must emit `gbrain put ` + * (the v0.18+ subcommand), never the renamed `gbrain put_page`. The resolver + * output ships into every generated SKILL.md file as user-facing + * copy-paste instructions; using the old subcommand teaches every + * gstack user to invoke a command that no longer exists. + * + * Two checks: + * 1. Resolver source: scripts/resolvers/gbrain.ts has no `put_page` + * tokens in active strings (comments OK — one annotated reference + * explains the rename for future contributors). + * 2. Generated SKILL.md: every tracked SKILL.md file is free of + * `gbrain put_page`. Run `bun run gen:skill-docs` if this fails. + */ + +import { describe, it, expect } from "bun:test"; +import { readFileSync, readdirSync, statSync } from "fs"; +import { join } from "path"; +import { execFileSync } from "child_process"; + +const REPO_ROOT = join(import.meta.dir, ".."); +const RESOLVER_PATH = join(REPO_ROOT, "scripts", "resolvers", "gbrain.ts"); + +function stripComments(src: string): string { + // Strip block comments first (may span newlines, may contain `//`). + const noBlock = src.replace(/\/\*[\s\S]*?\*\//g, ""); + return noBlock.replace(/\/\/[^\n]*/g, ""); +} + +function listTrackedSkillMd(): string[] { + const out = execFileSync("git", ["ls-files", "*SKILL.md"], { + cwd: REPO_ROOT, + encoding: "utf-8", + }); + return out.split("\n").filter((line) => line.trim().length > 0); +} + +describe("scripts/resolvers/gbrain.ts — no put_page in emitted instructions (regression for #1346)", () => { + it("resolver source ships only `gbrain put` instructions, not the renamed `put_page`", () => { + const src = readFileSync(RESOLVER_PATH, "utf-8"); + const stripped = stripComments(src); + expect(stripped).not.toContain("put_page"); + }); + + it("every tracked SKILL.md file is free of the renamed gbrain put_page subcommand", () => { + const files = listTrackedSkillMd(); + const offenders: string[] = []; + for (const f of files) { + const content = readFileSync(join(REPO_ROOT, f), "utf-8"); + if (content.includes("gbrain put_page")) { + offenders.push(f); + } + } + if (offenders.length > 0) { + throw new Error( + `Generated SKILL.md files still reference 'gbrain put_page'. ` + + `Run 'bun run gen:skill-docs' to regenerate after editing ` + + `scripts/resolvers/gbrain.ts. Offenders:\n - ${offenders.join("\n - ")}`, + ); + } + expect(offenders).toHaveLength(0); + }); +});