diff --git a/bin/install.js b/bin/install.js index 62792da60..93d909ed1 100755 --- a/bin/install.js +++ b/bin/install.js @@ -567,6 +567,8 @@ function convertClaudeToCopilotContent(content, isGlobal = false) { c = c.replace(/\.claude\//g, '.github/'); // CONV-07: Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); + // Runtime-neutral agent name replacement (#766) + c = neutralizeAgentReferences(c, 'copilot-instructions.md'); return c; } @@ -657,6 +659,8 @@ function convertClaudeToAntigravityContent(content, isGlobal = false) { c = c.replace(/\.claude\//g, '.agent/'); // Command name conversion (all gsd: references → gsd-) c = c.replace(/gsd:/g, 'gsd-'); + // Runtime-neutral agent name replacement (#766) + c = neutralizeAgentReferences(c, 'GEMINI.md'); return c; } @@ -867,6 +871,8 @@ function convertSlashCommandsToCodexSkillMentions(content) { function convertClaudeToCodexMarkdown(content) { let converted = convertSlashCommandsToCodexSkillMentions(content); converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}'); + // Runtime-neutral agent name replacement (#766) + converted = neutralizeAgentReferences(converted, 'AGENTS.md'); return converted; } @@ -1175,6 +1181,36 @@ function installCodexConfig(targetDir, agentsSrc) { * Terminals don't support subscript — Gemini renders these as raw HTML. * Converts text to italic *(text)* for readable terminal output. */ +/** + * Runtime-neutral agent name and instruction file replacement. + * Used by ALL non-Claude runtime converters to avoid Claude-specific + * references in workflow prompts, agent definitions, and documentation. + * + * Replaces: + * - Standalone "Claude" (agent name) → "the agent" + * Preserves: "Claude Code" (product), "Claude Opus/Sonnet/Haiku" (models), + * "claude-" (prefixes), "CLAUDE.md" (handled separately) + * - "CLAUDE.md" → runtime-appropriate instruction file + * - "Do NOT load full AGENTS.md" → removed (harmful for AGENTS.md runtimes) + * + * @param {string} content - File content to neutralize + * @param {string} instructionFile - Runtime's instruction file ('AGENTS.md', 'GEMINI.md', etc.) + * @returns {string} Content with runtime-neutral references + */ +function neutralizeAgentReferences(content, instructionFile) { + let c = content; + // Replace standalone "Claude" (the agent) but preserve product/model names. + // Negative lookahead avoids: Claude Code, Claude Opus/Sonnet/Haiku, Claude native, Claude-based + c = c.replace(/\bClaude(?! Code| Opus| Sonnet| Haiku| native| based|-)\b(?!\.md)/g, 'the agent'); + // Replace CLAUDE.md with runtime-appropriate instruction file + if (instructionFile) { + c = c.replace(/CLAUDE\.md/g, instructionFile); + } + // Remove instructions that conflict with AGENTS.md-based runtimes + c = c.replace(/Do NOT load full `AGENTS\.md` files[^\n]*/g, ''); + return c; +} + function stripSubTags(content) { return content.replace(/(.*?)<\/sub>/g, '*($1)*'); } @@ -1279,7 +1315,9 @@ function convertClaudeToGeminiAgent(content) { // is equivalent bash and invisible to Gemini's /\$\{(\w+)\}/g regex. const escapedBody = body.replace(/\$\{(\w+)\}/g, '$$$1'); - return `---\n${newFrontmatter}\n---${stripSubTags(escapedBody)}`; + // Runtime-neutral agent name replacement (#766) + const neutralBody = neutralizeAgentReferences(escapedBody, 'GEMINI.md'); + return `---\n${newFrontmatter}\n---${stripSubTags(neutralBody)}`; } function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { @@ -1295,6 +1333,8 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) { convertedContent = convertedContent.replace(/\$HOME\/\.claude\b/g, '$HOME/.config/opencode'); // Replace general-purpose subagent type with OpenCode's equivalent "general" convertedContent = convertedContent.replace(/subagent_type="general-purpose"/g, 'subagent_type="general"'); + // Runtime-neutral agent name replacement (#766) + convertedContent = neutralizeAgentReferences(convertedContent, 'AGENTS.md'); // Check if content has frontmatter if (!convertedContent.startsWith('---')) { @@ -3319,6 +3359,7 @@ if (process.env.GSD_TEST_MODE) { installCodexConfig, convertClaudeCommandToCodexSkill, convertClaudeToOpencodeFrontmatter, + neutralizeAgentReferences, GSD_CODEX_MARKER, CODEX_AGENT_SANDBOX, getDirName, diff --git a/tests/runtime-converters.test.cjs b/tests/runtime-converters.test.cjs index 5da337310..0dc33cd11 100644 --- a/tests/runtime-converters.test.cjs +++ b/tests/runtime-converters.test.cjs @@ -17,6 +17,7 @@ process.env.GSD_TEST_MODE = '1'; const { convertClaudeToOpencodeFrontmatter, convertClaudeToGeminiAgent, + neutralizeAgentReferences, } = require('../bin/install.js'); // Sample Claude agent frontmatter (matches actual GSD agent format) @@ -183,3 +184,56 @@ Use \${PHASE} in shell examples. assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern'); }); }); + +// ─── neutralizeAgentReferences (#766) ───────────────────────────────────────── + +describe('neutralizeAgentReferences', () => { + test('replaces standalone Claude with "the agent"', () => { + const input = 'Claude handles these decisions. Claude should read the file.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(!result.includes('Claude handles'), 'standalone Claude replaced'); + assert.ok(result.includes('the agent handles'), 'replaced with "the agent"'); + }); + + test('preserves Claude Code (product name)', () => { + const input = 'This is a Claude Code bug. Use Claude Code settings.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('Claude Code bug'), 'Claude Code preserved'); + assert.ok(result.includes('Claude Code settings'), 'Claude Code preserved'); + }); + + test('preserves Claude model names', () => { + const input = 'Use Claude Opus for planning. Claude Sonnet for execution. Claude Haiku for research.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('Claude Opus'), 'Opus preserved'); + assert.ok(result.includes('Claude Sonnet'), 'Sonnet preserved'); + assert.ok(result.includes('Claude Haiku'), 'Haiku preserved'); + }); + + test('replaces CLAUDE.md with runtime instruction file', () => { + const input = 'Read CLAUDE.md for project instructions. Check ./CLAUDE.md if exists.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('AGENTS.md'), 'CLAUDE.md -> AGENTS.md'); + assert.ok(!result.includes('CLAUDE.md'), 'no CLAUDE.md remains'); + }); + + test('uses different instruction file per runtime', () => { + const input = 'Read CLAUDE.md for instructions.'; + assert.ok(neutralizeAgentReferences(input, 'GEMINI.md').includes('GEMINI.md')); + assert.ok(neutralizeAgentReferences(input, 'copilot-instructions.md').includes('copilot-instructions.md')); + assert.ok(neutralizeAgentReferences(input, 'AGENTS.md').includes('AGENTS.md')); + }); + + test('removes AGENTS.md load-blocking instruction', () => { + const input = 'Do NOT load full `AGENTS.md` files — they contain agent definitions.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(!result.includes('Do NOT load full'), 'blocking instruction removed'); + }); + + test('preserves claude- prefixes (CSS classes, package names)', () => { + const input = 'The claude-ctx session and claude-code package.'; + const result = neutralizeAgentReferences(input, 'AGENTS.md'); + assert.ok(result.includes('claude-ctx'), 'claude- prefix preserved'); + assert.ok(result.includes('claude-code'), 'claude-code preserved'); + }); +});