diff --git a/cspell.json b/cspell.json index 07cf3b9dd..6c6c035e5 100644 --- a/cspell.json +++ b/cspell.json @@ -349,6 +349,7 @@ "Subprojects", "sury", "xsschema", + "xhigh", "Classmethod", "Asoview", "KAKEHASHI", diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 00111a34e..9dcc31045 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -269,7 +269,20 @@ description: >- # subagent description suggest a specification, implement a new feature, refactor the codebase, or fix a bug. This agent can be called by the user explicitly only. claudecode: # for claudecode-specific parameters - model: inherit # opus, sonnet, haiku or inherit + model: inherit # opus, sonnet, haiku, fable, a full model id, or inherit (default) + tools: ["Read", "Write"] # (optional) allowed tools (string or list) + disallowedTools: ["Bash"] # (optional) tools to remove (string or list) + permissionMode: default # (optional) default | acceptEdits | bypassPermissions | plan + maxTurns: 20 # (optional) maximum agentic turns + skills: ["skill-creator"] # (optional) Agent Skills to utilize (string or list) + color: cyan # (optional) UI color (e.g. red, blue, green, cyan, ...) + memory: project # (optional) user | project | local + effort: high # (optional) low | medium | high | xhigh | max + isolation: worktree # (optional) run the subagent in an isolated git worktree + background: false # (optional) run the subagent in the background + initialPrompt: "Start by reading the spec." # (optional) seed prompt for the subagent + mcpServers: {} # (optional) MCP server config (passed through verbatim) + hooks: {} # (optional) hook config (passed through verbatim) copilot: # for GitHub Copilot specific parameters tools: - web/fetch # agent/runSubagent is always included automatically diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index 00111a34e..9dcc31045 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -269,7 +269,20 @@ description: >- # subagent description suggest a specification, implement a new feature, refactor the codebase, or fix a bug. This agent can be called by the user explicitly only. claudecode: # for claudecode-specific parameters - model: inherit # opus, sonnet, haiku or inherit + model: inherit # opus, sonnet, haiku, fable, a full model id, or inherit (default) + tools: ["Read", "Write"] # (optional) allowed tools (string or list) + disallowedTools: ["Bash"] # (optional) tools to remove (string or list) + permissionMode: default # (optional) default | acceptEdits | bypassPermissions | plan + maxTurns: 20 # (optional) maximum agentic turns + skills: ["skill-creator"] # (optional) Agent Skills to utilize (string or list) + color: cyan # (optional) UI color (e.g. red, blue, green, cyan, ...) + memory: project # (optional) user | project | local + effort: high # (optional) low | medium | high | xhigh | max + isolation: worktree # (optional) run the subagent in an isolated git worktree + background: false # (optional) run the subagent in the background + initialPrompt: "Start by reading the spec." # (optional) seed prompt for the subagent + mcpServers: {} # (optional) MCP server config (passed through verbatim) + hooks: {} # (optional) hook config (passed through verbatim) copilot: # for GitHub Copilot specific parameters tools: - web/fetch # agent/runSubagent is always included automatically diff --git a/src/features/subagents/claudecode-subagent.test.ts b/src/features/subagents/claudecode-subagent.test.ts index 734fd90d1..2f0caab66 100644 --- a/src/features/subagents/claudecode-subagent.test.ts +++ b/src/features/subagents/claudecode-subagent.test.ts @@ -46,6 +46,49 @@ describe("ClaudecodeSubagentFrontmatterSchema", () => { expect(() => ClaudecodeSubagentFrontmatterSchema.parse(frontmatter)).not.toThrow(); }); + it("should accept the full set of Claude Code subagent frontmatter fields", () => { + const frontmatter = { + name: "test-agent", + description: "A test agent", + model: "fable", + tools: ["Read", "Write"], + disallowedTools: ["Bash"], + permissionMode: "default", + maxTurns: 20, + skills: ["skill-creator"], + color: "cyan", + memory: "project", + effort: "high", + isolation: "worktree", + background: true, + initialPrompt: "Start by reading the spec.", + mcpServers: { github: { command: "github-mcp" } }, + hooks: { PreToolUse: [] }, + }; + + expect(() => ClaudecodeSubagentFrontmatterSchema.parse(frontmatter)).not.toThrow(); + }); + + it("should reject non-number maxTurns", () => { + const invalidFrontmatter = { + name: "test-agent", + description: "A test agent", + maxTurns: "x", + }; + + expect(() => ClaudecodeSubagentFrontmatterSchema.parse(invalidFrontmatter)).toThrow(); + }); + + it("should reject non-boolean background", () => { + const invalidFrontmatter = { + name: "test-agent", + description: "A test agent", + background: "yes", + }; + + expect(() => ClaudecodeSubagentFrontmatterSchema.parse(invalidFrontmatter)).toThrow(); + }); + it("should reject frontmatter missing required fields", () => { // Missing name const missingName = { @@ -746,6 +789,72 @@ describe("ClaudecodeSubagent", () => { }); }); + it("should round-trip the full set of Claude Code subagent fields through the claudecode section", () => { + const original = new RulesyncSubagent({ + outputRoot: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "full-fields.md", + frontmatter: { + targets: ["claudecode"], + name: "full-agent", + description: "Full fields test", + claudecode: { + model: "fable", + tools: ["Read", "Write"], + disallowedTools: ["Bash"], + permissionMode: "default", + maxTurns: 20, + skills: ["skill-creator"], + color: "cyan", + memory: "project", + effort: "high", + isolation: "worktree", + background: true, + initialPrompt: "Start by reading the spec.", + mcpServers: { github: { command: "github-mcp" } }, + hooks: { PreToolUse: [] }, + }, + }, + body: "Body content", + }); + + const claudecode = ClaudecodeSubagent.fromRulesyncSubagent({ + outputRoot: testDir, + relativeDirPath: ".claude/agents", + rulesyncSubagent: original, + }) as ClaudecodeSubagent; + + const frontmatter = claudecode.getFrontmatter(); + expect(frontmatter.disallowedTools).toEqual(["Bash"]); + expect(frontmatter.maxTurns).toBe(20); + expect(frontmatter.color).toBe("cyan"); + expect(frontmatter.memory).toBe("project"); + expect(frontmatter.effort).toBe("high"); + expect(frontmatter.isolation).toBe("worktree"); + expect(frontmatter.background).toBe(true); + expect(frontmatter.initialPrompt).toBe("Start by reading the spec."); + expect(frontmatter.mcpServers).toEqual({ github: { command: "github-mcp" } }); + expect(frontmatter.hooks).toEqual({ PreToolUse: [] }); + + const backToRulesync = claudecode.toRulesyncSubagent(); + expect(backToRulesync.getFrontmatter().claudecode).toEqual({ + model: "fable", + tools: ["Read", "Write"], + disallowedTools: ["Bash"], + permissionMode: "default", + maxTurns: 20, + skills: ["skill-creator"], + color: "cyan", + memory: "project", + effort: "high", + isolation: "worktree", + background: true, + initialPrompt: "Start by reading the spec.", + mcpServers: { github: { command: "github-mcp" } }, + hooks: { PreToolUse: [] }, + }); + }); + it("should not include claudecode section when no model or extra fields", () => { const frontmatter: ClaudecodeSubagentFrontmatter = { name: "test-agent", diff --git a/src/features/subagents/claudecode-subagent.ts b/src/features/subagents/claudecode-subagent.ts index ebdaeb8dd..9d31202f8 100644 --- a/src/features/subagents/claudecode-subagent.ts +++ b/src/features/subagents/claudecode-subagent.ts @@ -23,8 +23,19 @@ export const ClaudecodeSubagentFrontmatterSchema = z.looseObject({ description: z.optional(z.string()), model: z.optional(z.string()), tools: z.optional(z.union([z.string(), z.array(z.string())])), + disallowedTools: z.optional(z.union([z.string(), z.array(z.string())])), permissionMode: z.optional(z.string()), + maxTurns: z.optional(z.number()), skills: z.optional(z.union([z.string(), z.array(z.string())])), + color: z.optional(z.string()), + memory: z.optional(z.string()), + effort: z.optional(z.string()), + isolation: z.optional(z.string()), + background: z.optional(z.boolean()), + initialPrompt: z.optional(z.string()), + // Nested config objects are accepted loosely for now; dedicated schemas are a follow-up. + mcpServers: z.optional(z.unknown()), + hooks: z.optional(z.unknown()), }); export type ClaudecodeSubagentFrontmatter = z.infer;