diff --git a/.gitignore b/.gitignore index 2cf0fc001..573af0954 100644 --- a/.gitignore +++ b/.gitignore @@ -261,7 +261,7 @@ rulesync.local.jsonc **/.factory/settings.json **/GEMINI.md **/.gemini/commands/ -**/.gemini/subagents/ +**/.gemini/agents/ **/.gemini/skills/ **/.geminiignore **/.gemini/memories/ diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 43b10a5fb..22ff8296b 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,7 +1,7 @@ export default { "*": ["npx secretlint"], "package.json": ["npx sort-package-json"], - "docs/**/*.md": ["tsx scripts/sync-skill-docs.ts", "git add skills/rulesync/"], + "docs/**/*.md": ["node --import tsx/esm scripts/sync-skill-docs.ts", "git add skills/rulesync/"], // Regenerate tool configurations when rulesync source files change ".rulesync/**/*": [() => "pnpm dev generate"], }; diff --git a/README.md b/README.md index 073f52288..207bb7e30 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ See [Quick Start guide](https://dyoshikawa.github.io/rulesync/getting-started/qu | AgentsSkills | agentsskills | | | | | | ✅ | | | Claude Code | claudecode | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | Codex CLI | codexcli | ✅ 🌏 | | ✅ 🌏 🔧 | 🌏 | ✅ 🌏 | ✅ 🌏 | | -| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 | +| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | | Goose | goose | ✅ 🌏 | ✅ | | | | | | | GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ | | GitHub Copilot CLI | copilotcli | | | ✅ 🌏 | | | | | diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 3fff4d348..4078e8327 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -149,6 +149,8 @@ Based on the user's instruction, create a plan while analyzing the related files Attention, again, you are just the planner, so though you can read any files and run any commands for analysis, please don't write any code. ``` +> **Gemini CLI note (as of 2026-04-01):** Subagents are generated to `.gemini/agents/`. To enable the agents feature, set `"experimental": { "enableAgents": true }` in your `.gemini/settings.json`. + ## `.rulesync/skills/*/SKILL.md` Example: diff --git a/docs/reference/supported-tools.md b/docs/reference/supported-tools.md index feec0669a..d4972231d 100644 --- a/docs/reference/supported-tools.md +++ b/docs/reference/supported-tools.md @@ -8,7 +8,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod | AgentsSkills | agentsskills | | | | | | ✅ | | | Claude Code | claudecode | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | Codex CLI | codexcli | ✅ 🌏 | | ✅ 🌏 🔧 | 🌏 | ✅ 🌏 | ✅ 🌏 | | -| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 | +| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | | GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ | | Goose | goose | ✅ 🌏 | ✅ | | | | | | | Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ | diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index 3fff4d348..4078e8327 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -149,6 +149,8 @@ Based on the user's instruction, create a plan while analyzing the related files Attention, again, you are just the planner, so though you can read any files and run any commands for analysis, please don't write any code. ``` +> **Gemini CLI note (as of 2026-04-01):** Subagents are generated to `.gemini/agents/`. To enable the agents feature, set `"experimental": { "enableAgents": true }` in your `.gemini/settings.json`. + ## `.rulesync/skills/*/SKILL.md` Example: diff --git a/skills/rulesync/supported-tools.md b/skills/rulesync/supported-tools.md index feec0669a..d4972231d 100644 --- a/skills/rulesync/supported-tools.md +++ b/skills/rulesync/supported-tools.md @@ -8,7 +8,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod | AgentsSkills | agentsskills | | | | | | ✅ | | | Claude Code | claudecode | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | Codex CLI | codexcli | ✅ 🌏 | | ✅ 🌏 🔧 | 🌏 | ✅ 🌏 | ✅ 🌏 | | -| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 | +| Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | | GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ | | Goose | goose | ✅ 🌏 | ✅ | | | | | | | Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ | diff --git a/src/cli/commands/gitignore-entries.ts b/src/cli/commands/gitignore-entries.ts index 893e9fddc..51673f249 100644 --- a/src/cli/commands/gitignore-entries.ts +++ b/src/cli/commands/gitignore-entries.ts @@ -111,7 +111,7 @@ export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray = [ // Gemini CLI { target: "geminicli", feature: "rules", entry: "**/GEMINI.md" }, { target: "geminicli", feature: "commands", entry: "**/.gemini/commands/" }, - { target: "geminicli", feature: "subagents", entry: "**/.gemini/subagents/" }, + { target: "geminicli", feature: "subagents", entry: "**/.gemini/agents/" }, { target: "geminicli", feature: "skills", entry: "**/.gemini/skills/" }, { target: "geminicli", feature: "ignore", entry: "**/.geminiignore" }, { target: "geminicli", feature: "general", entry: "**/.gemini/memories/" }, diff --git a/src/e2e/e2e-helper.ts b/src/e2e/e2e-helper.ts index 079ef9bb7..87ea28802 100644 --- a/src/e2e/e2e-helper.ts +++ b/src/e2e/e2e-helper.ts @@ -44,11 +44,13 @@ export async function runGenerate({ target, features, global = false, + simulateSubagents = false, env, }: { target: string; features: string; global?: boolean; + simulateSubagents?: boolean; env?: Record; }): Promise<{ stdout: string; stderr: string }> { const args = [ @@ -59,6 +61,7 @@ export async function runGenerate({ "--features", features, ...(global ? ["--global"] : []), + ...(simulateSubagents ? ["--simulate-subagents"] : []), ]; return execFileAsync(rulesyncCmd, args, env ? { env: { ...process.env, ...env } } : {}); } diff --git a/src/e2e/e2e-subagents.spec.ts b/src/e2e/e2e-subagents.spec.ts index 850d909be..b1aaa59c1 100644 --- a/src/e2e/e2e-subagents.spec.ts +++ b/src/e2e/e2e-subagents.spec.ts @@ -15,8 +15,18 @@ describe("E2E: subagents", () => { const { getTestDir } = useTestDirectory(); it.each([ - { target: "claudecode", outputPath: join(".claude", "agents", "planner.md") }, - { target: "cursor", outputPath: join(".cursor", "agents", "planner.md") }, + { + target: "claudecode", + outputPath: join(".claude", "agents", "planner.md"), + }, + { + target: "cursor", + outputPath: join(".cursor", "agents", "planner.md"), + }, + { + target: "geminicli", + outputPath: join(".gemini", "agents", "planner.md"), + }, ])("should generate $target subagents", async ({ target, outputPath }) => { const testDir = getTestDir(); diff --git a/src/features/subagents/geminicli-subagent.test.ts b/src/features/subagents/geminicli-subagent.test.ts index 2654c644e..1574cdeff 100644 --- a/src/features/subagents/geminicli-subagent.test.ts +++ b/src/features/subagents/geminicli-subagent.test.ts @@ -7,10 +7,7 @@ import { setupTestDirectory } from "../../test-utils/test-directories.js"; import { writeFileContent } from "../../utils/file.js"; import { GeminiCliSubagent } from "./geminicli-subagent.js"; import { RulesyncSubagent } from "./rulesync-subagent.js"; -import { - SimulatedSubagentFrontmatter, - SimulatedSubagentFrontmatterSchema, -} from "./simulated-subagent.js"; +import { ToolSubagent } from "./tool-subagent.js"; describe("GeminiCliSubagent", () => { let testDir: string; @@ -49,7 +46,7 @@ Body content`; it("should return correct paths for geminicli subagents", () => { const paths = GeminiCliSubagent.getSettablePaths(); expect(paths).toEqual({ - relativeDirPath: ".gemini/subagents", + relativeDirPath: join(".gemini", "agents"), }); }); }); @@ -58,7 +55,7 @@ Body content`; it("should create instance with valid markdown content", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test-agent.md", frontmatter: { name: "Test GeminiCli Agent", @@ -81,7 +78,7 @@ Body content`; it("should create instance with empty name and description", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test-agent.md", frontmatter: { name: "", @@ -101,7 +98,7 @@ Body content`; it("should create instance without validation when validate is false", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test-agent.md", frontmatter: { name: "Test Agent", @@ -119,11 +116,11 @@ Body content`; () => new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "invalid-agent.md", frontmatter: { // Missing required fields - } as SimulatedSubagentFrontmatter, + } as { name: string }, body: "Body content", validate: true, }), @@ -135,7 +132,7 @@ Body content`; it("should return the body content", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test-agent.md", frontmatter: { name: "Test Agent", @@ -153,7 +150,7 @@ Body content`; it("should return frontmatter with name and description", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test-agent.md", frontmatter: { name: "Test GeminiCli Agent", @@ -172,10 +169,10 @@ Body content`; }); describe("toRulesyncSubagent", () => { - it("should throw error as it is a simulated file", () => { + it("should convert to RulesyncSubagent", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test-agent.md", frontmatter: { name: "Test Agent", @@ -185,9 +182,11 @@ Body content`; validate: true, }); - expect(() => subagent.toRulesyncSubagent()).toThrow( - "Not implemented because it is a SIMULATED file.", - ); + const rulesyncSubagent = subagent.toRulesyncSubagent(); + expect(rulesyncSubagent).toBeInstanceOf(RulesyncSubagent); + expect(rulesyncSubagent.getFrontmatter().name).toBe("Test Agent"); + expect(rulesyncSubagent.getFrontmatter().description).toBe("Test description"); + expect(rulesyncSubagent.getBody()).toBe("Test body"); }); }); @@ -208,7 +207,7 @@ Body content`; const geminiCliSubagent = GeminiCliSubagent.fromRulesyncSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", rulesyncSubagent, validate: true, }) as GeminiCliSubagent; @@ -220,7 +219,7 @@ Body content`; description: "Test description from rulesync", }); expect(geminiCliSubagent.getRelativeFilePath()).toBe("test-agent.md"); - expect(geminiCliSubagent.getRelativeDirPath()).toBe(".gemini/subagents"); + expect(geminiCliSubagent.getRelativeDirPath()).toBe(".gemini/agents"); }); it("should handle RulesyncSubagent with different file extensions", () => { @@ -239,7 +238,7 @@ Body content`; const geminiCliSubagent = GeminiCliSubagent.fromRulesyncSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", rulesyncSubagent, validate: true, }) as GeminiCliSubagent; @@ -263,7 +262,7 @@ Body content`; const geminiCliSubagent = GeminiCliSubagent.fromRulesyncSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", rulesyncSubagent, validate: true, }) as GeminiCliSubagent; @@ -277,7 +276,7 @@ Body content`; describe("fromFile", () => { it("should load GeminiCliSubagent from file", async () => { - const subagentsDir = join(testDir, ".gemini", "subagents"); + const subagentsDir = join(testDir, ".gemini", "agents"); const filePath = join(subagentsDir, "test-file-agent.md"); await writeFileContent(filePath, validMarkdownContent); @@ -300,7 +299,7 @@ Body content`; }); it("should handle file path with subdirectories", async () => { - const subagentsDir = join(testDir, ".gemini", "subagents", "subdir"); + const subagentsDir = join(testDir, ".gemini", "agents", "subdir"); const filePath = join(subagentsDir, "nested-agent.md"); await writeFileContent(filePath, validMarkdownContent); @@ -325,7 +324,7 @@ Body content`; }); it("should throw error when file contains invalid frontmatter", async () => { - const subagentsDir = join(testDir, ".gemini", "subagents"); + const subagentsDir = join(testDir, ".gemini", "agents"); const filePath = join(subagentsDir, "invalid-agent.md"); await writeFileContent(filePath, invalidMarkdownContent); @@ -340,7 +339,7 @@ Body content`; }); it("should handle file without frontmatter", async () => { - const subagentsDir = join(testDir, ".gemini", "subagents"); + const subagentsDir = join(testDir, ".gemini", "agents"); const filePath = join(subagentsDir, "no-frontmatter.md"); await writeFileContent(filePath, markdownWithoutFrontmatter); @@ -359,14 +358,14 @@ Body content`; it("should return success for valid frontmatter", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "valid-agent.md", frontmatter: { name: "Valid Agent", description: "Valid description", }, body: "Valid body", - validate: false, // Skip validation in constructor to test validate method + validate: false, }); const result = subagent.validate(); @@ -377,68 +376,27 @@ Body content`; it("should handle frontmatter with additional properties", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "agent-with-extras.md", frontmatter: { name: "Agent", description: "Agent with extra properties", - // Additional properties should be allowed but not validated extra: "property", - } as any, + }, body: "Body content", validate: false, }); const result = subagent.validate(); - // The validation should pass as long as required fields are present expect(result.success).toBe(true); }); }); - describe("SimulatedSubagentFrontmatterSchema", () => { - it("should validate valid frontmatter with name and description", () => { - const validFrontmatter = { - name: "Test Agent", - description: "Test description", - }; - - const result = SimulatedSubagentFrontmatterSchema.parse(validFrontmatter); - expect(result).toEqual(validFrontmatter); - }); - - it("should throw error for frontmatter without name", () => { - const invalidFrontmatter = { - description: "Test description", - }; - - expect(() => SimulatedSubagentFrontmatterSchema.parse(invalidFrontmatter)).toThrow(); - }); - - it("should accept frontmatter without description (description is optional)", () => { - const frontmatter = { - name: "Test Agent", - }; - - const result = SimulatedSubagentFrontmatterSchema.parse(frontmatter); - expect(result.name).toBe("Test Agent"); - expect(result.description).toBeUndefined(); - }); - - it("should throw error for frontmatter with invalid types", () => { - const invalidFrontmatter = { - name: 123, // Should be string - description: "Test", - }; - - expect(() => SimulatedSubagentFrontmatterSchema.parse(invalidFrontmatter)).toThrow(); - }); - }); - describe("edge cases", () => { it("should handle empty body content", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "empty-body.md", frontmatter: { name: "Empty Body Agent", @@ -461,7 +419,7 @@ Body content`; const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "special-char.md", frontmatter: { name: "Special Agent", @@ -482,7 +440,7 @@ Body content`; const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "long-content.md", frontmatter: { name: "Long Agent", @@ -499,7 +457,7 @@ Body content`; it("should handle multi-line name and description", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "multiline-fields.md", frontmatter: { name: "Multi-line\nAgent Name", @@ -520,7 +478,7 @@ Body content`; const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "windows-lines.md", frontmatter: { name: "Windows Agent", @@ -535,10 +493,10 @@ Body content`; }); describe("inheritance", () => { - it("should inherit from SimulatedSubagent", () => { + it("should be an instance of ToolSubagent", () => { const subagent = new GeminiCliSubagent({ baseDir: testDir, - relativeDirPath: ".gemini/subagents", + relativeDirPath: ".gemini/agents", relativeFilePath: "test.md", frontmatter: { name: "Test", @@ -549,10 +507,7 @@ Body content`; }); expect(subagent).toBeInstanceOf(GeminiCliSubagent); - // Test that it inherits methods from parent class - expect(() => subagent.toRulesyncSubagent()).toThrow( - "Not implemented because it is a SIMULATED file.", - ); + expect(subagent).toBeInstanceOf(ToolSubagent); }); }); @@ -602,7 +557,7 @@ Body content`; description: "Test description", }, body: "Test content", - validate: false, // Skip validation to allow empty targets array + validate: false, }); expect(GeminiCliSubagent.isTargetedByRulesyncSubagent(rulesyncSubagent)).toBe(false); diff --git a/src/features/subagents/geminicli-subagent.ts b/src/features/subagents/geminicli-subagent.ts index 9a4dfd5ac..56ca06e3f 100644 --- a/src/features/subagents/geminicli-subagent.ts +++ b/src/features/subagents/geminicli-subagent.ts @@ -1,7 +1,13 @@ -import { join } from "node:path"; +import { basename, join } from "node:path"; -import { RulesyncSubagent } from "./rulesync-subagent.js"; -import { SimulatedSubagent } from "./simulated-subagent.js"; +import { z } from "zod/mini"; + +import { RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH } from "../../constants/rulesync-paths.js"; +import { AiFileParams, ValidationResult } from "../../types/ai-file.js"; +import { formatError } from "../../utils/error.js"; +import { readFileContent } from "../../utils/file.js"; +import { parseFrontmatter, stringifyFrontmatter } from "../../utils/frontmatter.js"; +import { RulesyncSubagent, RulesyncSubagentFrontmatter } from "./rulesync-subagent.js"; import { ToolSubagent, ToolSubagentForDeletionParams, @@ -10,21 +16,126 @@ import { ToolSubagentSettablePaths, } from "./tool-subagent.js"; -export class GeminiCliSubagent extends SimulatedSubagent { - static getSettablePaths(): ToolSubagentSettablePaths { +const GeminiCliSubagentFrontmatterSchema = z.looseObject({ + name: z.string(), + description: z.optional(z.string()), +}); + +type GeminiCliSubagentFrontmatter = z.infer; + +type GeminiCliSubagentParams = { + frontmatter: GeminiCliSubagentFrontmatter; + body: string; +} & Omit & { fileContent?: string }; + +export class GeminiCliSubagent extends ToolSubagent { + private readonly frontmatter: GeminiCliSubagentFrontmatter; + private readonly body: string; + + constructor({ frontmatter, body, fileContent, ...rest }: GeminiCliSubagentParams) { + if (rest.validate !== false) { + const result = GeminiCliSubagentFrontmatterSchema.safeParse(frontmatter); + if (!result.success) { + throw new Error( + `Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`, + ); + } + } + + super({ + ...rest, + fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter), + }); + + this.frontmatter = frontmatter; + this.body = body; + } + + static getSettablePaths(_options: { global?: boolean } = {}): ToolSubagentSettablePaths { return { - relativeDirPath: join(".gemini", "subagents"), + relativeDirPath: join(".gemini", "agents"), }; } - static async fromFile(params: ToolSubagentFromFileParams): Promise { - const baseParams = await this.fromFileDefault(params); - return new GeminiCliSubagent(baseParams); + getFrontmatter(): GeminiCliSubagentFrontmatter { + return this.frontmatter; + } + + getBody(): string { + return this.body; } - static fromRulesyncSubagent(params: ToolSubagentFromRulesyncSubagentParams): ToolSubagent { - const baseParams = this.fromRulesyncSubagentDefault(params); - return new GeminiCliSubagent(baseParams); + toRulesyncSubagent(): RulesyncSubagent { + const { name, description, ...rest } = this.frontmatter; + + const rulesyncFrontmatter: RulesyncSubagentFrontmatter = { + targets: ["*"] as const, + name, + description, + geminicli: { + ...rest, + }, + }; + + return new RulesyncSubagent({ + baseDir: ".", + frontmatter: rulesyncFrontmatter, + body: this.body, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: this.getRelativeFilePath(), + validate: true, + }); + } + + static fromRulesyncSubagent({ + baseDir = process.cwd(), + rulesyncSubagent, + validate = true, + global = false, + }: ToolSubagentFromRulesyncSubagentParams): ToolSubagent { + const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter(); + const geminicliSection = rulesyncFrontmatter.geminicli ?? {}; + + const geminicliSubagentFrontmatter: GeminiCliSubagentFrontmatter = { + name: rulesyncFrontmatter.name, + description: rulesyncFrontmatter.description, + ...geminicliSection, + }; + + const body = rulesyncSubagent.getBody(); + const fileContent = stringifyFrontmatter(body, geminicliSubagentFrontmatter, { + avoidBlockScalars: true, + }); + const paths = this.getSettablePaths({ global }); + + return new GeminiCliSubagent({ + baseDir, + frontmatter: geminicliSubagentFrontmatter, + body, + relativeDirPath: paths.relativeDirPath, + relativeFilePath: rulesyncSubagent.getRelativeFilePath(), + fileContent, + validate, + global, + }); + } + + validate(): ValidationResult { + if (!this.frontmatter) { + return { success: true, error: null }; + } + + const result = GeminiCliSubagentFrontmatterSchema.safeParse(this.frontmatter); + if (result.success) { + return { success: true, error: null }; + } else { + return { + success: false, + error: new Error( + `Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`, + ), + }; + } } static isTargetedByRulesyncSubagent(rulesyncSubagent: RulesyncSubagent): boolean { @@ -34,7 +145,47 @@ export class GeminiCliSubagent extends SimulatedSubagent { }); } - static forDeletion(params: ToolSubagentForDeletionParams): GeminiCliSubagent { - return new GeminiCliSubagent(this.forDeletionDefault(params)); + static async fromFile({ + baseDir = process.cwd(), + relativeFilePath, + validate = true, + global = false, + }: ToolSubagentFromFileParams): Promise { + const paths = this.getSettablePaths({ global }); + const filePath = join(baseDir, paths.relativeDirPath, relativeFilePath); + const fileContent = await readFileContent(filePath); + const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath); + + const result = GeminiCliSubagentFrontmatterSchema.safeParse(frontmatter); + if (!result.success) { + throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`); + } + + return new GeminiCliSubagent({ + baseDir, + relativeDirPath: paths.relativeDirPath, + relativeFilePath: basename(relativeFilePath), + frontmatter: result.data, + body: content.trim(), + fileContent, + validate, + global, + }); + } + + static forDeletion({ + baseDir = process.cwd(), + relativeDirPath, + relativeFilePath, + }: ToolSubagentForDeletionParams): GeminiCliSubagent { + return new GeminiCliSubagent({ + baseDir, + relativeDirPath, + relativeFilePath, + frontmatter: { name: "", description: "" }, + body: "", + fileContent: "", + validate: false, + }); } } diff --git a/src/features/subagents/subagents-processor.test.ts b/src/features/subagents/subagents-processor.test.ts index 2928a4458..ba70e5b5b 100644 --- a/src/features/subagents/subagents-processor.test.ts +++ b/src/features/subagents/subagents-processor.test.ts @@ -979,7 +979,7 @@ Second global content`; it("should export subagentsProcessorToolTargetsSimulated constant", () => { expect(new Set(subagentsProcessorToolTargetsSimulated)).toEqual( - new Set(["agentsmd", "factorydroid", "geminicli", "roo"]), + new Set(["agentsmd", "factorydroid", "roo"]), ); expect(Array.isArray(subagentsProcessorToolTargetsSimulated)).toBe(true); }); diff --git a/src/features/subagents/subagents-processor.ts b/src/features/subagents/subagents-processor.ts index 9281e2b7e..61bb4c74c 100644 --- a/src/features/subagents/subagents-processor.ts +++ b/src/features/subagents/subagents-processor.ts @@ -145,7 +145,7 @@ const toolSubagentFactories = new Map