From 079d636c87e78ed596acc498b07682dcfefff7c2 Mon Sep 17 00:00:00 2001 From: dyoshikawa-claw Date: Thu, 2 Apr 2026 23:51:05 +0900 Subject: [PATCH 1/2] fix: align deepagents project handling --- src/features/hooks/deepagents-hooks.test.ts | 46 ++++++++++++++++++++ src/features/hooks/deepagents-hooks.ts | 1 + src/features/hooks/hooks-processor.ts | 3 ++ src/features/mcp/deepagents-mcp.test.ts | 36 +++++++++++++++ src/features/rules/deepagents-rule.test.ts | 15 +++++++ src/features/rules/deepagents-rule.ts | 5 ++- src/features/skills/deepagents-skill.test.ts | 35 +++++++++++++-- src/features/skills/deepagents-skill.ts | 9 ++-- 8 files changed, 139 insertions(+), 11 deletions(-) diff --git a/src/features/hooks/deepagents-hooks.test.ts b/src/features/hooks/deepagents-hooks.test.ts index 8e9fe73e0..5cd990aeb 100644 --- a/src/features/hooks/deepagents-hooks.test.ts +++ b/src/features/hooks/deepagents-hooks.test.ts @@ -230,5 +230,51 @@ describe("DeepagentsHooks", () => { expect(canonical.hooks.sessionStart).toBeDefined(); expect(canonical.hooks.sessionEnd).toBeDefined(); }); + + it("should skip malformed hook entries", () => { + const hooks = new DeepagentsHooks({ + baseDir: testDir, + relativeDirPath: ".deepagents", + relativeFilePath: "hooks.json", + fileContent: JSON.stringify({ + hooks: [null, "invalid", { events: ["session.start"] }, { command: [] }], + }), + }); + + const rulesyncHooks = hooks.toRulesyncHooks(); + + expect(rulesyncHooks.getJson().hooks).toEqual({}); + }); + + it("should join command parts when bash fallback pattern is not used", () => { + const hooks = new DeepagentsHooks({ + baseDir: testDir, + relativeDirPath: ".deepagents", + relativeFilePath: "hooks.json", + fileContent: JSON.stringify({ + hooks: [{ command: ["pnpm", "test", "--runInBand"], events: ["task.complete"] }], + }), + }); + + const rulesyncHooks = hooks.toRulesyncHooks(); + + expect(rulesyncHooks.getJson().hooks.stop).toEqual([ + { type: "command", command: "pnpm test --runInBand" }, + ]); + }); + }); + + describe("forDeletion", () => { + it("should create a placeholder hooks file for deletion", () => { + const hooks = DeepagentsHooks.forDeletion({ + baseDir: testDir, + relativeDirPath: ".deepagents", + relativeFilePath: "hooks.json", + }); + + expect(hooks.getRelativeDirPath()).toBe(".deepagents"); + expect(hooks.getRelativeFilePath()).toBe("hooks.json"); + expect(JSON.parse(hooks.getFileContent())).toEqual({ hooks: [] }); + }); }); }); diff --git a/src/features/hooks/deepagents-hooks.ts b/src/features/hooks/deepagents-hooks.ts index 8c26b6689..838b42fa0 100644 --- a/src/features/hooks/deepagents-hooks.ts +++ b/src/features/hooks/deepagents-hooks.ts @@ -82,6 +82,7 @@ function deepagentsToCanonicalHooks(hooksEntries: DeepagentsHookEntry[]): HooksC const canonical: HooksConfig["hooks"] = {}; for (const entry of hooksEntries) { + if (typeof entry !== "object" || entry === null) continue; if (!Array.isArray(entry.command) || entry.command.length === 0) continue; // Reconstruct command string: if it's ["bash", "-c", "..."], extract the script diff --git a/src/features/hooks/hooks-processor.ts b/src/features/hooks/hooks-processor.ts index d3fbc1410..a03bf0ce8 100644 --- a/src/features/hooks/hooks-processor.ts +++ b/src/features/hooks/hooks-processor.ts @@ -180,6 +180,9 @@ const toolHooksFactories = new Map([ ], ]); +// Project-mode generation/import should only expose tools that actually write +// hooks into the workspace. This keeps global-only targets like deepagents out +// of project target lists while still allowing them in global mode. const hooksProcessorToolTargets: ToolTarget[] = [...toolHooksFactories.entries()] .filter(([, f]) => f.meta.supportsProject) .map(([t]) => t); diff --git a/src/features/mcp/deepagents-mcp.test.ts b/src/features/mcp/deepagents-mcp.test.ts index f515e96d9..6285d2068 100644 --- a/src/features/mcp/deepagents-mcp.test.ts +++ b/src/features/mcp/deepagents-mcp.test.ts @@ -112,4 +112,40 @@ describe("DeepagentsMcp", () => { expect(mcp.getRelativeFilePath()).toBe(".mcp.json"); }); }); + + describe("toRulesyncMcp", () => { + it("should convert deepagents mcp config to rulesync format", () => { + const mcp = new DeepagentsMcp({ + baseDir: testDir, + relativeDirPath: ".deepagents", + relativeFilePath: ".mcp.json", + fileContent: JSON.stringify({ + mcpServers: { + "test-server": { command: "npx", args: ["-y", "test-server"] }, + }, + extra: true, + }), + }); + + const rulesyncMcp = mcp.toRulesyncMcp(); + + expect(rulesyncMcp.getMcpServers()).toEqual({ + "test-server": { command: "npx", args: ["-y", "test-server"] }, + }); + }); + }); + + describe("forDeletion", () => { + it("should create a placeholder file for deletion", () => { + const mcp = DeepagentsMcp.forDeletion({ + baseDir: testDir, + relativeDirPath: ".deepagents", + relativeFilePath: ".mcp.json", + }); + + expect(mcp.getRelativeDirPath()).toBe(".deepagents"); + expect(mcp.getRelativeFilePath()).toBe(".mcp.json"); + expect(mcp.getFileContent()).toBe("{}"); + }); + }); }); diff --git a/src/features/rules/deepagents-rule.test.ts b/src/features/rules/deepagents-rule.test.ts index 3cc84e46b..6ea71f83e 100644 --- a/src/features/rules/deepagents-rule.test.ts +++ b/src/features/rules/deepagents-rule.test.ts @@ -138,6 +138,21 @@ describe("DeepagentsRule", () => { }); }); + describe("forDeletion", () => { + it("should create a placeholder root rule for deletion", () => { + const rule = DeepagentsRule.forDeletion({ + baseDir: testDir, + relativeDirPath: ".deepagents", + relativeFilePath: "AGENTS.md", + }); + + expect(rule.getRelativeDirPath()).toBe(".deepagents"); + expect(rule.getRelativeFilePath()).toBe("AGENTS.md"); + expect(rule.isRoot()).toBe(true); + expect(rule.getFileContent()).toBe(""); + }); + }); + describe("isTargetedByRulesyncRule", () => { it("should return true when target is deepagents", () => { const rulesyncRule = new RulesyncRule({ diff --git a/src/features/rules/deepagents-rule.ts b/src/features/rules/deepagents-rule.ts index 8960d8e30..06062482a 100644 --- a/src/features/rules/deepagents-rule.ts +++ b/src/features/rules/deepagents-rule.ts @@ -57,6 +57,7 @@ export class DeepagentsRule extends ToolRule { relativeFilePath, validate = true, }: ToolRuleFromFileParams): Promise { + const settablePaths = this.getSettablePaths(); const isRoot = relativeFilePath === "AGENTS.md"; const relativePath = isRoot ? join(".deepagents", "AGENTS.md") @@ -66,8 +67,8 @@ export class DeepagentsRule extends ToolRule { return new DeepagentsRule({ baseDir, relativeDirPath: isRoot - ? this.getSettablePaths().root.relativeDirPath - : this.getSettablePaths().nonRoot.relativeDirPath, + ? settablePaths.root.relativeDirPath + : settablePaths.nonRoot.relativeDirPath, relativeFilePath: isRoot ? "AGENTS.md" : relativeFilePath, fileContent, validate, diff --git a/src/features/skills/deepagents-skill.test.ts b/src/features/skills/deepagents-skill.test.ts index cfb2a9e39..eb7dc1ab3 100644 --- a/src/features/skills/deepagents-skill.test.ts +++ b/src/features/skills/deepagents-skill.test.ts @@ -28,10 +28,9 @@ describe("DeepagentsSkill", () => { expect(paths.relativeDirPath).toBe(join(".deepagents", "skills")); }); - it("should throw when global mode requested", () => { - expect(() => DeepagentsSkill.getSettablePaths({ global: true })).toThrow( - "DeepagentsSkill does not support global mode.", - ); + it("should return project path for global mode", () => { + const paths = DeepagentsSkill.getSettablePaths({ global: true }); + expect(paths.relativeDirPath).toBe(join(".deepagents", "skills")); }); }); @@ -119,6 +118,20 @@ Do the thing.`; expect(skill.getDirName()).toBe("my-skill"); expect(skill.getBody()).toBe("Instructions here."); }); + + it("should omit allowed-tools when deepagents frontmatter is absent", () => { + const rulesyncSkill = new RulesyncSkill({ + baseDir: testDir, + relativeDirPath: ".rulesync/skills", + dirName: "my-skill", + frontmatter: { name: "My Skill", description: "Does something.", targets: ["*"] }, + body: "Instructions here.", + }); + + const skill = DeepagentsSkill.fromRulesyncSkill({ baseDir: testDir, rulesyncSkill }); + + expect(skill.getFrontmatter()).not.toHaveProperty("allowed-tools"); + }); }); describe("isTargetedByRulesyncSkill", () => { @@ -175,4 +188,18 @@ Do the thing.`; expect(rulesyncSkill.getFrontmatter().targets).toEqual(["*"]); }); }); + + describe("forDeletion", () => { + it("should create a deletable placeholder skill", () => { + const skill = DeepagentsSkill.forDeletion({ + baseDir: testDir, + relativeDirPath: join(".deepagents", "skills"), + dirName: "my-skill", + }); + + expect(skill.getRelativeDirPath()).toBe(join(".deepagents", "skills")); + expect(skill.getDirName()).toBe("my-skill"); + expect(skill.getBody()).toBe(""); + }); + }); }); diff --git a/src/features/skills/deepagents-skill.ts b/src/features/skills/deepagents-skill.ts index db9610fe3..506ccb690 100644 --- a/src/features/skills/deepagents-skill.ts +++ b/src/features/skills/deepagents-skill.ts @@ -66,10 +66,7 @@ export class DeepagentsSkill extends ToolSkill { } } - static getSettablePaths(options?: { global?: boolean }): ToolSkillSettablePaths { - if (options?.global) { - throw new Error("DeepagentsSkill does not support global mode."); - } + static getSettablePaths(_options?: { global?: boolean }): ToolSkillSettablePaths { return { relativeDirPath: join(".deepagents", "skills"), }; @@ -140,7 +137,9 @@ export class DeepagentsSkill extends ToolSkill { const deepagentsFrontmatter: DeepagentsSkillFrontmatter = { name: rulesyncFrontmatter.name, description: rulesyncFrontmatter.description, - "allowed-tools": rulesyncFrontmatter.deepagents?.["allowed-tools"], + ...(rulesyncFrontmatter.deepagents?.["allowed-tools"] && { + "allowed-tools": rulesyncFrontmatter.deepagents["allowed-tools"], + }), }; return new DeepagentsSkill({ From 16344e34757dd758bde8f9c5a4393e532a950ea6 Mon Sep 17 00:00:00 2001 From: dyoshikawa-claw Date: Thu, 2 Apr 2026 23:51:24 +0900 Subject: [PATCH 2/2] chore: sort cspell deepagents entry --- cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index 1c324cba1..8a20c6171 100644 --- a/cspell.json +++ b/cspell.json @@ -70,10 +70,10 @@ "copilotcli", "cursorignore", "cursorindexingignore", - "deepagents", "cursorrrules", "cursorrules", "dbaeumer", + "deepagents", "deepview", "deepwiki", "depd",