Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@
"Subprojects",
"sury",
"xsschema",
"xhigh",
"Classmethod",
"Asoview",
"KAKEHASHI",
Expand Down
15 changes: 14 additions & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 109 additions & 0 deletions src/features/subagents/claudecode-subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions src/features/subagents/claudecode-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ClaudecodeSubagentFrontmatterSchema>;
Expand Down