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
45 changes: 31 additions & 14 deletions src/cli/utils/__tests__/sub-agent-instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ describe('installSubAgentInstructions', () => {
if (existsSync(tempDir)) rmSync(tempDir, { recursive: true, force: true });
});

const readClaude = () => readFileSync(join(tempDir, 'CLAUDE.md'), 'utf-8');
const readClaudeAgent = (id: string) =>
readFileSync(join(tempDir, '.claude', 'agents', `${id}.md`), 'utf-8');
const readCursorAgent = (id: string) =>
readFileSync(join(tempDir, '.cursor', 'agents', `${id}.md`), 'utf-8');

it('creates .claude/agents/{id}.md and CLAUDE.md for claude-code provider', () => {
it('creates .claude/agents/{id}.md for claude-code provider and leaves CLAUDE.md untouched', () => {
// Sub-agent context belongs in the dedicated sub-agent file, NOT folded
// back into CLAUDE.md — that would duplicate the content and bloat the
// primary agent's context window on every turn.
const subAgent: SubAgent = {
id: 'infra-agent',
description: 'CDK and Terraform specialist',
Expand All @@ -53,7 +55,7 @@ describe('installSubAgentInstructions', () => {
installSubAgentInstructions(tempDir, subAgent, capabilities, ['claude-code']);

expect(existsSync(join(tempDir, '.claude', 'agents', 'infra-agent.md'))).toBe(true);
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(true);
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(false);

const agentFile = readClaudeAgent('infra-agent');
expect(agentFile).toContain('name: infra-agent');
Expand Down Expand Up @@ -153,7 +155,7 @@ describe('installSubAgentInstructions', () => {
expect(readClaudeAgent('infra-agent')).toContain('Work only in backend-infra/ and user-infra/.');
});

it('upserts CLAUDE.md block — re-running replaces without duplicating', () => {
it('re-running install replaces the sub-agent file in place without duplicating', () => {
const subAgent: SubAgent = {
id: 'infra-agent',
description: 'v1',
Expand All @@ -169,11 +171,11 @@ describe('installSubAgentInstructions', () => {
['claude-code']
);

const content = readClaude();
const startCount = (content.match(/<!-- capa:start:sub-agent:infra-agent -->/g) || []).length;
expect(startCount).toBe(1);
expect(content).toContain('v2 updated');
expect(content).not.toContain('v1');
const agentFile = readClaudeAgent('infra-agent');
expect(agentFile).toContain('v2 updated');
expect(agentFile).not.toContain('description: v1');
// CLAUDE.md must NOT be created as a side effect.
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(false);
});

it('multiple sub-agents produce independent .claude/agents/ files', () => {
Expand Down Expand Up @@ -219,7 +221,7 @@ describe('installSubAgentInstructions', () => {
expect(content).toContain('aws-iac.search_cdk_docs');
});

it('writes both .claude/agents/ and .cursor/agents/ when both providers active', () => {
it('writes both .claude/agents/ and .cursor/agents/ when both providers active, with no primary instructions file touched', () => {
const subAgent: SubAgent = {
id: 'infra-agent',
description: 'Infra specialist',
Expand All @@ -231,7 +233,22 @@ describe('installSubAgentInstructions', () => {

expect(existsSync(join(tempDir, '.claude', 'agents', 'infra-agent.md'))).toBe(true);
expect(existsSync(join(tempDir, '.cursor', 'agents', 'infra-agent.md'))).toBe(true);
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(true);
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(false);
expect(existsSync(join(tempDir, 'AGENTS.md'))).toBe(false);
});

it('does not create .github/copilot-instructions.md for github-copilot — sub-agent file is enough', () => {
const subAgent: SubAgent = {
id: 'infra-agent',
description: 'Infra specialist',
skills: [],
tools: ['search_cdk_docs'],
};

installSubAgentInstructions(tempDir, subAgent, capabilities, ['github-copilot']);

expect(existsSync(join(tempDir, '.github', 'agents', 'infra-agent.md'))).toBe(true);
expect(existsSync(join(tempDir, '.github', 'copilot-instructions.md'))).toBe(false);
});
});

Expand All @@ -246,14 +263,14 @@ describe('removeSubAgentInstructions', () => {
if (existsSync(tempDir)) rmSync(tempDir, { recursive: true, force: true });
});

it('removes .claude/agents/{id}.md and CLAUDE.md block for claude-code', () => {
it('removes .claude/agents/{id}.md for claude-code', () => {
const subAgent: SubAgent = { id: 'infra-agent', description: '', skills: [], tools: [] };
installSubAgentInstructions(tempDir, subAgent, capabilities, ['claude-code']);
removeSubAgentInstructions(tempDir, 'infra-agent', ['claude-code']);

expect(existsSync(join(tempDir, '.claude', 'agents', 'infra-agent.md'))).toBe(false);
const content = readFileSync(join(tempDir, 'CLAUDE.md'), 'utf-8');
expect(content).not.toContain('capa:start:sub-agent:infra-agent');
// Sub-agent install never seeds CLAUDE.md, so remove is a no-op for it.
expect(existsSync(join(tempDir, 'CLAUDE.md'))).toBe(false);
});

it('leaves other sub-agent files intact', () => {
Expand Down
23 changes: 13 additions & 10 deletions src/cli/utils/agents-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,14 +440,14 @@ export function detectRepoCoordsFromRawUrl(

function writesSubAgentInstructionsContext(provider: NonNullable<ReturnType<typeof getProvider>>): boolean {
if (!provider.instructions) return false;
if (provider.foldSubAgentsIntoInstructions === true) return true;
if (
provider.subagents &&
provider.instructions.filename !== UNIVERSAL_AGENTS_FILENAME
) {
return true;
}
return false;
// A provider with its own sub-agents integration materialises each sub-agent
// as a separate file under `provider.subagents.dir`. Folding the same
// context into the primary instructions file (CLAUDE.md, AGENTS.md, …)
// would duplicate it and bloat the main agent's context window. Only fold
// when the provider has no dedicated sub-agent file format AND explicitly
// opts in via `foldSubAgentsIntoInstructions: true`.
if (provider.subagents) return false;
return provider.foldSubAgentsIntoInstructions === true;
}

function upsertSubAgentInstructionsSnippet(
Expand Down Expand Up @@ -538,10 +538,13 @@ function removeSubAgentFile(projectPath: string, providerId: string, agentId: st
* Install sub-agent definition files for each active provider.
*
* For providers with a `subagents` integration, writes the agent file using
* the provider-specific format (markdown frontmatter or TOML).
* the provider-specific format (markdown frontmatter or TOML). That file is
* the sole source of truth — the primary instructions file (CLAUDE.md,
* AGENTS.md, …) is intentionally left untouched so we don't bloat the main
* agent's context with the same content the sub-agent file already carries.
*
* For providers without separate sub-agent files, folds context into the
* instructions file when `foldSubAgentsIntoInstructions` is set.
* instructions file ONLY when `foldSubAgentsIntoInstructions: true` is set.
*/
export function installSubAgentInstructions(
projectPath: string,
Expand Down
1 change: 0 additions & 1 deletion src/shared/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export const providers: Record<string, ProviderIntegration> = {
},
pluginManifestPaths: ['.claude-plugin/plugin.json'],
pluginProviderId: 'claude',
foldSubAgentsIntoInstructions: false,
hooks: {
// Claude Code reads hooks from the project-local .claude/settings.json.
// Docs: https://docs.claude.com/en/docs/claude-code/hooks
Expand Down
Loading