From d14ac57799eb4956898478dcd366fb526fdf9a19 Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 17:24:29 +0900 Subject: [PATCH 1/3] fix: convert .md to .agent.md extension for GitHub Copilot subagents VSCode Copilot Chat requires agent files to have the .agent.md extension to be recognized and loaded. Previously, rulesync generated files as .md which caused them to be silently ignored by Copilot. Fixes dyoshikawa/rulesync#1432 Co-Authored-By: Claude Sonnet 4.6 --- .../subagents/copilot-subagent.test.ts | 50 +++++++++++++++++++ src/features/subagents/copilot-subagent.ts | 14 +++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/features/subagents/copilot-subagent.test.ts b/src/features/subagents/copilot-subagent.test.ts index ae4608f52..0fb504b10 100644 --- a/src/features/subagents/copilot-subagent.test.ts +++ b/src/features/subagents/copilot-subagent.test.ts @@ -96,6 +96,56 @@ Plan tasks`; expect(subagent.getFrontmatter().tools).toEqual(["agent/runSubagent"]); }); + + it("converts .md extension to .agent.md in output file path", () => { + const rulesyncSubagent = new RulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "planner.md", + frontmatter: { + targets: ["copilot"], + name: "planner", + description: "Plan things", + copilot: {}, + }, + body: "Plan tasks", + validate: true, + }); + + const subagent = CopilotSubagent.fromRulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + rulesyncSubagent, + validate: true, + }) as CopilotSubagent; + + expect(subagent.getRelativeFilePath()).toBe("planner.agent.md"); + }); + + it("preserves .agent.md extension when already present", () => { + const rulesyncSubagent = new RulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "planner.agent.md", + frontmatter: { + targets: ["copilot"], + name: "planner", + description: "Plan things", + copilot: {}, + }, + body: "Plan tasks", + validate: true, + }); + + const subagent = CopilotSubagent.fromRulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + rulesyncSubagent, + validate: true, + }) as CopilotSubagent; + + expect(subagent.getRelativeFilePath()).toBe("planner.agent.md"); + }); }); describe("toRulesyncSubagent", () => { diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index 5b4b62a28..f28b4b454 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -31,6 +31,18 @@ type CopilotSubagentParams = { body: string; } & AiFileParams; +const AGENT_MD_EXTENSION = ".agent.md"; + +const toAgentMdFilePath = (filePath: string): string => { + if (filePath.endsWith(AGENT_MD_EXTENSION)) { + return filePath; + } + if (filePath.endsWith(".md")) { + return filePath.slice(0, -3) + AGENT_MD_EXTENSION; + } + return filePath + AGENT_MD_EXTENSION; +}; + const normalizeTools = (tools: string | string[] | undefined): string[] => { if (!tools) { return []; @@ -134,7 +146,7 @@ export class CopilotSubagent extends ToolSubagent { frontmatter: copilotFrontmatter, body, relativeDirPath: paths.relativeDirPath, - relativeFilePath: rulesyncSubagent.getRelativeFilePath(), + relativeFilePath: toAgentMdFilePath(rulesyncSubagent.getRelativeFilePath()), fileContent, validate, global, From 70ff8689ceae65c8068894144930b413d4514358 Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 17:30:09 +0900 Subject: [PATCH 2/3] fix: update filePattern and add reverse .agent.md conversion - Change Copilot filePattern from "*.md" to "*.agent.md" so deletion (rulesync generate --delete) correctly finds and removes stale files - Add fromAgentMdFilePath() to convert .agent.md back to .md in toRulesyncSubagent() for correct import paths - Update subagents-processor test to use .agent.md file names Co-Authored-By: Claude Opus 4.6 --- .../subagents/copilot-subagent.test.ts | 19 +++++++++++++++++++ src/features/subagents/copilot-subagent.ts | 9 ++++++++- .../subagents/subagents-processor.test.ts | 2 +- src/features/subagents/subagents-processor.ts | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/features/subagents/copilot-subagent.test.ts b/src/features/subagents/copilot-subagent.test.ts index 0fb504b10..64371cffa 100644 --- a/src/features/subagents/copilot-subagent.test.ts +++ b/src/features/subagents/copilot-subagent.test.ts @@ -174,6 +174,25 @@ Plan tasks`; }); expect(rulesyncSubagent.getBody()).toBe("Plan tasks"); }); + + it("converts .agent.md back to .md for rulesync file path", () => { + const subagent = new CopilotSubagent({ + baseDir: testDir, + relativeDirPath: ".github/agents", + relativeFilePath: "planner.agent.md", + frontmatter: { + name: "planner", + description: "Plan things", + }, + body: "Plan tasks", + fileContent: validContent, + validate: true, + }); + + const rulesyncSubagent = subagent.toRulesyncSubagent(); + + expect(rulesyncSubagent.getRelativeFilePath()).toBe("planner.md"); + }); }); describe("fromFile", () => { diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index f28b4b454..786ee4939 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -43,6 +43,13 @@ const toAgentMdFilePath = (filePath: string): string => { return filePath + AGENT_MD_EXTENSION; }; +const fromAgentMdFilePath = (filePath: string): string => { + if (filePath.endsWith(AGENT_MD_EXTENSION)) { + return filePath.slice(0, -AGENT_MD_EXTENSION.length) + ".md"; + } + return filePath; +}; + const normalizeTools = (tools: string | string[] | undefined): string[] => { if (!tools) { return []; @@ -110,7 +117,7 @@ export class CopilotSubagent extends ToolSubagent { frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, - relativeFilePath: this.getRelativeFilePath(), + relativeFilePath: fromAgentMdFilePath(this.getRelativeFilePath()), validate: true, }); } diff --git a/src/features/subagents/subagents-processor.test.ts b/src/features/subagents/subagents-processor.test.ts index 7e4012eb2..67440afb7 100644 --- a/src/features/subagents/subagents-processor.test.ts +++ b/src/features/subagents/subagents-processor.test.ts @@ -646,7 +646,7 @@ description: Copilot agent description --- Copilot agent content`; - await writeFileContent(join(subagentsDir, "copilot-agent.md"), subagentContent); + await writeFileContent(join(subagentsDir, "copilot-agent.agent.md"), subagentContent); const toolFiles = await processor.loadToolFiles(); diff --git a/src/features/subagents/subagents-processor.ts b/src/features/subagents/subagents-processor.ts index 9675b99d2..62f8a0a9a 100644 --- a/src/features/subagents/subagents-processor.ts +++ b/src/features/subagents/subagents-processor.ts @@ -119,7 +119,7 @@ const toolSubagentFactories = new Map Date: Wed, 8 Apr 2026 17:41:23 +0900 Subject: [PATCH 3/3] fix: throw error for non-.md source file paths in toAgentMdFilePath Silently appending .agent.md to a non-.md path (e.g. planner.txt) would produce malformed filenames like planner.txt.agent.md that Copilot Chat would never load. Fail fast with a clear error instead. Co-Authored-By: Claude Sonnet 4.6 --- .../subagents/copilot-subagent.test.ts | 25 +++++++++++++++++++ src/features/subagents/copilot-subagent.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/features/subagents/copilot-subagent.test.ts b/src/features/subagents/copilot-subagent.test.ts index 64371cffa..cd8503f79 100644 --- a/src/features/subagents/copilot-subagent.test.ts +++ b/src/features/subagents/copilot-subagent.test.ts @@ -146,6 +146,31 @@ Plan tasks`; expect(subagent.getRelativeFilePath()).toBe("planner.agent.md"); }); + + it("throws when source file path does not end in .md", () => { + const rulesyncSubagent = new RulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "planner.txt", + frontmatter: { + targets: ["copilot"], + name: "planner", + description: "Plan things", + copilot: {}, + }, + body: "Plan tasks", + validate: true, + }); + + expect(() => + CopilotSubagent.fromRulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + rulesyncSubagent, + validate: true, + }), + ).toThrow("Expected .md file path"); + }); }); describe("toRulesyncSubagent", () => { diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index 786ee4939..4f9e03c8e 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -40,7 +40,7 @@ const toAgentMdFilePath = (filePath: string): string => { if (filePath.endsWith(".md")) { return filePath.slice(0, -3) + AGENT_MD_EXTENSION; } - return filePath + AGENT_MD_EXTENSION; + throw new Error(`Expected .md file path, got: ${filePath}`); }; const fromAgentMdFilePath = (filePath: string): string => {