diff --git a/bin/gstack-brain-context-load.ts b/bin/gstack-brain-context-load.ts index 8ad4eb63c..ae1a5b11f 100644 --- a/bin/gstack-brain-context-load.ts +++ b/bin/gstack-brain-context-load.ts @@ -175,12 +175,20 @@ function resolveSkillFile(args: CliArgs): string | null { return resolve(args.skillFile); } if (!args.skill) return null; + const skill = args.skill; + const codexSkill = skill.startsWith("gstack-") ? skill : `gstack-${skill}`; // Look in common gstack skill locations const candidates = [ - join(HOME, ".claude", "skills", args.skill, "SKILL.md"), - join(HOME, ".claude", "skills", "gstack", args.skill, "SKILL.md"), - join(process.cwd(), ".claude", "skills", args.skill, "SKILL.md"), - join(process.cwd(), args.skill, "SKILL.md"), + join(process.cwd(), ".agents", "skills", codexSkill, "SKILL.md"), + join(process.cwd(), ".agents", "skills", "gstack", skill, "SKILL.md"), + join(HOME, ".codex", "skills", codexSkill, "SKILL.md"), + join(HOME, ".codex", "skills", "gstack", skill, "SKILL.md"), + join(HOME, ".agents", "skills", codexSkill, "SKILL.md"), + join(HOME, ".agents", "skills", "gstack", skill, "SKILL.md"), + join(HOME, ".claude", "skills", skill, "SKILL.md"), + join(HOME, ".claude", "skills", "gstack", skill, "SKILL.md"), + join(process.cwd(), ".claude", "skills", skill, "SKILL.md"), + join(process.cwd(), skill, "SKILL.md"), ]; for (const c of candidates) { if (existsSync(c)) return c; @@ -391,7 +399,7 @@ function defaultManifest(args: CliArgs): GbrainManifest { { id: "skill-name-events", kind: "list", - filter: { type: "timeline", content_contains: "{skill_name}" }, + filter: { type: "timeline", "tags_contains": "repo:{repo_slug}", content_contains: "{skill_name}" }, limit: 5, render_as: "## Recent {skill_name} events", }, diff --git a/test/gstack-brain-context-load.test.ts b/test/gstack-brain-context-load.test.ts index 61985f0fc..4bcd7504e 100644 --- a/test/gstack-brain-context-load.test.ts +++ b/test/gstack-brain-context-load.test.ts @@ -14,11 +14,12 @@ import { spawnSync } from "child_process"; const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-brain-context-load.ts"); -function runScript(args: string[], env: Record = {}): { stdout: string; stderr: string; exitCode: number } { +function runScript(args: string[], env: Record = {}, cwd: string = process.cwd()): { stdout: string; stderr: string; exitCode: number } { const result = spawnSync("bun", [SCRIPT, ...args], { encoding: "utf-8", timeout: 30000, env: { ...process.env, ...env }, + cwd, }); return { stdout: result.stdout || "", @@ -89,6 +90,118 @@ describe("gstack-brain-context-load — manifest dispatch", () => { expect(r.stderr).toContain("queries=3"); }); + it("default manifest keeps every list query scoped to the active repo", () => { + const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); + const binDir = join(dir, "bin"); + mkdirSync(binDir); + writeFakeGbrain(binDir); + + try { + const r = runScript(["--skill", "nonexistent-skill-xyz", "--repo", "test-repo"], prependPath(binDir)); + expect(r.exitCode).toBe(0); + const repoFilterCount = (r.stdout.match(/tags_contains=repo:test-repo/g) || []).length; + expect(repoFilterCount).toBe(3); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("resolves generated Codex skill directories before falling back to the default manifest", () => { + const home = mkdtempSync(join(tmpdir(), "gstack-home-")); + const skillDir = join(home, ".codex", "skills", "gstack-codex-only"); + mkdirSync(skillDir, { recursive: true }); + const note = join(home, "note.md"); + writeFileSync(note, "codex manifest note\n", "utf-8"); + writeFileSync( + join(skillDir, "SKILL.md"), + `--- +name: codex-only +gbrain: + schema: 1 + context_queries: + - id: codex-note + kind: filesystem + glob: "${note}" + render_as: "## Codex manifest note" +--- + +body +`, + "utf-8" + ); + + try { + const r = runScript(["--skill", "codex-only", "--repo", "test-repo", "--explain"], { HOME: home }); + expect(r.exitCode).toBe(0); + expect(r.stderr).toContain("mode=manifest"); + expect(r.stdout).toContain("## Codex manifest note"); + expect(r.stdout).toContain("note.md"); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); + + it("prefers repo-local Codex skills over global Codex installs", () => { + const home = mkdtempSync(join(tmpdir(), "gstack-home-")); + const repo = mkdtempSync(join(tmpdir(), "gstack-repo-")); + const globalSkillDir = join(home, ".codex", "skills", "gstack-codex-only"); + const localSkillDir = join(repo, ".agents", "skills", "gstack-codex-only"); + mkdirSync(globalSkillDir, { recursive: true }); + mkdirSync(localSkillDir, { recursive: true }); + + const globalNote = join(home, "global-note.md"); + const localNote = join(repo, "local-note.md"); + writeFileSync(globalNote, "global codex manifest note\n", "utf-8"); + writeFileSync(localNote, "local codex manifest note\n", "utf-8"); + writeFileSync( + join(globalSkillDir, "SKILL.md"), + `--- +name: codex-only +gbrain: + schema: 1 + context_queries: + - id: global-note + kind: filesystem + glob: "${globalNote}" + render_as: "## Global Codex manifest" +--- + +body +`, + "utf-8" + ); + writeFileSync( + join(localSkillDir, "SKILL.md"), + `--- +name: codex-only +gbrain: + schema: 1 + context_queries: + - id: local-note + kind: filesystem + glob: "${localNote}" + render_as: "## Local Codex manifest" +--- + +body +`, + "utf-8" + ); + + try { + const r = runScript(["--skill", "codex-only", "--repo", "test-repo", "--explain"], { HOME: home }, repo); + expect(r.exitCode).toBe(0); + expect(r.stderr).toContain("mode=manifest"); + expect(r.stdout).toContain("## Local Codex manifest"); + expect(r.stdout).toContain("local-note.md"); + expect(r.stdout).not.toContain("## Global Codex manifest"); + expect(r.stdout).not.toContain("global-note.md"); + } finally { + rmSync(home, { recursive: true, force: true }); + rmSync(repo, { recursive: true, force: true }); + } + }); + it("uses skill manifest when --skill-file points at a valid SKILL.md", () => { const dir = mkdtempSync(join(tmpdir(), "gstack-bcl-")); const skillFile = join(dir, "SKILL.md");