diff --git a/src/features/commands/antigravity-command.ts b/src/features/commands/antigravity-command.ts index bf7b87705..04145037d 100644 --- a/src/features/commands/antigravity-command.ts +++ b/src/features/commands/antigravity-command.ts @@ -155,7 +155,7 @@ export class AntigravityCommand extends ToolCommand { // But we DO need to update the body to include the specific workflow header. body = `# Workflow: ${trigger}\n\n${body}${turboDirective}`; - const description = rulesyncFrontmatter.description; + const description = rulesyncFrontmatter.description ?? ""; const antigravityFrontmatter: AntigravityCommandFrontmatter = { description, diff --git a/src/features/commands/claudecode-command.ts b/src/features/commands/claudecode-command.ts index 442370960..8fec41fb2 100644 --- a/src/features/commands/claudecode-command.ts +++ b/src/features/commands/claudecode-command.ts @@ -105,7 +105,7 @@ export class ClaudecodeCommand extends ToolCommand { const claudecodeFields = rulesyncFrontmatter.claudecode ?? {}; const claudecodeFrontmatter: ClaudecodeCommandFrontmatter = { - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", ...claudecodeFields, }; diff --git a/src/features/commands/copilot-command.ts b/src/features/commands/copilot-command.ts index 596d60274..b3c1fed7b 100644 --- a/src/features/commands/copilot-command.ts +++ b/src/features/commands/copilot-command.ts @@ -120,7 +120,7 @@ export class CopilotCommand extends ToolCommand { const copilotFields = rulesyncFrontmatter.copilot ?? {}; const copilotFrontmatter: CopilotCommandFrontmatter = { - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", ...copilotFields, }; diff --git a/src/features/commands/geminicli-command.test.ts b/src/features/commands/geminicli-command.test.ts index a7b9e17c9..7f1717b25 100644 --- a/src/features/commands/geminicli-command.test.ts +++ b/src/features/commands/geminicli-command.test.ts @@ -75,7 +75,7 @@ prompt = "Unclosed string`; expect(command.getBody()).toBe("This is a test prompt without description.\n"); expect(command.getFrontmatter()).toEqual({ - description: "", + description: undefined, prompt: "This is a test prompt without description.\n", }); }); @@ -134,7 +134,7 @@ prompt = "Unclosed string`; }); const frontmatter = command.getFrontmatter() as GeminiCliCommandFrontmatter; - expect(frontmatter.description).toBe(""); + expect(frontmatter.description).toBeUndefined(); expect(frontmatter.prompt).toBe("This is a test prompt without description.\n"); }); @@ -223,7 +223,7 @@ prompt = "Unclosed string`; expect(rulesyncCommand.getFrontmatter()).toEqual({ targets: ["geminicli"], - description: "", + description: undefined, }); }); }); diff --git a/src/features/commands/geminicli-command.ts b/src/features/commands/geminicli-command.ts index 3c61df25f..b0f590086 100644 --- a/src/features/commands/geminicli-command.ts +++ b/src/features/commands/geminicli-command.ts @@ -58,7 +58,7 @@ export class GeminiCliCommand extends ToolCommand { // Preserve all fields including unknown ones (looseObject passthrough) return { ...result.data, - description: result.data.description || "", + description: result.data.description, }; } catch (error) { throw new Error( @@ -84,7 +84,7 @@ export class GeminiCliCommand extends ToolCommand { const rulesyncFrontmatter: RulesyncCommandFrontmatter = { targets: ["geminicli"], - description: description ?? "", + description, // Preserve extra fields in geminicli section (excluding prompt which is the body) ...(Object.keys(restFields).length > 0 && { geminicli: restFields }), }; @@ -123,8 +123,10 @@ export class GeminiCliCommand extends ToolCommand { // Generate proper file content with TOML format // Note: TOML format only supports description and prompt fields // Extra fields from geminicli section are stored in the object but not serialized to TOML - const tomlContent = `description = "${geminiFrontmatter.description}" -prompt = """ + const descriptionLine = geminiFrontmatter.description + ? `description = "${geminiFrontmatter.description}"\n` + : ""; + const tomlContent = `${descriptionLine}prompt = """ ${geminiFrontmatter.prompt} """`; diff --git a/src/features/commands/opencode-command.ts b/src/features/commands/opencode-command.ts index 1188825a4..5898ab266 100644 --- a/src/features/commands/opencode-command.ts +++ b/src/features/commands/opencode-command.ts @@ -100,7 +100,7 @@ export class OpenCodeCommand extends ToolCommand { const opencodeFields = rulesyncFrontmatter.opencode ?? {}; const opencodeFrontmatter: OpenCodeCommandFrontmatter = { - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", ...opencodeFields, }; diff --git a/src/features/commands/roo-command.ts b/src/features/commands/roo-command.ts index 8a9cb28df..61ccf4266 100644 --- a/src/features/commands/roo-command.ts +++ b/src/features/commands/roo-command.ts @@ -104,7 +104,7 @@ export class RooCommand extends ToolCommand { const rooFields = rulesyncFrontmatter.roo ?? {}; const rooFrontmatter: RooCommandFrontmatter = { - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", ...rooFields, }; diff --git a/src/features/commands/rulesync-command.test.ts b/src/features/commands/rulesync-command.test.ts index fe60bd41c..6295e686d 100644 --- a/src/features/commands/rulesync-command.test.ts +++ b/src/features/commands/rulesync-command.test.ts @@ -84,19 +84,19 @@ describe("RulesyncCommand", () => { expect(() => new RulesyncCommand(paramsWithInvalidData)).toThrow(); }); - it("should throw error for missing required frontmatter fields", () => { - const incompleteFrontmatter = { + it("should not throw error for missing optional description", () => { + const frontmatterWithoutDescription = { targets: ["cursor"], - // Missing description + // Missing description (now optional) }; - const paramsWithIncompleteData: RulesyncCommandParams = { + const paramsWithoutDescription: RulesyncCommandParams = { ...validParams, - frontmatter: incompleteFrontmatter as any, - validate: true, // Enable validation + frontmatter: frontmatterWithoutDescription as any, + validate: true, }; - expect(() => new RulesyncCommand(paramsWithIncompleteData)).toThrow(); + expect(() => new RulesyncCommand(paramsWithoutDescription)).not.toThrow(); }); it("should generate correct file content from frontmatter and body", () => { @@ -181,22 +181,21 @@ describe("RulesyncCommand", () => { expect(result.error).toBeDefined(); }); - it("should return failure for missing required fields", () => { - const incompleteFrontmatter = { + it("should return success for missing optional description", () => { + const frontmatterWithoutDescription = { targets: ["cursor"], - // Missing description + // Missing description (now optional) }; const command = new RulesyncCommand({ ...validParams, - frontmatter: incompleteFrontmatter as any, + frontmatter: frontmatterWithoutDescription as any, validate: false, }); const result = command.validate(); - expect(result.success).toBe(false); - expect(result.error).toBeDefined(); + expect(result.success).toBe(true); }); }); @@ -227,14 +226,14 @@ describe("RulesyncCommand", () => { expect(result.success).toBe(false); }); - it("should reject missing description", () => { - const invalidData = { + it("should accept missing description (now optional)", () => { + const dataWithoutDescription = { targets: ["cursor"], }; - const result = RulesyncCommandFrontmatterSchema.safeParse(invalidData); + const result = RulesyncCommandFrontmatterSchema.safeParse(dataWithoutDescription); - expect(result.success).toBe(false); + expect(result.success).toBe(true); }); it("should reject non-string description", () => { diff --git a/src/features/commands/rulesync-command.ts b/src/features/commands/rulesync-command.ts index 4662aaa0d..53a35ca95 100644 --- a/src/features/commands/rulesync-command.ts +++ b/src/features/commands/rulesync-command.ts @@ -18,7 +18,7 @@ import { parseFrontmatter, stringifyFrontmatter } from "../../utils/frontmatter. // Tool-specific sections (e.g., claudecode:, copilot:) are preserved as additional keys export const RulesyncCommandFrontmatterSchema = z.looseObject({ targets: z._default(RulesyncTargetsSchema, ["*"]), - description: z.string(), + description: z.optional(z.string()), }); // Input type allows targets to be omitted (will use default value) diff --git a/src/features/commands/simulated-command.ts b/src/features/commands/simulated-command.ts index 96cdef639..1b5b60b2c 100644 --- a/src/features/commands/simulated-command.ts +++ b/src/features/commands/simulated-command.ts @@ -68,7 +68,7 @@ export abstract class SimulatedCommand extends ToolCommand { const rulesyncFrontmatter = rulesyncCommand.getFrontmatter(); const claudecodeFrontmatter: SimulatedCommandFrontmatter = { - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", }; const body = rulesyncCommand.getBody(); diff --git a/src/features/subagents/codexcli-subagent.ts b/src/features/subagents/codexcli-subagent.ts index 18244169e..340822ddc 100644 --- a/src/features/subagents/codexcli-subagent.ts +++ b/src/features/subagents/codexcli-subagent.ts @@ -87,7 +87,7 @@ export class CodexCliSubagent extends ToolSubagent { const rulesyncFrontmatter: RulesyncSubagentFrontmatter = { targets: ["codexcli"], name, - description: description ?? "", + description, // Only include codexcli section if there are fields ...(Object.keys(codexcliSection).length > 0 && { codexcli: codexcliSection }), }; diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index 31e097ef9..87bc3f4c5 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -120,7 +120,7 @@ export class CopilotSubagent extends ToolSubagent { const copilotFrontmatter: CopilotSubagentFrontmatter = { name: rulesyncFrontmatter.name, - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", ...copilotSection, ...(mergedTools.length > 0 && { tools: mergedTools }), }; diff --git a/src/features/subagents/cursor-subagent.ts b/src/features/subagents/cursor-subagent.ts index 585363327..1d272260b 100644 --- a/src/features/subagents/cursor-subagent.ts +++ b/src/features/subagents/cursor-subagent.ts @@ -97,7 +97,7 @@ export class CursorSubagent extends ToolSubagent { const cursorFrontmatter: CursorSubagentFrontmatter = { name: rulesyncFrontmatter.name, - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", ...cursorSection, }; diff --git a/src/features/subagents/kiro-subagent.ts b/src/features/subagents/kiro-subagent.ts index 54104ac4f..03cab49a4 100644 --- a/src/features/subagents/kiro-subagent.ts +++ b/src/features/subagents/kiro-subagent.ts @@ -94,7 +94,7 @@ export class KiroSubagent extends ToolSubagent { const rulesyncFrontmatter: RulesyncSubagentFrontmatter = { targets: ["kiro"], name, - description: description ?? "", + description: description ?? undefined, // Only include kiro section if there are fields ...(Object.keys(kiroSection).length > 0 && { kiro: kiroSection }), }; diff --git a/src/features/subagents/opencode-subagent.ts b/src/features/subagents/opencode-subagent.ts index cac8ba36f..9c50b2d98 100644 --- a/src/features/subagents/opencode-subagent.ts +++ b/src/features/subagents/opencode-subagent.ts @@ -99,7 +99,7 @@ export class OpenCodeSubagent extends ToolSubagent { const opencodeFrontmatter: OpenCodeSubagentFrontmatter = { ...opencodeSection, - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", mode: "subagent", ...(rulesyncFrontmatter.name && { name: rulesyncFrontmatter.name }), }; diff --git a/src/features/subagents/rulesync-subagent.test.ts b/src/features/subagents/rulesync-subagent.test.ts index dff894eff..e47ba42e5 100644 --- a/src/features/subagents/rulesync-subagent.test.ts +++ b/src/features/subagents/rulesync-subagent.test.ts @@ -56,7 +56,7 @@ describe("RulesyncSubagentFrontmatterSchema", () => { }; expect(() => RulesyncSubagentFrontmatterSchema.parse(missingName)).toThrow(); - expect(() => RulesyncSubagentFrontmatterSchema.parse(missingDescription)).toThrow(); + expect(() => RulesyncSubagentFrontmatterSchema.parse(missingDescription)).not.toThrow(); }); it("should use default targets when omitted", () => { @@ -149,22 +149,22 @@ describe("RulesyncSubagent", () => { expect(subagent.getFrontmatter().claudecode?.model).toBe("opus"); }); - it("should throw error with invalid frontmatter", () => { - const invalidFrontmatter = { + it("should not throw error for missing optional description", () => { + const frontmatterWithoutDescription = { targets: ["*"], name: "test-subagent", - // missing description + // missing description (now optional) }; expect(() => { const _instance = new RulesyncSubagent({ baseDir: ".", relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, - relativeFilePath: "invalid.md", - frontmatter: invalidFrontmatter as any, + relativeFilePath: "valid.md", + frontmatter: frontmatterWithoutDescription as any, body: "Test body", }); - }).toThrow(); + }).not.toThrow(); }); it("should skip validation when validate=false", () => { @@ -298,23 +298,22 @@ describe("RulesyncSubagent", () => { expect(result.error).toBe(null); }); - it("should return error for invalid frontmatter", () => { + it("should return success for missing optional description", () => { const subagent = new RulesyncSubagent({ baseDir: ".", relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, - relativeFilePath: "invalid-validate.md", + relativeFilePath: "valid-validate.md", frontmatter: { targets: ["*"], - name: "invalid-subagent", - // missing description + name: "valid-subagent", + // missing description (now optional) } as any, - body: "Invalid body", - validate: false, // Skip validation in constructor for testing + body: "Valid body", + validate: false, }); const result = subagent.validate(); - expect(result.success).toBe(false); - expect(result.error).toBeInstanceOf(Error); + expect(result.success).toBe(true); }); }); @@ -412,8 +411,7 @@ Nested content.`; const filePath = join(subagentsDir, "test-fromfile-invalid.md"); const fileContent = `--- targets: ["*"] -name: invalid-subagent -# missing description +# missing name (required field) --- Invalid content.`; diff --git a/src/features/subagents/rulesync-subagent.ts b/src/features/subagents/rulesync-subagent.ts index a1bdc661f..d0ed08141 100644 --- a/src/features/subagents/rulesync-subagent.ts +++ b/src/features/subagents/rulesync-subagent.ts @@ -19,7 +19,7 @@ import { parseFrontmatter, stringifyFrontmatter } from "../../utils/frontmatter. export const RulesyncSubagentFrontmatterSchema = z.looseObject({ targets: z._default(RulesyncTargetsSchema, ["*"]), name: z.string(), - description: z.string(), + description: z.optional(z.string()), }); // Input type allows targets to be omitted (will use default value) diff --git a/src/features/subagents/simulated-subagent.ts b/src/features/subagents/simulated-subagent.ts index ac304266a..09eb70ef8 100644 --- a/src/features/subagents/simulated-subagent.ts +++ b/src/features/subagents/simulated-subagent.ts @@ -70,7 +70,7 @@ export abstract class SimulatedSubagent extends ToolSubagent { const simulatedFrontmatter: SimulatedSubagentFrontmatter = { name: rulesyncFrontmatter.name, - description: rulesyncFrontmatter.description, + description: rulesyncFrontmatter.description ?? "", }; const body = rulesyncSubagent.getBody();