diff --git a/src/cli/utils/__tests__/sub-agent-instructions.test.ts b/src/cli/utils/__tests__/sub-agent-instructions.test.ts index ace4ada..f027eac 100644 --- a/src/cli/utils/__tests__/sub-agent-instructions.test.ts +++ b/src/cli/utils/__tests__/sub-agent-instructions.test.ts @@ -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', @@ -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'); @@ -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', @@ -169,11 +171,11 @@ describe('installSubAgentInstructions', () => { ['claude-code'] ); - const content = readClaude(); - const startCount = (content.match(//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', () => { @@ -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', @@ -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); }); }); @@ -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', () => { diff --git a/src/cli/utils/agents-file.ts b/src/cli/utils/agents-file.ts index 33e38d6..ccf1a3c 100644 --- a/src/cli/utils/agents-file.ts +++ b/src/cli/utils/agents-file.ts @@ -440,14 +440,14 @@ export function detectRepoCoordsFromRawUrl( function writesSubAgentInstructionsContext(provider: NonNullable>): 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( @@ -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, diff --git a/src/shared/providers/registry.ts b/src/shared/providers/registry.ts index 76948ab..6113bdf 100644 --- a/src/shared/providers/registry.ts +++ b/src/shared/providers/registry.ts @@ -95,7 +95,6 @@ export const providers: Record = { }, 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